|
|
<script lang="ts">
|
|
|
import { onDestroy } from 'svelte';
|
|
|
import { createActiveUix, setActiveUix } from '$active-uix';
|
|
|
import {
|
|
|
readActivePrefsSlot,
|
|
|
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 type { SemaPreferences } from '$uix/sema';
|
|
|
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' | 'ar';
|
|
|
type Currency = 'EUR' | 'USD' | 'GBP' | 'JPY';
|
|
|
type UnitSystem = 'metric' | 'imperial';
|
|
|
type NumberFormat = 'es-ES' | 'en-US' | 'en-GB' | 'de-DE';
|
|
|
|
|
|
const docsPrefsSchema = standardPrefsDimensions({
|
|
|
// `ar` is here to make the language→direction derivation exercisable:
|
|
|
// picking it flips `prefs.direction` to rtl with no direction intent set.
|
|
|
// Untranslated keys fall back to the default locale (engine-langs.ts:51-56),
|
|
|
// so it degrades to Spanish copy rather than breaking.
|
|
|
languages: ['es', 'en', 'fr', 'de', 'ar'] 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'
|
|
|
}
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* The perceptual reduction levels the sema channels read. A LIVE object:
|
|
|
* both channels read `preferences.{sound,haptic}` on every emit, so
|
|
|
* mutating it silences immediately with no channel re-registration.
|
|
|
*
|
|
|
* Until 2026-08-06 this was not passed at all, so the channels defaulted to
|
|
|
* `'full'` and the topbar's Sound switch silenced NOTHING — it set the prefs
|
|
|
* intent (which drives the visual/CSS side) and stopped there. A mute that
|
|
|
* does not mute is worse on a page that teaches sound than no mute at all.
|
|
|
*/
|
|
|
const semaPreferences: SemaPreferences = $state({ sound: 'full', haptic: 'full' });
|
|
|
|
|
|
const uix = createActiveUix({
|
|
|
langs: { schema: { sium: siumLangs, secs: secsLangs }, defaultLocale: 'es' as const },
|
|
|
prefs: { schema: docsPrefsSchema },
|
|
|
events: {
|
|
|
sound: true,
|
|
|
haptic: true,
|
|
|
preferences: semaPreferences,
|
|
|
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();
|
|
|
|
|
|
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: 'El sonido',
|
|
|
items: [
|
|
|
{ slug: '/uix/docs/sound', label: 'Cómo suena una interfaz' },
|
|
|
{ slug: '/uix/docs/sound/catalogo', label: 'El catálogo' },
|
|
|
{ slug: '/uix/docs/sound/packs', label: 'Sound packs' },
|
|
|
{ slug: '/uix/docs/sound/gestos', label: 'Gestos' }
|
|
|
]
|
|
|
},
|
|
|
{
|
|
|
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' },
|
|
|
{ slug: '/uix/components/affix', label: 'Affix' }
|
|
|
]
|
|
|
}
|
|
|
];
|
|
|
|
|
|
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(() => {
|
|
|
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.
|
|
|
|
|
|
// Three states, not two. `auto` CLEARS the direction intent so the built-in
|
|
|
// `directionDimension` can derive it from `language` — the chain the
|
|
|
// architecture documents ("if the user sets no intent, it derives from
|
|
|
// prefs.language"). With a two-state toggle the shell set an intent from the
|
|
|
// first frame, which permanently masked that derivation: picking Arabic
|
|
|
// would have changed nothing.
|
|
|
let dirMode = $state<'auto' | 'ltr' | 'rtl'>('auto');
|
|
|
/** Effective direction after prefs resolves intent → derivation → default. */
|
|
|
const dirSlot = readActivePrefsSlot<'ltr' | 'rtl'>(uix.prefs, 'direction');
|
|
|
const effectiveDir = $derived(dirSlot?.get() ?? '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',
|
|
|
ar: 'العربية'
|
|
|
};
|
|
|
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 === 'auto' || p.dir === 'ltr' || p.dir === 'rtl') dirMode = 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: dirMode,
|
|
|
language,
|
|
|
currency,
|
|
|
units,
|
|
|
numberFormat,
|
|
|
soundOn,
|
|
|
motionOn,
|
|
|
hapticOn,
|
|
|
density,
|
|
|
scaling,
|
|
|
borderWidth
|
|
|
};
|
|
|
localStorage.setItem('uix-docs-prefs', JSON.stringify(blob));
|
|
|
});
|
|
|
|
|
|
$effect(() => {
|
|
|
uix.prefs.setIntent('language', language);
|
|
|
// `auto` must CLEAR, not set: intent beats derivation, so setting any
|
|
|
// value here would stop `direction` from following `language`.
|
|
|
if (dirMode === 'auto') uix.prefs.clearIntent('direction');
|
|
|
else uix.prefs.setIntent('direction', dirMode);
|
|
|
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');
|
|
|
// And the channels themselves: the prefs intent drives the visual side,
|
|
|
// this drives whether anything is heard or felt at all.
|
|
|
semaPreferences.sound = soundOn ? 'full' : 'off';
|
|
|
semaPreferences.haptic = hapticOn ? 'full' : 'off';
|
|
|
});
|
|
|
|
|
|
// 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. -->
|
|
|
<!-- Instrumented for `perm:check`: this is the ONLY control in the
|
|
|
shell that commits a preference, so without it the permutation
|
|
|
runner exercises component state transitions but never a prefs
|
|
|
commit — and the reactive path from `uix.prefs` into the ~64
|
|
|
components that resolve `dir` from it stays untested.
|
|
|
Index 90 so it runs AFTER each demo's own steps (0..N).
|
|
|
Two consequences a future reader should know: the runner shares
|
|
|
one browser context across routes, so this flip persists via
|
|
|
localStorage and routes alternate ltr/rtl through a run; and
|
|
|
because the scan is page-wide, EVERY route under this layout now
|
|
|
carries a step, so the runner no longer skips uninstrumented
|
|
|
demos. Both are deliberate — morfo contracts are direction
|
|
|
agnostic, and a prefs commit on every route is the coverage
|
|
|
this instrumentation exists for. -->
|
|
|
<button
|
|
|
type="button"
|
|
|
data-uix-icon-btn
|
|
|
data-perm-step="90"
|
|
|
data-perm-label="prefs: flip direction"
|
|
|
data-perm-settle="600"
|
|
|
title="Direction: {dirMode}{dirMode === 'auto'
|
|
|
? ` (from language → ${effectiveDir})`
|
|
|
: ''} — click to cycle auto → ltr → rtl"
|
|
|
aria-label="Toggle direction"
|
|
|
onclick={() =>
|
|
|
(dirMode = dirMode === 'auto' ? 'ltr' : dirMode === 'ltr' ? 'rtl' : 'auto')}
|
|
|
>
|
|
|
{#if effectiveDir === '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>
|
|
|
|
|
|
<!-- The docs copy is written in English, so it is DECLARED as English via
|
|
|
`lang` — but NOT via `dir`. A `dir="ltr"` here used to pin the prose
|
|
|
against the bidi artefact ("What is morfo?" rendering as "?What is
|
|
|
morfo"), and it worked, but it pinned the DEMOS too: a component in
|
|
|
`auto` mode passes no `dir` prop, so it stamps no attribute and
|
|
|
resolves its direction by INHERITANCE from the DOM. This container
|
|
|
was that inheritance, so the shell's RTL switch — and picking
|
|
|
Arabic — reached `<html>` and stopped here, leaving all 51 demos
|
|
|
frozen in LTR. Measured on the sidebar: `html dir="rtl"` with the
|
|
|
panel still on the left, and dropping this attribute alone moved it.
|
|
|
The prose artefact is cosmetic and only shows in RTL, which is an
|
|
|
inspection mode; demos that cannot demonstrate are not. If the
|
|
|
prose needs pinning again, it has to happen AROUND THE PROSE, not
|
|
|
around the canvas that also holds the live examples. -->
|
|
|
<main data-uix-canvas lang="en">
|
|
|
{@render children?.()}
|
|
|
</main>
|
|
|
</div>
|
|
|
</div>
|