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>
69 lines
2.4 KiB
Svelte
69 lines
2.4 KiB
Svelte
<script lang="ts">
|
|
// v0.2 Phase 5 primitive — single-select dropdown over Bits UI Select.
|
|
// High-level API: `value` (bindable) + `options` ([{value, label}]).
|
|
// For multiselect, a separate LumotiaMultiSelect primitive can land
|
|
// later — keeping the v0.2 surface narrow.
|
|
import { Select } from "bits-ui";
|
|
import { Check, ChevronDown } from "lucide-svelte";
|
|
|
|
type Option = { value: string; label: string; disabled?: boolean };
|
|
|
|
interface Props {
|
|
value: string;
|
|
options: Option[];
|
|
placeholder?: string;
|
|
disabled?: boolean;
|
|
label?: string;
|
|
name?: string; // hidden form input for native form submit
|
|
onValueChange?: (value: string) => void;
|
|
triggerClass?: string;
|
|
}
|
|
|
|
let {
|
|
value = $bindable(""),
|
|
options,
|
|
placeholder = "Select…",
|
|
disabled = false,
|
|
label,
|
|
name,
|
|
onValueChange,
|
|
triggerClass = "",
|
|
}: Props = $props();
|
|
|
|
const selectedLabel = $derived(
|
|
options.find((o) => o.value === value)?.label ?? placeholder,
|
|
);
|
|
</script>
|
|
|
|
<Select.Root type="single" bind:value {disabled} onValueChange={onValueChange} {name}>
|
|
<Select.Trigger
|
|
class="lumotia-select-trigger inline-flex items-center justify-between gap-2 rounded-md bg-bg-input border border-border-subtle px-3 py-2 text-[13px] text-text min-w-[160px] focus:outline-none focus:border-accent disabled:opacity-50 {triggerClass}"
|
|
aria-label={label}
|
|
>
|
|
<span class="truncate">{selectedLabel}</span>
|
|
<ChevronDown size={14} class="text-text-tertiary shrink-0" aria-hidden="true" />
|
|
</Select.Trigger>
|
|
<Select.Portal>
|
|
<Select.Content
|
|
class="z-50 rounded-md bg-bg-card border border-border-subtle shadow-xl py-1 min-w-[160px] max-h-[280px] overflow-auto"
|
|
sideOffset={4}
|
|
>
|
|
{#each options as opt (opt.value)}
|
|
<Select.Item
|
|
value={opt.value}
|
|
label={opt.label}
|
|
disabled={opt.disabled}
|
|
class="px-3 py-1.5 text-[13px] text-text data-[highlighted]:bg-hover data-[disabled]:opacity-50 cursor-pointer flex items-center gap-2"
|
|
>
|
|
{#snippet children({ selected })}
|
|
<span class="size-4 flex items-center justify-center shrink-0">
|
|
{#if selected}<Check size={14} class="text-accent" />{/if}
|
|
</span>
|
|
<span class="truncate">{opt.label}</span>
|
|
{/snippet}
|
|
</Select.Item>
|
|
{/each}
|
|
</Select.Content>
|
|
</Select.Portal>
|
|
</Select.Root>
|