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:
81
src/lib/ui/LumotiaButton.svelte
Normal file
81
src/lib/ui/LumotiaButton.svelte
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
// v0.2 Phase 5 primitive — native <button> + token-driven styling.
|
||||
// Replaces the ~60 ad-hoc button blocks scattered across pages with
|
||||
// one grammar (primary | secondary | tertiary | destructive × sm | md | lg).
|
||||
// Bypasses Bits UI deliberately: <button> is already accessible and
|
||||
// the headless library would only add weight here.
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
type Variant = "primary" | "secondary" | "tertiary" | "destructive";
|
||||
type Size = "sm" | "md" | "lg";
|
||||
|
||||
interface Props {
|
||||
variant?: Variant;
|
||||
size?: Size;
|
||||
type?: "button" | "submit" | "reset";
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
fullWidth?: boolean;
|
||||
title?: string;
|
||||
ariaLabel?: string;
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
children?: Snippet;
|
||||
classes?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
variant = "secondary" as Variant,
|
||||
size = "md" as Size,
|
||||
type = "button",
|
||||
disabled = false,
|
||||
loading = false,
|
||||
fullWidth = false,
|
||||
title,
|
||||
ariaLabel,
|
||||
onclick,
|
||||
children,
|
||||
classes = "",
|
||||
}: Props = $props();
|
||||
|
||||
const sizeClass: Record<Size, string> = {
|
||||
sm: "btn-sm",
|
||||
md: "btn-md",
|
||||
lg: "btn-lg",
|
||||
};
|
||||
|
||||
// Variant palette uses semantic tokens; primary fills with --color-accent
|
||||
// (amber/copper) and pairs with .btn-filled-text for AA. Destructive
|
||||
// mirrors primary on --color-danger.
|
||||
const variantClass: Record<Variant, string> = {
|
||||
primary:
|
||||
"bg-accent btn-filled-text font-medium hover:bg-accent-hover shadow-[var(--shadow-accent-pill)]",
|
||||
secondary:
|
||||
"bg-bg-elevated text-text border border-border-subtle hover:bg-hover",
|
||||
tertiary:
|
||||
"bg-transparent text-text-secondary hover:bg-hover hover:text-text",
|
||||
destructive:
|
||||
"bg-danger btn-filled-text font-medium hover:bg-danger/90",
|
||||
};
|
||||
|
||||
const busy = $derived(loading || disabled);
|
||||
</script>
|
||||
|
||||
<button
|
||||
{type}
|
||||
class="rounded-lg font-medium select-none disabled:opacity-50 disabled:cursor-not-allowed
|
||||
{sizeClass[size]}
|
||||
{variantClass[variant]}
|
||||
{fullWidth ? 'w-full' : ''}
|
||||
{classes}"
|
||||
style="transition-duration: var(--duration-ui)"
|
||||
disabled={busy}
|
||||
aria-busy={loading ? "true" : undefined}
|
||||
aria-label={ariaLabel}
|
||||
{title}
|
||||
{onclick}
|
||||
>
|
||||
{#if loading}
|
||||
<span class="inline-block animate-spin" aria-hidden="true">⟳</span>
|
||||
{/if}
|
||||
{#if children}{@render children()}{/if}
|
||||
</button>
|
||||
Reference in New Issue
Block a user