Files
Lumotia/src/lib/ui/LumotiaTabs.svelte
Jake c60f0aa5a5 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>
2026-05-15 08:47:53 +01:00

54 lines
1.7 KiB
Svelte

<script lang="ts">
// v0.2 Phase 5 primitive — Lumotia-tinted Bits UI Tabs.
// High-level API: `value` (bindable), `tabs` (list of {value, label})
// and a `panel` snippet rendered per-tab. Consumers can also drop to
// the explicit <Tabs.Content> form if they need different markup per
// tab — re-exported below.
import { Tabs } from "bits-ui";
import type { Snippet } from "svelte";
type Tab = { value: string; label: string; disabled?: boolean };
interface Props {
value: string;
tabs: Tab[];
onValueChange?: (value: string) => void;
panel?: Snippet<[{ value: string }]>;
listClass?: string;
}
let {
value = $bindable(""),
tabs,
onValueChange,
panel,
listClass = "",
}: Props = $props();
</script>
<Tabs.Root bind:value onValueChange={onValueChange}>
<Tabs.List
class="lumotia-tabs-list inline-flex bg-bg-elevated rounded-[10px] p-[3px] gap-[2px] {listClass}"
>
{#each tabs as tab (tab.value)}
<Tabs.Trigger
value={tab.value}
disabled={tab.disabled}
class="rounded-lg px-3.5 py-[6px] text-[12px] font-medium text-text-secondary
hover:text-text hover:bg-hover
data-[state=active]:bg-accent data-[state=active]:btn-filled-text data-[state=active]:shadow-[var(--shadow-accent-pill)]
data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed
transition-colors"
style="transition-duration: var(--duration-ui)"
>
{tab.label}
</Tabs.Trigger>
{/each}
</Tabs.List>
{#each tabs as tab (tab.value)}
<Tabs.Content value={tab.value}>
{#if panel}{@render panel({ value: tab.value })}{/if}
</Tabs.Content>
{/each}
</Tabs.Root>