You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
733 lines
23 KiB
733 lines
23 KiB
<script lang="ts">
|
|
import { onDestroy } from 'svelte';
|
|
import { createActiveUix, setActiveUix } from '$active-uix';
|
|
import { createActivePrefsDomProjection, standardPrefsDimensions } from '$prefs';
|
|
import { page } from '$app/state';
|
|
import { Soma } from '$soma/core/soma.svelte';
|
|
import { ActiveEidos } from '$uix/eidos';
|
|
import { dialogSema } from '$uix/sema/components/dialog';
|
|
import { drawerSema } from '$uix/sema/components/drawer';
|
|
import { editableSema } from '$uix/sema/components/editable';
|
|
import { fileUploadSema } from '$uix/sema/components/file-upload';
|
|
import { formSema } from '$uix/sema/components/form';
|
|
import { numberFieldSema } from '$uix/sema/components/number-field';
|
|
import { paginationSema } from '$uix/sema/components/pagination';
|
|
import { popoverSema } from '$uix/sema/components/popover';
|
|
import { radioGroupSema } from '$uix/sema/components/radio-group';
|
|
import { ratingGroupSema } from '$uix/sema/components/rating-group';
|
|
import { searchFieldSema } from '$uix/sema/components/search-field';
|
|
import { selectSema } from '$uix/sema/components/select';
|
|
import { sliderSema } from '$uix/sema/components/slider';
|
|
import { splitterSema } from '$uix/sema/components/splitter';
|
|
import { stepperSema } from '$uix/sema/components/stepper';
|
|
import { tabsSema } from '$uix/sema/components/tabs';
|
|
import { tagGroupSema } from '$uix/sema/components/tag-group';
|
|
import { tagsInputSema } from '$uix/sema/components/tags-input';
|
|
import { toastSema } from '$uix/sema/components/toast';
|
|
import { toolbarSema } from '$uix/sema/components/toolbar';
|
|
import { accordionSema } from '$uix/sema/components/accordion';
|
|
import { checkboxSema } from '$uix/sema/components/checkbox';
|
|
import { calendarSema } from '$uix/sema/components/calendar';
|
|
import { colorPickerSema } from '$uix/sema/components/color-picker';
|
|
import { comboboxSema } from '$uix/sema/components/combobox';
|
|
import { dateFieldSema } from '$uix/sema/components/date-field';
|
|
import { Popover } from '$uix/eidos/components/popover';
|
|
import '@/uix/eidos/index.css';
|
|
import './uix.css';
|
|
|
|
let { children } = $props();
|
|
|
|
type Language = 'es' | 'en' | 'fr' | 'de';
|
|
type Currency = 'EUR' | 'USD' | 'GBP' | 'JPY';
|
|
type UnitSystem = 'metric' | 'imperial';
|
|
type NumberFormat = 'es-ES' | 'en-US' | 'en-GB' | 'de-DE';
|
|
|
|
const docsPrefsSchema = standardPrefsDimensions({
|
|
languages: ['es', 'en', 'fr', 'de'] satisfies readonly Language[],
|
|
locales: ['es-ES', 'en-US', 'en-GB', 'de-DE'] satisfies readonly NumberFormat[],
|
|
currencies: ['EUR', 'USD', 'GBP', 'JPY'] satisfies readonly Currency[],
|
|
defaults: {
|
|
language: 'es',
|
|
locale: 'es-ES',
|
|
currency: 'EUR'
|
|
}
|
|
});
|
|
|
|
const uix = createActiveUix({
|
|
langs: { schema: {}, defaultLocale: 'es' as const },
|
|
prefs: { schema: docsPrefsSchema },
|
|
events: {
|
|
sound: true,
|
|
haptic: true,
|
|
components: [
|
|
accordionSema,
|
|
calendarSema,
|
|
checkboxSema,
|
|
colorPickerSema,
|
|
comboboxSema,
|
|
dateFieldSema,
|
|
dialogSema,
|
|
drawerSema,
|
|
editableSema,
|
|
fileUploadSema,
|
|
formSema,
|
|
numberFieldSema,
|
|
paginationSema,
|
|
popoverSema,
|
|
radioGroupSema,
|
|
ratingGroupSema,
|
|
searchFieldSema,
|
|
selectSema,
|
|
sliderSema,
|
|
splitterSema,
|
|
stepperSema,
|
|
tabsSema,
|
|
tagGroupSema,
|
|
tagsInputSema,
|
|
toastSema,
|
|
toolbarSema
|
|
]
|
|
}
|
|
});
|
|
|
|
setActiveUix(uix);
|
|
Soma.create();
|
|
const prefsProjection = createActivePrefsDomProjection({ prefs: uix.prefs, dom: uix.dom });
|
|
|
|
type Section =
|
|
| { kind: 'top'; slug: string; label: string }
|
|
| { kind: 'group'; heading: string; items: { slug: string; label: string; tag?: string }[] };
|
|
|
|
// Information architecture — single source of truth for sidebar +
|
|
// breadcrumb. Each leaf maps to a route under `/uix/...`.
|
|
const nav: Section[] = [
|
|
{
|
|
kind: 'group',
|
|
heading: 'Get started',
|
|
items: [
|
|
{ slug: '/uix', label: 'Overview' },
|
|
{ slug: '/uix/architecture', label: 'Architecture' },
|
|
{ slug: '/uix/getting-started', label: 'Installation' }
|
|
]
|
|
},
|
|
{
|
|
kind: 'group',
|
|
heading: 'Foundations',
|
|
items: [
|
|
{ slug: '/uix/tokens', label: 'Design tokens' },
|
|
{ slug: '/uix/morfo', label: 'Morfo · contracts' },
|
|
{ slug: '/uix/sema', label: 'Sema · perception' },
|
|
{ slug: '/uix/eidos', label: 'Eidos · visual layer' }
|
|
]
|
|
},
|
|
{
|
|
kind: 'group',
|
|
heading: 'Controls',
|
|
items: [
|
|
{ slug: '/uix/components/toggle', label: 'Toggle' },
|
|
{ slug: '/uix/components/toggle-group', label: 'Toggle group' },
|
|
{ slug: '/uix/components/switch', label: 'Switch' },
|
|
{ slug: '/uix/components/checkbox', label: 'Checkbox' },
|
|
{ slug: '/uix/components/radio-group', label: 'Radio group' },
|
|
{ slug: '/uix/components/rating-group', label: 'Rating group' },
|
|
{ slug: '/uix/components/slider', label: 'Slider' },
|
|
{ slug: '/uix/components/collapsible', label: 'Collapsible' },
|
|
{ slug: '/uix/components/accordion', label: 'Accordion' },
|
|
{ slug: '/uix/components/tabs', label: 'Tabs' },
|
|
{ slug: '/uix/components/toolbar', label: 'Toolbar' }
|
|
]
|
|
},
|
|
{
|
|
kind: 'group',
|
|
heading: 'Forms',
|
|
items: [
|
|
{ slug: '/uix/components/form', label: 'Form' },
|
|
{ slug: '/uix/components/field', label: 'Field' },
|
|
{ slug: '/uix/components/editable', label: 'Editable' },
|
|
{ slug: '/uix/components/number-field', label: 'Number field' },
|
|
{ slug: '/uix/components/search-field', label: 'Search field' },
|
|
{ slug: '/uix/components/select', label: 'Select' },
|
|
{ slug: '/uix/components/combobox', label: 'Combobox' },
|
|
{ slug: '/uix/components/calendar', label: 'Calendar' },
|
|
{ slug: '/uix/components/month-grid', label: 'Month grid' },
|
|
{ slug: '/uix/components/year-grid', label: 'Year grid' },
|
|
{ slug: '/uix/components/date-field', label: 'Date field' },
|
|
{ slug: '/uix/components/date-picker', label: 'Date picker' },
|
|
{ slug: '/uix/components/date-range-picker', label: 'Date range picker' },
|
|
{ slug: '/uix/components/time-field', label: 'Time field' },
|
|
{ slug: '/uix/components/time-picker', label: 'Time picker' },
|
|
{ slug: '/uix/components/time-range-picker', label: 'Time range picker' },
|
|
{ slug: '/uix/components/color-field', label: 'Color field' },
|
|
{ slug: '/uix/components/color-picker', label: 'Color picker' },
|
|
{ slug: '/uix/components/stepper', label: 'Stepper' },
|
|
{ slug: '/uix/components/tag-group', label: 'Tag group' },
|
|
{ slug: '/uix/components/tags-input', label: 'Tags input' },
|
|
{ slug: '/uix/components/pin-input', label: 'Pin input' },
|
|
{ slug: '/uix/components/file-upload', label: 'File upload' }
|
|
]
|
|
},
|
|
{
|
|
kind: 'group',
|
|
heading: 'Navigation',
|
|
items: [
|
|
{ slug: '/uix/components/breadcrumb', label: 'Breadcrumb' },
|
|
{ slug: '/uix/components/pagination', label: 'Pagination' }
|
|
]
|
|
},
|
|
{
|
|
kind: 'group',
|
|
heading: 'Media',
|
|
items: [
|
|
{ slug: '/uix/components/avatar', label: 'Avatar' },
|
|
{ slug: '/uix/components/icon', label: 'Icon' }
|
|
]
|
|
},
|
|
{
|
|
kind: 'group',
|
|
heading: 'Status',
|
|
items: [
|
|
{ slug: '/uix/components/progress', label: 'Progress' },
|
|
{ slug: '/uix/components/meter', label: 'Meter' }
|
|
]
|
|
},
|
|
{
|
|
kind: 'group',
|
|
heading: 'Overlays',
|
|
items: [
|
|
{ slug: '/uix/components/dialog', label: 'Dialog' },
|
|
{ slug: '/uix/components/alert-dialog', label: 'Alert dialog' },
|
|
{ slug: '/uix/components/drawer', label: 'Drawer' },
|
|
{ slug: '/uix/components/popover', label: 'Popover' },
|
|
{ slug: '/uix/components/dropdown-menu', label: 'Dropdown menu' },
|
|
{ slug: '/uix/components/context-menu', label: 'Context menu' },
|
|
{ slug: '/uix/components/toast', label: 'Toast' },
|
|
{ slug: '/uix/components/tooltip', label: 'Tooltip' }
|
|
]
|
|
},
|
|
{
|
|
kind: 'group',
|
|
heading: 'Typography',
|
|
items: [
|
|
{ slug: '/uix/components/text', label: 'Text' },
|
|
{ slug: '/uix/components/heading', label: 'Heading' },
|
|
{ slug: '/uix/components/display', label: 'Display' },
|
|
{ slug: '/uix/components/code', label: 'Code' },
|
|
{ slug: '/uix/components/code-block', label: 'CodeBlock' },
|
|
{ slug: '/uix/components/kbd', label: 'Kbd' },
|
|
{ slug: '/uix/components/mark', label: 'Mark' },
|
|
{ slug: '/uix/components/highlight', label: 'Highlight' },
|
|
{ slug: '/uix/components/link', label: 'Link' }
|
|
]
|
|
},
|
|
{
|
|
kind: 'group',
|
|
heading: 'Layout',
|
|
items: [
|
|
{ slug: '/uix/components/box', label: 'Box' },
|
|
{ slug: '/uix/components/flex', label: 'Flex' },
|
|
{ slug: '/uix/components/grid', label: 'Grid' },
|
|
{ slug: '/uix/components/stack', label: 'Stack' },
|
|
{ slug: '/uix/components/group', label: 'Group' },
|
|
{ slug: '/uix/components/wrap', label: 'Wrap' },
|
|
{ slug: '/uix/components/container', label: 'Container' },
|
|
{ slug: '/uix/components/section', label: 'Section' },
|
|
{ slug: '/uix/components/aspect-ratio', label: 'AspectRatio' },
|
|
{ slug: '/uix/components/auto-grid', label: 'AutoGrid' },
|
|
{ slug: '/uix/components/banner', label: 'Banner' },
|
|
{ slug: '/uix/components/float', label: 'Float' },
|
|
{ slug: '/uix/components/scroll-area', label: 'ScrollArea' },
|
|
{ slug: '/uix/components/separator', label: 'Separator' },
|
|
{ slug: '/uix/components/splitter', label: 'Splitter' }
|
|
]
|
|
}
|
|
];
|
|
|
|
const componentCount = $derived(
|
|
nav
|
|
.filter((s): s is Extract<Section, { kind: 'group' }> => s.kind === 'group')
|
|
.filter((g) => ['Controls', 'Media', 'Overlays'].includes(g.heading))
|
|
.reduce((acc, g) => acc + g.items.length, 0)
|
|
);
|
|
|
|
const current = $derived(page.url.pathname.replace(/\/$/, '') || '/uix');
|
|
|
|
// ── Theme ─────────────────────────────────────────────────────────────
|
|
let theme = $state<'light' | 'dark'>('light');
|
|
const eidosModeListeners = new Set<(value: 'light' | 'dark') => void>();
|
|
const activeEidos = ActiveEidos.create({
|
|
applyDom: true,
|
|
theme: 'base',
|
|
modeSource: {
|
|
get: () => theme,
|
|
onChange(handler) {
|
|
eidosModeListeners.add(handler);
|
|
return () => eidosModeListeners.delete(handler);
|
|
}
|
|
}
|
|
});
|
|
|
|
onDestroy(() => {
|
|
prefsProjection.dispose();
|
|
activeEidos.dispose();
|
|
uix.dispose();
|
|
});
|
|
|
|
$effect(() => {
|
|
if (typeof document === 'undefined') return;
|
|
const stored = localStorage.getItem('uix-docs-theme');
|
|
if (stored === 'dark' || stored === 'light') {
|
|
theme = stored;
|
|
return;
|
|
}
|
|
const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches;
|
|
theme = prefersDark ? 'dark' : 'light';
|
|
});
|
|
|
|
$effect(() => {
|
|
if (typeof localStorage === 'undefined') return;
|
|
localStorage.setItem('uix-docs-theme', theme);
|
|
});
|
|
|
|
$effect(() => {
|
|
const currentTheme = theme;
|
|
for (const listener of eidosModeListeners) listener(currentTheme);
|
|
});
|
|
|
|
let search = $state('');
|
|
|
|
// ── Topbar prefs ──────────────────────────────────────────────────────
|
|
// All persisted to localStorage. The sema toggles will wire to the
|
|
// engine's channels once those expose runtime setEnabled APIs; today
|
|
// they store preference + reflect it in the UI.
|
|
|
|
let dir = $state<'ltr' | 'rtl'>('ltr');
|
|
let language = $state<Language>('es');
|
|
let currency = $state<Currency>('EUR');
|
|
let units = $state<UnitSystem>('metric');
|
|
let numberFormat = $state<NumberFormat>('es-ES');
|
|
let soundOn = $state(true);
|
|
let motionOn = $state(true);
|
|
let hapticOn = $state(true);
|
|
|
|
const LANGUAGE_LABELS: Record<Language, string> = {
|
|
es: 'Español',
|
|
en: 'English',
|
|
fr: 'Français',
|
|
de: 'Deutsch'
|
|
};
|
|
const CURRENCY_LABELS: Record<Currency, string> = {
|
|
EUR: 'Euro',
|
|
USD: 'US Dollar',
|
|
GBP: 'British Pound',
|
|
JPY: 'Japanese Yen'
|
|
};
|
|
const UNIT_LABELS: Record<UnitSystem, string> = { metric: 'Metric', imperial: 'Imperial' };
|
|
const NUMBER_LABELS: Record<NumberFormat, string> = {
|
|
'es-ES': 'Spain · 1.234,56',
|
|
'en-US': 'US · 1,234.56',
|
|
'en-GB': 'UK · 1,234.56',
|
|
'de-DE': 'Germany · 1.234,56'
|
|
};
|
|
|
|
// Persist all topbar prefs in one bag.
|
|
$effect(() => {
|
|
if (typeof localStorage === 'undefined') return;
|
|
const stored = localStorage.getItem('uix-docs-prefs');
|
|
if (!stored) return;
|
|
try {
|
|
const p = JSON.parse(stored);
|
|
if (p.dir === 'ltr' || p.dir === 'rtl') dir = p.dir;
|
|
if (p.language) language = p.language;
|
|
if (p.currency) currency = p.currency;
|
|
if (p.units) units = p.units;
|
|
if (p.numberFormat) numberFormat = p.numberFormat;
|
|
if (typeof p.soundOn === 'boolean') soundOn = p.soundOn;
|
|
if (typeof p.motionOn === 'boolean') motionOn = p.motionOn;
|
|
if (typeof p.hapticOn === 'boolean') hapticOn = p.hapticOn;
|
|
} catch {
|
|
/* ignore corrupt prefs blob */
|
|
}
|
|
});
|
|
|
|
$effect(() => {
|
|
if (typeof localStorage === 'undefined') return;
|
|
const blob = { dir, language, currency, units, numberFormat, soundOn, motionOn, hapticOn };
|
|
localStorage.setItem('uix-docs-prefs', JSON.stringify(blob));
|
|
});
|
|
|
|
$effect(() => {
|
|
uix.prefs.setIntent('language', language);
|
|
uix.prefs.setIntent('direction', dir);
|
|
uix.prefs.setIntent('locale', numberFormat);
|
|
uix.prefs.setIntent('currency', currency);
|
|
uix.prefs.setIntent('unitSystem', units);
|
|
uix.prefs.setIntent('sound', soundOn ? 'allow' : 'reduce');
|
|
uix.prefs.setIntent('motion', motionOn ? 'allow' : 'reduce');
|
|
uix.prefs.setIntent('haptic', hapticOn ? 'allow' : 'reduce');
|
|
});
|
|
|
|
const filteredNav = $derived.by(() => {
|
|
const q = search.trim().toLowerCase();
|
|
if (!q) return nav;
|
|
return nav
|
|
.map((s) => {
|
|
if (s.kind !== 'group') return s;
|
|
const items = s.items.filter((i) => i.label.toLowerCase().includes(q));
|
|
return items.length ? { ...s, items } : null;
|
|
})
|
|
.filter((s): s is Section => s !== null);
|
|
});
|
|
</script>
|
|
|
|
<div data-uix-docs data-theme={theme} data-mode={theme}>
|
|
<div data-uix-shell>
|
|
<header data-uix-topbar>
|
|
<a
|
|
href="/uix"
|
|
data-uix-brand
|
|
data-sveltekit-preload-code="hover"
|
|
data-sveltekit-preload-data="hover"
|
|
>
|
|
<span data-uix-brand-mark>U</span>
|
|
<span>UIX</span>
|
|
<span data-uix-brand-version>v0.1</span>
|
|
</a>
|
|
|
|
<nav data-uix-topbar-nav aria-label="Primary">
|
|
<a
|
|
href="/uix"
|
|
data-sveltekit-preload-code="hover"
|
|
data-sveltekit-preload-data="hover"
|
|
data-active={current === '/uix' ||
|
|
current.startsWith('/uix/architecture') ||
|
|
current.startsWith('/uix/getting-started')
|
|
? 'true'
|
|
: 'false'}>Docs</a
|
|
>
|
|
<a
|
|
href="/uix/components"
|
|
data-sveltekit-preload-code="hover"
|
|
data-sveltekit-preload-data="hover"
|
|
data-active={current.startsWith('/uix/components') ? 'true' : 'false'}>Components</a
|
|
>
|
|
<a href="https://github.com/" target="_blank" rel="noreferrer">GitHub ↗</a>
|
|
</nav>
|
|
|
|
<div data-uix-topbar-tools>
|
|
<!-- DIR: simple toggle. RTL flips the entire document direction
|
|
so all eidos components (which use logical properties)
|
|
reverse correctly. -->
|
|
<button
|
|
type="button"
|
|
data-uix-icon-btn
|
|
title={dir === 'ltr' ? 'Switch to RTL' : 'Switch to LTR'}
|
|
aria-label="Toggle direction"
|
|
aria-pressed={dir === 'rtl'}
|
|
onclick={() => (dir = dir === 'ltr' ? 'rtl' : 'ltr')}
|
|
>
|
|
{#if dir === 'ltr'}
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
aria-hidden="true"
|
|
>
|
|
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
</svg>
|
|
{:else}
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
aria-hidden="true"
|
|
>
|
|
<path d="M19 12H5M11 6l-6 6 6 6" />
|
|
</svg>
|
|
{/if}
|
|
</button>
|
|
|
|
<!-- LANG: short uppercase code + popover with full language list -->
|
|
<Popover>
|
|
<Popover.Trigger>
|
|
<span data-uix-label-btn>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
aria-hidden="true"
|
|
>
|
|
<circle cx="12" cy="12" r="10" />
|
|
<path d="M2 12h20M12 2a15 15 0 0 1 0 20M12 2a15 15 0 0 0 0 20" />
|
|
</svg>
|
|
{language.toUpperCase()}
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
data-caret
|
|
aria-hidden="true"
|
|
>
|
|
<path d="m6 9 6 6 6-6" />
|
|
</svg>
|
|
</span>
|
|
</Popover.Trigger>
|
|
<Popover.Content side="bottom" align="end" sideOffset={6}>
|
|
<div data-uix-menu>
|
|
<div data-uix-menu-section>
|
|
<div data-uix-menu-label>Language</div>
|
|
{#each Object.entries(LANGUAGE_LABELS) as [code, label]}
|
|
<button
|
|
type="button"
|
|
data-uix-menu-item
|
|
data-checked={language === code ? 'true' : 'false'}
|
|
onclick={() => (language = code as Language)}
|
|
>
|
|
<span>{label}</span>
|
|
<span data-uix-menu-item-meta>{code.toUpperCase()}</span>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</Popover.Content>
|
|
</Popover>
|
|
|
|
<!-- FORMATS: currency + units + number formatting in one popover -->
|
|
<Popover>
|
|
<Popover.Trigger>
|
|
<span data-uix-label-btn>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
aria-hidden="true"
|
|
>
|
|
<path d="M12 1v22M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
|
|
</svg>
|
|
Formats
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
data-caret
|
|
aria-hidden="true"
|
|
>
|
|
<path d="m6 9 6 6 6-6" />
|
|
</svg>
|
|
</span>
|
|
</Popover.Trigger>
|
|
<Popover.Content side="bottom" align="end" sideOffset={6}>
|
|
<div data-uix-menu style="min-inline-size: 240px;">
|
|
<div data-uix-menu-section>
|
|
<div data-uix-menu-label>Currency</div>
|
|
{#each Object.entries(CURRENCY_LABELS) as [code, label]}
|
|
<button
|
|
type="button"
|
|
data-uix-menu-item
|
|
data-checked={currency === code ? 'true' : 'false'}
|
|
onclick={() => (currency = code as Currency)}
|
|
>
|
|
<span>{label}</span>
|
|
<span data-uix-menu-item-meta>{code}</span>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
<div data-uix-menu-section>
|
|
<div data-uix-menu-label>Units</div>
|
|
{#each Object.entries(UNIT_LABELS) as [code, label]}
|
|
<button
|
|
type="button"
|
|
data-uix-menu-item
|
|
data-checked={units === code ? 'true' : 'false'}
|
|
onclick={() => (units = code as UnitSystem)}
|
|
>
|
|
<span>{label}</span>
|
|
<span data-uix-menu-item-meta
|
|
>{code === 'metric' ? 'KM · KG · °C' : 'MI · LB · °F'}</span
|
|
>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
<div data-uix-menu-section>
|
|
<div data-uix-menu-label>Numbers</div>
|
|
{#each Object.entries(NUMBER_LABELS) as [code, label]}
|
|
<button
|
|
type="button"
|
|
data-uix-menu-item
|
|
data-checked={numberFormat === code ? 'true' : 'false'}
|
|
onclick={() => (numberFormat = code as NumberFormat)}
|
|
>
|
|
<span>{label}</span>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</Popover.Content>
|
|
</Popover>
|
|
|
|
<!-- SEMANTICS: sound / motion / haptic toggles. These will hook
|
|
into the sema engine channels' enabled state once the
|
|
channels expose runtime setEnabled APIs; today they
|
|
persist preference + reflect in the UI. -->
|
|
<Popover>
|
|
<Popover.Trigger>
|
|
<span data-uix-label-btn>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
d="M12 2a4 4 0 0 1 4 4v6a4 4 0 0 1-8 0V6a4 4 0 0 1 4-4zM19 10v2a7 7 0 0 1-14 0v-2M12 19v3"
|
|
/>
|
|
</svg>
|
|
Semantics
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
data-caret
|
|
aria-hidden="true"
|
|
>
|
|
<path d="m6 9 6 6 6-6" />
|
|
</svg>
|
|
</span>
|
|
</Popover.Trigger>
|
|
<Popover.Content side="bottom" align="end" sideOffset={6}>
|
|
<div data-uix-menu style="min-inline-size: 240px;">
|
|
<div data-uix-menu-section>
|
|
<div data-uix-menu-label>Perceptual channels</div>
|
|
<label data-uix-menu-toggle>
|
|
<input type="checkbox" bind:checked={soundOn} />
|
|
Sound
|
|
<span data-uix-menu-toggle-meta>{soundOn ? 'on' : 'off'}</span>
|
|
</label>
|
|
<label data-uix-menu-toggle>
|
|
<input type="checkbox" bind:checked={motionOn} />
|
|
Motion
|
|
<span data-uix-menu-toggle-meta>{motionOn ? 'on' : 'off'}</span>
|
|
</label>
|
|
<label data-uix-menu-toggle>
|
|
<input type="checkbox" bind:checked={hapticOn} />
|
|
Haptic
|
|
<span data-uix-menu-toggle-meta>{hapticOn ? 'on' : 'off'}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</Popover.Content>
|
|
</Popover>
|
|
|
|
<!-- THEME -->
|
|
<button
|
|
type="button"
|
|
data-uix-icon-btn
|
|
title={theme === 'light' ? 'Switch to dark theme' : 'Switch to light theme'}
|
|
aria-label="Toggle theme"
|
|
onclick={() => (theme = theme === 'light' ? 'dark' : 'light')}
|
|
>
|
|
{#if theme === 'light'}
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
aria-hidden="true"
|
|
>
|
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
</svg>
|
|
{:else}
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
aria-hidden="true"
|
|
>
|
|
<circle cx="12" cy="12" r="4" />
|
|
<path
|
|
d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"
|
|
/>
|
|
</svg>
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<aside data-uix-rail>
|
|
<div data-uix-rail-search>
|
|
<input type="search" placeholder="Search…" bind:value={search} />
|
|
</div>
|
|
|
|
{#each filteredNav as section}
|
|
{#if section.kind === 'group'}
|
|
<div data-uix-rail-group>
|
|
<div data-uix-rail-group-head>
|
|
<span>{section.heading}</span>
|
|
<span data-uix-rail-group-count>{section.items.length}</span>
|
|
</div>
|
|
<ul>
|
|
{#each section.items as item}
|
|
<li>
|
|
<a
|
|
href={item.slug}
|
|
data-uix-rail-link
|
|
data-sveltekit-preload-code="hover"
|
|
data-sveltekit-preload-data="hover"
|
|
data-active={current === item.slug ? 'true' : 'false'}
|
|
>
|
|
{item.label}
|
|
{#if item.tag}
|
|
<span data-uix-rail-link-tag>{item.tag}</span>
|
|
{/if}
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|
|
{/if}
|
|
{/each}
|
|
|
|
{#if filteredNav.length === 0}
|
|
<div data-uix-rail-group-head>No matches for "{search}"</div>
|
|
{/if}
|
|
</aside>
|
|
|
|
<main data-uix-canvas>
|
|
{@render children?.()}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
/* This layout owns its UIX context directly so components declared in
|
|
* the same layout hydrate with ActiveUix/Soma available. */
|
|
</style>
|