diff --git a/CLAUDE.md b/CLAUDE.md index b893a93bf..4f5ac0ebc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,25 +29,26 @@ npm run format # Auto-format with Prettier Aliases must be synced in BOTH `svelte.config.js` AND `vite.config.ts`: -| Alias | Target | -|---|---| -| `@/` | `src/` | -| `@/ling` | `src/lib/ling` (i18n) | -| `@/logr` | `src/lib/logr` (logging) | -| `@/glob` | `src/lib/glob` (globalization: dates, numbers, locale) | -| `@/actx` | `src/lib/actx` (app context) | -| `@/uiux` | `src/lib/uiux` | -| `$uix` | `src/uix` (UI layers) | -| `$terra` | `src/uix/terra` (headless primitives) | -| `$terra/utils` | `src/uix/terra/utils/sound.svelte.ts` | -| `$terra/system` | `src/uix/terra/system/exports.ts` | -| `$terra/external/dates` | `src/uix/terra/external/dates/sound.svelte.ts` | -| `$glob` | `src/lib/glob` | -| `$lib` | `src/lib` | +| Alias | Target | +| ----------------------- | ------------------------------------------------------ | +| `@/` | `src/` | +| `@/ling` | `src/lib/ling` (i18n) | +| `@/logr` | `src/lib/logr` (logging) | +| `@/glob` | `src/lib/glob` (legacy; do not use for new code) | +| `@/actx` | `src/lib/actx` (app context) | +| `@/uiux` | `src/lib/uiux` | +| `$uix` | `src/uix` (UI layers) | +| `$terra` | `src/uix/terra` (headless primitives) | +| `$terra/utils` | `src/uix/terra/utils/sound.svelte.ts` | +| `$terra/system` | `src/uix/terra/system/exports.ts` | +| `$terra/external/dates` | `src/uix/terra/external/dates/sound.svelte.ts` | +| `$lib` | `src/lib` | +| `$lib/util/dias` | `src/lib/util/dias` (date/time library — canonical) | ## Vitest Two-Project Structure `vite.config.ts` defines two test projects: + - **client**: Browser tests via Playwright for `*.svelte.{test,spec}.{js,ts}` - **server**: Node environment for `*.{test,spec}.{js,ts}` (excludes svelte tests) @@ -66,6 +67,7 @@ The dependency flows one way: `air` → `terra`. Terra never imports from air. Headless primitive library. Each component follows `Element.SubElement` pattern (`Dialog.Root`, `Dialog.Trigger`, `Dialog.Content`). **Key patterns:** + - Thin `.svelte` wrappers + logic in `*.svelte.ts` State classes - State classes use `static create()` for context registration, `createComposed()` for internal composition without context - Reactive system: `Active`, `State`, `readableActive()`, `writableActive()` — values travel as boxes with `.current` @@ -75,6 +77,7 @@ Headless primitive library. Each component follows `Element.SubElement` pattern - `child` snippet replaces the rendered node; `children` fills the interior **Structure:** + - `[component]/components/` — Svelte wrappers - `[component]/types.ts` — public props - `[component]/*.svelte.ts` — State classes @@ -92,21 +95,25 @@ Headless primitive library. Each component follows `Element.SubElement` pattern Visual layer built on terra. Consumes terra's public anatomy and `data-*` attrs — never internal classes or incidental DOM structure. **Three-level token system:** + 1. **Primitive tokens** (`tokens/primitive.css`) — absolute scale values, never used directly by components 2. **Semantic tokens** (`tokens/semantic.css`) — theme-aware, switch via `[data-theme]` 3. **Component tokens** (`tokens/components/*.css`) — scoped per component (`--air-button-*`), the only level component CSS writes **Token naming:** + - Public: `--air-{component}-*` (stable, overridable) - Private: `--_air-{component}-*` (implementation detail, may change) **Contracts** (`contracts/`): + - `contract.css` — minimum semantic tokens any theme must implement - `contract-categorical.css` — optional categorical color families - `contract-primitives.css` — optional advanced 12-step scales - `contracts/components/*.css` — per-component public token API **CSS rules:** + - Tokens and `data-*` selectors must be global CSS (imported `.css` files), not ` diff --git a/src/routes/test/soma/command/+page.svelte b/src/routes/test/soma/command/+page.svelte new file mode 100644 index 000000000..c4dff3f4a --- /dev/null +++ b/src/routes/test/soma/command/+page.svelte @@ -0,0 +1,766 @@ + + + + Command · Soma + + +
+
+

Soma · Overlay

+

Command

+

+ A command palette with fuzzy scoring, vim-style navigation + (Ctrl+N/Ctrl+P), grid mode, keyword aliases, empty/loading states, + groups that auto-hide when empty, and link items. +

+
+ + +
+
+

1 · App commands

+ fuzzy · vim · keywords +
+ + +
+ + + +
No commands found.
+
+ + {#each groups as groupName (groupName)} + + + {groupName} + + + {#each commands.filter((c) => c.group === groupName) as c (c.id)} + + + {c.label} + {c.group} + + {/each} + + + {/each} + + + + + Links + + + + + Open documentation + svelte.dev + + + +
+ +
+
+ +

+ last action: {lastAction ?? '—'} · highlighted: + {selected || '—'} +

+
+ + +
+
+

2 · Grid mode — emoji picker

+ columns=10 · Arrow keys in 2D +
+ + +
+ + +
+ +
Nothing here.
+
+ {#each emojis as e (e)} + + {e} + + {/each} +
+
+
+
+ +

picked: {picked || '—'}

+
+ + +
+
+

3 · Async loading + empty

+ Loading · indeterminate progress +
+ +
+ + +
+ + +
+ + + {#if loading} + +
+ + Fetching results… +
+
+ {/if} + + {#if asyncItems.length > 0} + + + Results + + + {#each asyncItems as item (item.id)} + + {item.label} + + {/each} + + + {:else if !loading} + +
Press "Load results" to fetch data.
+
+ {/if} +
+
+
+
+ + +
+
+

4 · External filter (shouldFilter=false)

+ consumer controls items +
+

+ When shouldFilter=false the palette doesn't run the scorer — the list you + provide is shown as-is. Useful when results come from a remote API. +

+ + +
+ + + {#each commands.slice(0, 5) as c (c.id)} + + {c.label} + + {/each} + +
+
+
+ + +
+
+

5 · Command.Dialog — composition helper

+ Dialog + Command in one +
+

+ Command.Dialog packages a Dialog with Overlay + Content and a Command + palette inside. All Command props are pass-through; the Dialog opens/closes via + bind:open. +

+ +
+ + last: {lastDialogAction ?? '—'} +
+ + +
+ + + +
No results.
+
+ {#each groups as groupName (groupName)} + + + {groupName} + + + {#each commands.filter((c) => c.group === groupName) as c (c.id)} + + + {c.label} + {c.group} + + {/each} + + + {/each} +
+
+
+
+ + +
+
+

6 · Imperative API — snippet props

+ next · prev · select · setSearch +
+

+ The Provider's children snippet receives + {`{ value, search, visible, isEmpty, next, prev, select, setValue, setSearch, clearSearch, isVisible, isSelected }`}. + Use it to drive the palette from outside the default UI. +

+ + + {#snippet children({ visible, isEmpty, next, prev, first, last, clearSearch })} +
+ + + +
Nothing matches "{apiSearch}".
+
+ {#each commands as c (c.id)} + + {c.label} + {c.group} + + {/each} +
+ +
+ {/snippet} +
+
+
+ + diff --git a/src/routes/test/soma/range-calendar/+page.svelte b/src/routes/test/soma/range-calendar/+page.svelte new file mode 100644 index 000000000..a06713096 --- /dev/null +++ b/src/routes/test/soma/range-calendar/+page.svelte @@ -0,0 +1,558 @@ + + + + RangeCalendar · Soma + + +
+

RangeCalendar

+

+ Date-range picker. Beyond parity with ark/bits/radix/react-aria: live + hover preview, minDays/maxDays, + allowSingleDay, ISO week numbers, holidays, weekend flag, and + navigation announcements. +

+ +
+

1 · Basic range (click start, click end)

+

Hover between clicks shows live preview.

+ + {#snippet children({ months, weekdays })} + + ‹ + + › + + {#each months as m (m.value.toString())} + + + + {#each weekdays as wd, i (i)} + {wd} + {/each} + + + + {#each m.weeks as week (week[0].toString())} + + {#each week as date (date.toString())} + + + + {/each} + + {/each} + + + {/each} + {/snippet} + +

Range: {fmt(range1)}

+
+ +
+

2 · Two months + paged navigation + MonthSelect / YearSelect

+ + {#snippet children({ months, weekdays })} + + ‹ + + + › + +
+ {#each months as m (m.value.toString())} + + + + {#each weekdays as wd, i (i)} + {wd} + {/each} + + + + {#each m.weeks as week (week[0].toString())} + + {#each week as date (date.toString())} + + + + {/each} + + {/each} + + + {/each} +
+ {/snippet} +
+

Range: {fmt(range2)}

+
+ +
+

3 · Fixed weeks + ISO week numbers

+ + {#snippet children({ months, weekdays })} + + ‹ + + › + + {#each months as m (m.value.toString())} + + + + Wk + {#each weekdays as wd, i (i)} + {wd} + {/each} + + + + {#each m.weeks as week (week[0].toString())} + + {#snippet children({ weekNumber })} + {weekNumber} + {#each week as date (date.toString())} + + + + {/each} + {/snippet} + + {/each} + + + {/each} + {/snippet} + +
+ +
+

4 · Holidays

+ + {#snippet children({ months, weekdays })} + + ‹ + + › + + {#each months as m (m.value.toString())} + + + + {#each weekdays as wd, i (i)} + {wd} + {/each} + + + + {#each m.weeks as week (week[0].toString())} + + {#each week as date (date.toString())} + + + + {/each} + + {/each} + + + {/each} + {/snippet} + +
+ +
+

5 · minDays 2 + maxDays 14

+

Single-day ranges rejected; ranges longer than 14 days get clamped to 14.

+ + {#snippet children({ months, weekdays })} + + ‹ + + › + + {#each months as m (m.value.toString())} + + + + {#each weekdays as wd, i (i)} + {wd} + {/each} + + + + {#each m.weeks as week (week[0].toString())} + + {#each week as date (date.toString())} + + + + {/each} + + {/each} + + + {/each} + {/snippet} + +

Range: {fmt(range5)}

+
+ +
+

6 · allowSingleDay = false

+

Same-day second click is rejected.

+ + {#snippet children({ months, weekdays })} + + ‹ + + › + + {#each months as m (m.value.toString())} + + + + {#each weekdays as wd, i (i)} + {wd} + {/each} + + + + {#each m.weeks as week (week[0].toString())} + + {#each week as date (date.toString())} + + + + {/each} + + {/each} + + + {/each} + {/snippet} + +
+ +
+

7 · Read-only

+ + {#snippet children({ months, weekdays })} + + ‹ + + › + + {#each months as m (m.value.toString())} + + + + {#each weekdays as wd, i (i)} + {wd} + {/each} + + + + {#each m.weeks as week (week[0].toString())} + + {#each week as date (date.toString())} + + + + {/each} + + {/each} + + + {/each} + {/snippet} + +
+
+ + diff --git a/src/routes/test/time-field/+page.svelte b/src/routes/test/time-field/+page.svelte index 2c6000ce4..fdcb0fcec 100644 --- a/src/routes/test/time-field/+page.svelte +++ b/src/routes/test/time-field/+page.svelte @@ -2,23 +2,23 @@ import { TestLayout, StateItem, PropsTable, DemoSection } from '../_components'; import { Terra, TimeField } from '$uix/terra'; import { createTerraTranslator, createTerraDateTimeFormatter } from '$uix/terra/system'; - import { Time, type TimeValue, type HourCycle } from '$lib/util/dates'; + import { Time, type TimeValue, type HourCycle } from '$lib/util/dias'; const LOCALES = [ { id: 'es-ES', label: 'es-ES' }, { id: 'en-US', label: 'en-US' }, { id: 'en-GB', label: 'en-GB' }, { id: 'de-DE', label: 'de-DE' }, - { id: 'ja-JP', label: 'ja-JP' }, + { id: 'ja-JP', label: 'ja-JP' } ]; let locale = $state('es-ES'); const translator = createTerraTranslator({ - locale: () => locale, + locale: () => locale }); let timeFormat = $state(24); const dateTimeFormatter = createTerraDateTimeFormatter({ - timeFormat: () => timeFormat, + timeFormat: () => timeFormat }); let value1 = $state