Add terra drawer primitive and fix test page parsing

main
dev 6 months ago
parent b8b24d9fb2
commit 27bcc65eee

@ -107,6 +107,7 @@
<div class="comp-links">
<a href="/test/alert-dialog" class="comp-link">AlertDialog</a>
<a href="/test/dialog" class="comp-link">Dialog</a>
<a href="/test/drawer" class="comp-link">Drawer</a>
</div>
</div>
<div class="comp-group">
@ -427,3 +428,4 @@

@ -1,5 +1,5 @@
<script lang="ts">
import { AcelConfig, Calendar } from '$uix/terra';
import { TerraConfig, Calendar } from '$uix/terra';
import type { DatiDateOrder } from '$lib/glob/lib';
import type { DateValue } from '$lib/glob/lib/date';
import { today, getLocalTimeZone, CalendarDate, isSameDay } from '$lib/glob/lib/date';
@ -649,15 +649,15 @@
</div>
</section>
<!-- ── 08. AcelConfig ─────────────────────────────────────────── -->
<!-- ── 08. TerraConfig ─────────────────────────────────────────── -->
<section class="card" data-color="lime">
<header class="card-hd">
<span class="tag">08</span>
<span class="card-title">Calendar vía AcelConfig</span>
<span class="card-title">Calendar vía TerraConfig</span>
</header>
<p class="card-desc">Este calendario hereda <code>locale</code> y <code>dateOrder</code> desde <code>AcelConfig</code> sin props directas.</p>
<p class="card-desc">Este calendario hereda <code>locale</code> y <code>dateOrder</code> desde <code>TerraConfig</code> sin props directas.</p>
<AcelConfig defaultLocale={locale} defaultDateOrder={dateOrder}>
<TerraConfig defaultLocale={locale} defaultDateOrder={dateOrder}>
<Calendar.Root type="single" bind:value={configValue} class="cal">
{#snippet children({ months, weekdays, dateOrder })}
<Calendar.Header class="cal-hd cal-hd--sel">
@ -697,7 +697,7 @@
{/each}
{/snippet}
</Calendar.Root>
</AcelConfig>
</TerraConfig>
<div class="val-row">
<span class="val-label">value:</span>

@ -1,462 +1,927 @@
<script lang="ts">
import { AcelConfig, DateRangeField } from '$uix/terra';
import { TerraConfig, DateRangeField } from '$uix/terra';
import type { DatiDateOrder, DatiTimeFormat } from '$lib/glob/lib';
import type { DateValue } from '$lib/glob/lib/date';
import { today, getLocalTimeZone, CalendarDate } from '$lib/glob/lib/date';
const tz = getLocalTimeZone();
const now = today(tz);
// ── Selector de locale global ─────────────────────────────────────────────
const LOCALES: { id: string; label: string; hint: string }[] = [
{ id: 'es-ES', label: 'es-ES', hint: 'DD/MM/AAAA' },
{ id: 'en-US', label: 'en-US', hint: 'MM/DD/YYYY' },
{ id: 'en-GB', label: 'en-GB', hint: 'DD/MM/YYYY' },
{ id: 'de-DE', label: 'de-DE', hint: 'TT.MM.JJJJ' },
{ id: 'fr-FR', label: 'fr-FR', hint: 'JJ/MM/AAAA' },
{ id: 'ja-JP', label: 'ja-JP', hint: 'AAAA/MM/DD' },
{ id: 'ar-SA', label: 'ar-SA', hint: 'RTL هـ' },
];
let locale = $state('es-ES');
const DATE_ORDERS: DatiDateOrder[] = ['DMY', 'MDY', 'YMD'];
const TIME_FORMATS: DatiTimeFormat[] = ['24h', '12h'];
let dateOrder = $state<DatiDateOrder>('DMY');
let timeFormat = $state<DatiTimeFormat>('24h');
// ── 01. Básico ───────────────────────────────────────────────────────────
let value1 = $state<{ start: DateValue | undefined; end: DateValue | undefined } | undefined>(undefined);
// ── 02. Pre-seleccionado ─────────────────────────────────────────────────
let value2 = $state<{ start: DateValue | undefined; end: DateValue | undefined }>({
start: new CalendarDate(now.year, now.month, 5),
end: new CalendarDate(now.year, now.month, 20),
});
// ── 03. Validación ───────────────────────────────────────────────────────
let value3 = $state<{ start: DateValue | undefined; end: DateValue | undefined } | undefined>(undefined);
function validateRange(range: { start: DateValue; end: DateValue }) {
const days = Math.round(
(range.end.toDate(tz).getTime() - range.start.toDate(tz).getTime()) / 86_400_000
);
if (days < 2) return 'El rango debe ser de al menos 2 días';
if (days > 30) return 'El rango no puede superar 30 días';
}
// ── 04. Hora (granularity=hour) ──────────────────────────────────────────
let value4 = $state<{ start: DateValue | undefined; end: DateValue | undefined } | undefined>(undefined);
// ── 05. disabled / readonly ──────────────────────────────────────────────
const fixedVal = {
start: new CalendarDate(now.year, now.month, 10),
end: new CalendarDate(now.year, now.month, 25),
};
// ── 06. vía AcelConfig ───────────────────────────────────────────────────
// ── 06. vía TerraConfig ───────────────────────────────────────────────────
let value6 = $state<{ start: DateValue | undefined; end: DateValue | undefined } | undefined>(undefined);
function fmt(v: { start?: DateValue; end?: DateValue } | undefined) {
if (!v) return '—';
const d = (x: DateValue | undefined) =>
x ? `${x.year}-${String(x.month).padStart(2,'0')}-${String(x.day).padStart(2,'0')}` : '?';
x
? `${x.year}-${String(x.month).padStart(2, '0')}-${String(x.day).padStart(2, '0')}`
: '?';
return `${d(v.start)} → ${d(v.end)}`;
}
</script>
<div class="page">
<header class="ph">
<div class="ph-inner">
<div class="ph-brand">
<span class="ph-dot"></span>
<span class="ph-title">date-range<em>-field</em></span>
</div>
<span class="ph-sub">acel · campo de entrada de rango de fechas</span>
<span class="ph-sub">terra · campo de entrada de rango de fechas</span>
</div>
</header>
<div class="type-bar">
<span class="type-bar-label">Locale:</span>
{#each LOCALES as loc}
<button
class="type-btn"
class:type-btn--active={locale === loc.id}
onclick={() => (locale = loc.id)}
title={loc.hint}
>{loc.label}</button>
{/each}
<span class="type-badge">{LOCALES.find(l => l.id === locale)?.hint ?? ''}</span>
</div>
<div class="type-bar type-bar--sub">
<span class="type-bar-label">Date order:</span>
{#each DATE_ORDERS as order}
<button
class="type-btn"
class:type-btn--active={dateOrder === order}
onclick={() => (dateOrder = order)}
>{order}</button>
{/each}
<span class="type-bar-label type-bar-label--spaced">Time format:</span>
{#each TIME_FORMATS as format}
<button
class="type-btn"
class:type-btn--active={timeFormat === format}
onclick={() => (timeFormat = format)}
>{format}</button>
{/each}
</div>
<main>
<div class="grid">
<!-- ── 01. Básico ────────────────────────────────────────────── -->
<section class="card" data-color="teal">
<header class="card-hd">
<span class="tag">01</span>
<span class="card-title">Básico</span>
</header>
<p class="card-desc">Campo de rango de fechas. Navega con Tab entre segmentos.</p>
<div class="field-wrap">
<DateRangeField.Root bind:value={value1} {locale} {dateOrder} {timeFormat} class="drf">
<DateRangeField.Label class="drf-label">Rango de fechas</DateRangeField.Label>
<div class="drf-inputs">
<DateRangeField.Input type="start" class="drf-input">
{#snippet children({ segments })}
{#each segments as { part, value }}
<DateRangeField.Segment {part} class="drf-seg drf-seg--{part}">{value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
<span class="drf-sep">→</span>
<DateRangeField.Input type="end" class="drf-input">
{#snippet children({ segments })}
{#each segments as { part, value }}
<DateRangeField.Segment {part} class="drf-seg drf-seg--{part}">{value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
</div>
</DateRangeField.Root>
</div>
<div class="val-row">
<span class="val-label">value:</span>
<span class="val-val">{fmt(value1)}</span>
</div>
</section>
<!-- ── 02. Pre-seleccionado ──────────────────────────────────── -->
<section class="card" data-color="violet">
<header class="card-hd">
<span class="tag">02</span>
<span class="card-title">Pre-seleccionado</span>
</header>
<p class="card-desc">Rango cargado con días 5–20 del mes actual.</p>
<div class="field-wrap">
<DateRangeField.Root bind:value={value2} {locale} {dateOrder} {timeFormat} class="drf">
<DateRangeField.Label class="drf-label">Rango de fechas</DateRangeField.Label>
<div class="drf-inputs">
<DateRangeField.Input type="start" class="drf-input">
{#snippet children({ segments })}
{#each segments as { part, value }}
<DateRangeField.Segment {part} class="drf-seg drf-seg--{part}">{value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
<span class="drf-sep">→</span>
<DateRangeField.Input type="end" class="drf-input">
{#snippet children({ segments })}
{#each segments as { part, value }}
<DateRangeField.Segment {part} class="drf-seg drf-seg--{part}">{value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
</div>
</DateRangeField.Root>
</div>
<div class="val-row">
<span class="val-label">value:</span>
<span class="val-val">{fmt(value2)}</span>
</div>
</section>
<!-- ── 03. Validación ────────────────────────────────────────── -->
<section class="card" data-color="amber">
<header class="card-hd">
<span class="tag">03</span>
<span class="card-title">Validación: 2–30 días</span>
</header>
<p class="card-desc">El rango debe tener entre 2 y 30 días.</p>
<div class="field-wrap">
<DateRangeField.Root bind:value={value3} {locale} {dateOrder} {timeFormat} validate={validateRange} class="drf">
<DateRangeField.Label class="drf-label">Rango de fechas</DateRangeField.Label>
<div class="drf-inputs">
<DateRangeField.Input type="start" class="drf-input">
{#snippet children({ segments })}
{#each segments as { part, value }}
<DateRangeField.Segment {part} class="drf-seg drf-seg--{part}">{value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
<span class="drf-sep">→</span>
<DateRangeField.Input type="end" class="drf-input">
{#snippet children({ segments })}
{#each segments as { part, value }}
<DateRangeField.Segment {part} class="drf-seg drf-seg--{part}">{value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
</div>
</DateRangeField.Root>
</div>
<div class="val-row">
<span class="val-label">value:</span>
<span class="val-val">{fmt(value3)}</span>
</div>
</section>
<!-- ── 04. granularity=hour ──────────────────────────────────── -->
<section class="card" data-color="indigo">
<header class="card-hd">
<span class="tag">04</span>
<span class="card-title">granularity="hour"</span>
</header>
<p class="card-desc">Incluye hora de inicio y fin. Alterna entre 12h y 24h.</p>
<div class="field-wrap">
<DateRangeField.Root bind:value={value4} {locale} {dateOrder} {timeFormat} granularity="hour" class="drf">
<DateRangeField.Label class="drf-label">Reserva de sala</DateRangeField.Label>
<div class="drf-inputs">
<DateRangeField.Input type="start" class="drf-input">
{#snippet children({ segments })}
{#each segments as { part, value }}
<DateRangeField.Segment {part} class="drf-seg drf-seg--{part}">{value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
<span class="drf-sep">→</span>
<DateRangeField.Input type="end" class="drf-input">
{#snippet children({ segments })}
{#each segments as { part, value }}
<DateRangeField.Segment {part} class="drf-seg drf-seg--{part}">{value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
</div>
</DateRangeField.Root>
</div>
<div class="val-row">
<span class="val-label">value:</span>
<span class="val-val">{fmt(value4)}</span>
</div>
</section>
<!-- ── 05. disabled / readonly ───────────────────────────────── -->
<section class="card" data-color="rose">
<header class="card-hd">
<span class="tag">05</span>
<span class="card-title">disabled / readonly</span>
</header>
<p class="card-desc">Estados de interacción bloqueada.</p>
<div class="pair-col">
<div>
<p class="pair-label">readonly</p>
<div class="field-wrap">
<DateRangeField.Root value={fixedVal} {dateOrder} {timeFormat} readonly class="drf">
<div class="drf-inputs">
<DateRangeField.Input type="start" class="drf-input">
{#snippet children({ segments })}
{#each segments as { part, value }}
<DateRangeField.Segment {part} class="drf-seg drf-seg--{part}">{value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
<span class="drf-sep">→</span>
<DateRangeField.Input type="end" class="drf-input">
{#snippet children({ segments })}
{#each segments as { part, value }}
<DateRangeField.Segment {part} class="drf-seg drf-seg--{part}">{value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
</div>
</DateRangeField.Root>
</div>
</div>
<div>
<p class="pair-label">disabled</p>
<div class="field-wrap">
<DateRangeField.Root value={fixedVal} {dateOrder} {timeFormat} disabled class="drf">
<div class="drf-inputs">
<DateRangeField.Input type="start" class="drf-input">
{#snippet children({ segments })}
{#each segments as { part, value }}
<DateRangeField.Segment {part} class="drf-seg drf-seg--{part}">{value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
<span class="drf-sep">→</span>
<DateRangeField.Input type="end" class="drf-input">
{#snippet children({ segments })}
{#each segments as { part, value }}
<DateRangeField.Segment {part} class="drf-seg drf-seg--{part}">{value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
</div>
</DateRangeField.Root>
</div>
</div>
</div>
</section>
<!-- ── 06. vía AcelConfig ─────────────────────────────────────── -->
<!-- ── 06. vía TerraConfig ─────────────────────────────────────── -->
<section class="card" data-color="cyan">
<header class="card-hd">
<span class="tag">06</span>
<span class="card-title">Defaults vía AcelConfig</span>
<span class="card-title">Defaults vía TerraConfig</span>
</header>
<p class="card-desc">El componente no recibe <code>locale</code>, <code>dateOrder</code> ni <code>timeFormat</code> directamente; los hereda del config reactivo.</p>
<div class="field-wrap">
<AcelConfig defaultLocale={locale} defaultDateOrder={dateOrder} defaultTimeFormat={timeFormat}>
<TerraConfig defaultLocale={locale} defaultDateOrder={dateOrder} defaultTimeFormat={timeFormat}>
<DateRangeField.Root bind:value={value6} class="drf">
<DateRangeField.Label class="drf-label">Rango desde config</DateRangeField.Label>
<div class="drf-inputs">
<DateRangeField.Input type="start" class="drf-input">
{#snippet children({ segments })}
{#each segments as { part, value }}
<DateRangeField.Segment {part} class="drf-seg drf-seg--{part}">{value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
<span class="drf-sep">→</span>
<DateRangeField.Input type="end" class="drf-input">
{#snippet children({ segments })}
{#each segments as { part, value }}
<DateRangeField.Segment {part} class="drf-seg drf-seg--{part}">{value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
</div>
</DateRangeField.Root>
</AcelConfig>
</TerraConfig>
</div>
<div class="val-row">
<span class="val-label">value:</span>
<span class="val-val">{fmt(value6)}</span>
</div>
</section>
</div>
</main>
</div>
<style>
:global(*, *::before, *::after) { box-sizing: border-box; margin: 0; padding: 0; }
.page {
min-height: 100vh; background: #0e0e14; color: #d4d2cc;
font-family: 'DM Mono', 'Fira Code', monospace; font-size: 13px;
}
.ph { border-bottom: 2px solid #818cf8; background: #080810; }
.ph-inner {
max-width: 1200px; margin: 0 auto; padding: .75rem 1.5rem;
display: flex; align-items: center; justify-content: space-between;
}
.ph-brand { display: flex; align-items: center; gap: .5rem; }
.ph-dot { width: 8px; height: 8px; border-radius: 50%; background: #818cf8; }
.ph-title { font-size: .85rem; font-weight: 700; letter-spacing: .15em; text-transform: uppercase; color: #f4f3ef; }
.ph-title em { color: #818cf8; font-style: normal; }
.ph-sub { font-size: .65rem; color: #555; letter-spacing: .1em; }
main { max-width: 1200px; margin: 0 auto; padding: 1.5rem; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: .75rem; }
.card {
background: #16161f; border: 1px solid #26262f; border-radius: 6px;
padding: 1.1rem; display: flex; flex-direction: column; gap: .75rem;
border-top: 3px solid transparent;
}
.card[data-color="teal"] { border-top-color: #2dd4bf; }
.card[data-color="violet"] { border-top-color: #a78bfa; }
.card[data-color="amber"] { border-top-color: #e5a100; }
.card[data-color="indigo"] { border-top-color: #818cf8; }
.card[data-color="rose"] { border-top-color: #fb7185; }
.card[data-color="cyan"] { border-top-color: #22d3ee; }
.card-hd { display: flex; align-items: center; gap: .5rem; padding-bottom: .6rem; border-bottom: 1px solid #26262f; }
.tag { font-size: .6rem; font-weight: 700; letter-spacing: .15em; color: #555; }
.card-title { font-size: .75rem; font-weight: 600; color: #aaa; }
.card-desc { font-size: .72rem; color: #555; line-height: 1.5; }
/* Field */
.field-wrap { display: flex; flex-direction: column; gap: .4rem; }
:global(.drf) { display: flex; flex-direction: column; gap: .3rem; }
:global(.drf-label) { font-size: .65rem; color: #666; letter-spacing: .08em; }
.drf-inputs {
display: flex; align-items: center; gap: .4rem;
background: #1a1a24; border: 1px solid #26262f; border-radius: 4px;
padding: .4rem .6rem;
}
:global(.drf-input) { display: flex; align-items: center; gap: 1px; }
.drf-sep { color: #555; font-size: .75rem; flex-shrink: 0; }
:global(.drf-seg) {
display: inline-block; padding: .05rem .15rem; border-radius: 2px;
min-width: 1.6em; text-align: center; font-size: .78rem; color: #d4d2cc;
cursor: default; outline: none;
}
:global(.drf-seg[data-type="literal"]) { color: #444; min-width: auto; padding: 0 1px; }
:global(.drf-seg:focus), :global(.drf-seg[data-focused]) {
background: #818cf8; color: #fff;
}
:global(.drf-seg[data-placeholder]) { color: #555; }
:global(.drf[data-readonly] .drf-seg) { pointer-events: none; color: #777; }
:global(.drf[data-disabled] .drf-seg) { pointer-events: none; color: #444; opacity: .6; }
:global(.drf[data-invalid] .drf-input) { border-color: #fb7185; }
/* Value display */
.val-row { display: flex; align-items: center; gap: .5rem; font-size: .72rem; }
.val-label { color: #555; }
.val-val { font-weight: 700; color: #818cf8; }
/* Pair */
.pair-col { display: flex; flex-direction: column; gap: .75rem; }
.pair-label { font-size: .62rem; color: #555; margin-bottom: .3rem; letter-spacing: .1em; }
/* Type switcher */
.type-bar {
max-width: 1200px; margin: 0 auto; padding: .6rem 1.5rem;
display: flex; align-items: center; gap: .5rem;
border-bottom: 1px solid #26262f;
background: #0e0e14;
}
.type-bar-label { font-size: .65rem; color: #555; letter-spacing: .08em; margin-right: .25rem; }
.type-btn {
font-family: inherit; font-size: .65rem; letter-spacing: .05em;
padding: .2rem .55rem; border-radius: 3px; cursor: pointer;
background: #1a1a24; border: 1px solid #26262f; color: #666;
transition: border-color .15s, color .15s;
}
.type-btn:hover { border-color: #818cf8; color: #aaa; }
.type-btn--active { border-color: #818cf8; color: #818cf8; background: #1e1e30; }
.type-badge { margin-left: auto; font-size: .6rem; color: #2dd4bf; letter-spacing: .1em; }
.type-bar--sub { border-top: 1px solid #171720; }
.type-bar-label--spaced { margin-left: 1rem; }
</style>

@ -0,0 +1,297 @@
<script lang="ts">
import { Drawer } from '$uix/terra';
import type { DrawerSide } from '$uix/terra/drawer/types';
const SIDES: DrawerSide[] = ['left', 'right', 'top', 'bottom'];
let side = $state<DrawerSide>('right');
let controlledOpen = $state(false);
</script>
<svelte:head>
<title>Drawer</title>
</svelte:head>
<div class="page">
<header class="hero">
<div>
<p class="eyebrow">Terra</p>
<h1>Drawer</h1>
<p class="lead">
Primitive headless para paneles laterales o inferiores, montado sobre el stack de
overlays de Dialog.
</p>
</div>
<div class="links">
<a href="/test">Indice general</a>
<a href="/test/terra">Hub terra</a>
</div>
</header>
<section class="panel">
<header class="panel-header">
<h2>Lado</h2>
<div class="chips">
{#each SIDES as value}
<button class:chip--active={side === value} class="chip" onclick={() => (side = value)}>
{value}
</button>
{/each}
</div>
</header>
<div class="examples">
<div class="card">
<h3>Básico</h3>
<p>Abre un drawer usando el lado seleccionado arriba.</p>
<Drawer.Root>
<Drawer.Trigger class="trigger">Open drawer</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay class="overlay" />
<Drawer.Content {side} class={`drawer drawer--${side}`}>
{#snippet children({ side: activeSide })}
<div class="drawer-header">
<div>
<Drawer.Title level={2}>Panel {activeSide}</Drawer.Title>
<Drawer.Description>
Usa el mismo comportamiento de foco, escape y dismiss que Dialog.
</Drawer.Description>
</div>
<Drawer.Close class="close">Close</Drawer.Close>
</div>
<div class="drawer-body">
<p>Este contenido recibe <code>data-side</code> y <code>data-drawer-content</code>.</p>
<p>Eso nos deja estilizar o animar en air más adelante sin tocar la semántica base.</p>
</div>
{/snippet}
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
</div>
<div class="card">
<h3>Controlado</h3>
<p>El estado se gobierna desde fuera.</p>
<label class="check-row">
<input type="checkbox" bind:checked={controlledOpen} />
<span>open = {controlledOpen ? 'true' : 'false'}</span>
</label>
<Drawer.Root bind:open={controlledOpen}>
<Drawer.Trigger class="trigger">Controlled drawer</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay class="overlay overlay--soft" />
<Drawer.Content side="bottom" class="drawer drawer--bottom">
<div class="drawer-header">
<Drawer.Title level={2}>Controlled bottom drawer</Drawer.Title>
<Drawer.Close class="close">Cerrar</Drawer.Close>
</div>
<p class="drawer-body">Ideal para mobile flows, filtros o paneles contextuales.</p>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
</div>
</div>
</section>
</div>
<style>
:global(*, *::before, *::after) {
box-sizing: border-box;
}
.page {
min-height: 100vh;
padding: 2rem;
background: #0b1020;
color: #e5e7eb;
font-family: Inter, system-ui, sans-serif;
}
.hero,
.panel {
max-width: 960px;
margin: 0 auto 1.5rem;
padding: 1.25rem;
border: 1px solid #24324a;
border-radius: 18px;
background: #111827;
}
.hero {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
}
.eyebrow {
margin: 0 0 .35rem;
font-size: .72rem;
font-weight: 700;
letter-spacing: .14em;
text-transform: uppercase;
color: #67e8f9;
}
h1,
h2,
h3,
p {
margin: 0;
}
.lead {
max-width: 62ch;
margin-top: .5rem;
color: #94a3b8;
line-height: 1.5;
}
.links {
display: flex;
gap: .75rem;
flex-wrap: wrap;
}
.links a {
color: #93c5fd;
text-decoration: none;
}
.panel-header {
display: flex;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.chips {
display: flex;
gap: .5rem;
flex-wrap: wrap;
}
.chip,
:global(.trigger),
:global(.close) {
border: 1px solid #334155;
background: #0f172a;
color: #e5e7eb;
border-radius: 999px;
padding: .5rem .9rem;
font: inherit;
cursor: pointer;
}
.chip--active,
:global(.trigger:hover),
:global(.close:hover) {
border-color: #67e8f9;
color: #67e8f9;
}
.examples {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
}
.card {
display: flex;
flex-direction: column;
gap: .85rem;
padding: 1rem;
border: 1px solid #1f2937;
border-radius: 14px;
background: #0f172a;
}
.card p {
color: #94a3b8;
line-height: 1.5;
}
.check-row {
display: inline-flex;
align-items: center;
gap: .5rem;
color: #cbd5e1;
}
:global(.overlay) {
position: fixed;
inset: 0;
background: rgba(2, 6, 23, .72);
}
:global(.overlay--soft) {
background: rgba(15, 23, 42, .48);
}
:global(.drawer) {
position: fixed;
background: #111827;
border: 1px solid #334155;
box-shadow: 0 20px 80px rgba(0, 0, 0, .45);
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
outline: none;
}
:global(.drawer--right) {
top: 0;
right: 0;
height: 100vh;
width: min(420px, 92vw);
}
:global(.drawer--left) {
top: 0;
left: 0;
height: 100vh;
width: min(420px, 92vw);
}
:global(.drawer--top) {
top: 0;
left: 0;
width: 100vw;
height: min(360px, 92vh);
}
:global(.drawer--bottom) {
left: 0;
bottom: 0;
width: 100vw;
height: min(360px, 92vh);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
:global(.drawer-header) {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
}
:global(.drawer-header h3) {
font-size: 1rem;
}
:global(.drawer-body) {
color: #cbd5e1;
line-height: 1.6;
}
code {
padding: .1rem .35rem;
border-radius: 6px;
background: #020617;
color: #93c5fd;
}
</style>

@ -1,5 +1,5 @@
<script lang="ts">
import { AcelConfig, LinkPreview } from '$uix/terra';
import { TerraConfig, LinkPreview } from '$uix/terra';
let controlledOpen = $state(false);
</script>
@ -61,8 +61,8 @@
</section>
<section class="demo">
<h2>ContentStatic + AcelConfig</h2>
<AcelConfig defaultLocale="es-ES">
<h2>ContentStatic + TerraConfig</h2>
<TerraConfig defaultLocale="es-ES">
<div class="row row--static-preview">
<LinkPreview.Root openDelay={150} closeDelay={150}>
<LinkPreview.Trigger href="https://acel.dev/static">Preview estático</LinkPreview.Trigger>
@ -75,7 +75,7 @@
</LinkPreview.ContentStatic>
</LinkPreview.Root>
</div>
</AcelConfig>
</TerraConfig>
</section>
</div>

@ -1,5 +1,5 @@
<script lang="ts">
import { AcelConfig, RangeCalendar } from '$uix/terra';
import { TerraConfig, RangeCalendar } from '$uix/terra';
import type { DatiDateOrder } from '$lib/glob/lib';
import type { DateValue } from '$lib/glob/lib/date';
import { today, getLocalTimeZone, CalendarDate } from '$lib/glob/lib/date';
@ -453,15 +453,15 @@
</div>
</section>
<!-- ── 07. AcelConfig ─────────────────────────────────────────── -->
<!-- ── 07. TerraConfig ─────────────────────────────────────────── -->
<section class="card" data-color="cyan">
<header class="card-hd">
<span class="tag">07</span>
<span class="card-title">RangeCalendar vía AcelConfig</span>
<span class="card-title">RangeCalendar vía TerraConfig</span>
</header>
<p class="card-desc">Este rango hereda <code>locale</code> y <code>dateOrder</code> desde <code>AcelConfig</code> sin pasarlos por props.</p>
<p class="card-desc">Este rango hereda <code>locale</code> y <code>dateOrder</code> desde <code>TerraConfig</code> sin pasarlos por props.</p>
<AcelConfig defaultLocale={locale} defaultDateOrder={dateOrder}>
<TerraConfig defaultLocale={locale} defaultDateOrder={dateOrder}>
<RangeCalendar.Root bind:value={value6} class="cal">
{#snippet children({ months, weekdays })}
<RangeCalendar.Header class="cal-hd">
@ -493,7 +493,7 @@
{/each}
{/snippet}
</RangeCalendar.Root>
</AcelConfig>
</TerraConfig>
<div class="val-row">
<span class="val-label">value:</span>

@ -36,6 +36,7 @@
{ name: 'ContextMenu', path: '/test/context-menu', status: 'stable' },
{ name: 'Menubar', path: '/test/menubar', status: 'stable' },
{ name: 'Dialog', path: '/test/dialog', status: 'stable' },
{ name: 'Drawer', path: '/test/drawer', status: 'wip' },
{ name: 'AlertDialog', path: '/test/alert-dialog', status: 'stable' },
],
},
@ -313,3 +314,4 @@

@ -0,0 +1,8 @@
<script lang="ts">
import { Dialog } from '../../dialog';
import type { DrawerCloseProps } from '../types';
let props: DrawerCloseProps = $props();
</script>
<Dialog.Close {...props} />

@ -0,0 +1,64 @@
<script lang="ts">
import { readableActive, writableActive, mergeProps, afterSleep } from '../../utils';
import { DialogContentState } from '../../dialog/dialog.svelte';
import type { DrawerContentProps } from '../types';
import { createId } from '../../utils/create-id';
import { noop } from '../../utils/noop';
const uid = $props.id();
let {
id = createId(uid),
side = 'right',
children,
child,
ref = $bindable(null),
forceMount = false,
onCloseAutoFocus = noop,
onEscapeKeydown = noop,
onOpenAutoFocus = noop,
...restProps
}: DrawerContentProps = $props();
const contentState = DialogContentState.create({
id: readableActive(() => id),
ref: writableActive(() => ref, (v) => (ref = v)),
});
const mergedProps = $derived(
mergeProps(restProps, contentState.props, {
'data-drawer-content': '',
'data-side': side,
})
);
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
onEscapeKeydown(e);
if (!e.defaultPrevented) {
contentState.root.handleClose();
}
}
}
$effect(() => {
if (contentState.root.opts.open.current) {
onOpenAutoFocus(new Event('focus'));
afterSleep(0, () => ref?.focus());
} else {
onCloseAutoFocus(new Event('focus'));
}
});
</script>
<svelte:window onkeydown={handleKeydown} />
{#if contentState.shouldRender || forceMount}
{#if child}
{@render child({ props: mergedProps, open: contentState.root.opts.open.current, side })}
{:else}
<div {...mergedProps} bind:this={ref}>
{@render children?.({ open: contentState.root.opts.open.current, side })}
</div>
{/if}
{/if}

@ -0,0 +1,8 @@
<script lang="ts">
import { Dialog } from '../../dialog';
import type { DrawerDescriptionProps } from '../types';
let props: DrawerDescriptionProps = $props();
</script>
<Dialog.Description {...props} />

@ -0,0 +1,19 @@
<script lang="ts">
import { mergeProps } from '../../utils';
import { Dialog } from '../../dialog';
import type { DrawerOverlayProps } from '../types';
let { child, children, ...restProps }: DrawerOverlayProps = $props();
const mergedProps = $derived(
mergeProps(restProps, {
'data-drawer-overlay': '',
})
);
</script>
<Dialog.Overlay {...mergedProps} {child}>
{#snippet children(props)}
{@render children?.(props)}
{/snippet}
</Dialog.Overlay>

@ -0,0 +1,8 @@
<script lang="ts">
import { Dialog } from '../../dialog';
import type { DrawerTitleProps } from '../types';
let props: DrawerTitleProps = $props();
</script>
<Dialog.Title {...props} />

@ -0,0 +1,8 @@
<script lang="ts">
import { Dialog } from '../../dialog';
import type { DrawerTriggerProps } from '../types';
let props: DrawerTriggerProps = $props();
</script>
<Dialog.Trigger {...props} />

@ -0,0 +1,24 @@
<script lang="ts">
import { readableActive, writableActive } from '../../utils';
import { DialogRootState } from '../../dialog/dialog.svelte';
import type { DrawerRootProps } from '../types';
import { noop } from '../../utils/noop';
let {
open = $bindable(false),
onOpenChange = noop,
onOpenChangeComplete = noop,
children,
}: DrawerRootProps = $props();
DialogRootState.create({
variant: readableActive(() => 'dialog'),
open: writableActive(() => open, (v) => {
open = v;
onOpenChange(v);
}),
onOpenChangeComplete: readableActive(() => onOpenChangeComplete),
});
</script>
{@render children?.()}

@ -0,0 +1,20 @@
export { default as Root } from './components/drawer.svelte';
export { default as Trigger } from './components/drawer-trigger.svelte';
export { default as Content } from './components/drawer-content.svelte';
export { default as Overlay } from './components/drawer-overlay.svelte';
export { default as Title } from './components/drawer-title.svelte';
export { default as Description } from './components/drawer-description.svelte';
export { default as Close } from './components/drawer-close.svelte';
export { default as Portal } from '../base/portal/portal.svelte';
export type {
DrawerRootProps as RootProps,
DrawerTriggerProps as TriggerProps,
DrawerPortalProps as PortalProps,
DrawerContentProps as ContentProps,
DrawerOverlayProps as OverlayProps,
DrawerTitleProps as TitleProps,
DrawerDescriptionProps as DescriptionProps,
DrawerCloseProps as CloseProps,
DrawerSide,
} from './types';

@ -0,0 +1,2 @@
export * as Drawer from './exports';
export * from './exports';

@ -0,0 +1,44 @@
import type {
DialogCloseProps,
DialogContentProps,
DialogDescriptionProps,
DialogOverlayProps,
DialogPortalProps,
DialogRootProps,
DialogTitleProps,
DialogTriggerProps,
} from '../dialog/types';
import type { WithChild, Without } from '../utils/types';
import type { TerraPrimitiveDivAttributes } from '../utils/attributes';
export type DrawerSide = 'top' | 'right' | 'bottom' | 'left';
export type DrawerRootProps = DialogRootProps;
export type DrawerTriggerProps = DialogTriggerProps;
export type DrawerPortalProps = DialogPortalProps;
export type DrawerOverlayProps = DialogOverlayProps;
export type DrawerContentSnippetProps = { open: boolean; side: DrawerSide };
export type DrawerContentPropsWithoutHTML = WithChild<
{
side?: DrawerSide;
forceMount?: boolean;
onOpenAutoFocus?: (event: Event) => void;
onCloseAutoFocus?: (event: Event) => void;
onEscapeKeydown?: (event: KeyboardEvent) => void;
},
DrawerContentSnippetProps
>;
export type DrawerContentProps = DrawerContentPropsWithoutHTML &
Without<TerraPrimitiveDivAttributes, DrawerContentPropsWithoutHTML>;
export type DrawerTitleProps = DialogTitleProps;
export type DrawerDescriptionProps = DialogDescriptionProps;
export type DrawerCloseProps = DialogCloseProps;

@ -41,6 +41,7 @@ export * as Command from './command';
export * as AlertDialog from './alert-dialog';
export * as Dialog from './dialog';
export * as Drawer from './drawer';
export { TerraConfig } from './base/config/exports';
export type {
@ -65,3 +66,4 @@ export * as DateRangePicker from './date-range-picker';

Loading…
Cancel
Save

Powered by TurnKey Linux.