From c60f0aa5a5016c1cd7510f58071f203b2144f475 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 15 May 2026 08:47:53 +0100 Subject: [PATCH] v0.2 Phase 5: 11 primitives + gated design-system-v2 preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Custom-styled primitives (no headless dep): LumotiaButton — primary/secondary/tertiary/destructive × sm/md/lg LumotiaIconButton — square icon-only; ghost/filled/destructive LumotiaNotice — info/caution/danger/success inline notice LumotiaProgress — native + token theming LumotiaField — plain + Formsnap modes share the same markup Bits UI 2.18.1 wrappers (warm-brutalist styling): LumotiaSelect — single-select, options=[] LumotiaCombobox — searchable; one-way inputValue + oninput LumotiaDialog — controlled open; closable + footer snippet LumotiaTabs — orchestrates List/Trigger/Content from a tabs array LumotiaTooltip — wraps Provider + Root + Trigger + Content LumotiaMenu — DropdownMenu items=[] with destructive variant design-system-v2 preview route: src/routes/design-system-v2/+page.ts gates with VITE_LUMOTIA_DESIGN_SYSTEM_V2=1. Without the flag the load() throws 404 — route-level gate, not nav- hidden. Run via VITE_LUMOTIA_DESIGN_SYSTEM_V2=1 npm run dev:frontend to see the showcase. Browser-mode component test: src/lib/ui/LumotiaButton.browser.test.ts. Covers render, click, and disabled-blocks-click. Validates that vitest-browser-svelte + the @vitest/browser-playwright provider land Phase 1's tooling contract end-to-end. 3/3 passing in Chromium. Type fix: LumotiaIconButton and LumotiaMenu accept icon: any so lucide-svelte's legacy SvelteComponentTyped shape composes with our Svelte 5 wrappers without forcing a // @ts-nocheck escape hatch on every call site. Tightens to Component<…> once lucide-svelte ships a Svelte 5 build. Type fix: LumotiaCombobox honours Bits UI 2.x Combobox.Root's one-way inputValue contract. The wrapper drops bind:inputValue and exposes an oninput callback so caller-owned filter pipelines (HistoryPage FTS5, ModelDownloader) can drive options upstream. Phase 5 per-page gate green: npm run check (0/0/5700 files), npm test, npm run test:browser (3/3 in Chromium), npm run test:e2e (16/16), guard-no-skeleton clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/release/v0.2-frontend-overhaul.md | 24 +-- src/lib/ui/LumotiaButton.browser.test.ts | 41 +++++ src/lib/ui/LumotiaButton.svelte | 81 +++++++++ src/lib/ui/LumotiaCombobox.svelte | 103 ++++++++++++ src/lib/ui/LumotiaDialog.svelte | 70 ++++++++ src/lib/ui/LumotiaField.svelte | 127 ++++++++++++++ src/lib/ui/LumotiaIconButton.svelte | 64 +++++++ src/lib/ui/LumotiaMenu.svelte | 67 ++++++++ src/lib/ui/LumotiaNotice.svelte | 92 ++++++++++ src/lib/ui/LumotiaProgress.svelte | 69 ++++++++ src/lib/ui/LumotiaSelect.svelte | 68 ++++++++ src/lib/ui/LumotiaTabs.svelte | 53 ++++++ src/lib/ui/LumotiaTooltip.svelte | 41 +++++ src/routes/design-system-v2/+page.svelte | 206 +++++++++++++++++++++++ src/routes/design-system-v2/+page.ts | 27 +++ 15 files changed, 1121 insertions(+), 12 deletions(-) create mode 100644 src/lib/ui/LumotiaButton.browser.test.ts create mode 100644 src/lib/ui/LumotiaButton.svelte create mode 100644 src/lib/ui/LumotiaCombobox.svelte create mode 100644 src/lib/ui/LumotiaDialog.svelte create mode 100644 src/lib/ui/LumotiaField.svelte create mode 100644 src/lib/ui/LumotiaIconButton.svelte create mode 100644 src/lib/ui/LumotiaMenu.svelte create mode 100644 src/lib/ui/LumotiaNotice.svelte create mode 100644 src/lib/ui/LumotiaProgress.svelte create mode 100644 src/lib/ui/LumotiaSelect.svelte create mode 100644 src/lib/ui/LumotiaTabs.svelte create mode 100644 src/lib/ui/LumotiaTooltip.svelte create mode 100644 src/routes/design-system-v2/+page.svelte create mode 100644 src/routes/design-system-v2/+page.ts diff --git a/docs/release/v0.2-frontend-overhaul.md b/docs/release/v0.2-frontend-overhaul.md index abb7c79..601ed34 100644 --- a/docs/release/v0.2-frontend-overhaul.md +++ b/docs/release/v0.2-frontend-overhaul.md @@ -96,17 +96,17 @@ Filled progressively during Phases 4–5. Wrappers live in `src/lib/ui/`; bespok | Wrapper | Implementation | Status | |---|---|---| -| `LumotiaButton` | native + `.btn-*` class system; primary / secondary / tertiary / destructive | pending | -| `LumotiaIconButton` | native + lucide-svelte; standard sizing + tooltip slot | pending | -| `LumotiaNotice` | custom; info / caution / danger / success inline notices | pending | -| `LumotiaProgress` | native `` + tokens; fall back to `role="progressbar"` only if native styling proves inconsistent | pending | -| `LumotiaField` | native + Formsnap Field integration; text + textarea + label + error wrapper | pending | -| `LumotiaSelect` | Bits UI Select | pending | -| `LumotiaCombobox` | Bits UI Combobox | pending | -| `LumotiaDialog` | Bits UI Dialog | pending | -| `LumotiaTabs` | Bits UI Tabs | pending | -| `LumotiaTooltip` | Bits UI Tooltip | pending | -| `LumotiaMenu` / `LumotiaPopover` | Bits UI DropdownMenu / Popover | pending | +| `LumotiaButton` | native + `.btn-*` class system; primary / secondary / tertiary / destructive | ✅ Phase 5 | +| `LumotiaIconButton` | native + lucide-svelte; standard sizing + tooltip slot | ✅ Phase 5 | +| `LumotiaNotice` | custom; info / caution / danger / success inline notices | ✅ Phase 5 | +| `LumotiaProgress` | native `` + tokens; fall back to `role="progressbar"` only if native styling proves inconsistent | ✅ Phase 5 (native `` held up; no fallback needed) | +| `LumotiaField` | native + Formsnap Field integration; text + textarea + label + error wrapper | ✅ Phase 5 | +| `LumotiaSelect` | Bits UI Select | ✅ Phase 5 | +| `LumotiaCombobox` | Bits UI Combobox | ✅ Phase 5 | +| `LumotiaDialog` | Bits UI Dialog | ✅ Phase 5 | +| `LumotiaTabs` | Bits UI Tabs | ✅ Phase 5 | +| `LumotiaTooltip` | Bits UI Tooltip | ✅ Phase 5 | +| `LumotiaMenu` (DropdownMenu) | Bits UI DropdownMenu | ✅ Phase 5 (Popover deferred — DropdownMenu covers the v0.2 use cases) | ### 6.3 Bespoke surfaces — DO NOT wrap, DO NOT rewrite @@ -202,7 +202,7 @@ Filled during execution. Each entry: phase #, date, what landed, what's next. | 2 | ✅ complete | bits-ui 2.18.1, formsnap 2.0.1, sveltekit-superforms 2.30.1, zod 4.4.3, @internationalized/date 3.12.1. `npm audit signatures` 273 verified + 93 attestations. | | 3 | ✅ complete | New tokens `--color-caution`, `--color-info`, `--color-accent-environment` (dark + light), `--color-warning` aliased to `var(--color-caution)`. KI-05 resolved: `theme` dropped from SettingsState type + defaults, all four route-layout migration `$effect`s deleted, two SettingsPage SegmentedButton bindings repointed to `preferences.theme` via Svelte 5 function bindings, one-shot legacy-theme migration on first mount strips the field after copying. Gate green (check 0/0, vitest 13/13, e2e 16/16). | | 4 | ✅ complete | Six wrapper aliases under `src/lib/ui/`: Lumotia{Card,StatusPill,Toggle,SettingsGroup,EmptyState,PostCaptureCard}. Same prop APIs, $bindable forwarded for Toggle. Underlying `src/lib/components/*.svelte` untouched. | -| 5 | pending | New primitives + design-system-v2 preview | +| 5 | ✅ complete | 11 primitives shipped under `src/lib/ui/`. `design-system-v2` preview route gated behind `VITE_LUMOTIA_DESIGN_SYSTEM_V2=1` (route-level 404 via `+page.ts` load, not nav-hidden). Browser-mode component test (LumotiaButton): 3/3 passing in Chromium. Gate green (check 0/0/5700 files, vitest 0/0, test:browser 3/3, e2e 16/16). | | 6 | pending | Shell split | | 7.1 ShutdownRitualPage | pending | | | 7.2 FilesPage | pending | | diff --git a/src/lib/ui/LumotiaButton.browser.test.ts b/src/lib/ui/LumotiaButton.browser.test.ts new file mode 100644 index 0000000..fb1d165 --- /dev/null +++ b/src/lib/ui/LumotiaButton.browser.test.ts @@ -0,0 +1,41 @@ +// v0.2 Phase 5 — minimal browser-mode component test for LumotiaButton. +// Verifies the primary contract: a click on the rendered button fires +// the onclick handler the parent passed. Covers the most common Phase 7 +// migration path (replacing ad-hoc diff --git a/src/lib/ui/LumotiaCombobox.svelte b/src/lib/ui/LumotiaCombobox.svelte new file mode 100644 index 0000000..c554059 --- /dev/null +++ b/src/lib/ui/LumotiaCombobox.svelte @@ -0,0 +1,103 @@ + + + +
+ +
+ {#if allowClear && (inputValue || value)} + + {/if} + + + +
+
+ + + {#each options as opt (opt.value)} + + {#snippet children({ selected })} + + {#if selected}{/if} + + {opt.label} + {/snippet} + + {/each} + {#if options.length === 0} +

No matches

+ {/if} +
+
+
diff --git a/src/lib/ui/LumotiaDialog.svelte b/src/lib/ui/LumotiaDialog.svelte new file mode 100644 index 0000000..ba19676 --- /dev/null +++ b/src/lib/ui/LumotiaDialog.svelte @@ -0,0 +1,70 @@ + + + + + + + {#if title || description} +
+ {#if title} + {title} + {/if} + {#if description} + {description} + {/if} +
+ {/if} + {#if children} +
+ {@render children()} +
+ {/if} + {#if footer} +
+ {@render footer()} +
+ {/if} + + + +
+
+
diff --git a/src/lib/ui/LumotiaField.svelte b/src/lib/ui/LumotiaField.svelte new file mode 100644 index 0000000..be3c144 --- /dev/null +++ b/src/lib/ui/LumotiaField.svelte @@ -0,0 +1,127 @@ + + +{#if form && name} + + + {#snippet children({ props })} +
+ {label} + {#if description} +

{description}

+ {/if} + {#if type === "textarea"} + + {:else} + + {/if} + +
+ {/snippet} +
+
+{:else} +
+ + {#if description} +

{description}

+ {/if} + {#if type === "textarea"} + + {:else} + + {/if} + {#if error} +

{error}

+ {/if} + {#if children} +
{@render children()}
+ {/if} +
+{/if} diff --git a/src/lib/ui/LumotiaIconButton.svelte b/src/lib/ui/LumotiaIconButton.svelte new file mode 100644 index 0000000..729985f --- /dev/null +++ b/src/lib/ui/LumotiaIconButton.svelte @@ -0,0 +1,64 @@ + + + diff --git a/src/lib/ui/LumotiaMenu.svelte b/src/lib/ui/LumotiaMenu.svelte new file mode 100644 index 0000000..572d374 --- /dev/null +++ b/src/lib/ui/LumotiaMenu.svelte @@ -0,0 +1,67 @@ + + + + + {@render trigger()} + + + + {#each items as item, i (i)} + {#if item.kind === "separator"} + + {:else} + + {#if item.icon} + {@const I = item.icon} + + {/if} + {item.label} + + {/if} + {/each} + + + diff --git a/src/lib/ui/LumotiaNotice.svelte b/src/lib/ui/LumotiaNotice.svelte new file mode 100644 index 0000000..bbbbac4 --- /dev/null +++ b/src/lib/ui/LumotiaNotice.svelte @@ -0,0 +1,92 @@ + + +
+
diff --git a/src/lib/ui/LumotiaProgress.svelte b/src/lib/ui/LumotiaProgress.svelte new file mode 100644 index 0000000..be0853a --- /dev/null +++ b/src/lib/ui/LumotiaProgress.svelte @@ -0,0 +1,69 @@ + + +{#if label} +
+ {label} + {#if showValue} + {value} / {max} + {/if} +
+{/if} +{value} / {max} + + diff --git a/src/lib/ui/LumotiaSelect.svelte b/src/lib/ui/LumotiaSelect.svelte new file mode 100644 index 0000000..4280ea5 --- /dev/null +++ b/src/lib/ui/LumotiaSelect.svelte @@ -0,0 +1,68 @@ + + + + + {selectedLabel} + + + + {#each options as opt (opt.value)} + + {#snippet children({ selected })} + + {#if selected}{/if} + + {opt.label} + {/snippet} + + {/each} + + + diff --git a/src/lib/ui/LumotiaTabs.svelte b/src/lib/ui/LumotiaTabs.svelte new file mode 100644 index 0000000..4479a51 --- /dev/null +++ b/src/lib/ui/LumotiaTabs.svelte @@ -0,0 +1,53 @@ + + + + + {#each tabs as tab (tab.value)} + + {tab.label} + + {/each} + + {#each tabs as tab (tab.value)} + + {#if panel}{@render panel({ value: tab.value })}{/if} + + {/each} + diff --git a/src/lib/ui/LumotiaTooltip.svelte b/src/lib/ui/LumotiaTooltip.svelte new file mode 100644 index 0000000..ba55f21 --- /dev/null +++ b/src/lib/ui/LumotiaTooltip.svelte @@ -0,0 +1,41 @@ + + + + + + {#if trigger}{@render trigger()}{/if} + + + + {text} + + + + diff --git a/src/routes/design-system-v2/+page.svelte b/src/routes/design-system-v2/+page.svelte new file mode 100644 index 0000000..976638f --- /dev/null +++ b/src/routes/design-system-v2/+page.svelte @@ -0,0 +1,206 @@ + + +Lumotia design system v0.2 preview + +
+
+

v0.2 primitives

+

+ Warm brutalist notebook cockpit. Calm, local, tactile, low-noise. Not SaaS. +

+
+ +
+

LumotiaButton

+
+ Record + Cancel + Skip + Delete + Small + Large + Loading + Disabled +
+
+ +
+

LumotiaIconButton

+
+ + + + + +
+
+ +
+

LumotiaNotice

+
+ Using default input device. + Download the Whisper-Base model before recording. + Check the input group permissions on /dev/input/event*. + Transcript copied to clipboard. + {}}>Dismissible variant. +
+
+ +
+

LumotiaProgress

+
+ + + +
+ progressValue = Math.max(0, progressValue - 10)}>-10 + progressValue = Math.min(100, progressValue + 10)}>+10 +
+
+
+ +
+

LumotiaField

+
+ + + +
+
+ +
+

LumotiaSelect

+
+ + selected: {selectValue} +
+
+ +
+

LumotiaCombobox

+
+ comboboxInput = e.currentTarget.value} + /> +
+
+ +
+

LumotiaTabs

+ + {#snippet panel({ value })} + +

Panel: {value}

+
+ {/snippet} +
+
+ +
+

LumotiaTooltip

+
+ + {#snippet trigger()} + Hover me + {/snippet} + +
+
+ +
+

LumotiaMenu

+ {} }, + { value: "export", label: "Export", icon: FileText, onSelect: () => {} }, + { kind: "separator" as const }, + { value: "delete", label: "Delete", icon: Trash2, destructive: true, onSelect: () => {} }, + ]} + > + {#snippet trigger()} + More actions ⌄ + {/snippet} + +
+ +
+

LumotiaDialog

+ dialogOpen = true}>Open dialog + +

Last opened five minutes ago.

+ {#snippet footer()} + dialogOpen = false}>Cancel + dialogOpen = false}>Delete + {/snippet} +
+
+ +
+

Wrappers — Card / StatusPill / Toggle / EmptyState

+
+

Card body

+ + + +
+
+ + +
+ +
+
diff --git a/src/routes/design-system-v2/+page.ts b/src/routes/design-system-v2/+page.ts new file mode 100644 index 0000000..c6baff2 --- /dev/null +++ b/src/routes/design-system-v2/+page.ts @@ -0,0 +1,27 @@ +import { error } from "@sveltejs/kit"; + +// v0.2 Phase 5 — route-level gate. +// +// design-system-v2 is a developer-only preview surface for the new +// Lumotia primitives. It MUST NOT be reachable from production +// builds: a hidden-but-routable URL is still a URL. This load +// function throws 404 unless the build was started with +// VITE_LUMOTIA_DESIGN_SYSTEM_V2=1, which means: +// +// - `npm run dev:frontend` exposes the route only when the env +// var is set in the developer's shell +// - the static `npm run build` skips the route entirely because +// `prerender = false` keeps the static adapter from running +// `load`, and the SPA fallback returns the SvelteKit error +// page on direct navigation +// +// To open the preview locally: +// VITE_LUMOTIA_DESIGN_SYSTEM_V2=1 npm run dev:frontend +export const prerender = false; + +export function load() { + if (import.meta.env.VITE_LUMOTIA_DESIGN_SYSTEM_V2 !== "1") { + throw error(404, "Not Found"); + } + return {}; +}