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.
svelte-kit-vice/web/routes/uix/+layout@.svelte

1130 lines
40 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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 { siumLangs } from '$sium/langs/langs';
import { secsLangs } from '$libs/secs';
import { ActiveEidos } from '$uix/eidos';
import type { Gradient } from '$libs/gradient';
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 { floatPanelSema } from '$uix/sema/components/float-panel';
import { formSema } from '$uix/sema/components/form';
import { gradientBuilderSema } from '$uix/sema/components/gradient-builder';
import { gradientPickerSema } from '$uix/sema/components/gradient-picker';
import { imageAdjustmentsSema } from '$uix/sema/components/image-adjustments';
import { imagePickerSema } from '$uix/sema/components/image-picker';
import { knobSema } from '$uix/sema/components/knob';
import { maskFieldSema } from '$uix/sema/components/mask-field';
import { navTreeSema } from '$uix/sema/components/nav-tree';
import { sidebarSema } from '$uix/sema/components/sidebar';
import { numberFieldSema } from '$uix/sema/components/number-field';
import { paginationSema } from '$uix/sema/components/pagination';
import { passwordFieldSema } from '$uix/sema/components/password-field';
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 { textareaSema } from '$uix/sema/components/textarea';
import { mediaPlayerSema } from '$uix/sema/components/media-player';
import { timelineSema } from '$uix/sema/components/timeline';
import { toastSema } from '$uix/sema/components/toast';
import { toolbarSema } from '$uix/sema/components/toolbar';
import { accordionSema } from '$uix/sema/components/accordion';
import { chatComposerSema } from '$uix/sema/components/chat-composer';
import { chatLogSema } from '$uix/sema/components/chat-log';
import { chatMessageSema } from '$uix/sema/components/chat-message';
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 { cropperSema } from '$uix/sema/components/cropper';
import { cssFieldSema } from '$uix/sema/components/css-field';
import { dateFieldSema } from '$uix/sema/components/date-field';
// The rest of the pack index, loaded 2026-07-31. The media-player report
// (63f20f345) exposed the class: packs are OPT-IN per app, and every demo
// whose pack was missing here played raw family defaults instead of its
// authored signature.
import { chronosSema } from '$uix/sema/components/chronos';
import { clipboardSema } from '$uix/sema/components/clipboard';
import { collapsibleSema } from '$uix/sema/components/collapsible';
import { colorFieldSema } from '$uix/sema/components/color-field';
import { contextMenuSema } from '$uix/sema/components/context-menu';
import { dragDropSema } from '$uix/sema/components/drag-drop';
import { dropdownMenuSema } from '$uix/sema/components/dropdown-menu';
import { listboxSema } from '$uix/sema/components/listbox';
import { menubarSema } from '$uix/sema/components/menubar';
import { menuDialSema } from '$uix/sema/components/menu-dial';
import { monthGridSema } from '$uix/sema/components/month-grid';
import { navigationMenuSema } from '$uix/sema/components/navigation-menu';
import { onionMenuSema } from '$uix/sema/components/onion-menu';
import { pathTraceSema } from '$uix/sema/components/path-trace';
import { proofOfHumanSema } from '$uix/sema/components/proof-of-human';
import { rangeCalendarSema } from '$uix/sema/components/range-calendar';
import { rotateAlignSema } from '$uix/sema/components/rotate-align';
import { switchSema } from '$uix/sema/components/switch';
import { timeFieldSema } from '$uix/sema/components/time-field';
import { toggleSema } from '$uix/sema/components/toggle';
import { toggleGroupSema } from '$uix/sema/components/toggle-group';
import { tooltipSema } from '$uix/sema/components/tooltip';
import { treeViewSema } from '$uix/sema/components/tree-view';
import { treeGridSema } from '$uix/sema/components/tree-grid';
import { yearGridSema } from '$uix/sema/components/year-grid';
import { Popover } from '$uix/eidos/components/popover';
import '@/uix/eidos/index.css';
// Opt-in full 31-scale donor palette — lets component `color="teal"` /
// `color="plum"` (THEMING §25.5, the Radix per-instance override) resolve
// the raw `--scale-{name}-*` tokens in the docs. The slim foundation
// (`generated/base.css`) ships only role-referenced scales; the demos need
// the whole palette so the System axis can exercise every scale.
import '@/uix/eidos/generated/palette.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: { sium: siumLangs, secs: secsLangs }, defaultLocale: 'es' as const },
prefs: { schema: docsPrefsSchema },
events: {
sound: true,
haptic: true,
components: [
accordionSema,
calendarSema,
chatComposerSema,
chatLogSema,
chatMessageSema,
checkboxSema,
colorPickerSema,
comboboxSema,
cropperSema,
cssFieldSema,
dateFieldSema,
dialogSema,
drawerSema,
editableSema,
fileUploadSema,
floatPanelSema,
formSema,
gradientBuilderSema,
gradientPickerSema,
imageAdjustmentsSema,
imagePickerSema,
knobSema,
maskFieldSema,
mediaPlayerSema,
navTreeSema,
sidebarSema,
numberFieldSema,
paginationSema,
passwordFieldSema,
popoverSema,
radioGroupSema,
ratingGroupSema,
searchFieldSema,
selectSema,
sliderSema,
splitterSema,
stepperSema,
tabsSema,
tagGroupSema,
tagsInputSema,
textareaSema,
timelineSema,
toastSema,
toolbarSema,
// The rest of the index (2026-07-31) — see the import note above.
chronosSema,
clipboardSema,
collapsibleSema,
colorFieldSema,
contextMenuSema,
dragDropSema,
dropdownMenuSema,
listboxSema,
menubarSema,
menuDialSema,
monthGridSema,
navigationMenuSema,
onionMenuSema,
pathTraceSema,
proofOfHumanSema,
rangeCalendarSema,
rotateAlignSema,
switchSema,
timeFieldSema,
toggleSema,
toggleGroupSema,
tooltipSema,
treeViewSema,
treeGridSema,
yearGridSema
]
}
});
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/radio-cards', label: 'Radio cards' },
{ 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' },
{ slug: '/uix/components/button', label: 'Button' },
{ slug: '/uix/components/button-group', label: 'Button group' },
{ slug: '/uix/components/split-button', label: 'Split button' },
{ slug: '/uix/components/fab', label: 'Fab' }
]
},
{
kind: 'group',
heading: 'Forms',
items: [
{ slug: '/uix/components/form', label: 'Form' },
{ slug: '/uix/components/field', label: 'Field' },
{ slug: '/uix/components/field-langs', label: 'Field langs' },
{ slug: '/uix/components/label', label: 'Label' },
{ slug: '/uix/components/editable', label: 'Editable' },
{ slug: '/uix/components/palabras', label: 'Palabras' },
{ slug: '/uix/components/number-field', label: 'Number field' },
{ slug: '/uix/components/css-field', label: 'CSS field' },
{ slug: '/uix/components/search-field', label: 'Search field' },
{ slug: '/uix/components/password-field', label: 'Password field' },
{ slug: '/uix/components/mask-field', label: 'Mask field' },
{ slug: '/uix/components/prose', label: 'Prose' },
{ slug: '/uix/components/textarea', label: 'Text area' },
{ slug: '/uix/components/select', label: 'Select' },
{ slug: '/uix/components/combobox', label: 'Combobox' },
{ slug: '/uix/components/listbox', label: 'Listbox' },
{ slug: '/uix/components/grid-list', label: 'Grid list' },
{ slug: '/uix/components/calendar', label: 'Calendar' },
{ slug: '/uix/components/range-calendar', label: 'Range 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-range-field', label: 'Date range 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-range-field', label: 'Time range 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/picker', label: 'Picker (deferValue)' },
{ slug: '/uix/components/natural-time-picker', label: 'Natural time 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/anchor-nav', label: 'Anchor nav' },
{ slug: '/uix/components/breadcrumb', label: 'Breadcrumb' },
{ slug: '/uix/components/pagination', label: 'Pagination' },
{ slug: '/uix/components/navigation-menu', label: 'Navigation menu' },
{ slug: '/uix/components/nav-tree', label: 'Nav tree' },
{ slug: '/uix/components/sidebar', label: 'Sidebar' }
]
},
{
kind: 'group',
heading: 'Media',
items: [
{ slug: '/uix/components/avatar', label: 'Avatar' },
{ slug: '/uix/components/avatar-group', label: 'AvatarGroup' },
{ slug: '/uix/components/icon', label: 'Icon' },
{ slug: '/uix/components/image', label: 'Image' },
{ slug: '/uix/components/image-adjustments', label: 'Image adjustments' },
{ slug: '/uix/components/image-picker', label: 'Image picker' },
{ slug: '/uix/components/cropper', label: 'Cropper' },
{ slug: '/uix/components/carousel', label: 'Carousel' },
{ slug: '/uix/components/qr-code', label: 'QR code' },
{ slug: '/uix/components/barcode', label: 'Barcode' },
{ slug: '/uix/components/media-player', label: 'Media player' }
]
},
{
kind: 'group',
heading: 'Status',
items: [
{ slug: '/uix/components/badge', label: 'Badge' },
{ slug: '/uix/components/empty-state', label: 'Empty state' },
{ slug: '/uix/components/result', label: 'Result' },
{ slug: '/uix/components/callout', label: 'Callout' },
{ slug: '/uix/components/progress', label: 'Progress' },
{ slug: '/uix/components/meter', label: 'Meter' },
{ slug: '/uix/components/spinner', label: 'Spinner' },
{ slug: '/uix/components/skeleton', label: 'Skeleton' }
]
},
{
kind: 'group',
heading: 'Data',
items: [
{ slug: '/uix/components/metrics', label: 'Metrics' },
{ slug: '/uix/components/table', label: 'Table' },
{ slug: '/uix/components/tree-view', label: 'Tree view' },
{ slug: '/uix/components/tree-grid', label: 'Tree grid' },
{ slug: '/uix/components/chat-composer', label: 'Chat composer' },
{ slug: '/uix/components/chat-list', label: 'Chat list' },
{ slug: '/uix/components/chat-log', label: 'Chat log' },
{ slug: '/uix/components/chat-message', label: 'Chat message' },
{ slug: '/uix/components/chat-typing', label: 'Chat typing' },
{ slug: '/uix/components/emoji-picker', label: 'Emoji picker' },
{ slug: '/uix/components/feed', label: 'Feed' },
{ slug: '/uix/components/timeline', label: 'Timeline' },
{ slug: '/uix/components/virtual-list', label: 'Virtual list' },
{ slug: '/uix/components/virtual-grid', label: 'Virtual grid' }
]
},
{
kind: 'group',
heading: 'Charts',
items: [
{ slug: '/uix/components/chart', label: 'Chart · overview' },
{ slug: '/uix/components/line-chart', label: 'Line chart' },
{ slug: '/uix/components/area-chart', label: 'Area chart' },
{ slug: '/uix/components/bar-chart', label: 'Bar chart' },
{ slug: '/uix/components/scatter-chart', label: 'Scatter chart' },
{ slug: '/uix/components/sparkline', label: 'Sparkline' },
{ slug: '/uix/components/pie-chart', label: 'Pie chart' },
{ slug: '/uix/components/bar-list', label: 'Bar list' },
{ slug: '/uix/components/bar-segment', label: 'Bar segment' },
{ slug: '/uix/components/radar-chart', label: 'Radar chart' },
{ slug: '/uix/components/gauge', label: 'Gauge' },
{ slug: '/uix/components/funnel', label: 'Funnel' },
{ slug: '/uix/components/bubble-chart', label: 'Bubble chart' },
{ slug: '/uix/components/polar-area', label: 'Polar area' },
{ slug: '/uix/components/heatmap', label: 'Heatmap' },
{ slug: '/uix/components/smith-chart', label: 'Smith chart' }
]
},
{
kind: 'group',
heading: 'Utilities',
items: [
{ slug: '/uix/components/announce', label: 'Announce' },
{ slug: '/uix/components/clipboard', label: 'Clipboard' },
{ slug: '/uix/components/drag-drop', label: 'Drag & drop' },
{ slug: '/uix/components/proof-of-human', label: 'Proof of human' },
{ slug: '/uix/components/trans', label: 'Trans' },
{ slug: '/uix/components/format-number', label: 'FormatNumber' },
{ slug: '/uix/components/format-date', label: 'FormatDate' },
{ slug: '/uix/components/relative-time', label: 'RelativeTime' }
]
},
{
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/float-panel', label: 'Float panel' },
{ slug: '/uix/components/dropdown-menu', label: 'Dropdown menu' },
{ slug: '/uix/components/context-menu', label: 'Context menu' },
{ slug: '/uix/components/menubar', label: 'Menubar' },
{ slug: '/uix/components/onion-menu', label: 'Onion menu' },
{ slug: '/uix/components/menu-dial', label: 'Menu dial' },
{ slug: '/uix/components/command', label: 'Command' },
{ slug: '/uix/components/link-preview', label: 'Link preview' },
{ 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/s-text', label: 'SText' },
{ slug: '/uix/components/s-text-virtual-list', label: 'STextVirtualList' },
{ 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: 'Text effects',
items: [
{ slug: '/uix/components/count-up', label: 'CountUp' },
{ slug: '/uix/components/text-gradient', label: 'TextGradient' },
{ slug: '/uix/components/text-circular', label: 'TextCircular' },
{ slug: '/uix/components/text-blur', label: 'TextBlur' },
{ slug: '/uix/components/text-focus', label: 'TextFocus' },
{ slug: '/uix/components/text-scramble', label: 'TextScramble' }
]
},
{
kind: 'group',
heading: 'Packs',
items: [{ slug: '/uix/packs/ambient', label: 'Ambient · backgrounds', tag: 'pack' }]
},
{
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/card', label: 'Card' },
{ slug: '/uix/components/card-group', label: 'CardGroup' },
{ 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' }
,
{ slug: '/uix/components/sticky', label: 'Sticky' }
]
}
];
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);
}
}
});
// A small canonical named-gradient set for the docs site — role-derived, so
// they re-tint with the seed and flip light/dark for free (the whole point of
// the gradient axis). Components name them: `<TextGradient colors="brand">`,
// `background: var(--gradient-brand)`. Both are LINEAR (a single `<image>`),
// so they're valid `background-image` values and paint through `background-clip:
// text` — a role-derived MESH (aurora) carries a base color and would not.
const brandGradient: Gradient = {
kind: 'linear',
angle: 110,
interpolation: { space: 'oklch' },
stops: [
{ color: { kind: 'role', role: 'primary', slot: 'solid' } },
{ color: { kind: 'role', role: 'secondary', slot: 'solid' } },
{ color: { kind: 'role', role: 'tertiary', slot: 'solid' } }
]
};
const spectrumGradient: Gradient = {
kind: 'linear',
angle: 90,
interpolation: { space: 'oklch', hue: 'longer' },
stops: [
{ color: { kind: 'role', role: 'primary', slot: 'solid' } },
{ color: { kind: 'role', role: 'affirm', slot: 'solid' } },
{ color: { kind: 'role', role: 'secondary', slot: 'solid' } },
{ color: { kind: 'role', role: 'risk', slot: 'solid' } },
{ color: { kind: 'role', role: 'loss', slot: 'solid' } }
]
};
activeEidos.applyGradients({ brand: brandGradient, spectrum: spectrumGradient });
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);
// ── Display axes — the interrelated theming knobs (density · scaling · border
// width). Projected onto :root (below) via a managed stylesheet so EVERY demo —
// including portaled overlays (dialog/popover) — reflects the change live.
type Density = 'compact' | 'comfortable' | 'spacious';
const DENSITIES: readonly Density[] = ['compact', 'comfortable', 'spacious'];
const DENSITY_SCALES: Record<Density, { space: number; control: number }> = {
compact: { space: 0.84, control: 0.9 },
comfortable: { space: 1, control: 1 },
spacious: { space: 1.16, control: 1.12 }
};
let density = $state<Density>('comfortable');
let scaling = $state(1);
let borderWidth = $state(1);
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;
if (p.density === 'compact' || p.density === 'comfortable' || p.density === 'spacious')
density = p.density;
if (typeof p.scaling === 'number') scaling = p.scaling;
if (typeof p.borderWidth === 'number') borderWidth = p.borderWidth;
} catch {
/* ignore corrupt prefs blob */
}
});
$effect(() => {
if (typeof localStorage === 'undefined') return;
const blob = {
dir,
language,
currency,
units,
numberFormat,
soundOn,
motionOn,
hapticOn,
density,
scaling,
borderWidth
};
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');
});
// Project the display axes onto :root through a managed <style> (id-keyed, so
// it replaces in place). :root — not the docs shell — so portaled overlays,
// which render outside the shell, inherit the same density/scaling/border.
$effect(() => {
const d = DENSITY_SCALES[density];
uix.dom.writeStyle(
'uix-docs-display',
`:root{--density-space-scale:${d.space};--density-control-scale:${d.control};--scaling:${scaling};--border-width:${borderWidth}px;}`
);
});
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);
});
const currentBreakpoint = $derived(uix.dom.currentBreakpoint.current);
const compactShell = $derived(
currentBreakpoint === 'base' || currentBreakpoint === 'sm' || currentBreakpoint === 'md'
);
const phoneShell = $derived(currentBreakpoint === 'base');
</script>
<div
data-uix-docs
data-theme={theme}
data-mode={theme}
data-uix-breakpoint={currentBreakpoint}
data-uix-compact={compactShell ? 'true' : 'false'}
data-uix-phone={phoneShell ? 'true' : 'false'}
>
<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>
<!-- DISPLAY: density / scaling / border width — the interrelated
theming axes, applied to :root so every demo (including
portaled overlays) reflects the change live. -->
<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"
>
<line x1="4" y1="7" x2="20" y2="7" />
<circle cx="9" cy="7" r="2.5" />
<line x1="4" y1="17" x2="20" y2="17" />
<circle cx="15" cy="17" r="2.5" />
</svg>
Display
<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: 252px;">
<div data-uix-menu-section>
<div data-uix-menu-label>Density</div>
{#each DENSITIES as d}
<button
type="button"
data-uix-menu-item
data-checked={density === d ? 'true' : 'false'}
onclick={() => (density = d)}
>
<span style="text-transform: capitalize;">{d}</span>
<span data-uix-menu-item-meta
>{DENSITY_SCALES[d].space}× · {DENSITY_SCALES[d].control}×</span
>
</button>
{/each}
</div>
<div data-uix-menu-section>
<div data-uix-menu-label>Scaling · {scaling.toFixed(2)}×</div>
<input
data-uix-range
type="range"
min="0.85"
max="1.25"
step="0.05"
bind:value={scaling}
aria-label="Scaling factor"
/>
</div>
<div data-uix-menu-section>
<div data-uix-menu-label>Border width · {borderWidth}px</div>
<input
data-uix-range
type="range"
min="0"
max="4"
step="0.5"
bind:value={borderWidth}
aria-label="Border width"
/>
</div>
<div data-uix-menu-section>
<button
type="button"
data-uix-menu-item
onclick={() => {
density = 'comfortable';
scaling = 1;
borderWidth = 1;
}}
>
<span>Reset</span>
<span data-uix-menu-item-meta>comfortable · 1× · 1px</span>
</button>
</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>

Powered by TurnKey Linux.