v0.2 Phase 5: 11 primitives + gated design-system-v2 preview
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 <progress> + 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) <noreply@anthropic.com>
This commit is contained in:
92
src/lib/ui/LumotiaNotice.svelte
Normal file
92
src/lib/ui/LumotiaNotice.svelte
Normal file
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
// v0.2 Phase 5 primitive — inline notice block. Replaces the ~10
|
||||
// ad-hoc <div class="bg-{danger,warning,success}/10 …"> patterns
|
||||
// scattered across pages. Four tones map to the semantic tokens
|
||||
// introduced in Phase 3:
|
||||
// info — soft blue-grey (--color-info)
|
||||
// caution — tuned amber (--color-caution)
|
||||
// danger — tuned red (--color-danger)
|
||||
// success — moss (--color-success)
|
||||
// The icon and aria-role are tone-aware. Dismissible variant is
|
||||
// opt-in for transient toasts inline; persistent notices stay
|
||||
// visible.
|
||||
import { Info, AlertTriangle, AlertOctagon, CheckCircle, X } from "lucide-svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
type Tone = "info" | "caution" | "danger" | "success";
|
||||
|
||||
interface Props {
|
||||
tone?: Tone;
|
||||
title?: string;
|
||||
dismissible?: boolean;
|
||||
onDismiss?: () => void;
|
||||
children?: Snippet;
|
||||
classes?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
tone = "info" as Tone,
|
||||
title,
|
||||
dismissible = false,
|
||||
onDismiss,
|
||||
children,
|
||||
classes = "",
|
||||
}: Props = $props();
|
||||
|
||||
const palette: Record<Tone, { bg: string; border: string; fg: string; icon: typeof Info }> = {
|
||||
info: {
|
||||
bg: "bg-info/10",
|
||||
border: "border-info/30",
|
||||
fg: "text-info",
|
||||
icon: Info,
|
||||
},
|
||||
caution: {
|
||||
bg: "bg-caution/10",
|
||||
border: "border-caution/30",
|
||||
fg: "text-caution",
|
||||
icon: AlertTriangle,
|
||||
},
|
||||
danger: {
|
||||
bg: "bg-danger/10",
|
||||
border: "border-danger/30",
|
||||
fg: "text-danger",
|
||||
icon: AlertOctagon,
|
||||
},
|
||||
success: {
|
||||
bg: "bg-success/10",
|
||||
border: "border-success/30",
|
||||
fg: "text-success",
|
||||
icon: CheckCircle,
|
||||
},
|
||||
};
|
||||
|
||||
// Danger gets role="alert" so screen readers announce immediately;
|
||||
// the others use the less-interrupting role="status".
|
||||
const ariaRole = $derived(tone === "danger" ? "alert" : "status");
|
||||
const tonePal = $derived(palette[tone]);
|
||||
const ToneIcon = $derived(tonePal.icon);
|
||||
</script>
|
||||
|
||||
<div
|
||||
role={ariaRole}
|
||||
class="px-4 py-3 rounded-lg border text-[13px] flex items-start gap-2.5
|
||||
{tonePal.bg} {tonePal.border} {classes}"
|
||||
>
|
||||
<ToneIcon size={16} class="mt-0.5 shrink-0 {tonePal.fg}" aria-hidden="true" />
|
||||
<div class="flex-1 min-w-0 leading-relaxed text-text">
|
||||
{#if title}
|
||||
<p class="font-semibold text-text mb-1">{title}</p>
|
||||
{/if}
|
||||
{#if children}{@render children()}{/if}
|
||||
</div>
|
||||
{#if dismissible}
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 text-text-tertiary hover:text-text size-5 flex items-center justify-center rounded"
|
||||
aria-label="Dismiss notice"
|
||||
onclick={onDismiss}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user