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

1182 lines
43 KiB

<script lang="ts">
import { onDestroy } from 'svelte';
import { createActiveUix, setActiveUix } from '$active-uix';
fix(direction): getDir() no era reactivo, y 33 componentes estampaban su valor congelado El slider no respondia al cambio rtl/ltr. Resultaron ser dos defectos independientes, y el segundo era del ecosistema entero. 1) EL SLIDER. slider.css emparejaba un inset LOGICO con un transform FISICO en las tres reglas verticales (inset-inline-start: 50% + translateX(-50%)). Los transforms no se voltean, asi que en RTL el rail y el relleno quedaban fuera de eje un ancho entero. Medido en Chrome: raiz/pulgar/ticks en x=961, rail y relleno en x=955. El Tick se libraba porque ya usaba margin-inline-start — ese era el idioma correcto del propio fichero y es el que aplico. Mismo defecto en accordion.css: text-align: left en el trigger, que era el UNICO text-align fisico de todo eidos; ahora quedan cero. 2) LA RAIZ. makeDimension().get() leia engine.snapshot() esquivando la celda $state, asi que prefs.<dim>.get() — y con ella el soma.prefs.getDir() que la guia manda usar — era una lectura sin tracking. Los ~64 componentes que resuelven su direccion desde prefs la congelaban al montarse. La proyeccion DOM se salvaba porque usa onChange: misma dimension, dos caminos de lectura, solo uno reactivo. Arreglado con un contador de cambios POR dimension, bombeado desde el effectiveDiff que el motor ya calcula, de modo que cubre tambien el cambio por DERIVACION (direction siguiendo a language) y no solo el intent. Un contador por clave y no una celda global: con una sola celda, tocar motion despertaria las 64 derivaciones de direccion. El test negativo lo clava. 3) VALOR NO ES AFIRMACION. 33 de 37 componentes estampaban dir en su raiz con un valor siempre concreto, asi que una app que ponga html dir=rtl sin registrar la preferencia se encontraba 33 islas volteadas del reves. Investigadas las cinco librerias de referencia: MUI y react-aria no estampan nunca, Radix estampa siempre (sus mantenedores arrastran discussions/1405 por esto), y Zag estampa prop("dir") — presente si alguien lo pidio, ausente si no. Adoptado el modelo de Zag pero uniforme: el atributo NUNCA tiene defecto. La herencia la hace el navegador por AUSENCIA de atributo, sin ninguna lectura del DOM, asi que la cadena prop -> prefs -> 'ltr' se queda intacta. 4) HOMOGENEIDAD. Habia DIEZ formas distintas de resolver lo mismo en los wrappers y CINCO de declarar el tipo, con un cuarto escalon semantico —heredar del menu padre— enterrado en dos expresiones sueltas. Normalizado a activeDir(getter, soma) en los 36, un solo tipo Direction | undefined, y el escalon de los submenus declarado en la llamada, donde se ve. 5) LA SHELL. El toggle de direccion pasa de 2 a 3 estados: auto hace clearIntent, que es lo unico que deja ganar al derive. Y entra el arabe en el catalogo de idiomas, porque sin un idioma RTL la derivacion no era disparable. Con las dos cosas, elegir arabe voltea la direccion sin tocar el control — la cadena documentada funcionando de punta a punta por primera vez. Verificado: check 74 = linea base exacta, cero nuevos. 602/602 tests en 101 ficheros. En Chrome real: el topbar mueve al slider y al accordion en vivo, y el arabe voltea por derivacion. QUEDA: 48 demos siguen clavando dir='ltr'; el interrogante desplazado del accordion necesita dir="ltr" lang="en" en el contenedor de la demo (no es un fallo nuestro, es bidi correcto sobre texto ingles en parrafo RTL); html lang no sigue al idioma; smoke y perm:check sin ejecutar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
import {
createActivePrefsDomProjection,
readActivePrefsSlot,
standardPrefsDimensions
} from '$prefs';
import { page } from '$app/state';
import { Soma } from '$soma/core/soma.svelte';
fix(password-field): visual issues — double border, broken layout, stale lang refs Three visual bugs spotted on /uix/components/password-field: 1. **Double-bordered inputs.** The demo wrapped `<PasswordField>` inside `<Field.Control>`, which adds its own border + padding. PasswordField IS the field's input shell — it ships its own chrome. Removed the five offending Field.Control wrappers from login/registration/change forms; PasswordField now sits directly inside `<Field>`. 2. **Icon wrapping to a new row + StrengthMeter sharing the bordered box.** Replaced `display: inline-flex; flex-wrap: wrap` on the root with `display: grid; grid-template-columns: minmax(0, 1fr) auto auto` so Input + CapsLockIndicator + VisibilityTrigger stay on the first row. When StrengthMeter is composed, `:has()` switches the root to a 2-row grid where the bordered chrome lifts onto a `::before` pseudo-element that only spans the first row, and the meter sits below as a sibling. State chrome (`data-focused`, `data-invalid`) propagates to the pseudo via the same `:has()` selectors. 3. **CapsLock pill claiming space even when inactive.** Was `color: transparent; background: transparent` which still consumed width in the grid. Now `display: none` by default and `display: inline-flex` only when `[data-active]` is set. Also fixed: `#?sium.errors.min_length|…` and friends rendered literal in the demo because the docs shell didn't register `$sium/langs/langs`. Registered `siumLangs` under the `sium` namespace in the layout's `createActiveUix({ langs: { schema: { sium: siumLangs } } })`. svelte-check: 0 errors. Page now loads at 200. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
import { siumLangs } from '$sium/langs/langs';
fix(uix audit): apply Kim audit remediations across morfo/soma/eidos/recipe layers Resolves 34 of 40 failures from src/uix/audit-uix-kimi-1.md. The remaining 6 are all Words component (excluded per project owner — actively developed on a separate track) or unrelated infra (cookie-adapter-client jsdom timeout). Key changes: - soma/provider/context.ts: swallow Svelte's `lifecycle_outside_component` in `context.set()` so unit tests can instantiate providers without a component tree (unblocked 18 picker tests in one line). - soma/textarea: route inline style writes through `dom.apply({attrs})` instead of direct `el.style.X = Y` (contract regex compliance). - soma/picker-shell: add minimal Provider component + morfo so the shared handle utility passes the morfo/component contract. - eidos/_layout: lift `shared.ts` to `eidos/lib/layout-helpers.ts` (the `_layout` directory wasn't a real component) and update 8 layout primitive imports. - eidos/command: replace `onMount` with `$effect` (visual layer can't use Svelte 4 lifecycle hooks per component-api contract); restructure `Item.Icon`/`Item.Shortcut` namespace so contract regex matches. - eidos/color-picker: extract hue spectrum hsl literals to sidecar `color-picker-spectrum.css`; keep main recipe color-literal-free. - eidos/lib/recipes/base.ts: declare 196 layout-primitive + chrome CSS vars (box, flex, grid, float, group, banner, container, section, separator, toggle-group, pin-input, scroll-area, splitter, navigation-menu, dropdown-menu, context-menu, date-range-field, time-range-field, plus per-component additions to date-field, color-picker, button, select). 197 missing → 0 non-Words. - morfo: add `data-dir` to carousel, `data-kind` to date-picker, add StrengthMeterLabel sub-part to password-field, register picker-shell. - 5 pickers (date/color/date-range/time/time-range): add `internals.ts` indirection so `exports.ts` doesn't re-export `*provider.svelte.ts` directly (contract D); rewrite virtual root types to plain object literals (no `WithChild`/`PrimitiveDivAttributes`). - 4 new READMEs (button, textarea, password-field, picker-shell) following the search-field canonical template. Test result: 2391/2397 passing (99.75%). Remaining 6 failures are all Words-scope or jsdom infra timeout — explicitly out of scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
import { secsLangs } from '$libs/secs';
import { ActiveEidos } from '$uix/eidos';
feat(text-gradient): `colors` discriminates canonical name vs stop list (folds `preset`) Per request: one `colors` prop that discriminates by shape instead of two rival props. A `string[]` is an explicit stop list (unchanged); a bare `string` names a canonical gradient from the theming's named set, resolved as `var(--gradient-{name})` — exactly what the removed `preset` prop did. The set is an open cage (`EidosConfig.gradients` / `applyGradients`), so the type stays `string | readonly string[]`, not a closed union. Removed `preset` outright (no back-compat shim, per project rule); the canonical S9 vocabulary and raw stops are now the SAME input. Docs shell: register a small canonical named set on `uix/+layout@` via `applyGradients` (`brand`, `spectrum`) — role-derived, so they re-tint with the seed and flip light/dark. Demo gains a live stops|named toggle so both branches are a real testbed. Finding worth recording (types + README): a named gradient painted through `background-clip: text` must be a single `<image>` (linear/radial/conic). A role-derived MESH serializes to radials + a base COLOR, and a bare color is not a valid `background-image` (only the `background` shorthand accepts it) — so a mesh name computes to `none` and never paints. The docs set is linear-only for this reason. Verified in real Chrome: stops branch paints the token gradient; `colors="spectrum"` paints `var(--gradient-spectrum)` through the text. svelte-check 0 own errors, component:audit 141/0/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
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';
feat(gradient-picker): rebuild on the generic Picker core + presets panel + result-commit sema Rebuilds the GradientPicker on the generic Picker.Provider<Gradient> transaction core (open / commit / cancel / clear / workingValue + a composed Popover), like the natural-time-picker — the bespoke coordinator is now a thin domain provider (morfo runtime + working-value CSS + the presets state). - deferValue prop (live vs deferred draft-until-Listo). The trigger chip reflects the COMMITTED value, so a deferred picker's chip stays put while you edit the working draft. - Clear restores the INITIAL bound value the picker was given, not a hardcoded default gradient. - No tabs: the editor IS the popover body; the presets grid opens in a panel from a Layers button next to "Añadir degradado" (the Add <-> Remove toggle) — ColorPicker-style "select a saved preset -> Remove", and it reclaims the tab-header height. - Add / Remove presets fire commit.fulfill / commit.loss firmas (new gradient-picker sema pack, registered in the demo). Per the Button doctrine the press is a neutral contact.activate and the consumer emits the RESULT commit; the events target the REGISTERED `provider` part (targeting the unregistered `content` part was also why the Clear commit-reset was silent). - Stop color editor hides the remove-stop button for the endpoint stops (first / last by position — they anchor 0% / 100%). - maxPresets cap (default 10). StopList in a ScrollArea (autosize, slim scrollbar with a gap so the rows never sit under it). Composes the existing components AS-IS (Popover / Button / ScrollArea / the GradientBuilder parts) without overriding their styles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
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';
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
import { maskFieldSema } from '$uix/sema/components/mask-field';
import { navTreeSema } from '$uix/sema/components/nav-tree';
uix(sidebar): F1.7 eidos + demo · raíl, modo icono y submenú flotante Segunda tanda del sidebar: la capa visual, la demo canónica de 9 pestañas y la verificación en navegador real. Con esto el componente está vivo; queda el review adversarial. Eidos - Recipe con los dos ejes: off-canvas por `transform` (trabajo de compositor, el inset no reflowea) y `width` animado SOLO en modo icono, donde el inset sí tiene que seguir al raíl. El raíl NO lleva `will-change` (una franja fina tiembla a DPR fraccionario — memoria NavMenu). - Anchos como TOKENS: `--sidebar-width` 16rem · `--sidebar-width-icon` 3rem · `--sidebar-width-mobile` 18rem, más raíl, gaps, altura de fila y colores. - Tres composiciones que la capa headless no puede hacer: `Drawer` para la presentación móvil, `Tooltip` para nombrar filas en modo icono y `Badge` para los contadores. El trigger compone `Button` por el patrón `child`. - Convención de icono documentada: sobrevive el PRIMER elemento de la fila. Arreglos que destapó el navegador - **`mobile` en SSR**: sin viewport, `isAtLeast` responde «por debajo de todo» → el HTML salía con `data-mobile` y la hidratación NO lo corrige (no diffea atributos y el valor ya no cambia). Viewport desconocido = ESCRITORIO. - **Estado móvil propio** (arranca cerrado): en escritorio `open` es preferencia de layout persistida; en móvil sería un overlay tapando la página, que jamás puede ser el estado inicial. `onOpenChange` solo se dispara con el estado de escritorio. - **`open` controlado/no controlado**: se adopta el patrón de la casa (`$bindable` con default, el provider escribe siempre) — mi versión previa con estado interno no propagaba `bind:open`. - **El submenú flotante no se posicionaba**: `FloatingAnchor` resuelve su provider por CONTEXTO y el eidos envuelve la fila en un `Tooltip`, que publica el suyo → el ancla caía en el tooltip y el submenú se quedaba en `translate(0,-200%)`. Ahora el ancla se cablea directamente al provider flotante del item. Además el `<ul>` ya no se posiciona a sí mismo (colapsaba el wrapper a tamaño cero) y toma elevación del plano `overlay` por `data-depth`, como cualquier panel flotante del ecosistema. - **Tooltip vs submenú**: una fila con submenú no lleva tooltip en modo icono (dos superficies sobre el mismo ancla se pisan) — nuevo `useSidebarItemOr()`. - El item envuelve (`flex-wrap`) para que el submenú caiga en su propia línea en vez de ponerse al lado de la fila. Demo: shell de app real (grupos, badge, acción, submenú, fila deshabilitada), controles vivos de los tres props y las 9 pestañas del canon. Verificado en navegador real (Playwright): dos landmarks nombrados en el idioma activo · `aria-current="page"` en la fila activa · cada menú etiquetado por SU grupo · raíl enfocable que alterna con Enter · colapso 256→48px · off-canvas por transform · submenú flotante anclado a su fila y Escape que devuelve el foco · sema `emerge-expand/collapse` estampando en `panel` · móvil 600px = Drawer cerrado con el landmark dentro · RTL espeja el shell entero · oscuro · 0 errores de consola. Gates: `component:audit --only sidebar` PASS 0E/0W · eidos-lint 45 morfo-backed / 0 invalid / 0 class-hooks · `svelte-check` 0 errores propios · `vitest src/uix/eidos` 353/353 · `contracts.test` solo con fallos ajenos (menubar, radio-group y las claves camelCase de aura). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
import { sidebarSema } from '$uix/sema/components/sidebar';
import { numberFieldSema } from '$uix/sema/components/number-field';
import { paginationSema } from '$uix/sema/components/pagination';
feat: build PasswordField (full stack) Dedicated password input component — matches the Ark UI / Chakra v3 / Mantine / PrimeReact / Ant Design pattern of treating PasswordField as its own first-class component (not a TextField variant). Includes one feature that NO surveyed library ships: a CapsLock indicator driven by KeyboardEvent.getModifierState('CapsLock'). Anatomy (5 parts, 4 events): - Provider (root, value + visibility state) - Input (the textbox; type flips between password / text) - VisibilityTrigger (eye toggle button; aria-pressed + aria-controls) - CapsLockIndicator (status live region; data-active toggles via soma listening to keydown/keyup on Input) - StrengthMeter (composable; consumer provides 0-4 score; UIX bundles no zxcvbn-style algorithm — dependency-free by design) Events: - commit-input (every keystroke) - commit-submit (Enter) - shift-toggle-visibility (eye click) - shift-caps-state (Caps Lock state flips while focused) Key behaviors: - `purpose: 'current' | 'new'` drives native autocomplete attribute ('current-password' for login / 'new-password' for registration). - Field integration: disabled/readonly/required/invalid OR-merged. - VisibilityTrigger gets tabindex=-1 so it doesn't break the form's natural tab order — mouse / programmatic focus only, matching Ark UI. - Caps Lock indicator is aria-live="polite" — announces on activation while focused, silent when inactive. - Letter-spacing increased on the input for legibility while masked. Reference library survey performed (10 libraries) before implementation — all consensus around dedicated component, all have built-in toggle, ZERO have caps lock warning. Documented in the demo's API tab with a feature comparison table. Demo follows DEMO_AUTHORING_GUIDE canonical 6-tab depth: Live (controls grouped by layer + code snippets at the bottom), API (props + reference comparison), Morfo (field/value + Parts + Events tables), Sema (events table with ▶ play), Recipe (selectors with morfo/eidos owner tags), A11y (concern/contract table). Also includes external work in words/ from a separate session (unrelated to PasswordField) — not touching Words is the standing rule, but the diff was already in the working tree. svelte-check: 0 errors in PasswordField files (Words errors are tracked by its own development branch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
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';
fix(splitter): register sema scope + cascade so handle-pick/drag actually sound Two missing registrations meant the splitter sema cascade from the previous commit never reached the engine — handle-pick and handle-drag were silent, only commit-set sounded (because the `commit` family has a sound payload baked into its SEMA_MAP base, while `handle` only has haptic and depends on the cascade for sound). Fixes: 1. morfo scope: ['soma'] → ['soma', 'sema', 'eidos']. Without 'sema' the per-component cascade rules in src/uix/sema/components/splitter.ts are never honored. 2. web/routes/uix/+layout@.svelte: add splitterSema to the `components: [...]` array passed to defineEngineSemantic. The sema engine only loads cascade packs it's explicitly given, so forgetting this line silently drops all splitter cascade rules. Verified in browser at /uix/components/splitter via Live tab (real pointer interaction, AudioContext.createOscillator counter): handle-pick → 2 oscillators (handle.pickup.air, gain 0.10) handle-drag → 16 oscillators across 8 throttled emits — the dynamic resolveSplitterDragSound curve is playing per-frame as expected commit-set → 2 oscillators (handle.snap.chime, gain 0.14) Note: the Sema tab's static "▶ play" button still shows 0 oscillators for handle-drag — same as Slider's demo. By design: that button calls engine.emit() directly without the per-emit `signal.overrides` that soma injects during real drag. The dynamic curve IS the payload. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
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';
Build TextArea component + fix locale propagation bug across soma providers Major work in this session: 1. NumberField demo refactored to expose Intl.NumberFormatOptions live - 9 canonical presets (currency/unit/percent/compact/plain) - Locale/currency/unit lists derived from $libs/langs, $libs/currency, $format/units (no more hardcoded arrays) - Default locale from uix.format.numbers.getLocale() 2. Bug fix: locale prop ignored when soma.format service is active - soma/NumberField: 5 bypass points via new `hasOwnLocale` getter (decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue) - soma/Calendar: dateOrder respects locale prop (matches DateField precedence) - soma/RangeCalendar: same dateOrder fix - When the consumer passes `locale` prop, soma.nums/dates is bypassed and Intl is used directly with the prop locale 3. TextArea — new component, full 4-layer stack - morfo: 3 parts (provider/input/count), 3 events (commit-input/commit-submit/shift-count-overflow), APG textbox - soma: TextAreaProvider with autosize (scrollHeight measurement), submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp, Field integration (OR-merge), counter overflow tracking - eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize - sema: commit-submit + shift-count-overflow perceptual cues - demo: canonical 6-tab depth, all props exposed as live controls - PrimitiveTextareaAttributes added to soma/types/html.ts 4. Also includes accumulated unreleased work from prior sessions: - Eidos ports: announce, clipboard, drag-drop, feed, table, tree-grid, tree-view, virtual-grid, virtual-list, words - Their canonical demos under web/routes/uix/components/ - Semantic events defined for utility morfos (announce/clipboard/virtual-list/virtual-grid) - Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients) - Sidebar reorganized with Data + Utilities groups - Eidos words content layer and primitives Reference library survey performed before TextArea API design (Mantine, Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime). Decision documented: no separate CurrencyField/UnitField — single NumberField with formatOptions is the canon (matches all surveyed headless libs except PrimeReact's mode enum, which still uses 1 component). svelte-check: 0 errors in modified/created files. See CONTINUE.md for full session notes and next steps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
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';
uix(chat): bloque de mensajería chat-* v1 (log · message · composer · typing) Familia chat-* completa, componiendo el ecosistema (Feed + VirtualList anclado, Textarea, FileUpload, Avatar, Popover, Icon). Transporte-agnóstica: el modelo de datos es de la app, los componentes reciben props + snippets. - chat-log: Feed + VirtualList(anchor:'end') en triple registro; day separators, unread divider, filas de sistema (Feed.Article), pill "N nuevos" + notifyIncoming (announce a live-region), to-latest distance-aware. - chat-message: raíz Feed.Article; runs ($libs/chat), ticks de entrega, reply embebido, reacciones (chip pressed + tapback panel), acciones hover. - chat-composer: Textarea (doble registro) + FileUpload; teclado Enter/Shift+Enter/Esc/ArrowUp; typing saliente debounced (uix.timers); barra reply/edit. - chat-typing: sustain-streaming (visual-only, loop del sistema); aggregateTypers 1/2/3+ con plantillas plurales. - $libs/chat: groupIntoRuns (ventana 8 min estilo Discord) + aggregateTypers. - VirtualList: extensión anchor:'end' (followOutput, prepend por clave estable, sticky monótono, onAtEndChange) — sin scroller paralelo. Diseño de referencia (WhatsApp/Telegram, pase lado-a-lado): burbujas suaves (out = primary-element + texto normal; in = surface card + sombra) sobre lienzo muted del log, meta dentro de la burbuja, tapback bar horizontal, iconos lucide del ecosistema (nada de SVG a mano). Contrato de tamaños: size="md" = 16px canónico. Fixes de framework (afectaban más allá de chat): - EngineLangs.t() ahora pela el prefijo '#?': los idlangrefs imperativos (pagination/file-upload/chronos/chat) caían al fallback EN en locale es. Fix parseLangRef ?? parsePathFallback + test de regresión (132/132). - virtual-list commit-set-resize con channels:[]: es bookkeeping de ResizeObserver, no gesto → dejaba de emitir beep/vibrate pre-interacción (interventions de navigator.vibrate / AudioContext). Escaparate messenger en /uix/demos/chat (bot de eco con uix.timers; variante $connection señalada). Gaps v2 registrados en next-features §7. Pase: component:audit 4×PASS · eidos-lint invalid=0 · recipe-contract 29/29 · Playwright escaparate PASS end-to-end · consola limpia · npm run check en baseline (76, cero errores nuevos). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
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';
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
// 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();
fix(direction): getDir() no era reactivo, y 33 componentes estampaban su valor congelado El slider no respondia al cambio rtl/ltr. Resultaron ser dos defectos independientes, y el segundo era del ecosistema entero. 1) EL SLIDER. slider.css emparejaba un inset LOGICO con un transform FISICO en las tres reglas verticales (inset-inline-start: 50% + translateX(-50%)). Los transforms no se voltean, asi que en RTL el rail y el relleno quedaban fuera de eje un ancho entero. Medido en Chrome: raiz/pulgar/ticks en x=961, rail y relleno en x=955. El Tick se libraba porque ya usaba margin-inline-start — ese era el idioma correcto del propio fichero y es el que aplico. Mismo defecto en accordion.css: text-align: left en el trigger, que era el UNICO text-align fisico de todo eidos; ahora quedan cero. 2) LA RAIZ. makeDimension().get() leia engine.snapshot() esquivando la celda $state, asi que prefs.<dim>.get() — y con ella el soma.prefs.getDir() que la guia manda usar — era una lectura sin tracking. Los ~64 componentes que resuelven su direccion desde prefs la congelaban al montarse. La proyeccion DOM se salvaba porque usa onChange: misma dimension, dos caminos de lectura, solo uno reactivo. Arreglado con un contador de cambios POR dimension, bombeado desde el effectiveDiff que el motor ya calcula, de modo que cubre tambien el cambio por DERIVACION (direction siguiendo a language) y no solo el intent. Un contador por clave y no una celda global: con una sola celda, tocar motion despertaria las 64 derivaciones de direccion. El test negativo lo clava. 3) VALOR NO ES AFIRMACION. 33 de 37 componentes estampaban dir en su raiz con un valor siempre concreto, asi que una app que ponga html dir=rtl sin registrar la preferencia se encontraba 33 islas volteadas del reves. Investigadas las cinco librerias de referencia: MUI y react-aria no estampan nunca, Radix estampa siempre (sus mantenedores arrastran discussions/1405 por esto), y Zag estampa prop("dir") — presente si alguien lo pidio, ausente si no. Adoptado el modelo de Zag pero uniforme: el atributo NUNCA tiene defecto. La herencia la hace el navegador por AUSENCIA de atributo, sin ninguna lectura del DOM, asi que la cadena prop -> prefs -> 'ltr' se queda intacta. 4) HOMOGENEIDAD. Habia DIEZ formas distintas de resolver lo mismo en los wrappers y CINCO de declarar el tipo, con un cuarto escalon semantico —heredar del menu padre— enterrado en dos expresiones sueltas. Normalizado a activeDir(getter, soma) en los 36, un solo tipo Direction | undefined, y el escalon de los submenus declarado en la llamada, donde se ve. 5) LA SHELL. El toggle de direccion pasa de 2 a 3 estados: auto hace clearIntent, que es lo unico que deja ganar al derive. Y entra el arabe en el catalogo de idiomas, porque sin un idioma RTL la derivacion no era disparable. Con las dos cosas, elegir arabe voltea la direccion sin tocar el control — la cadena documentada funcionando de punta a punta por primera vez. Verificado: check 74 = linea base exacta, cero nuevos. 602/602 tests en 101 ficheros. En Chrome real: el topbar mueve al slider y al accordion en vivo, y el arabe voltea por derivacion. QUEDA: 48 demos siguen clavando dir='ltr'; el interrogante desplazado del accordion necesita dir="ltr" lang="en" en el contenedor de la demo (no es un fallo nuestro, es bidi correcto sobre texto ingles en parrafo RTL); html lang no sigue al idioma; smoke y perm:check sin ejecutar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
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({
fix(direction): getDir() no era reactivo, y 33 componentes estampaban su valor congelado El slider no respondia al cambio rtl/ltr. Resultaron ser dos defectos independientes, y el segundo era del ecosistema entero. 1) EL SLIDER. slider.css emparejaba un inset LOGICO con un transform FISICO en las tres reglas verticales (inset-inline-start: 50% + translateX(-50%)). Los transforms no se voltean, asi que en RTL el rail y el relleno quedaban fuera de eje un ancho entero. Medido en Chrome: raiz/pulgar/ticks en x=961, rail y relleno en x=955. El Tick se libraba porque ya usaba margin-inline-start — ese era el idioma correcto del propio fichero y es el que aplico. Mismo defecto en accordion.css: text-align: left en el trigger, que era el UNICO text-align fisico de todo eidos; ahora quedan cero. 2) LA RAIZ. makeDimension().get() leia engine.snapshot() esquivando la celda $state, asi que prefs.<dim>.get() — y con ella el soma.prefs.getDir() que la guia manda usar — era una lectura sin tracking. Los ~64 componentes que resuelven su direccion desde prefs la congelaban al montarse. La proyeccion DOM se salvaba porque usa onChange: misma dimension, dos caminos de lectura, solo uno reactivo. Arreglado con un contador de cambios POR dimension, bombeado desde el effectiveDiff que el motor ya calcula, de modo que cubre tambien el cambio por DERIVACION (direction siguiendo a language) y no solo el intent. Un contador por clave y no una celda global: con una sola celda, tocar motion despertaria las 64 derivaciones de direccion. El test negativo lo clava. 3) VALOR NO ES AFIRMACION. 33 de 37 componentes estampaban dir en su raiz con un valor siempre concreto, asi que una app que ponga html dir=rtl sin registrar la preferencia se encontraba 33 islas volteadas del reves. Investigadas las cinco librerias de referencia: MUI y react-aria no estampan nunca, Radix estampa siempre (sus mantenedores arrastran discussions/1405 por esto), y Zag estampa prop("dir") — presente si alguien lo pidio, ausente si no. Adoptado el modelo de Zag pero uniforme: el atributo NUNCA tiene defecto. La herencia la hace el navegador por AUSENCIA de atributo, sin ninguna lectura del DOM, asi que la cadena prop -> prefs -> 'ltr' se queda intacta. 4) HOMOGENEIDAD. Habia DIEZ formas distintas de resolver lo mismo en los wrappers y CINCO de declarar el tipo, con un cuarto escalon semantico —heredar del menu padre— enterrado en dos expresiones sueltas. Normalizado a activeDir(getter, soma) en los 36, un solo tipo Direction | undefined, y el escalon de los submenus declarado en la llamada, donde se ve. 5) LA SHELL. El toggle de direccion pasa de 2 a 3 estados: auto hace clearIntent, que es lo unico que deja ganar al derive. Y entra el arabe en el catalogo de idiomas, porque sin un idioma RTL la derivacion no era disparable. Con las dos cosas, elegir arabe voltea la direccion sin tocar el control — la cadena documentada funcionando de punta a punta por primera vez. Verificado: check 74 = linea base exacta, cero nuevos. 602/602 tests en 101 ficheros. En Chrome real: el topbar mueve al slider y al accordion en vivo, y el arabe voltea por derivacion. QUEDA: 48 demos siguen clavando dir='ltr'; el interrogante desplazado del accordion necesita dir="ltr" lang="en" en el contenedor de la demo (no es un fallo nuestro, es bidi correcto sobre texto ingles en parrafo RTL); html lang no sigue al idioma; smoke y perm:check sin ejecutar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// `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'
}
});
const uix = createActiveUix({
fix(uix audit): apply Kim audit remediations across morfo/soma/eidos/recipe layers Resolves 34 of 40 failures from src/uix/audit-uix-kimi-1.md. The remaining 6 are all Words component (excluded per project owner — actively developed on a separate track) or unrelated infra (cookie-adapter-client jsdom timeout). Key changes: - soma/provider/context.ts: swallow Svelte's `lifecycle_outside_component` in `context.set()` so unit tests can instantiate providers without a component tree (unblocked 18 picker tests in one line). - soma/textarea: route inline style writes through `dom.apply({attrs})` instead of direct `el.style.X = Y` (contract regex compliance). - soma/picker-shell: add minimal Provider component + morfo so the shared handle utility passes the morfo/component contract. - eidos/_layout: lift `shared.ts` to `eidos/lib/layout-helpers.ts` (the `_layout` directory wasn't a real component) and update 8 layout primitive imports. - eidos/command: replace `onMount` with `$effect` (visual layer can't use Svelte 4 lifecycle hooks per component-api contract); restructure `Item.Icon`/`Item.Shortcut` namespace so contract regex matches. - eidos/color-picker: extract hue spectrum hsl literals to sidecar `color-picker-spectrum.css`; keep main recipe color-literal-free. - eidos/lib/recipes/base.ts: declare 196 layout-primitive + chrome CSS vars (box, flex, grid, float, group, banner, container, section, separator, toggle-group, pin-input, scroll-area, splitter, navigation-menu, dropdown-menu, context-menu, date-range-field, time-range-field, plus per-component additions to date-field, color-picker, button, select). 197 missing → 0 non-Words. - morfo: add `data-dir` to carousel, `data-kind` to date-picker, add StrengthMeterLabel sub-part to password-field, register picker-shell. - 5 pickers (date/color/date-range/time/time-range): add `internals.ts` indirection so `exports.ts` doesn't re-export `*provider.svelte.ts` directly (contract D); rewrite virtual root types to plain object literals (no `WithChild`/`PrimitiveDivAttributes`). - 4 new READMEs (button, textarea, password-field, picker-shell) following the search-field canonical template. Test result: 2391/2397 passing (99.75%). Remaining 6 failures are all Words-scope or jsdom infra timeout — explicitly out of scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
langs: { schema: { sium: siumLangs, secs: secsLangs }, defaultLocale: 'es' as const },
prefs: { schema: docsPrefsSchema },
events: {
sound: true,
haptic: true,
components: [
accordionSema,
calendarSema,
uix(chat): bloque de mensajería chat-* v1 (log · message · composer · typing) Familia chat-* completa, componiendo el ecosistema (Feed + VirtualList anclado, Textarea, FileUpload, Avatar, Popover, Icon). Transporte-agnóstica: el modelo de datos es de la app, los componentes reciben props + snippets. - chat-log: Feed + VirtualList(anchor:'end') en triple registro; day separators, unread divider, filas de sistema (Feed.Article), pill "N nuevos" + notifyIncoming (announce a live-region), to-latest distance-aware. - chat-message: raíz Feed.Article; runs ($libs/chat), ticks de entrega, reply embebido, reacciones (chip pressed + tapback panel), acciones hover. - chat-composer: Textarea (doble registro) + FileUpload; teclado Enter/Shift+Enter/Esc/ArrowUp; typing saliente debounced (uix.timers); barra reply/edit. - chat-typing: sustain-streaming (visual-only, loop del sistema); aggregateTypers 1/2/3+ con plantillas plurales. - $libs/chat: groupIntoRuns (ventana 8 min estilo Discord) + aggregateTypers. - VirtualList: extensión anchor:'end' (followOutput, prepend por clave estable, sticky monótono, onAtEndChange) — sin scroller paralelo. Diseño de referencia (WhatsApp/Telegram, pase lado-a-lado): burbujas suaves (out = primary-element + texto normal; in = surface card + sombra) sobre lienzo muted del log, meta dentro de la burbuja, tapback bar horizontal, iconos lucide del ecosistema (nada de SVG a mano). Contrato de tamaños: size="md" = 16px canónico. Fixes de framework (afectaban más allá de chat): - EngineLangs.t() ahora pela el prefijo '#?': los idlangrefs imperativos (pagination/file-upload/chronos/chat) caían al fallback EN en locale es. Fix parseLangRef ?? parsePathFallback + test de regresión (132/132). - virtual-list commit-set-resize con channels:[]: es bookkeeping de ResizeObserver, no gesto → dejaba de emitir beep/vibrate pre-interacción (interventions de navigator.vibrate / AudioContext). Escaparate messenger en /uix/demos/chat (bot de eco con uix.timers; variante $connection señalada). Gaps v2 registrados en next-features §7. Pase: component:audit 4×PASS · eidos-lint invalid=0 · recipe-contract 29/29 · Playwright escaparate PASS end-to-end · consola limpia · npm run check en baseline (76, cero errores nuevos). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
chatComposerSema,
chatLogSema,
chatMessageSema,
checkboxSema,
colorPickerSema,
comboboxSema,
cropperSema,
cssFieldSema,
dateFieldSema,
dialogSema,
drawerSema,
editableSema,
fileUploadSema,
floatPanelSema,
formSema,
feat(gradient-picker): rebuild on the generic Picker core + presets panel + result-commit sema Rebuilds the GradientPicker on the generic Picker.Provider<Gradient> transaction core (open / commit / cancel / clear / workingValue + a composed Popover), like the natural-time-picker — the bespoke coordinator is now a thin domain provider (morfo runtime + working-value CSS + the presets state). - deferValue prop (live vs deferred draft-until-Listo). The trigger chip reflects the COMMITTED value, so a deferred picker's chip stays put while you edit the working draft. - Clear restores the INITIAL bound value the picker was given, not a hardcoded default gradient. - No tabs: the editor IS the popover body; the presets grid opens in a panel from a Layers button next to "Añadir degradado" (the Add <-> Remove toggle) — ColorPicker-style "select a saved preset -> Remove", and it reclaims the tab-header height. - Add / Remove presets fire commit.fulfill / commit.loss firmas (new gradient-picker sema pack, registered in the demo). Per the Button doctrine the press is a neutral contact.activate and the consumer emits the RESULT commit; the events target the REGISTERED `provider` part (targeting the unregistered `content` part was also why the Clear commit-reset was silent). - Stop color editor hides the remove-stop button for the endpoint stops (first / last by position — they anchor 0% / 100%). - maxPresets cap (default 10). StopList in a ScrollArea (autosize, slim scrollbar with a gap so the rows never sit under it). Composes the existing components AS-IS (Popover / Button / ScrollArea / the GradientBuilder parts) without overriding their styles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
gradientBuilderSema,
gradientPickerSema,
imageAdjustmentsSema,
imagePickerSema,
knobSema,
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
maskFieldSema,
mediaPlayerSema,
navTreeSema,
uix(sidebar): F1.7 eidos + demo · raíl, modo icono y submenú flotante Segunda tanda del sidebar: la capa visual, la demo canónica de 9 pestañas y la verificación en navegador real. Con esto el componente está vivo; queda el review adversarial. Eidos - Recipe con los dos ejes: off-canvas por `transform` (trabajo de compositor, el inset no reflowea) y `width` animado SOLO en modo icono, donde el inset sí tiene que seguir al raíl. El raíl NO lleva `will-change` (una franja fina tiembla a DPR fraccionario — memoria NavMenu). - Anchos como TOKENS: `--sidebar-width` 16rem · `--sidebar-width-icon` 3rem · `--sidebar-width-mobile` 18rem, más raíl, gaps, altura de fila y colores. - Tres composiciones que la capa headless no puede hacer: `Drawer` para la presentación móvil, `Tooltip` para nombrar filas en modo icono y `Badge` para los contadores. El trigger compone `Button` por el patrón `child`. - Convención de icono documentada: sobrevive el PRIMER elemento de la fila. Arreglos que destapó el navegador - **`mobile` en SSR**: sin viewport, `isAtLeast` responde «por debajo de todo» → el HTML salía con `data-mobile` y la hidratación NO lo corrige (no diffea atributos y el valor ya no cambia). Viewport desconocido = ESCRITORIO. - **Estado móvil propio** (arranca cerrado): en escritorio `open` es preferencia de layout persistida; en móvil sería un overlay tapando la página, que jamás puede ser el estado inicial. `onOpenChange` solo se dispara con el estado de escritorio. - **`open` controlado/no controlado**: se adopta el patrón de la casa (`$bindable` con default, el provider escribe siempre) — mi versión previa con estado interno no propagaba `bind:open`. - **El submenú flotante no se posicionaba**: `FloatingAnchor` resuelve su provider por CONTEXTO y el eidos envuelve la fila en un `Tooltip`, que publica el suyo → el ancla caía en el tooltip y el submenú se quedaba en `translate(0,-200%)`. Ahora el ancla se cablea directamente al provider flotante del item. Además el `<ul>` ya no se posiciona a sí mismo (colapsaba el wrapper a tamaño cero) y toma elevación del plano `overlay` por `data-depth`, como cualquier panel flotante del ecosistema. - **Tooltip vs submenú**: una fila con submenú no lleva tooltip en modo icono (dos superficies sobre el mismo ancla se pisan) — nuevo `useSidebarItemOr()`. - El item envuelve (`flex-wrap`) para que el submenú caiga en su propia línea en vez de ponerse al lado de la fila. Demo: shell de app real (grupos, badge, acción, submenú, fila deshabilitada), controles vivos de los tres props y las 9 pestañas del canon. Verificado en navegador real (Playwright): dos landmarks nombrados en el idioma activo · `aria-current="page"` en la fila activa · cada menú etiquetado por SU grupo · raíl enfocable que alterna con Enter · colapso 256→48px · off-canvas por transform · submenú flotante anclado a su fila y Escape que devuelve el foco · sema `emerge-expand/collapse` estampando en `panel` · móvil 600px = Drawer cerrado con el landmark dentro · RTL espeja el shell entero · oscuro · 0 errores de consola. Gates: `component:audit --only sidebar` PASS 0E/0W · eidos-lint 45 morfo-backed / 0 invalid / 0 class-hooks · `svelte-check` 0 errores propios · `vitest src/uix/eidos` 353/353 · `contracts.test` solo con fallos ajenos (menubar, radio-group y las claves camelCase de aura). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
sidebarSema,
numberFieldSema,
paginationSema,
feat: build PasswordField (full stack) Dedicated password input component — matches the Ark UI / Chakra v3 / Mantine / PrimeReact / Ant Design pattern of treating PasswordField as its own first-class component (not a TextField variant). Includes one feature that NO surveyed library ships: a CapsLock indicator driven by KeyboardEvent.getModifierState('CapsLock'). Anatomy (5 parts, 4 events): - Provider (root, value + visibility state) - Input (the textbox; type flips between password / text) - VisibilityTrigger (eye toggle button; aria-pressed + aria-controls) - CapsLockIndicator (status live region; data-active toggles via soma listening to keydown/keyup on Input) - StrengthMeter (composable; consumer provides 0-4 score; UIX bundles no zxcvbn-style algorithm — dependency-free by design) Events: - commit-input (every keystroke) - commit-submit (Enter) - shift-toggle-visibility (eye click) - shift-caps-state (Caps Lock state flips while focused) Key behaviors: - `purpose: 'current' | 'new'` drives native autocomplete attribute ('current-password' for login / 'new-password' for registration). - Field integration: disabled/readonly/required/invalid OR-merged. - VisibilityTrigger gets tabindex=-1 so it doesn't break the form's natural tab order — mouse / programmatic focus only, matching Ark UI. - Caps Lock indicator is aria-live="polite" — announces on activation while focused, silent when inactive. - Letter-spacing increased on the input for legibility while masked. Reference library survey performed (10 libraries) before implementation — all consensus around dedicated component, all have built-in toggle, ZERO have caps lock warning. Documented in the demo's API tab with a feature comparison table. Demo follows DEMO_AUTHORING_GUIDE canonical 6-tab depth: Live (controls grouped by layer + code snippets at the bottom), API (props + reference comparison), Morfo (field/value + Parts + Events tables), Sema (events table with ▶ play), Recipe (selectors with morfo/eidos owner tags), A11y (concern/contract table). Also includes external work in words/ from a separate session (unrelated to PasswordField) — not touching Words is the standing rule, but the diff was already in the working tree. svelte-check: 0 errors in PasswordField files (Words errors are tracked by its own development branch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
passwordFieldSema,
popoverSema,
radioGroupSema,
ratingGroupSema,
searchFieldSema,
selectSema,
sliderSema,
fix(splitter): register sema scope + cascade so handle-pick/drag actually sound Two missing registrations meant the splitter sema cascade from the previous commit never reached the engine — handle-pick and handle-drag were silent, only commit-set sounded (because the `commit` family has a sound payload baked into its SEMA_MAP base, while `handle` only has haptic and depends on the cascade for sound). Fixes: 1. morfo scope: ['soma'] → ['soma', 'sema', 'eidos']. Without 'sema' the per-component cascade rules in src/uix/sema/components/splitter.ts are never honored. 2. web/routes/uix/+layout@.svelte: add splitterSema to the `components: [...]` array passed to defineEngineSemantic. The sema engine only loads cascade packs it's explicitly given, so forgetting this line silently drops all splitter cascade rules. Verified in browser at /uix/components/splitter via Live tab (real pointer interaction, AudioContext.createOscillator counter): handle-pick → 2 oscillators (handle.pickup.air, gain 0.10) handle-drag → 16 oscillators across 8 throttled emits — the dynamic resolveSplitterDragSound curve is playing per-frame as expected commit-set → 2 oscillators (handle.snap.chime, gain 0.14) Note: the Sema tab's static "▶ play" button still shows 0 oscillators for handle-drag — same as Slider's demo. By design: that button calls engine.emit() directly without the per-emit `signal.overrides` that soma injects during real drag. The dynamic curve IS the payload. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
splitterSema,
stepperSema,
tabsSema,
tagGroupSema,
tagsInputSema,
Build TextArea component + fix locale propagation bug across soma providers Major work in this session: 1. NumberField demo refactored to expose Intl.NumberFormatOptions live - 9 canonical presets (currency/unit/percent/compact/plain) - Locale/currency/unit lists derived from $libs/langs, $libs/currency, $format/units (no more hardcoded arrays) - Default locale from uix.format.numbers.getLocale() 2. Bug fix: locale prop ignored when soma.format service is active - soma/NumberField: 5 bypass points via new `hasOwnLocale` getter (decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue) - soma/Calendar: dateOrder respects locale prop (matches DateField precedence) - soma/RangeCalendar: same dateOrder fix - When the consumer passes `locale` prop, soma.nums/dates is bypassed and Intl is used directly with the prop locale 3. TextArea — new component, full 4-layer stack - morfo: 3 parts (provider/input/count), 3 events (commit-input/commit-submit/shift-count-overflow), APG textbox - soma: TextAreaProvider with autosize (scrollHeight measurement), submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp, Field integration (OR-merge), counter overflow tracking - eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize - sema: commit-submit + shift-count-overflow perceptual cues - demo: canonical 6-tab depth, all props exposed as live controls - PrimitiveTextareaAttributes added to soma/types/html.ts 4. Also includes accumulated unreleased work from prior sessions: - Eidos ports: announce, clipboard, drag-drop, feed, table, tree-grid, tree-view, virtual-grid, virtual-list, words - Their canonical demos under web/routes/uix/components/ - Semantic events defined for utility morfos (announce/clipboard/virtual-list/virtual-grid) - Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients) - Sidebar reorganized with Data + Utilities groups - Eidos words content layer and primitives Reference library survey performed before TextArea API design (Mantine, Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime). Decision documented: no separate CurrencyField/UnitField — single NumberField with formatOptions is the canon (matches all surveyed headless libs except PrimeReact's mode enum, which still uses 1 component). svelte-check: 0 errors in modified/created files. See CONTINUE.md for full session notes and next steps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
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' },
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
{ 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' },
feat: build PasswordField (full stack) Dedicated password input component — matches the Ark UI / Chakra v3 / Mantine / PrimeReact / Ant Design pattern of treating PasswordField as its own first-class component (not a TextField variant). Includes one feature that NO surveyed library ships: a CapsLock indicator driven by KeyboardEvent.getModifierState('CapsLock'). Anatomy (5 parts, 4 events): - Provider (root, value + visibility state) - Input (the textbox; type flips between password / text) - VisibilityTrigger (eye toggle button; aria-pressed + aria-controls) - CapsLockIndicator (status live region; data-active toggles via soma listening to keydown/keyup on Input) - StrengthMeter (composable; consumer provides 0-4 score; UIX bundles no zxcvbn-style algorithm — dependency-free by design) Events: - commit-input (every keystroke) - commit-submit (Enter) - shift-toggle-visibility (eye click) - shift-caps-state (Caps Lock state flips while focused) Key behaviors: - `purpose: 'current' | 'new'` drives native autocomplete attribute ('current-password' for login / 'new-password' for registration). - Field integration: disabled/readonly/required/invalid OR-merged. - VisibilityTrigger gets tabindex=-1 so it doesn't break the form's natural tab order — mouse / programmatic focus only, matching Ark UI. - Caps Lock indicator is aria-live="polite" — announces on activation while focused, silent when inactive. - Letter-spacing increased on the input for legibility while masked. Reference library survey performed (10 libraries) before implementation — all consensus around dedicated component, all have built-in toggle, ZERO have caps lock warning. Documented in the demo's API tab with a feature comparison table. Demo follows DEMO_AUTHORING_GUIDE canonical 6-tab depth: Live (controls grouped by layer + code snippets at the bottom), API (props + reference comparison), Morfo (field/value + Parts + Events tables), Sema (events table with ▶ play), Recipe (selectors with morfo/eidos owner tags), A11y (concern/contract table). Also includes external work in words/ from a separate session (unrelated to PasswordField) — not touching Words is the standing rule, but the diff was already in the working tree. svelte-check: 0 errors in PasswordField files (Words errors are tracked by its own development branch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/password-field', label: 'Password field' },
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
{ slug: '/uix/components/mask-field', label: 'Mask field' },
{ slug: '/uix/components/prose', label: 'Prose' },
Build TextArea component + fix locale propagation bug across soma providers Major work in this session: 1. NumberField demo refactored to expose Intl.NumberFormatOptions live - 9 canonical presets (currency/unit/percent/compact/plain) - Locale/currency/unit lists derived from $libs/langs, $libs/currency, $format/units (no more hardcoded arrays) - Default locale from uix.format.numbers.getLocale() 2. Bug fix: locale prop ignored when soma.format service is active - soma/NumberField: 5 bypass points via new `hasOwnLocale` getter (decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue) - soma/Calendar: dateOrder respects locale prop (matches DateField precedence) - soma/RangeCalendar: same dateOrder fix - When the consumer passes `locale` prop, soma.nums/dates is bypassed and Intl is used directly with the prop locale 3. TextArea — new component, full 4-layer stack - morfo: 3 parts (provider/input/count), 3 events (commit-input/commit-submit/shift-count-overflow), APG textbox - soma: TextAreaProvider with autosize (scrollHeight measurement), submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp, Field integration (OR-merge), counter overflow tracking - eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize - sema: commit-submit + shift-count-overflow perceptual cues - demo: canonical 6-tab depth, all props exposed as live controls - PrimitiveTextareaAttributes added to soma/types/html.ts 4. Also includes accumulated unreleased work from prior sessions: - Eidos ports: announce, clipboard, drag-drop, feed, table, tree-grid, tree-view, virtual-grid, virtual-list, words - Their canonical demos under web/routes/uix/components/ - Semantic events defined for utility morfos (announce/clipboard/virtual-list/virtual-grid) - Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients) - Sidebar reorganized with Data + Utilities groups - Eidos words content layer and primitives Reference library survey performed before TextArea API design (Mantine, Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime). Decision documented: no separate CurrencyField/UnitField — single NumberField with formatOptions is the canon (matches all surveyed headless libs except PrimeReact's mode enum, which still uses 1 component). svelte-check: 0 errors in modified/created files. See CONTINUE.md for full session notes and next steps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/textarea', label: 'Text area' },
{ slug: '/uix/components/select', label: 'Select' },
{ slug: '/uix/components/combobox', label: 'Combobox' },
feat(eidos): port Listbox (embedded selection list) #131. Eidos wrapper + types + recipe + canonical-depth demo. Soma owns single/multi selection (value as string[]), roving-tabindex keyboard nav (Arrow + Home/End + typeahead + Ctrl+A), Field-merged flags (disabled/readonly/required/invalid), hidden form inputs. Eidos adds the visual surface and exposes variant + size + color. Reference parity (no air baseline — port directly from soma): | Feature | soma | react-aria | Headless UI | bits-ui | this port | |---|---|---|---|---|---| | Single + multiple selection | ✓ | ✓ | ✓ | ✓ | ✓ | | bind:value (controlled) | ✓ | ✓ | ✓ | ✓ | ✓ | | Typeahead + Arrow / Home / End | ✓ | ✓ | ✓ | ✓ | ✓ | | Loop nav | ✓ | ✗ | ✗ | ✗ | ✓ | | Section / Group + label | ✓ | ✓ | ✗ | ✓ | ✓ | | Ctrl+A select-all (multi) | ✓ | ✓ | ✗ | ✗ | ✓ | | Form participation (hidden inputs) | ✓ | ✗ | ✓ | ✗ | ✓ | | Field.Provider merge (disabled/invalid/…) | ✓ | ✗ | ✗ | ✗ | ✓ | | Horizontal orientation | ✓ | ✓ | ✓ | ✓ | ✓ | | Item indicator (✓ glyph) | ✓ | ✓ | ✗ | ✓ | ✓ shared partial | | Drag-to-reorder | ✗ | ✓ | ✗ | ✗ | ⚠ deferred | | Async / infinite loading | ✗ | ✓ | ✗ | ✗ | ⚠ deferred | | selectionBehavior (toggle/replace) | ⚠ implicit | ✓ | ✗ | ✓ | ⚠ deferred | | PageUp/Down nav | ✗ | ✓ | ✗ | ✗ | ⚠ deferred | Parts (5): Listbox (Root, replaces soma's Provider name), Item, ItemIndicator, Group, GroupLabel. Variant set: - variant: ControlVariant (`'surface' | 'outline' | 'ghost'`), default 'surface' - size: xs..xl (5-step canonical form-control scale) - color: ColorRole (primary/secondary/neutral/affirm/fulfill/risk/threat/loss) - block: stretch to container width Cross-component reuse: - `ItemIndicator` shares the menu-indicator visual (`a2709476`) — ✓ glyph identical to standalone Checkbox and Dropdown/Context Menu CheckboxItems. CSS-only difference: shown only inside `[data-listbox-item][data-selected]`. - Item row hover / highlight tokens mirror Combobox.Item / Command.Item — the form-control surface family stays coherent. Disambiguation note (in index.ts comment): - Listbox: atomic options, text rows, no inner interactive elements (ARIA forbids interactive descendants inside role="listbox") - GridList: rows with buttons / links / checkboxes inside (2D nav) - Select / Combobox: closed picker surface (Listbox-like dropdown) Gotcha: listbox.svelte and listbox-item.svelte rename `children` to `bodyContent` on destructure to avoid shadowing the inner `{#snippet children}` block (same recursion trap documented in `feedback_button_consumer_pattern.md` Rule 3). Demo: 6 tabs at canonical depth — Live wires all soma + eidos props, all 3 ControlVariant chips, 5 size chips, 8 ColorRole chips, single + multi mode toggle, RTL/horizontal toggle. Sidebar entry added under Forms (between Combobox and Calendar). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/listbox', label: 'Listbox' },
feat(eidos): port GridList (selectable list w/ interactive row children) Closes #133. Eidos wrapper + types + recipe + canonical-depth demo. 4 parts: Provider, Row, Cell, SelectionCheckbox. Soma owns selection (none/single/multiple), 2D keyboard nav (Up/Down rows + Left/Right cells), Shift/Ctrl range/toggle, Ctrl+A select-all, Escape-clear, typeahead, hidden form input, Field merge. Eidos adds variant + size + color + defaultValue (uncontrolled init). Reference parity (no air baseline): | Feature | soma | react-aria GridList | this port | |---|---|---|---| | selectionMode none/single/multiple | ✓ | ✓ | ✓ | | bind:value (controlled) | ✓ | ✓ | ✓ | | defaultValue (uncontrolled init) | ✗ | ✓ | ✓ added (parity with Combobox/Select) | | Item with interactive descendants | ✓ | ✓ | ✓ | | 2D keyboard nav (Up/Down rows, L/R cells) | ✓ | ✓ | ✓ | | Shift/Ctrl range/toggle | ✓ | ✓ | ✓ | | Ctrl+A select-all (multiple) | ✓ | ✓ | ✓ | | Escape-clear | ✓ | ✓ | ✓ | | Typeahead | ✓ | ✓ | ✓ | | ARIA grid/row/gridcell | ✓ | ✓ | ✓ | | Field merge (disabled/invalid/required/readonly) | ✓ | ✗ | ✓ | | Form participation (hidden input) | ✓ | ✗ | ✓ | | Drag-to-reorder | ✗ | ✓ | ⚠ deferred (compose w/ future DragDrop) | | onAction (row click → navigate) | ✗ | ✓ | ⚠ deferred (use button/link inside Cell) | | selectionBehavior toggle/replace | ✗ implicit | ✓ | ⚠ deferred | | disabledBehavior 'selection'/'all' | ✗ skip-only | ✓ | ⚠ deferred | | Async / infinite loading | ✗ | ✓ | ⚠ deferred | | Empty state slot | ✗ | ✓ | ⚠ document `data-empty` styling | Naming: keeps soma's part names (Provider, Row, Cell, SelectionCheckbox). react-aria's "GridListItem" maps to `Row`; soma's vocabulary is more precise and consistent with future Table / TreeGrid in the same family. Variant set: - variant: ControlVariant (surface/outline/ghost), default 'surface' - size: xs..xl (5-step, mirrors Combobox/Listbox) - color: ColorRole (primary/secondary/neutral/affirm/fulfill/risk/threat/loss) - block: stretch to container width Disambiguation (in index.ts comment): use GridList for rows with interactive children; use Listbox for atomic text options (ARIA forbids interactive descendants inside role="listbox"); use Select/Combobox for closed-picker patterns; use Table for tabular data with columns + sort. Visual reuse: - SelectionCheckbox uses the same `mask-image: url(svg-check)` pattern as the shared menu-indicator partial (commit a2709476). Filled accent box → white ✓ on `[data-state='checked']`. - Row hover/highlight tokens mirror Combobox.Item / Listbox.Item so the form-control surface family stays coherent. - Same `max-block-size: min(60vh, 22rem) + overflow-y: auto` scroll contract as Listbox (commit 830025b4). Sidebar entry under Forms (between Listbox and Calendar). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/grid-list', label: 'Grid list' },
{ slug: '/uix/components/calendar', label: 'Calendar' },
feat(eidos): port range-calendar from soma + extract shared partial Standalone eidos `<RangeCalendar>` wrapper over the soma range-calendar primitive. Adds size / variant / color cascades; otherwise pass-through 14 compound parts (Provider, Header, Heading, PrevButton, NextButton, MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell, Cell, Day). ## Feature comparison | Feature | bits-ui | ark-ui | react-aria | Eidos | |----------------------|---------|--------|------------|-------| | Range start/end | ✓ | ✓ | ✓ | ✓ | | Hover preview state | ✓ | ✓ | ✓ | ✓ | | minDays / maxDays | — | — | ✓ | ✓ | | allowSingleDay | ✓ | ✓ | ✓ | ✓ | | preventDeselect | ✓ | ✓ | — | ✓ | | Multi-month display | ✓ | ✓ | ✓ | ✓ | | Week-number column | — | — | ✓ | ✓ | | Month/Year selects | — | ✓ | — | ✓ | | Disabled / readonly | ✓ | ✓ | ✓ | ✓ | | isDateUnavailable | ✓ | ✓ | ✓ | ✓ | | Holiday markers | — | — | — | ✓ | | Locale / dir | ✓ | ✓ | ✓ | ✓ | | Size cascade | — | — | — | ✓ 4 | | Variant cascade | — | — | — | ✓ 3 | | Color cascade | — | — | — | ✓ 8 | ## Range-cells partial extracted The recipe at `src/uix/eidos/components/range-calendar/range-calendar.css` now owns ALL `[data-range-calendar-*]` selectors — the shared rules that previously lived inside `date-range-picker.css` (lines 537-834) moved here. Both consumers (the standalone `<RangeCalendar>` and `<DateRangePicker>`'s popover content) stamp the same data-attrs, so the same selectors paint without duplication. `date-range-picker.css` drops 298 lines of duplication; it keeps only the picker-specific scope overrides (`[data-date-range-picker-calendar]` with no chrome, max-content sizing, transparent bg). ## Morfo Extended `rangeCalendarMorfo.scope` to include `'eidos'` so the new wrapper appears in the eidos-layer inventory and the demo Morfo tab declares it correctly. ## Demo Canonical 6-tab depth (~600L) at `/uix/components/range-calendar`. Live tab exposes every range-specific prop: minDays / maxDays sliders, allowSingleDay, preventDeselect, numberOfMonths chips (1 / 2 / 3), pagedNavigation, fixedWeeks, showWeekNumbers, disableDaysOutsideMonth, weekStartsOn, disabled, readonly, locale, plus size / variant / color cascades. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/range-calendar', label: 'Range calendar' },
feat(month-grid, year-grid): standalone primitives parallel to date-picker views DatePicker.MonthView / YearView require a DatePickerProvider context and can't be used standalone. This commit introduces MonthGrid and YearGrid as independent primitives that accept value / placeholder / minValue / maxValue as props and work anywhere. morfo - New morfo/components/month-grid.ts and year-grid.ts. - scope: ['soma','sema'], apg: grid pattern. - 7 parts each: Provider (role=application) / Header / Heading / PrevButton / NextButton / Grid (role=grid) / Cell (role=gridcell). - Events: commit-set (cell click) + nav-step (prev/next). - Cell data-attrs: data-value, data-selected, data-current, data-disabled, data-focused. - Keyboard map covers arrows / Home / End / PageUp/Down / Enter / Space. soma - New soma/components/{month-grid,year-grid}/ with provider class + langs + types + exports + 7 thin svelte wrappers each. - MonthGridProvider: 12-cell grid keyed to placeholder.year; localized month names via Intl.DateTimeFormat with configurable monthFormat ('long'|'short'|'narrow'). Prev/Next step ±1 year. - YearGridProvider: decadeSize-cell grid (default 12); page anchored at placeholder.year - (placeholder.year % decadeSize). Prev/Next step ± decadeSize. - Both expose Cell sub-provider with snippetProps {selected, disabled, current, value, label} + roving tabindex + RTL-aware keyboard nav. eidos - New eidos/components/{month-grid,year-grid}/ — namespace-style index with size / variant / color visual props + dedicated CSS reusing the --calendar-* token family. - Each grid renders Header (Prev/Heading/Next) + 3-col Grid. demo - New /uix/components/month-grid and /uix/components/year-grid demo pages with the standard live/api/morfo/sema/recipe/a11y tabs, interactive controls for every public prop, soma+eidos snippets that reactively rebuild from controls, and sema event play buttons. Verified: svelte-check --threshold error -> 0 errors / 0 warnings / 4572 files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ 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' },
uix: date-picker + date-range-picker components + component audit infra Two new Eidos components shipped end-to-end (wrapper + recipe + demo + Soma provider hardening) plus a checklist-driven audit pipeline that scores all 67 morfo components against doctrinal completion criteria. New components: - date-picker: full popover-anchored picker over date-field + calendar, with calendar/content/trigger parts and demo route. - date-range-picker: standalone wrapper with own calendar/grid/segment surface, demo route, and recipe CSS. - Both wrappers follow Option C disciplined (root + parts attached via explicit assignment, no Object.assign). Supporting Soma changes: - range-calendar provider tightened (211 LOC of behavior, 167 LOC of tests), README brought up to component doctrine. - date-field, date-picker, date-range-field, date-range-picker Soma providers + READMEs updated for new wrappers. - popover provider/close gain props needed by the picker wrappers. Morfo updates: - date-picker / date-range-picker / range-calendar morfos refined for the new APIs (parts, events, ARIA). Audit infrastructure (new): - src/uix/COMPONENT_COMPLETION_CHECKLIST.md — 81 doctrinal rules across morfo / eidos wrapper / recipe CSS / demo / README / cross-layer scripts. Each rule keyed to active_architecture.md, GUIA_IMPLEMENTACION and DEMO_AUTHORING_GUIDE. - scripts/component-audit.ts + `npm run component:audit` — regex parser over all 67 components, emits tmp/component-audit.md with summary scoreboard + per-component findings. Validates against canonical SEMA_FAMILIES / SEMA_VERBS / ARCHETYPE_VOCABULARY / INTENTS. - Initial baseline: 1 PASS, 64 NEEDS-WORK, 2 BROKEN (tooltip, date-range-picker). Top systemic gaps: translations.label (49), README Gaps/Comparativa/Baseline sections (87 combined), keyboard /event ratio under-declaration (15), apg URL absent (19). Misc: - src/uix/kimi-audit-eidos.md — supplementary audit notes. - .gitignore: ignore .codex-* agent scratch artifacts at repo root. - continue.md + READMEs updated through the migration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ 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' },
feat(eidos): port time-range-field from soma Eidos wrapper for time range selection — start/end TimeField inputs sharing a single ZonedDateTime range value. Mirrors DateRangeField pattern with `data-time-field` overlay trick so item segments inherit the time-field recipe. ## Feature comparison | Feature | react-aria | Mantine | Eidos | |----------------------|------------|---------|-------| | Range value | ✓ | ✓ | ✓ | | Per-endpoint Input | ✓ | ✓ | ✓ | | Segment editing | ✓ | — | ✓ | | Granularity prop | ✓ | ✓ | ✓ | | Hour cycle (12/24) | ✓ | ✓ | ✓ | | Min/max value | ✓ | ✓ | ✓ | | Placeholder per-end | ✓ | — | ✓ | | Invalid signalling | ✓ | ✓ | ✓ | | Disabled / readOnly | ✓ | ✓ | ✓ | | Form integration | ✓ | ✓ | ✓ | | Size cascade | — | ✓ | ✓ xs–xl | | Variant cascade | — | ✓ | ✓ 3 | | Color cascade | — | — | ✓ 6 | ## Parts Root · Label · Input (type='start' | 'end') · Segment (re-exported from TimeField). Each Input scopes its own TimeField context so segment editing routes to the correct endpoint. ## Recipe Grid `1fr · auto · 1fr` with em-dash separator. Specificity bump on `[data-time-range-field][data-time-field]` to win over the overlay data-time-field rules. ## Demo Canonical 6-tab depth (~530L): Live / API / Morfo / Sema / Recipe / A11y. Variant / size / color / granularity / hour-cycle / min-max / disabled / readOnly / required controls. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/time-range-field', label: 'Time range field' },
{ slug: '/uix/components/time-picker', label: 'Time picker' },
feat(time-range-picker): eidos wrapper + canonical picker-shell parity Builds the Eidos time-range-picker layer end-to-end and brings color-picker's morfo into parity with the other pickers. time-range-picker - soma: `mode` prop, `pickerShellContext` registration with commit/cancel/clear and `valueOnOpen` snapshot for revert; Provider now renders a div with data-state/disabled/readonly/required + aria mirrors. - soma: new `Clock` part (mirrors TimePicker.Clock with role="dialog" and data-state). - morfo: scope ['soma','sema'] + apg dialog-modal; promoted Provider to public div with states/data-state; added Clock part; declared the 6 canonical picker events (open / close-commit / close-cancel / close-dismiss / close-dismiss-outside / commit-clear); slider parts declare data-endpoint + data-disabled. - eidos: full namespace (Trigger / Content / Clock / HourSlider / MinuteSlider / SecondSlider / DayPeriodToggle + re-exported PickerShell Footer / Clear / Cancel / Close). Sliders compose generic SliderProvider (N-8) and ship with optional rulers above hour / below minute+second. - eidos css: 2-column endpoint grid in the clock; per-endpoint range tint; AM/PM toggle styling; reuses time-field tokens. - demo: interactive testbed at /uix/components/time-range-picker mirroring the time-picker demo — every soma + eidos prop has a live control, closeOnRangeComplete + start/endReadonlySegments toggles, footer composition switches, sema play buttons, soma+eidos snippets. - nav: added entry between Time picker and Color field. color-picker (audit fixes) - morfo: Provider declares states ['open','closed'] + data-state attr; added the same 6 canonical picker events targeting the `content` part (handle-pick / handle-drag / commit-set preserved). - soma: runtime now feeds open state + disabled/readonly/required/invalid props to the morfo runtime so data-state actually emits. Verified: svelte-check --threshold error → 0 errors / 0 warnings / 4524 files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/time-range-picker', label: 'Time range picker' },
{ slug: '/uix/components/color-field', label: 'Color field' },
feat(eidos): add ColorPicker wrapper + demo Composite popover: 2D saturation/brightness area, hue + alpha channel sliders, preset swatches, eye-dropper, and a segmented channel input composed from ColorField. Builds on the existing soma color-picker (23 parts) — eidos layer is mostly thin pass-throughs adding visual chrome. What landed ----------- - New `src/uix/eidos/components/color-picker/`: - Root visual (Provider) + visual-context (size/variant/color) - Trigger with a default swatch dot tinted from `--cp-trigger-color` (the air pattern from morfo-runtime) - Content (popover wrapper), Area (+ Background + Thumb), TransparencyGrid - ChannelSlider (+ Track + Thumb) — generic per-channel slider; default-composes Track+Thumb when no children - SwatchGroup + SwatchTrigger + Swatch + SwatchIndicator - EyeDropper (Pipette icon by default) - ChannelInput + ChannelSegment + FormatSelect (composed from ColorField parts; same provider injected automatically) - Label, Control, ValueSwatch, ValueText, HiddenInput - Recipe section `color-picker` in `recipes/base.ts` (regenerated `generated/base.css`) — trigger, content, area, channel slider, swatches, eye-dropper tokens. - CSS reuses color-field tokens for the segmented input portion and adds picker-specific visuals. Transparency checker on trigger swatch + value swatch + alpha slider track via conic-gradient pattern. - Morfo updates: scope `['soma','sema']`, `apg: dialog-modal`, added `commit-set` event so audit classifies it as interactive. - Soma exports: ColorPickerProvider class so eidos Trigger can read the current value for the swatch tint. - Demo at `/uix/components/color-picker` with the standard 6 tabs; live stage exercises area drag + channel sliders + preset swatches + eye-dropper + format switch. - README documents the surface, comparativa vs air/Ark/Chakra, and flags the picker contract (mode/Footer/commit/cancel) as gap to align with date/time-picker in a follow-up. Checks ------ - 67/67 component-audit PASS - 40/40 morfo:check PASS (new color-picker demo validates against morfo DOM) - svelte-check 0 errors / 0 warnings Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/color-picker', label: 'Color picker' },
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
{ 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' },
feat(pin-input): port from soma to eidos (Tier 1 sprint, 5/5 — DONE) Multi-cell OTP / verification code input. Two parts (Provider, Cell) with a snippet-driven cell iteration. Architecture: - A transparent absolutely-positioned <input> captures every keystroke, paste and IME event (soma). - Visible bordered cells follow the hidden input's caret via data-active / data-filled emitted by the morfo. - Single-input approach (shadcn / Bits family) — preserves paste distribution, autocomplete='one-time-code' for SMS autofill on iOS/Android, and IME quality. Eidos surface: - size: 'sm' | 'md' | 'lg' | 'xl' (responsive), cascades cell dimension + glyph size tokens to all cells - Recipe paints square bordered cells with focus ring tracking data-active, subtle bg lift on data-filled, threat-toned border when [data-invalid] cascades from the provider Morfo: - scope: ['soma'] → ['soma', 'sema', 'eidos'] - apg URL added (Textbox pattern — OTP has no formal ARIA pattern) - Passive at the morfo layer (no events array; perceptual feedback via the commit family base when onComplete fires) - README documents the passive classification Comparativa: aligned with shadcn / Bits UI (single hidden input). Ark and Chakra use one input per cell which loses paste / IME quality. Sidebar nav: 'Pin input' added under Forms (next to Tags input). === Sprint Tier 1 — DONE === 1. toggle-group 61ffbade 2. alert-dialog d5630a52 3. dropdown-menu 4e477e63 4. context-menu 21eb8a62 5. pin-input (this) Checks: svelte-check 0 errors, component:audit 91/91 PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ 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' },
feat(eidos): port NavigationMenu (top-level site navigation w/ mega-menus) #132. Eidos wrapper + types + recipe + canonical-depth demo. 7 parts: Provider, List, Item, Trigger, Content, Link, Indicator. Soma owns active-item state, hover delays (openDelay/skipDelayDuration/closeDelay), hoverEnabled gate, loop arrow nav, data-motion directional swap, Indicator rect CSS vars. Eidos paints the bar + trigger chrome + Content panel + slide animations. Reference parity (no air baseline — port directly from soma): | Feature | soma | Radix | bits-ui | this port | |---|---|---|---|---| | Provider + List + Item + Trigger + Content + Link + Indicator | ✓ | ✓ | ✓ | ✓ | | Hover open (openDelay/skipDelayDuration/closeDelay) | ✓ | ✓ | ✓ | ✓ | | hoverEnabled (click-only mode) | ✓ | ⚠ | ✓ | ✓ | | orientation horizontal + vertical | ✓ | ✓ | ⚠ | ✓ | | dir / RTL | ✓ | ✓ | ✓ | ✓ | | Indicator CSS vars (x/y/w/h rect) | ✓ | ✗ | ✗ | ✓ | | data-motion directional swap (from-/to-start/end) | ✓ | ✓ | ⚠ | ✓ | | forceMount on Content + Indicator | ✓ | ✓ | ✓ | ✓ | | ARIA <nav> + translated aria-label | ✓ | ⚠ | ⚠ | ✓ | | Keyboard nav (Arrow/Home/End/Enter/Space/Esc, ArrowDown → Content) | ✓ | ✓ | ✓ | ✓ | | Sub (nested viewports) | ✗ | ✓ | ✓ | ⚠ deferred (matches soma) | | Viewport (shared swap pane) | ✗ | ✓ | ✓ | ⚠ deferred | | Modal variant (focus trap) | ✗ | ✓ | ✗ | ⚠ deferred | | SafePolygon hover bridge | ✗ | ✗ | ✗ | ⚠ deferred | Naming: matches soma's part names exactly. Both Trigger (button) and Link (anchor) stay native — NOT routed through <Button> — because top-nav has flat/borderless chrome distinct from action buttons. The Link automatically picks up `aria-current="page"` styling for the active route. Variant set: only `size: 'sm' | 'md' | 'lg'` (no xs/xl). No variant/color — NavigationMenu is chrome surface, not actionable intent. Fresh recipe — does NOT reuse dropdown-menu (floating click-driven) or menubar (roving tabindex bar). NavigationMenu lives inline in the page; per-item Content panels render via Floating but conceptually attach to the bar. Indicator is a 2px underline that slides via CSS vars (--navigation-menu-indicator-{x,y,w,h}) written by soma. Trigger chevron rotates on [data-state='open']. Content animates via data-motion directional slide on sibling swap. Sidebar entry added under Navigation (between Pagination and a future NavigationMenu entry). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/pagination', label: 'Pagination' },
uix(nav-tree): F1.8 CERRADA · demo + navegador + review adversarial Cierra el árbol de navegación data-driven (E-1) de F1: demo canónica de 9 pestañas con el mapa real de docs (43 nodos, 3 niveles), verificación en navegador real y review adversarial (5 dimensiones × 3 verificadores escépticos; 22 hallazgos brutos, 10 confirmados) con todos los confirmados arreglados. Arreglos del review - sema: la parte `group` —target de los eventos emerge— se registraba SIN `ref`, así que `runtime.trigger` lanzaba `SomaRuntimeTargetError` en silencio y el pack no sonaba nunca (cero `data-event-*` en el grupo frente a los de collapsible). El provider posee ahora el ref del `<ul>`. - eidos: en una fila navegable el chevron resolvía `inline-size: 100%` como flex-basis y ocupaba media fila (101 de 231 px en «Soma»), robándole clics al enlace. Toggle compacto con suelo de diana de 24 px (WCAG 2.5.8). - soma: una clave duplicada podía volver cíclico `parentByKey` y colgar la pestaña dentro de `trailKeys` (deriva en render) → clave sufijada + aviso del logger + guarda de ciclo en el paseo. - soma: el colapso es CONTEXTUAL (recuerda el `activeKey` bajo el que se hizo): cerrar la sección que lees se respeta, pero caduca al navegar DENTRO del grupo, para que la página actual nunca quede sin fila visible. Sigue siendo query pura, sin `$effect` que escriba estado. - soma: `child` recibe también `children` (el árbol renderizado); antes dejaba el landmark vacío, porque un árbol data-driven no lo puede reautorar el consumidor. - morfo + langs: el nombre accesible del chevron se declara en el contrato y se localiza («Alternar sección {label}»); ya no duplica el del enlace. - eidos: RTL completo — el glyph espeja solo (bordes lógicos), lo que no espeja es el giro, así que bajo `[dir='rtl']` las dos rotaciones se intercambian. El Gap «dirección del chevron en RTL» queda RESUELTO. - demo: paridad de snippet con los controles vivos; fuera el token fantasma `--nav-tree-rail-width` del docblock del recipe. badge en v1 (decisión del usuario, delegada) Está en el suelo de paridad del dossier §P5 (los 6 refs lo llevan). Se resuelve con SNIPPET, no con recursión a nivel de eidos: el morfo declara la parte `badge`, soma renderiza el snippet recibido (sin él, el valor crudo — sigue siendo headless) y el wrapper de eidos pasa el `Badge` canónico. Va DENTRO del control de la fila, así su texto entra en el nombre accesible («TSC, New, enlace»). `disabled` se descarta en v1 (fuera del suelo, y un enlace de navegación deshabilitado es semánticamente dudoso); ambos quedan registrados en la tabla de Gaps. Verificado: `component:audit` PASS · eidos-lint 26 morfo-backed / 0 invalid · `svelte-check` 0 errores en estos archivos · `vitest src/uix/eidos` 353/353 · navegador real (Playwright): trail auto-expandido, sema estampando en el grupo, teclado nativo, foco visible, claro y oscuro, RTL, 375 px sin desbordes, 0 errores de consola. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
{ slug: '/uix/components/navigation-menu', label: 'Navigation menu' },
uix(sidebar): F1.7 eidos + demo · raíl, modo icono y submenú flotante Segunda tanda del sidebar: la capa visual, la demo canónica de 9 pestañas y la verificación en navegador real. Con esto el componente está vivo; queda el review adversarial. Eidos - Recipe con los dos ejes: off-canvas por `transform` (trabajo de compositor, el inset no reflowea) y `width` animado SOLO en modo icono, donde el inset sí tiene que seguir al raíl. El raíl NO lleva `will-change` (una franja fina tiembla a DPR fraccionario — memoria NavMenu). - Anchos como TOKENS: `--sidebar-width` 16rem · `--sidebar-width-icon` 3rem · `--sidebar-width-mobile` 18rem, más raíl, gaps, altura de fila y colores. - Tres composiciones que la capa headless no puede hacer: `Drawer` para la presentación móvil, `Tooltip` para nombrar filas en modo icono y `Badge` para los contadores. El trigger compone `Button` por el patrón `child`. - Convención de icono documentada: sobrevive el PRIMER elemento de la fila. Arreglos que destapó el navegador - **`mobile` en SSR**: sin viewport, `isAtLeast` responde «por debajo de todo» → el HTML salía con `data-mobile` y la hidratación NO lo corrige (no diffea atributos y el valor ya no cambia). Viewport desconocido = ESCRITORIO. - **Estado móvil propio** (arranca cerrado): en escritorio `open` es preferencia de layout persistida; en móvil sería un overlay tapando la página, que jamás puede ser el estado inicial. `onOpenChange` solo se dispara con el estado de escritorio. - **`open` controlado/no controlado**: se adopta el patrón de la casa (`$bindable` con default, el provider escribe siempre) — mi versión previa con estado interno no propagaba `bind:open`. - **El submenú flotante no se posicionaba**: `FloatingAnchor` resuelve su provider por CONTEXTO y el eidos envuelve la fila en un `Tooltip`, que publica el suyo → el ancla caía en el tooltip y el submenú se quedaba en `translate(0,-200%)`. Ahora el ancla se cablea directamente al provider flotante del item. Además el `<ul>` ya no se posiciona a sí mismo (colapsaba el wrapper a tamaño cero) y toma elevación del plano `overlay` por `data-depth`, como cualquier panel flotante del ecosistema. - **Tooltip vs submenú**: una fila con submenú no lleva tooltip en modo icono (dos superficies sobre el mismo ancla se pisan) — nuevo `useSidebarItemOr()`. - El item envuelve (`flex-wrap`) para que el submenú caiga en su propia línea en vez de ponerse al lado de la fila. Demo: shell de app real (grupos, badge, acción, submenú, fila deshabilitada), controles vivos de los tres props y las 9 pestañas del canon. Verificado en navegador real (Playwright): dos landmarks nombrados en el idioma activo · `aria-current="page"` en la fila activa · cada menú etiquetado por SU grupo · raíl enfocable que alterna con Enter · colapso 256→48px · off-canvas por transform · submenú flotante anclado a su fila y Escape que devuelve el foco · sema `emerge-expand/collapse` estampando en `panel` · móvil 600px = Drawer cerrado con el landmark dentro · RTL espeja el shell entero · oscuro · 0 errores de consola. Gates: `component:audit --only sidebar` PASS 0E/0W · eidos-lint 45 morfo-backed / 0 invalid / 0 class-hooks · `svelte-check` 0 errores propios · `vitest src/uix/eidos` 353/353 · `contracts.test` solo con fallos ajenos (menubar, radio-group y las claves camelCase de aura). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
{ 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' },
feat(eidos): Card, Avatar ext, Image, SText, Badge, service components + dates engine fixes Sprint completo: - Card (eidos + sema): elevation/intent/interactive, eventos select+activate. - Avatar: ring, AvatarBadge, AvatarGroup separados; demo Avatar standalone. - Image: status surface (idle/loading/loaded/error) consumiendo soma ImageProvider. - SText + STextVirtualList: canvas-text engine portado de morfo-driven-soma como componentes separados para optimizar bundle (no se importa canvas si no se usa). - Badge: primitivo eidos puro. - Trans, FormatNumber, FormatDate, RelativeTime: 4 service components greppables alrededor de uix.langs y uix.format. Nav group "Utilities". Bugs fix arts/format/dates (engine-dates.ts): 1. Preset / per-axis conflict: Intl.DateTimeFormat rechaza dateStyle/timeStyle con cualquier per-axis (weekday/era/year/month/day/dayPeriod/hour/minute/ second/fractionalSecondDigits) o con timeZoneName. Engine drop-ea ambos presets por defecto cuando detecta conflict (regla estricta — un solo per-axis hace ilegales los dos presets). 2. Per-call locale override: formatDate/Time/DateTime aceptan tercer arg locale?: string. No muta engine state. Cache (getCachedDateFormat) keyea por (locale x options). resolveCallHourCycle: cuando hay locale override y pref auto, el ciclo horario se resuelve desde el locale OVERRIDE. 3. withHourCycle precedence: caller hourCycle/hour12 ahora gana sobre preferencia del engine. Antes spread mal ordenado sobrescribia 'h12' del caller con 'h23' del engine cuando locale resolvia a 24h. FormatDate component vuelve a rutear siempre por uix.format.dates (cache + hourCycle + locale reactivo), pasando locale como tercer arg. Demos service components autonomos del toolbar global: cada uno owns su demoLocale state con 7 chips (es-ES/en-US/en-GB/de-DE/fr-FR/ja-JP/ar-EG). FormatDate stage filtra dateStyle/timeStyle por mode + chip hourCycle anadido. Tests: 14/14 passing en engine-dates.test.ts (+6 nuevos: conflict handling, locale override, hourCycle precedence). Docs: arts/format/README.md (handoff 2026-05-25 con las 3 reglas), README.md por service component, project_service_components_sprint_2026-05-25.md, feedback_service_demos_own_locale.md (pattern reusable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/avatar-group', label: 'AvatarGroup' },
feat(eidos): port carousel from soma Eidos wrapper for the soma Carousel primitive — horizontally or vertically scrollable rail of slides with keyboard, button, indicator, drag and autoplay nav. Soma owns the index + autoplay timer + gesture; eidos adds size/variant/color cascades and renders prev/next triggers as `<Button iconOnly variant="ghost" rounded="full">` per the canonical Button consumer pattern. ## Feature comparison | Feature | embla | Mantine | ark-ui | Eidos | |----------------------|-------|---------|--------|-------| | Index nav | ✓ | ✓ | ✓ | ✓ | | Loop | ✓ | ✓ | ✓ | ✓ | | Drag / scrub | ✓ | — | ✓ | ✓ | | Autoplay | plugin| ✓ | ✓ | ✓ | | Pause on hover | plugin| ✓ | ✓ | ✓ | | Orientation V/H | ✓ | ✓ | ✓ | ✓ | | Keyboard nav | ✓ | ✓ | ✓ | ✓ | | Indicators | — | ✓ | ✓ | ✓ | | Prev/Next triggers | — | ✓ | ✓ | ✓ | | Size cascade | — | — | — | ✓ 5 | | Variant cascade | — | — | — | ✓ 3 | | Color cascade | — | — | — | ✓ 8 | | Multi-slides view | ✓ | ✓ | — | deferred | | Lazy-mount slides | plugin| — | — | deferred | ## Parts (8) Provider · Viewport · ItemGroup · Item · PrevTrigger · NextTrigger · IndicatorGroup · Indicator. PrevTrigger and NextTrigger render Button via the soma `child` snippet (canonical Button consumer pattern). Default chrome: `iconOnly variant='ghost' rounded='full'` + chevron glyph. Size cascades from Carousel root; consumers can override every Button visual prop inline. Passing `child` bypasses Button entirely. ## Recipe Card chrome around the rail (`variant='surface'`), absolute-positioned triggers over the viewport edges (vertical flips to top/bottom). Active indicator stretches inline-size 2.25× for clearer current-position hint. 8 ColorRole accents. ## Demo Canonical 6-tab depth (~470L): Live / API / Morfo / Sema / Recipe / A11y. Live controls cover all soma props (orientation, loop, dragEnabled, autoplay + delay slider, pauseOnHover, disabled) and all eidos props (variant, size, color). 4 colored placeholder slides for visual contrast. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/icon', label: 'Icon' },
feat(eidos): Card, Avatar ext, Image, SText, Badge, service components + dates engine fixes Sprint completo: - Card (eidos + sema): elevation/intent/interactive, eventos select+activate. - Avatar: ring, AvatarBadge, AvatarGroup separados; demo Avatar standalone. - Image: status surface (idle/loading/loaded/error) consumiendo soma ImageProvider. - SText + STextVirtualList: canvas-text engine portado de morfo-driven-soma como componentes separados para optimizar bundle (no se importa canvas si no se usa). - Badge: primitivo eidos puro. - Trans, FormatNumber, FormatDate, RelativeTime: 4 service components greppables alrededor de uix.langs y uix.format. Nav group "Utilities". Bugs fix arts/format/dates (engine-dates.ts): 1. Preset / per-axis conflict: Intl.DateTimeFormat rechaza dateStyle/timeStyle con cualquier per-axis (weekday/era/year/month/day/dayPeriod/hour/minute/ second/fractionalSecondDigits) o con timeZoneName. Engine drop-ea ambos presets por defecto cuando detecta conflict (regla estricta — un solo per-axis hace ilegales los dos presets). 2. Per-call locale override: formatDate/Time/DateTime aceptan tercer arg locale?: string. No muta engine state. Cache (getCachedDateFormat) keyea por (locale x options). resolveCallHourCycle: cuando hay locale override y pref auto, el ciclo horario se resuelve desde el locale OVERRIDE. 3. withHourCycle precedence: caller hourCycle/hour12 ahora gana sobre preferencia del engine. Antes spread mal ordenado sobrescribia 'h12' del caller con 'h23' del engine cuando locale resolvia a 24h. FormatDate component vuelve a rutear siempre por uix.format.dates (cache + hourCycle + locale reactivo), pasando locale como tercer arg. Demos service components autonomos del toolbar global: cada uno owns su demoLocale state con 7 chips (es-ES/en-US/en-GB/de-DE/fr-FR/ja-JP/ar-EG). FormatDate stage filtra dateStyle/timeStyle por mode + chip hourCycle anadido. Tests: 14/14 passing en engine-dates.test.ts (+6 nuevos: conflict handling, locale override, hourCycle precedence). Docs: arts/format/README.md (handoff 2026-05-25 con las 3 reglas), README.md por service component, project_service_components_sprint_2026-05-25.md, feedback_service_demos_own_locale.md (pattern reusable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ 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' },
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
{ slug: '/uix/components/qr-code', label: 'QR code' },
feat(eidos): `Barcode` — el código de barras 1D, con encoder propio y cero dependencias Ocho simbologías desde su estándar ISO publicado (Code 128 con auto-switching A/B/C, EAN-13/8, UPC-A/E, Code 39, ITF/ITF-14) en `$libs/barcode`: tablas transcritas de la especificación o derivadas de su regla de construcción, sin una sola dependencia npm añadida. JsBarcode y bwip-js son referencia de corrección, jamás import — la misma doctrina con la que `$libs/qr` usó a Nayuki. Ningún headless del mercado trae barcode: el terreno lo ocupan encoders imperativos sin anatomía ni a11y. - **La física manda sobre la ergonomía visual.** La dimensión X (`moduleWidth`) es el ancla de escaneabilidad y la altura de barra un eje libre — un escáner lee una franja horizontal. Por eso la geometría es px absolutos en el viewBox (precedente: la familia `chart`) y no el token `size` cuadrado del QR: colapsar los dos ejes en uno haría que la X dependiera de la altura, que es justo lo que no puede flotar. La zona muda va en módulos, con el mínimo de cada estándar. - **Lo que hace reconocible a la familia.** Las guardas EAN/UPC bajan hasta la línea base y los dígitos se imprimen dígito-bajo-dígito sobre su celda de 7 módulos, en mono (la HRI del estándar es OCR-B, de ancho fijo). ITF-14 lleva su barra portadora por fuera de la zona muda, que no puede comerse. - **Un valor no codificable es un ESTADO, no una excepción.** `encode` lanza un `BarcodeError` tipado; el wrapper lo captura, marca `data-invalid`, avisa por `onInvalid` (deduplicado por razón: `encoded` es un objeto nuevo por pulsación) y deja un marco placeholder decorativo. Un EAN a medio teclear es un paso normal de edición; dejarlo reventar dentro de un `$derived` tumbaría la página. - **`'text'` entra en `MorfoElement`** — y también en el schema de sium, que tiene su propia lista literal. Añadir solo la unión de TypeScript compila y falla en runtime; lo cazó `morfo:check`, no `npm run check`. Verificación: 48 tests de encoder en tres capas (invariantes estructurales de cada tabla · vectores publicados transcritos aparte · decoders independientes que releen la retícula), la lectura real confirmada con escáner por el usuario, y la retícula decodificada desde el SVG ya pintado en navegador. `component:audit` PASS con 0 errores; los 2 warnings (D-1.5 / D-4.3) son los mismos que da `button`, la demo canaria: sus regex buscan la forma v1 inline del MutationObserver y del `emit`, que la v2 movió al harness compartido. Demo v2 canónica de 9 pestañas. El alcance diferido (Codabar/MSI/Pharmacode, GS1-128, add-ons EAN-2/5, y las 2D como componentes aparte) queda registrado en `next-features.md` §10 — el hueco de numeración se cierra cuando el track de blocks commitee sus §8/§9, hoy sin commitear. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
{ slug: '/uix/components/barcode', label: 'Barcode' },
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
{ slug: '/uix/components/media-player', label: 'Media player' }
]
},
{
kind: 'group',
heading: 'Status',
items: [
feat(eidos): Card, Avatar ext, Image, SText, Badge, service components + dates engine fixes Sprint completo: - Card (eidos + sema): elevation/intent/interactive, eventos select+activate. - Avatar: ring, AvatarBadge, AvatarGroup separados; demo Avatar standalone. - Image: status surface (idle/loading/loaded/error) consumiendo soma ImageProvider. - SText + STextVirtualList: canvas-text engine portado de morfo-driven-soma como componentes separados para optimizar bundle (no se importa canvas si no se usa). - Badge: primitivo eidos puro. - Trans, FormatNumber, FormatDate, RelativeTime: 4 service components greppables alrededor de uix.langs y uix.format. Nav group "Utilities". Bugs fix arts/format/dates (engine-dates.ts): 1. Preset / per-axis conflict: Intl.DateTimeFormat rechaza dateStyle/timeStyle con cualquier per-axis (weekday/era/year/month/day/dayPeriod/hour/minute/ second/fractionalSecondDigits) o con timeZoneName. Engine drop-ea ambos presets por defecto cuando detecta conflict (regla estricta — un solo per-axis hace ilegales los dos presets). 2. Per-call locale override: formatDate/Time/DateTime aceptan tercer arg locale?: string. No muta engine state. Cache (getCachedDateFormat) keyea por (locale x options). resolveCallHourCycle: cuando hay locale override y pref auto, el ciclo horario se resuelve desde el locale OVERRIDE. 3. withHourCycle precedence: caller hourCycle/hour12 ahora gana sobre preferencia del engine. Antes spread mal ordenado sobrescribia 'h12' del caller con 'h23' del engine cuando locale resolvia a 24h. FormatDate component vuelve a rutear siempre por uix.format.dates (cache + hourCycle + locale reactivo), pasando locale como tercer arg. Demos service components autonomos del toolbar global: cada uno owns su demoLocale state con 7 chips (es-ES/en-US/en-GB/de-DE/fr-FR/ja-JP/ar-EG). FormatDate stage filtra dateStyle/timeStyle por mode + chip hourCycle anadido. Tests: 14/14 passing en engine-dates.test.ts (+6 nuevos: conflict handling, locale override, hourCycle precedence). Docs: arts/format/README.md (handoff 2026-05-25 con las 3 reglas), README.md por service component, project_service_components_sprint_2026-05-25.md, feedback_service_demos_own_locale.md (pattern reusable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/badge', label: 'Badge' },
uix(empty-state): F1.2 · componente base display (morfo+eidos) al suelo del dossier Primera pieza F1 del plan blocks (PLAN-blocks.md; alcance E-2 = suelo de paridad, dossier §P3). Ruta de 9 fases completa: - morfo: 5 partes display (provider/media/title/description/actions), scope ['eidos'], 0 eventos con justificacion pasiva (patron renderEmptyState de las refs headless), Title role:'heading', texts.label - langs: components.empty-state.label (es/en) — el indice tambien recoge la retirada foranea del import de words (inseparable por staging de archivo; coherente con la migracion palabras ya enviada; words.ts sigue en su arbol) - eidos: compound EmptyState + Media(kind icon|media, placa 2x glifo) + Title(level 2-6, default h3 — modelo Atlaskit, tamano visual desacoplado) + Description(measure 45ch) + Actions(label -> role=group +aria-label, buttonGroupLabel); recipe sobre el bundle --size-* (titulo un paso discreto arriba; sm=in-collection, lg=hero); tokens publicos minimos (gap/actions-gap/media-bg/media-fg/media-radius/description-measure) - demo v2 9 tabs (harness: SystemAxes/MotionPanel/SemaPanel; snippet con paridad; escena in-collection via Card) + entrada nav (grupo Status) - README: Baseline · Comparativa (shadcn/Chakra/Atlaskit/AntD/Polaris) · Decisiones · Passive justification · Gaps con disposicion Verificacion: component:audit PASS · eidos-lint 5 morfo-backed + 4 eidos-only sancionados · morfo:check + morfo:vocabulary verdes · recipe-css-contract/api-contract/visual-attrs 37/37 · svelte-check 76E/51W = baseline exacto (cero regresion) · navegador claro Y oscuro por estilos computados (titulo 18->24px, placa 40->64px, chips vivos, cero errores de consola). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
{ slug: '/uix/components/empty-state', label: 'Empty state' },
uix(result): F1.3 · estado terminal de flujo/pagina (morfo+eidos) al suelo del dossier Segunda pieza F1 del plan blocks (alcance E-2 = suelo de paridad, dossier §P3). Comparte el esqueleto de empty-state a proposito: EmptyState describe AUSENCIA de datos, Result reporta un RESULTADO — un lenguaje de layout, dos contratos (duplicacion consciente registrada en el README; revisable al 3er consumidor). - morfo: 6 partes display (provider/media/title/description/actions/extra; extra SIN archetype — parte genuinamente propia), scope ['eidos'], 0 eventos justificados (el resultado ya OCURRIO antes de renderizar) - status: enum de 7 = paridad AntD CON `warning` (decision firme del dossier) y HTTP renombrados semanticos (forbidden/not-found/server-error, nunca '404' stringly); default 'info'; data-status = attr eidos-only - media default: compone el mapa doctrinal IntentIcon (success→fulfill · error→threat · warning→risk) + Info de catalogo; HTTP = codigo mono grande NEUTRO aria-hidden (situaciones, no fallos — AntD jamas pinta 404 de rojo); children reemplazan el default entero (context local eidos-only con getter reactivo) - Title default h2 (vs h3 de empty-state — Result suele SER la pagina); Actions con label→role=group; Extra alineado a inicio (detalle que lee) - recipe: glifo display 3× xl bundle; tokens publicos gap/actions-gap/measures + {status}-color como forwarders de rol retintables; sin eje size (paridad AntD, gap diferido) - demo v2 9 tabs con copy por status + entrada nav (Status) + README (Comparativa · Decisiones · Passive justification · Gaps con disposicion) Verificacion: component:audit PASS 0E/0W · eidos-lint 6 morfo-backed + 5 eidos-only sancionados · recipe/api/visual-attrs 37/37 · morfo:check verde (los 7 fallos listados son deuda foranea preexistente) · svelte-check 76E/51W = baseline exacto · navegador: success=fulfill verde 84px · error=threat rojo · not-found=«404» mono neutro, copy conmutando, cero errores de consola. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
{ slug: '/uix/components/result', label: 'Result' },
uix(callout): F1.4 · admonicion inline (morfo+eidos) al suelo del dossier Tercera pieza F1 del plan blocks (alcance E-2, dossier §P3). Banner = tira de anuncio de pagina; Callout = el aside del documento (split deliberado). - morfo: 4 partes (provider role=note / icon / title / content) + texts con TITULOS DEFAULT LOCALIZADOS por intent (note/tip/warning/caution — patron GitHub de label visible: la semantica nunca viaja solo en color/icono); 0 eventos justificados (dismissible DIFERIDO a pasada soma+sema por la regla de admision — dismiss ES un evento real) - el hueco IMPORTANT resuelto con el modelo Radix, sin 5o enum: `intent` (neutral|affirm|risk|threat — mapea NOTE/TIP/WARNING/CAUTION) + `color` override SOLO bajo neutral (doctrina §4: intent evaluativo gana); IMPORTANT = intent neutral + color + titulo propio (preset en la demo) - pintura sobre la maquinaria C6/THM-2 (patron badge): forwarders por color + slots `_palette-track/text/solid` → el generador emite la cascada de 8 roles Y el forward presence-guarded al shared layer — 33 escalas y colores custom con CERO CSS extra - a11y: role=note NOMBRADO via aria-labelledby → Title SOLO mientras esta montado (registro reactivo por context local); escalacion tipada role=status|alert|none (el role=alert estatico de shadcn = bug de referencia que NO copiamos); Title NO es heading (protege el outline que escaneara anchor-nav); icono decorativo = mapa doctrinal IntentIcon - fix cazado en navegador: el `+=` del registro leia el estado dentro del tracking del $effect del hijo → effect_update_depth silencioso (contador a -997, attr nunca estampado) — untrack() en register/cleanup; leccion registrada en memoria (incidente 2 de la clase) - recipe: grid con acento logico border-inline-start (RTL-correcto), nesting tolerado (Docusaurus); demo v2 9 tabs con preset IMPORTANT + PalettePicker showIntent=false; README completo Verificacion: component:audit PASS 0E/0W · eidos-lint 8 morfo-backed/0 invalid · guards 37/37 · svelte-check 76E/51W = baseline exacto · navegador: «Nota»/«Atencion» localizados · risk=ambar hue 45-60 · IMPORTANT=plum hue 326 resuelto por shared layer · labelledby=titleId · cero errores de consola. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
{ slug: '/uix/components/callout', label: 'Callout' },
{ slug: '/uix/components/progress', label: 'Progress' },
feat(eidos): add Skeleton and Spinner loader primitives Two pure-eidos loader components with morfos at scope `['eidos']` (no soma backing, no semantic events). Skeleton — content placeholder: - 3 shapes: rect (default), circle, text (multi-line with staggered widths) - 5 sizes (xs..xl), 8 color accents - Optional shimmer animation via CSS `background-position` keyframes - Single `<div role="status" aria-busy="true">` with translated aria-label - Reduced-motion freezes animation, keeps visual Spinner — indeterminate progress: - 3 variants: ring (rotating arc), dots (bouncing), bars (stretching) - 5 sizes, 8 colors - Optional inline label via children snippet - `role="status"` + `aria-live="polite"` for SR announcements - Reduced-motion freezes animation, keeps visual Files: - morfo: skeleton.ts, spinner.ts (scope eidos, no events) - eidos: full component + types + css + langs + index per component - langs: skeleton/spinner under componentLangs barrel - demos: canonical 6-tab depth + composition examples (avatar+text card) - layout: sidebar additions under Status group Note: also includes responsive shell additions to web/routes/uix/uix.css that surfaced from an external session — same file, leaving them in. Reference libraries reviewed (Mantine, Chakra v3, Radix Themes, MUI): all expose the same shape/variant/size/color surface. shadcn Skeleton uses the same pulse animation. No surprises in our API. svelte-check: 0 errors in skeleton/spinner files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/meter', label: 'Meter' },
{ slug: '/uix/components/spinner', label: 'Spinner' },
{ slug: '/uix/components/skeleton', label: 'Skeleton' }
]
},
Build TextArea component + fix locale propagation bug across soma providers Major work in this session: 1. NumberField demo refactored to expose Intl.NumberFormatOptions live - 9 canonical presets (currency/unit/percent/compact/plain) - Locale/currency/unit lists derived from $libs/langs, $libs/currency, $format/units (no more hardcoded arrays) - Default locale from uix.format.numbers.getLocale() 2. Bug fix: locale prop ignored when soma.format service is active - soma/NumberField: 5 bypass points via new `hasOwnLocale` getter (decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue) - soma/Calendar: dateOrder respects locale prop (matches DateField precedence) - soma/RangeCalendar: same dateOrder fix - When the consumer passes `locale` prop, soma.nums/dates is bypassed and Intl is used directly with the prop locale 3. TextArea — new component, full 4-layer stack - morfo: 3 parts (provider/input/count), 3 events (commit-input/commit-submit/shift-count-overflow), APG textbox - soma: TextAreaProvider with autosize (scrollHeight measurement), submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp, Field integration (OR-merge), counter overflow tracking - eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize - sema: commit-submit + shift-count-overflow perceptual cues - demo: canonical 6-tab depth, all props exposed as live controls - PrimitiveTextareaAttributes added to soma/types/html.ts 4. Also includes accumulated unreleased work from prior sessions: - Eidos ports: announce, clipboard, drag-drop, feed, table, tree-grid, tree-view, virtual-grid, virtual-list, words - Their canonical demos under web/routes/uix/components/ - Semantic events defined for utility morfos (announce/clipboard/virtual-list/virtual-grid) - Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients) - Sidebar reorganized with Data + Utilities groups - Eidos words content layer and primitives Reference library survey performed before TextArea API design (Mantine, Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime). Decision documented: no separate CurrencyField/UnitField — single NumberField with formatOptions is the canon (matches all surveyed headless libs except PrimeReact's mode enum, which still uses 1 component). svelte-check: 0 errors in modified/created files. See CONTINUE.md for full session notes and next steps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{
kind: 'group',
heading: 'Data',
items: [
{ slug: '/uix/components/metrics', label: 'Metrics' },
Build TextArea component + fix locale propagation bug across soma providers Major work in this session: 1. NumberField demo refactored to expose Intl.NumberFormatOptions live - 9 canonical presets (currency/unit/percent/compact/plain) - Locale/currency/unit lists derived from $libs/langs, $libs/currency, $format/units (no more hardcoded arrays) - Default locale from uix.format.numbers.getLocale() 2. Bug fix: locale prop ignored when soma.format service is active - soma/NumberField: 5 bypass points via new `hasOwnLocale` getter (decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue) - soma/Calendar: dateOrder respects locale prop (matches DateField precedence) - soma/RangeCalendar: same dateOrder fix - When the consumer passes `locale` prop, soma.nums/dates is bypassed and Intl is used directly with the prop locale 3. TextArea — new component, full 4-layer stack - morfo: 3 parts (provider/input/count), 3 events (commit-input/commit-submit/shift-count-overflow), APG textbox - soma: TextAreaProvider with autosize (scrollHeight measurement), submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp, Field integration (OR-merge), counter overflow tracking - eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize - sema: commit-submit + shift-count-overflow perceptual cues - demo: canonical 6-tab depth, all props exposed as live controls - PrimitiveTextareaAttributes added to soma/types/html.ts 4. Also includes accumulated unreleased work from prior sessions: - Eidos ports: announce, clipboard, drag-drop, feed, table, tree-grid, tree-view, virtual-grid, virtual-list, words - Their canonical demos under web/routes/uix/components/ - Semantic events defined for utility morfos (announce/clipboard/virtual-list/virtual-grid) - Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients) - Sidebar reorganized with Data + Utilities groups - Eidos words content layer and primitives Reference library survey performed before TextArea API design (Mantine, Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime). Decision documented: no separate CurrencyField/UnitField — single NumberField with formatOptions is the canon (matches all surveyed headless libs except PrimeReact's mode enum, which still uses 1 component). svelte-check: 0 errors in modified/created files. See CONTINUE.md for full session notes and next steps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/table', label: 'Table' },
{ slug: '/uix/components/tree-view', label: 'Tree view' },
{ slug: '/uix/components/tree-grid', label: 'Tree grid' },
uix(chat): bloque de mensajería chat-* v1 (log · message · composer · typing) Familia chat-* completa, componiendo el ecosistema (Feed + VirtualList anclado, Textarea, FileUpload, Avatar, Popover, Icon). Transporte-agnóstica: el modelo de datos es de la app, los componentes reciben props + snippets. - chat-log: Feed + VirtualList(anchor:'end') en triple registro; day separators, unread divider, filas de sistema (Feed.Article), pill "N nuevos" + notifyIncoming (announce a live-region), to-latest distance-aware. - chat-message: raíz Feed.Article; runs ($libs/chat), ticks de entrega, reply embebido, reacciones (chip pressed + tapback panel), acciones hover. - chat-composer: Textarea (doble registro) + FileUpload; teclado Enter/Shift+Enter/Esc/ArrowUp; typing saliente debounced (uix.timers); barra reply/edit. - chat-typing: sustain-streaming (visual-only, loop del sistema); aggregateTypers 1/2/3+ con plantillas plurales. - $libs/chat: groupIntoRuns (ventana 8 min estilo Discord) + aggregateTypers. - VirtualList: extensión anchor:'end' (followOutput, prepend por clave estable, sticky monótono, onAtEndChange) — sin scroller paralelo. Diseño de referencia (WhatsApp/Telegram, pase lado-a-lado): burbujas suaves (out = primary-element + texto normal; in = surface card + sombra) sobre lienzo muted del log, meta dentro de la burbuja, tapback bar horizontal, iconos lucide del ecosistema (nada de SVG a mano). Contrato de tamaños: size="md" = 16px canónico. Fixes de framework (afectaban más allá de chat): - EngineLangs.t() ahora pela el prefijo '#?': los idlangrefs imperativos (pagination/file-upload/chronos/chat) caían al fallback EN en locale es. Fix parseLangRef ?? parsePathFallback + test de regresión (132/132). - virtual-list commit-set-resize con channels:[]: es bookkeeping de ResizeObserver, no gesto → dejaba de emitir beep/vibrate pre-interacción (interventions de navigator.vibrate / AudioContext). Escaparate messenger en /uix/demos/chat (bot de eco con uix.timers; variante $connection señalada). Gaps v2 registrados en next-features §7. Pase: component:audit 4×PASS · eidos-lint invalid=0 · recipe-contract 29/29 · Playwright escaparate PASS end-to-end · consola limpia · npm run check en baseline (76, cero errores nuevos). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
{ slug: '/uix/components/chat-composer', label: 'Chat composer' },
uix(chat): v2 · chat-list — panel de conversaciones (compone Listbox) Componente nuevo del bloque chat v2: el panel de lista de salas/DMs. Compone el Listbox del ecosistema (doble registro id/ref compartido): selección única + roving-tabindex + typeahead + role=option vienen de Listbox; chat-list aporta la disposición de fila (avatar · título/preview · meta) y el estado visual data-unread (negrita + Badge compuesto). - morfo: expression 'delegated', sin eventos propios (D.1 — el actor es el Listbox compuesto); parts provider/item(data-unread)/avatar/title/preview/meta. - soma: ChatListProvider crea ListboxProvider dentro (value string <-> string[]); ChatListItemProvider per-instance (unread = boolean crudo); 4 clases de contenido explicitas. - eidos: rejilla avatar | (titulo/preview) | meta, avatar y meta abarcan las dos filas; truncado; unread -> titulo en negrita. No neutraliza el arquetipo item (la conversacion ES una opcion seleccionable). Sin @keyframes; ejes logicos. - demo: 5 salas, seleccion limpia unread, typing-en-lista (compone ChatTyping), Badge de no-leidos; tabs live/system/api/morfo/a11y. - READMEs dobles (eidos con Passive justification, delegated); next-features 7 marca chat-list SHIPPED. Verificacion: check baseline 76 (0 nuevos) · component:audit PASS · eidos-lint invalid=0 · recipe-contract 30/30 · morfo:vocabulary 0 · Playwright PASS (role=listbox, 5 salas, aria-selected, unread=2, badges=2, typing-en-lista, click + teclado seleccionan). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
{ slug: '/uix/components/chat-list', label: 'Chat list' },
uix(chat): bloque de mensajería chat-* v1 (log · message · composer · typing) Familia chat-* completa, componiendo el ecosistema (Feed + VirtualList anclado, Textarea, FileUpload, Avatar, Popover, Icon). Transporte-agnóstica: el modelo de datos es de la app, los componentes reciben props + snippets. - chat-log: Feed + VirtualList(anchor:'end') en triple registro; day separators, unread divider, filas de sistema (Feed.Article), pill "N nuevos" + notifyIncoming (announce a live-region), to-latest distance-aware. - chat-message: raíz Feed.Article; runs ($libs/chat), ticks de entrega, reply embebido, reacciones (chip pressed + tapback panel), acciones hover. - chat-composer: Textarea (doble registro) + FileUpload; teclado Enter/Shift+Enter/Esc/ArrowUp; typing saliente debounced (uix.timers); barra reply/edit. - chat-typing: sustain-streaming (visual-only, loop del sistema); aggregateTypers 1/2/3+ con plantillas plurales. - $libs/chat: groupIntoRuns (ventana 8 min estilo Discord) + aggregateTypers. - VirtualList: extensión anchor:'end' (followOutput, prepend por clave estable, sticky monótono, onAtEndChange) — sin scroller paralelo. Diseño de referencia (WhatsApp/Telegram, pase lado-a-lado): burbujas suaves (out = primary-element + texto normal; in = surface card + sombra) sobre lienzo muted del log, meta dentro de la burbuja, tapback bar horizontal, iconos lucide del ecosistema (nada de SVG a mano). Contrato de tamaños: size="md" = 16px canónico. Fixes de framework (afectaban más allá de chat): - EngineLangs.t() ahora pela el prefijo '#?': los idlangrefs imperativos (pagination/file-upload/chronos/chat) caían al fallback EN en locale es. Fix parseLangRef ?? parsePathFallback + test de regresión (132/132). - virtual-list commit-set-resize con channels:[]: es bookkeeping de ResizeObserver, no gesto → dejaba de emitir beep/vibrate pre-interacción (interventions de navigator.vibrate / AudioContext). Escaparate messenger en /uix/demos/chat (bot de eco con uix.timers; variante $connection señalada). Gaps v2 registrados en next-features §7. Pase: component:audit 4×PASS · eidos-lint invalid=0 · recipe-contract 29/29 · Playwright escaparate PASS end-to-end · consola limpia · npm run check en baseline (76, cero errores nuevos). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
{ slug: '/uix/components/chat-log', label: 'Chat log' },
{ slug: '/uix/components/chat-message', label: 'Chat message' },
{ slug: '/uix/components/chat-typing', label: 'Chat typing' },
uix(chat): v2 · emoji-picker (picker completo) + escaparate rico del bloque Nuevo componente emoji-picker y rediseño del escaparate de mensajeria. emoji-picker (ruta 9 fases): - Compone Popover (open/close/foco/portal) + Command (busqueda + grid + nav de teclado + a11y combobox/listbox, su "grid mode" documentado) + dos ToggleGroup (categorias + tono de piel). Delegated: sin eventos propios (D.1); el grid y las celdas doble-registran Command.List/Item, conservando la a11y pero con contrato visual propio (gana especificidad con selectores de 2 niveles, sin tocar attrs [data-command-*] ajenos — eidos-lint limpio). - Datos: $libs/emoji, dataset vendorizado de emojibase (MIT) horneado por scripts/generate-emoji-data.ts (precedente lucide: JSON crudo en static/ + script generador). ~1906 emoji con nombre CLDR + keywords, 316 con tono de piel single-person. Grid acotado (categoria activa o resultados de busqueda), sin virtualizacion (refinamiento v2). Helpers puros + test (12/12). - morfo delegated (7 partes + header/footer de layout); soma (root + trigger + content + grid + option + preview, provider-test 4/4); eidos (panel empaquetado, receta con tokens --emoji-picker-*); langs es/en; demo 5 tabs (excepcion delegated documentada); READMEs dobles. Escaparate del bloque (web/routes/uix/demos/chat) — rehecho rico y correcto: - Avatares por-autor con color solido (Avatar del ecosistema): antes gris neutro indefinido, ahora teal/ambar/verde/indigo/primary bien contrastados. - Dos columnas: chat-list (sidebar con badges + typing-en-lista) + sala. - Conversacion rica: separador de dia, run agrupado, reaccion, reply citado, recibos de lectura, typing con avatar, y el emoji-picker integrado en el composer (inserta en el draft). - Solape de burbujas (transitorio, estimateSize=48 con mensajes ~90-126px) mitigado subiendo el estimate a 76. Auditoria de tamanos/espacios: recipes del bloque limpios (todo con tokens --space-*/--size-*/--color-*); unico literal (punto tono-capable) tokenizado. Verificacion: check baseline 76 (0 nuevos) · component:audit emoji-picker PASS · eidos-lint invalid=0 · recipe-contract 30/30 · morfo:vocabulary 0 · emoji lib 12/12 · provider-test 4/4 · Playwright PASS (busqueda 'rocket'->rocket+ astronautas, click->onSelect+cierra, teclado, tono oscuro 👋->👋🏿, picker abre en el composer). Capturas claro miradas. next-features §7 marca emoji-picker SHIPPED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
{ slug: '/uix/components/emoji-picker', label: 'Emoji picker' },
Build TextArea component + fix locale propagation bug across soma providers Major work in this session: 1. NumberField demo refactored to expose Intl.NumberFormatOptions live - 9 canonical presets (currency/unit/percent/compact/plain) - Locale/currency/unit lists derived from $libs/langs, $libs/currency, $format/units (no more hardcoded arrays) - Default locale from uix.format.numbers.getLocale() 2. Bug fix: locale prop ignored when soma.format service is active - soma/NumberField: 5 bypass points via new `hasOwnLocale` getter (decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue) - soma/Calendar: dateOrder respects locale prop (matches DateField precedence) - soma/RangeCalendar: same dateOrder fix - When the consumer passes `locale` prop, soma.nums/dates is bypassed and Intl is used directly with the prop locale 3. TextArea — new component, full 4-layer stack - morfo: 3 parts (provider/input/count), 3 events (commit-input/commit-submit/shift-count-overflow), APG textbox - soma: TextAreaProvider with autosize (scrollHeight measurement), submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp, Field integration (OR-merge), counter overflow tracking - eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize - sema: commit-submit + shift-count-overflow perceptual cues - demo: canonical 6-tab depth, all props exposed as live controls - PrimitiveTextareaAttributes added to soma/types/html.ts 4. Also includes accumulated unreleased work from prior sessions: - Eidos ports: announce, clipboard, drag-drop, feed, table, tree-grid, tree-view, virtual-grid, virtual-list, words - Their canonical demos under web/routes/uix/components/ - Semantic events defined for utility morfos (announce/clipboard/virtual-list/virtual-grid) - Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients) - Sidebar reorganized with Data + Utilities groups - Eidos words content layer and primitives Reference library survey performed before TextArea API design (Mantine, Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime). Decision documented: no separate CurrencyField/UnitField — single NumberField with formatOptions is the canon (matches all surveyed headless libs except PrimeReact's mode enum, which still uses 1 component). svelte-check: 0 errors in modified/created files. See CONTINUE.md for full session notes and next steps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/feed', label: 'Feed' },
{ slug: '/uix/components/timeline', label: 'Timeline' },
Build TextArea component + fix locale propagation bug across soma providers Major work in this session: 1. NumberField demo refactored to expose Intl.NumberFormatOptions live - 9 canonical presets (currency/unit/percent/compact/plain) - Locale/currency/unit lists derived from $libs/langs, $libs/currency, $format/units (no more hardcoded arrays) - Default locale from uix.format.numbers.getLocale() 2. Bug fix: locale prop ignored when soma.format service is active - soma/NumberField: 5 bypass points via new `hasOwnLocale` getter (decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue) - soma/Calendar: dateOrder respects locale prop (matches DateField precedence) - soma/RangeCalendar: same dateOrder fix - When the consumer passes `locale` prop, soma.nums/dates is bypassed and Intl is used directly with the prop locale 3. TextArea — new component, full 4-layer stack - morfo: 3 parts (provider/input/count), 3 events (commit-input/commit-submit/shift-count-overflow), APG textbox - soma: TextAreaProvider with autosize (scrollHeight measurement), submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp, Field integration (OR-merge), counter overflow tracking - eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize - sema: commit-submit + shift-count-overflow perceptual cues - demo: canonical 6-tab depth, all props exposed as live controls - PrimitiveTextareaAttributes added to soma/types/html.ts 4. Also includes accumulated unreleased work from prior sessions: - Eidos ports: announce, clipboard, drag-drop, feed, table, tree-grid, tree-view, virtual-grid, virtual-list, words - Their canonical demos under web/routes/uix/components/ - Semantic events defined for utility morfos (announce/clipboard/virtual-list/virtual-grid) - Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients) - Sidebar reorganized with Data + Utilities groups - Eidos words content layer and primitives Reference library survey performed before TextArea API design (Mantine, Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime). Decision documented: no separate CurrencyField/UnitField — single NumberField with formatOptions is the canon (matches all surveyed headless libs except PrimeReact's mode enum, which still uses 1 component). svelte-check: 0 errors in modified/created files. See CONTINUE.md for full session notes and next steps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ 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' }
]
},
Build TextArea component + fix locale propagation bug across soma providers Major work in this session: 1. NumberField demo refactored to expose Intl.NumberFormatOptions live - 9 canonical presets (currency/unit/percent/compact/plain) - Locale/currency/unit lists derived from $libs/langs, $libs/currency, $format/units (no more hardcoded arrays) - Default locale from uix.format.numbers.getLocale() 2. Bug fix: locale prop ignored when soma.format service is active - soma/NumberField: 5 bypass points via new `hasOwnLocale` getter (decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue) - soma/Calendar: dateOrder respects locale prop (matches DateField precedence) - soma/RangeCalendar: same dateOrder fix - When the consumer passes `locale` prop, soma.nums/dates is bypassed and Intl is used directly with the prop locale 3. TextArea — new component, full 4-layer stack - morfo: 3 parts (provider/input/count), 3 events (commit-input/commit-submit/shift-count-overflow), APG textbox - soma: TextAreaProvider with autosize (scrollHeight measurement), submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp, Field integration (OR-merge), counter overflow tracking - eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize - sema: commit-submit + shift-count-overflow perceptual cues - demo: canonical 6-tab depth, all props exposed as live controls - PrimitiveTextareaAttributes added to soma/types/html.ts 4. Also includes accumulated unreleased work from prior sessions: - Eidos ports: announce, clipboard, drag-drop, feed, table, tree-grid, tree-view, virtual-grid, virtual-list, words - Their canonical demos under web/routes/uix/components/ - Semantic events defined for utility morfos (announce/clipboard/virtual-list/virtual-grid) - Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients) - Sidebar reorganized with Data + Utilities groups - Eidos words content layer and primitives Reference library survey performed before TextArea API design (Mantine, Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime). Decision documented: no separate CurrencyField/UnitField — single NumberField with formatOptions is the canon (matches all surveyed headless libs except PrimeReact's mode enum, which still uses 1 component). svelte-check: 0 errors in modified/created files. See CONTINUE.md for full session notes and next steps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{
kind: 'group',
heading: 'Utilities',
items: [
{ slug: '/uix/components/announce', label: 'Announce' },
{ slug: '/uix/components/clipboard', label: 'Clipboard' },
feat(eidos): Card, Avatar ext, Image, SText, Badge, service components + dates engine fixes Sprint completo: - Card (eidos + sema): elevation/intent/interactive, eventos select+activate. - Avatar: ring, AvatarBadge, AvatarGroup separados; demo Avatar standalone. - Image: status surface (idle/loading/loaded/error) consumiendo soma ImageProvider. - SText + STextVirtualList: canvas-text engine portado de morfo-driven-soma como componentes separados para optimizar bundle (no se importa canvas si no se usa). - Badge: primitivo eidos puro. - Trans, FormatNumber, FormatDate, RelativeTime: 4 service components greppables alrededor de uix.langs y uix.format. Nav group "Utilities". Bugs fix arts/format/dates (engine-dates.ts): 1. Preset / per-axis conflict: Intl.DateTimeFormat rechaza dateStyle/timeStyle con cualquier per-axis (weekday/era/year/month/day/dayPeriod/hour/minute/ second/fractionalSecondDigits) o con timeZoneName. Engine drop-ea ambos presets por defecto cuando detecta conflict (regla estricta — un solo per-axis hace ilegales los dos presets). 2. Per-call locale override: formatDate/Time/DateTime aceptan tercer arg locale?: string. No muta engine state. Cache (getCachedDateFormat) keyea por (locale x options). resolveCallHourCycle: cuando hay locale override y pref auto, el ciclo horario se resuelve desde el locale OVERRIDE. 3. withHourCycle precedence: caller hourCycle/hour12 ahora gana sobre preferencia del engine. Antes spread mal ordenado sobrescribia 'h12' del caller con 'h23' del engine cuando locale resolvia a 24h. FormatDate component vuelve a rutear siempre por uix.format.dates (cache + hourCycle + locale reactivo), pasando locale como tercer arg. Demos service components autonomos del toolbar global: cada uno owns su demoLocale state con 7 chips (es-ES/en-US/en-GB/de-DE/fr-FR/ja-JP/ar-EG). FormatDate stage filtra dateStyle/timeStyle por mode + chip hourCycle anadido. Tests: 14/14 passing en engine-dates.test.ts (+6 nuevos: conflict handling, locale override, hourCycle precedence). Docs: arts/format/README.md (handoff 2026-05-25 con las 3 reglas), README.md por service component, project_service_components_sprint_2026-05-25.md, feedback_service_demos_own_locale.md (pattern reusable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/drag-drop', label: 'Drag & drop' },
feat(proof-of-human): re-architect to orchestrator + pluggable challenge (PathTrace) Split the anti-bot component into a purpose- and interaction-agnostic orchestrator that owns the lifecycle stages + verdict, and a pluggable challenge that runs the interaction and reports up via ProofOfHumanChallengeHost. The primary challenge, PathTrace, has the user follow a random curved path by pointer OR arrow keys. This fixes the drag version's two real defects: - keyboard bypass: the accessible path now yields genuine, non-replayable behavioural signal (pathAdherence over a per-instance random curve); a two-key script can't solve it. - over-coupled abstraction: interaction + animation are decoupled from the component identity (per-stage consumer slots; challenge is pluggable). Layers: - morfo: proof-of-human slimmed to orchestrator (Provider + 3 commits); new path-trace morfo (Provider + Track + Token; handle-*). - soma: orchestrator + ProofOfHumanChallengeHost; new path-trace (seeded Catmull-Rom curve, corridor-gated monotonic progress, adherence telemetry, pointer + arrow-key input). Old drag Token/Target wrappers retired. - sema: verdict commits (proof-of-human) + trace handle earcons (path-trace). - eidos: orchestrator + PathTrace that paints the curve/corridor/trail. - demo rebuilt: carrier-pigeon skin over the per-stage slots. Auto-focuses the token when the challenge is armed so the arrow keys start the trace immediately (gated on the disabled->enabled transition). Verified: 32/32 tests (incl. arrow trace completes with real signal + two-key script can't solve, in real chromium) + visual in real Chrome (curve, failed, verified; scorer rejects instant traces). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
{ slug: '/uix/components/proof-of-human', label: 'Proof of human' },
feat(eidos): Card, Avatar ext, Image, SText, Badge, service components + dates engine fixes Sprint completo: - Card (eidos + sema): elevation/intent/interactive, eventos select+activate. - Avatar: ring, AvatarBadge, AvatarGroup separados; demo Avatar standalone. - Image: status surface (idle/loading/loaded/error) consumiendo soma ImageProvider. - SText + STextVirtualList: canvas-text engine portado de morfo-driven-soma como componentes separados para optimizar bundle (no se importa canvas si no se usa). - Badge: primitivo eidos puro. - Trans, FormatNumber, FormatDate, RelativeTime: 4 service components greppables alrededor de uix.langs y uix.format. Nav group "Utilities". Bugs fix arts/format/dates (engine-dates.ts): 1. Preset / per-axis conflict: Intl.DateTimeFormat rechaza dateStyle/timeStyle con cualquier per-axis (weekday/era/year/month/day/dayPeriod/hour/minute/ second/fractionalSecondDigits) o con timeZoneName. Engine drop-ea ambos presets por defecto cuando detecta conflict (regla estricta — un solo per-axis hace ilegales los dos presets). 2. Per-call locale override: formatDate/Time/DateTime aceptan tercer arg locale?: string. No muta engine state. Cache (getCachedDateFormat) keyea por (locale x options). resolveCallHourCycle: cuando hay locale override y pref auto, el ciclo horario se resuelve desde el locale OVERRIDE. 3. withHourCycle precedence: caller hourCycle/hour12 ahora gana sobre preferencia del engine. Antes spread mal ordenado sobrescribia 'h12' del caller con 'h23' del engine cuando locale resolvia a 24h. FormatDate component vuelve a rutear siempre por uix.format.dates (cache + hourCycle + locale reactivo), pasando locale como tercer arg. Demos service components autonomos del toolbar global: cada uno owns su demoLocale state con 7 chips (es-ES/en-US/en-GB/de-DE/fr-FR/ja-JP/ar-EG). FormatDate stage filtra dateStyle/timeStyle por mode + chip hourCycle anadido. Tests: 14/14 passing en engine-dates.test.ts (+6 nuevos: conflict handling, locale override, hourCycle precedence). Docs: arts/format/README.md (handoff 2026-05-25 con las 3 reglas), README.md por service component, project_service_components_sprint_2026-05-25.md, feedback_service_demos_own_locale.md (pattern reusable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ 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' }
Build TextArea component + fix locale propagation bug across soma providers Major work in this session: 1. NumberField demo refactored to expose Intl.NumberFormatOptions live - 9 canonical presets (currency/unit/percent/compact/plain) - Locale/currency/unit lists derived from $libs/langs, $libs/currency, $format/units (no more hardcoded arrays) - Default locale from uix.format.numbers.getLocale() 2. Bug fix: locale prop ignored when soma.format service is active - soma/NumberField: 5 bypass points via new `hasOwnLocale` getter (decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue) - soma/Calendar: dateOrder respects locale prop (matches DateField precedence) - soma/RangeCalendar: same dateOrder fix - When the consumer passes `locale` prop, soma.nums/dates is bypassed and Intl is used directly with the prop locale 3. TextArea — new component, full 4-layer stack - morfo: 3 parts (provider/input/count), 3 events (commit-input/commit-submit/shift-count-overflow), APG textbox - soma: TextAreaProvider with autosize (scrollHeight measurement), submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp, Field integration (OR-merge), counter overflow tracking - eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize - sema: commit-submit + shift-count-overflow perceptual cues - demo: canonical 6-tab depth, all props exposed as live controls - PrimitiveTextareaAttributes added to soma/types/html.ts 4. Also includes accumulated unreleased work from prior sessions: - Eidos ports: announce, clipboard, drag-drop, feed, table, tree-grid, tree-view, virtual-grid, virtual-list, words - Their canonical demos under web/routes/uix/components/ - Semantic events defined for utility morfos (announce/clipboard/virtual-list/virtual-grid) - Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients) - Sidebar reorganized with Data + Utilities groups - Eidos words content layer and primitives Reference library survey performed before TextArea API design (Mantine, Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime). Decision documented: no separate CurrencyField/UnitField — single NumberField with formatOptions is the canon (matches all surveyed headless libs except PrimeReact's mode enum, which still uses 1 component). svelte-check: 0 errors in modified/created files. See CONTINUE.md for full session notes and next steps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
]
},
{
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' }
]
},
feat(typography): Text — POC consuming new --style-* foundation tokens First typography primitive consuming the named-style tokens added in `015bd761`. Sets the canon for the remaining typography ports (Heading, Display, Code, CodeBlock, Kbd, Mark, Highlight, Link). API: - `style: 'body' | 'prose' | 'label' | 'caption'` (default `body`). Selects which `--style-{name}-*` token set the recipe consumes. - Per-axis overrides via `size`, `family`, `weight`, `color`, `align`. Each writes a local `--_text-{axis}` CSS variable that wins over the named style's default via the cascade. - `italic`, `underline` shorthand booleans. - `truncate` (single-line ellipsis) + `clamp={N}` (multi-line via CSS `-webkit-line-clamp`). - `as` prop switches the tag (default `'span'` — inline composable). - `tagStyle` escape hatch for one-off inline CSS that the prop surface doesn't cover (separate from the `style` named-style prop). Architectural choices documented in README: - Sizes map to eidos `xxs..xxxl` (not air's `1..12`). - Families use the eidos slot names (`primary/secondary/mono/display`) not air's domain aliases (`ui/prose/code`) — coherence with the foundation contract. - Colors are restricted to the content-role slots (`--color-content-*`) to preserve contrast guarantees. Free colors via `tagStyle`. - Canvas line-count measurement (air's `text` + bindable `lineCount`) deferred; CSS line-clamp covers the visual-truncate use case. Sidebar nav gains a `Typography` group with Text as its first entry. `npm run component:audit`: 82 / 82 PASS, 0 NEEDS-WORK. svelte-check 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{
kind: 'group',
heading: 'Typography',
feat(typography): 8 primitives — Heading, Display, Code, CodeBlock, Kbd, Mark, Highlight, Link Full canon for the rest of the typography family, following Text (commit `03531709`) as template. Each primitive ships morfo (passive, scope eidos, 0 events) + full eidos set (svelte/css/types/index + README with required sections) + canon 6-tab demo (≥400 lines each). **Heading** — semantic h1..h6 via `level` prop, visual style decoupled via `style` prop (defaults to match level but `<Heading level={3} style="h1">` allowed). Demo 594 lines. **Display** — always-h1 page-title primitive. Always uses `style="hero"`. Tracking via literal em values until foundation emits hero-scale tracking tokens. Demo 546 lines. **Code** — inline code via `<code>`. `variant: 'plain' | 'soft' | 'outline'` for subtle background chrome. Demo 423 lines. **CodeBlock** — multi-line `<pre><code>` with optional title + copy button (button utility, NOT a sema verb — kept out of morfo per 2-of-3 rule). Demo 402 lines. **Kbd** — keyboard shortcut chrome over `style="code"`. Demo 418 lines. **Mark** — text highlight via `<mark>`. Palette uses canonical `ColorRole` intents (default `fulfill` ≈ legacy yellow). Demo 402 lines. **Highlight** — substring highlighter inside Text. Scans children for `query` and wraps matches. Same intent palette as Mark. Demo 407 lines. **Link** — text link via `<a>`. `variant: 'default' | 'subtle' | 'plain'` for underline style, `intent: ColorRole` for color. Passive on the morfo — hover/active is CSS-only, navigation belongs to the consumer (router). Demo 448 lines. **Architectural deviations from air documented per README's Decisiones section:** - Heading: semantic level vs visual style decoupling - Code split into inline `<Code>` + block `<CodeBlock>` (was one component with `variant='block'` in air) - Mark/Highlight palette swapped from literal hue names to canonical ColorRole intents (theme contract) - Link palette opened to full ColorRole; `quiet`/`emphasized` air variants dropped in favour of composable patterns `npm run component:audit`: **90 / 90 PASS**, 0 NEEDS-WORK. svelte-check: 0 errors, 0 new warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
items: [
{ slug: '/uix/components/text', label: 'Text' },
feat(eidos): Card, Avatar ext, Image, SText, Badge, service components + dates engine fixes Sprint completo: - Card (eidos + sema): elevation/intent/interactive, eventos select+activate. - Avatar: ring, AvatarBadge, AvatarGroup separados; demo Avatar standalone. - Image: status surface (idle/loading/loaded/error) consumiendo soma ImageProvider. - SText + STextVirtualList: canvas-text engine portado de morfo-driven-soma como componentes separados para optimizar bundle (no se importa canvas si no se usa). - Badge: primitivo eidos puro. - Trans, FormatNumber, FormatDate, RelativeTime: 4 service components greppables alrededor de uix.langs y uix.format. Nav group "Utilities". Bugs fix arts/format/dates (engine-dates.ts): 1. Preset / per-axis conflict: Intl.DateTimeFormat rechaza dateStyle/timeStyle con cualquier per-axis (weekday/era/year/month/day/dayPeriod/hour/minute/ second/fractionalSecondDigits) o con timeZoneName. Engine drop-ea ambos presets por defecto cuando detecta conflict (regla estricta — un solo per-axis hace ilegales los dos presets). 2. Per-call locale override: formatDate/Time/DateTime aceptan tercer arg locale?: string. No muta engine state. Cache (getCachedDateFormat) keyea por (locale x options). resolveCallHourCycle: cuando hay locale override y pref auto, el ciclo horario se resuelve desde el locale OVERRIDE. 3. withHourCycle precedence: caller hourCycle/hour12 ahora gana sobre preferencia del engine. Antes spread mal ordenado sobrescribia 'h12' del caller con 'h23' del engine cuando locale resolvia a 24h. FormatDate component vuelve a rutear siempre por uix.format.dates (cache + hourCycle + locale reactivo), pasando locale como tercer arg. Demos service components autonomos del toolbar global: cada uno owns su demoLocale state con 7 chips (es-ES/en-US/en-GB/de-DE/fr-FR/ja-JP/ar-EG). FormatDate stage filtra dateStyle/timeStyle por mode + chip hourCycle anadido. Tests: 14/14 passing en engine-dates.test.ts (+6 nuevos: conflict handling, locale override, hourCycle precedence). Docs: arts/format/README.md (handoff 2026-05-25 con las 3 reglas), README.md por service component, project_service_components_sprint_2026-05-25.md, feedback_service_demos_own_locale.md (pattern reusable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/s-text', label: 'SText' },
{ slug: '/uix/components/s-text-virtual-list', label: 'STextVirtualList' },
feat(typography): 8 primitives — Heading, Display, Code, CodeBlock, Kbd, Mark, Highlight, Link Full canon for the rest of the typography family, following Text (commit `03531709`) as template. Each primitive ships morfo (passive, scope eidos, 0 events) + full eidos set (svelte/css/types/index + README with required sections) + canon 6-tab demo (≥400 lines each). **Heading** — semantic h1..h6 via `level` prop, visual style decoupled via `style` prop (defaults to match level but `<Heading level={3} style="h1">` allowed). Demo 594 lines. **Display** — always-h1 page-title primitive. Always uses `style="hero"`. Tracking via literal em values until foundation emits hero-scale tracking tokens. Demo 546 lines. **Code** — inline code via `<code>`. `variant: 'plain' | 'soft' | 'outline'` for subtle background chrome. Demo 423 lines. **CodeBlock** — multi-line `<pre><code>` with optional title + copy button (button utility, NOT a sema verb — kept out of morfo per 2-of-3 rule). Demo 402 lines. **Kbd** — keyboard shortcut chrome over `style="code"`. Demo 418 lines. **Mark** — text highlight via `<mark>`. Palette uses canonical `ColorRole` intents (default `fulfill` ≈ legacy yellow). Demo 402 lines. **Highlight** — substring highlighter inside Text. Scans children for `query` and wraps matches. Same intent palette as Mark. Demo 407 lines. **Link** — text link via `<a>`. `variant: 'default' | 'subtle' | 'plain'` for underline style, `intent: ColorRole` for color. Passive on the morfo — hover/active is CSS-only, navigation belongs to the consumer (router). Demo 448 lines. **Architectural deviations from air documented per README's Decisiones section:** - Heading: semantic level vs visual style decoupling - Code split into inline `<Code>` + block `<CodeBlock>` (was one component with `variant='block'` in air) - Mark/Highlight palette swapped from literal hue names to canonical ColorRole intents (theme contract) - Link palette opened to full ColorRole; `quiet`/`emphasized` air variants dropped in favour of composable patterns `npm run component:audit`: **90 / 90 PASS**, 0 NEEDS-WORK. svelte-check: 0 errors, 0 new warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ 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' }
]
feat(typography): Text — POC consuming new --style-* foundation tokens First typography primitive consuming the named-style tokens added in `015bd761`. Sets the canon for the remaining typography ports (Heading, Display, Code, CodeBlock, Kbd, Mark, Highlight, Link). API: - `style: 'body' | 'prose' | 'label' | 'caption'` (default `body`). Selects which `--style-{name}-*` token set the recipe consumes. - Per-axis overrides via `size`, `family`, `weight`, `color`, `align`. Each writes a local `--_text-{axis}` CSS variable that wins over the named style's default via the cascade. - `italic`, `underline` shorthand booleans. - `truncate` (single-line ellipsis) + `clamp={N}` (multi-line via CSS `-webkit-line-clamp`). - `as` prop switches the tag (default `'span'` — inline composable). - `tagStyle` escape hatch for one-off inline CSS that the prop surface doesn't cover (separate from the `style` named-style prop). Architectural choices documented in README: - Sizes map to eidos `xxs..xxxl` (not air's `1..12`). - Families use the eidos slot names (`primary/secondary/mono/display`) not air's domain aliases (`ui/prose/code`) — coherence with the foundation contract. - Colors are restricted to the content-role slots (`--color-content-*`) to preserve contrast guarantees. Free colors via `tagStyle`. - Canvas line-count measurement (air's `text` + bindable `lineCount`) deferred; CSS line-clamp covers the visual-truncate use case. Sidebar nav gains a `Typography` group with Text as its first entry. `npm run component:audit`: 82 / 82 PASS, 0 NEEDS-WORK. svelte-check 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
},
fix(demos): reachable nav + real interactive Ambient testbed + visible fog/mesh The catalog work shipped but was unreachable and impoverished: - **Nav**: the sidebar IA (web/routes/uix/+layout@.svelte) is a manual list; the new components were never added, so nothing linked to them. Add a "Text effects" group (TextGradient/Circular/Blur/Focus/Scramble), CountUp under Utilities, and a "Packs" group (Ambient · backgrounds). - **Ambient demo → real testbed**: rebuild web/routes/uix/packs/ambient as a proper interactive page — effect selector (32, grouped Agentic/Tier A/B), live param controls AUTO-GENERATED from each effect's `defaults` (number→slider, boolean→toggle, colors→editable token list, string→text), a stage-background toggle (surface/black/white/transparent), reset, and a copyable usage snippet. The old page only switched effect, no tuning. - **fog + mesh painted nothing**: their seeded colors were near-invisible on the dark base theme — fog used neutral-bg2 (L 0.21) + neutral-element (L 0.25), mesh used four -element slots (L ~0.26) against surface L 0.18. Reseed with high-contrast tokens (fog: surface bg + neutral-text; mesh: -solid stops). Verified in a real browser: aurora/mesh/fog all paint. - Repoint the six demo ledes from the ephemeral plan to docs/decisions/design-text-effects.md (matches the READMEs). check 0 own errors. Verified live (real Chrome): all effects paint, controls tune params, nav reaches every new page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
{
kind: 'group',
heading: 'Text effects',
items: [
{ slug: '/uix/components/count-up', label: 'CountUp' },
fix(demos): reachable nav + real interactive Ambient testbed + visible fog/mesh The catalog work shipped but was unreachable and impoverished: - **Nav**: the sidebar IA (web/routes/uix/+layout@.svelte) is a manual list; the new components were never added, so nothing linked to them. Add a "Text effects" group (TextGradient/Circular/Blur/Focus/Scramble), CountUp under Utilities, and a "Packs" group (Ambient · backgrounds). - **Ambient demo → real testbed**: rebuild web/routes/uix/packs/ambient as a proper interactive page — effect selector (32, grouped Agentic/Tier A/B), live param controls AUTO-GENERATED from each effect's `defaults` (number→slider, boolean→toggle, colors→editable token list, string→text), a stage-background toggle (surface/black/white/transparent), reset, and a copyable usage snippet. The old page only switched effect, no tuning. - **fog + mesh painted nothing**: their seeded colors were near-invisible on the dark base theme — fog used neutral-bg2 (L 0.21) + neutral-element (L 0.25), mesh used four -element slots (L ~0.26) against surface L 0.18. Reseed with high-contrast tokens (fog: surface bg + neutral-text; mesh: -solid stops). Verified in a real browser: aurora/mesh/fog all paint. - Repoint the six demo ledes from the ephemeral plan to docs/decisions/design-text-effects.md (matches the READMEs). check 0 own errors. Verified live (real Chrome): all effects paint, controls tune params, nav reaches every new page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
{ 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' },
feat(layout): Layout Batch 2 — aspect-ratio, auto-grid, banner, float 4 new layout primitives, full canon (morfo + eidos full set + canon demo + README). `npm run component:audit`: 81 / 81 PASS, 0 NEEDS-WORK. **AspectRatio** — composes through Box; emits single `--aspect-ratio` CSS var (uses modern CSS `aspect-ratio` property, drops legacy padding-bottom hack). Accepts numeric (16/9 → 1.777…) or string ("16/9") values. **AutoGrid** — composes through Grid. Resolves `templateColumns` from `minItemWidth` to `repeat(auto-fill, minmax(MIN, 1fr))` in JS so the recipe stays declarative. Uses `auto-fill` (not auto-fit) to preserve empty tracks when item count is low. **Banner** — `<header role="banner">` announcement strip with intent (full 8-role UIX `ColorRole`), variant (`ChipVariant` soft/solid/ outline/ghost), size (sm/md/lg). Dismissal is composition-driven: consumer wraps in {#if show} and adds `<Banner.Close>`. No `dismissible` boolean. **Float** — CSS `float` primitive with logical `inline-start` / `inline-end` sides. Redefined from air's 9-zone overlay primitive (which becomes a future `<Positioned>` component if real demand surfaces). Useful for inline images / pull-quotes / drop caps. Each component ships morfo (`scope: ['eidos']`, 1 part, 0 events, justified) + eidos set + README (Baseline / Comparativa / Decisiones / Eventos Sema / Gaps with disposition markers / Referencias / Passive justification) + canon demo with 6 tabs. Sidebar nav appends the 4 entries to the existing Layout group. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ 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' },
feat(eidos): Card, Avatar ext, Image, SText, Badge, service components + dates engine fixes Sprint completo: - Card (eidos + sema): elevation/intent/interactive, eventos select+activate. - Avatar: ring, AvatarBadge, AvatarGroup separados; demo Avatar standalone. - Image: status surface (idle/loading/loaded/error) consumiendo soma ImageProvider. - SText + STextVirtualList: canvas-text engine portado de morfo-driven-soma como componentes separados para optimizar bundle (no se importa canvas si no se usa). - Badge: primitivo eidos puro. - Trans, FormatNumber, FormatDate, RelativeTime: 4 service components greppables alrededor de uix.langs y uix.format. Nav group "Utilities". Bugs fix arts/format/dates (engine-dates.ts): 1. Preset / per-axis conflict: Intl.DateTimeFormat rechaza dateStyle/timeStyle con cualquier per-axis (weekday/era/year/month/day/dayPeriod/hour/minute/ second/fractionalSecondDigits) o con timeZoneName. Engine drop-ea ambos presets por defecto cuando detecta conflict (regla estricta — un solo per-axis hace ilegales los dos presets). 2. Per-call locale override: formatDate/Time/DateTime aceptan tercer arg locale?: string. No muta engine state. Cache (getCachedDateFormat) keyea por (locale x options). resolveCallHourCycle: cuando hay locale override y pref auto, el ciclo horario se resuelve desde el locale OVERRIDE. 3. withHourCycle precedence: caller hourCycle/hour12 ahora gana sobre preferencia del engine. Antes spread mal ordenado sobrescribia 'h12' del caller con 'h23' del engine cuando locale resolvia a 24h. FormatDate component vuelve a rutear siempre por uix.format.dates (cache + hourCycle + locale reactivo), pasando locale como tercer arg. Demos service components autonomos del toolbar global: cada uno owns su demoLocale state con 7 chips (es-ES/en-US/en-GB/de-DE/fr-FR/ja-JP/ar-EG). FormatDate stage filtra dateStyle/timeStyle por mode + chip hourCycle anadido. Tests: 14/14 passing en engine-dates.test.ts (+6 nuevos: conflict handling, locale override, hourCycle precedence). Docs: arts/format/README.md (handoff 2026-05-25 con las 3 reglas), README.md por service component, project_service_components_sprint_2026-05-25.md, feedback_service_demos_own_locale.md (pattern reusable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/card', label: 'Card' },
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
{ slug: '/uix/components/card-group', label: 'CardGroup' },
feat(layout): Layout Batch 3 — scroll-area, separator, splitter + splitter sema rewire 3 new layout primitives close the air port set (15 / 15 done). `npm run component:audit`: 91 / 91 PASS, all 3 new PASS with 0 errors. **scroll-area** — eidos compound wrapper over the existing soma: `<ScrollArea><ScrollArea.Viewport>{content}</ScrollArea.Viewport> <ScrollArea.Scrollbar orientation="vertical"><ScrollArea.Thumb /> </ScrollArea.Scrollbar><ScrollArea.Corner /></ScrollArea>`. Mirrors Radix Primitives / Bits UI shape. Demo 667 lines. **separator** — visual primitive (`scope: ['eidos']`) with conditional ARIA: `role="separator"` + `aria-orientation` when not decorative, `aria-hidden="true"` when decorative. New morfo `src/uix/morfo/components/separator.ts`. Demo 645 lines. **splitter** — eidos compound over existing soma: `<Splitter orientation="horizontal"><Splitter.Panel>{a}</Splitter.Panel> <Splitter.ResizeTrigger /><Splitter.Panel>{b}</Splitter.Panel> </Splitter>`. Demo 824 lines. **Splitter sema rewire — match Slider's handle/drag/commit shape:** The morfo previously declared only `commit-resize`. Direct-manipulation primitives (Slider, Splitter) should share the same event vocabulary so audio/haptic projections feel coherent across the family. Renamed + expanded to mirror Slider: - `handle-pick` (family: handle, verb: pick, sequence: coincident, target: resize-trigger) — fires on pointerdown when user grabs the trigger. - `handle-drag` (family: handle, verb: drag, sequence: coincident, target: resize-trigger) — fires on each pointermove frame. - `commit-set` (family: commit, verb: set, intent: neutral, sequence: post, target: provider) — fires on pointerup. The runtime `commit-set` handler is wired to call the consumer's `onResizeEnd` callback, so sema and JS callback land at the same moment (parity with Slider's `onValueCommit`). Soma provider triggers these via `this.runtime.trigger(name)` in `onpointerdown` / `onpointermove` / `notifyResizeEnd` (the latter unchanged externally — internally now routes through the runtime trigger). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ slug: '/uix/components/float', label: 'Float' },
{ slug: '/uix/components/scroll-area', label: 'ScrollArea' },
{ slug: '/uix/components/separator', label: 'Separator' },
fix(direction): getDir() no era reactivo, y 33 componentes estampaban su valor congelado El slider no respondia al cambio rtl/ltr. Resultaron ser dos defectos independientes, y el segundo era del ecosistema entero. 1) EL SLIDER. slider.css emparejaba un inset LOGICO con un transform FISICO en las tres reglas verticales (inset-inline-start: 50% + translateX(-50%)). Los transforms no se voltean, asi que en RTL el rail y el relleno quedaban fuera de eje un ancho entero. Medido en Chrome: raiz/pulgar/ticks en x=961, rail y relleno en x=955. El Tick se libraba porque ya usaba margin-inline-start — ese era el idioma correcto del propio fichero y es el que aplico. Mismo defecto en accordion.css: text-align: left en el trigger, que era el UNICO text-align fisico de todo eidos; ahora quedan cero. 2) LA RAIZ. makeDimension().get() leia engine.snapshot() esquivando la celda $state, asi que prefs.<dim>.get() — y con ella el soma.prefs.getDir() que la guia manda usar — era una lectura sin tracking. Los ~64 componentes que resuelven su direccion desde prefs la congelaban al montarse. La proyeccion DOM se salvaba porque usa onChange: misma dimension, dos caminos de lectura, solo uno reactivo. Arreglado con un contador de cambios POR dimension, bombeado desde el effectiveDiff que el motor ya calcula, de modo que cubre tambien el cambio por DERIVACION (direction siguiendo a language) y no solo el intent. Un contador por clave y no una celda global: con una sola celda, tocar motion despertaria las 64 derivaciones de direccion. El test negativo lo clava. 3) VALOR NO ES AFIRMACION. 33 de 37 componentes estampaban dir en su raiz con un valor siempre concreto, asi que una app que ponga html dir=rtl sin registrar la preferencia se encontraba 33 islas volteadas del reves. Investigadas las cinco librerias de referencia: MUI y react-aria no estampan nunca, Radix estampa siempre (sus mantenedores arrastran discussions/1405 por esto), y Zag estampa prop("dir") — presente si alguien lo pidio, ausente si no. Adoptado el modelo de Zag pero uniforme: el atributo NUNCA tiene defecto. La herencia la hace el navegador por AUSENCIA de atributo, sin ninguna lectura del DOM, asi que la cadena prop -> prefs -> 'ltr' se queda intacta. 4) HOMOGENEIDAD. Habia DIEZ formas distintas de resolver lo mismo en los wrappers y CINCO de declarar el tipo, con un cuarto escalon semantico —heredar del menu padre— enterrado en dos expresiones sueltas. Normalizado a activeDir(getter, soma) en los 36, un solo tipo Direction | undefined, y el escalon de los submenus declarado en la llamada, donde se ve. 5) LA SHELL. El toggle de direccion pasa de 2 a 3 estados: auto hace clearIntent, que es lo unico que deja ganar al derive. Y entra el arabe en el catalogo de idiomas, porque sin un idioma RTL la derivacion no era disparable. Con las dos cosas, elegir arabe voltea la direccion sin tocar el control — la cadena documentada funcionando de punta a punta por primera vez. Verificado: check 74 = linea base exacta, cero nuevos. 602/602 tests en 101 ficheros. En Chrome real: el topbar mueve al slider y al accordion en vivo, y el arabe voltea por derivacion. QUEDA: 48 demos siguen clavando dir='ltr'; el interrogante desplazado del accordion necesita dir="ltr" lang="en" en el contenedor de la demo (no es un fallo nuestro, es bidi correcto sobre texto ingles en parrafo RTL); html lang no sigue al idioma; smoke y perm:check sin ejecutar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
{ 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);
}
}
});
feat(text-gradient): `colors` discriminates canonical name vs stop list (folds `preset`) Per request: one `colors` prop that discriminates by shape instead of two rival props. A `string[]` is an explicit stop list (unchanged); a bare `string` names a canonical gradient from the theming's named set, resolved as `var(--gradient-{name})` — exactly what the removed `preset` prop did. The set is an open cage (`EidosConfig.gradients` / `applyGradients`), so the type stays `string | readonly string[]`, not a closed union. Removed `preset` outright (no back-compat shim, per project rule); the canonical S9 vocabulary and raw stops are now the SAME input. Docs shell: register a small canonical named set on `uix/+layout@` via `applyGradients` (`brand`, `spectrum`) — role-derived, so they re-tint with the seed and flip light/dark. Demo gains a live stops|named toggle so both branches are a real testbed. Finding worth recording (types + README): a named gradient painted through `background-clip: text` must be a single `<image>` (linear/radial/conic). A role-derived MESH serializes to radials + a base COLOR, and a bare color is not a valid `background-image` (only the `background` shorthand accepts it) — so a mesh name computes to `none` and never paints. The docs set is linear-only for this reason. Verified in real Chrome: stops branch paints the token gradient; `colors="spectrum"` paints `var(--gradient-spectrum)` through the text. svelte-check 0 own errors, component:audit 141/0/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
// 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.
fix(direction): getDir() no era reactivo, y 33 componentes estampaban su valor congelado El slider no respondia al cambio rtl/ltr. Resultaron ser dos defectos independientes, y el segundo era del ecosistema entero. 1) EL SLIDER. slider.css emparejaba un inset LOGICO con un transform FISICO en las tres reglas verticales (inset-inline-start: 50% + translateX(-50%)). Los transforms no se voltean, asi que en RTL el rail y el relleno quedaban fuera de eje un ancho entero. Medido en Chrome: raiz/pulgar/ticks en x=961, rail y relleno en x=955. El Tick se libraba porque ya usaba margin-inline-start — ese era el idioma correcto del propio fichero y es el que aplico. Mismo defecto en accordion.css: text-align: left en el trigger, que era el UNICO text-align fisico de todo eidos; ahora quedan cero. 2) LA RAIZ. makeDimension().get() leia engine.snapshot() esquivando la celda $state, asi que prefs.<dim>.get() — y con ella el soma.prefs.getDir() que la guia manda usar — era una lectura sin tracking. Los ~64 componentes que resuelven su direccion desde prefs la congelaban al montarse. La proyeccion DOM se salvaba porque usa onChange: misma dimension, dos caminos de lectura, solo uno reactivo. Arreglado con un contador de cambios POR dimension, bombeado desde el effectiveDiff que el motor ya calcula, de modo que cubre tambien el cambio por DERIVACION (direction siguiendo a language) y no solo el intent. Un contador por clave y no una celda global: con una sola celda, tocar motion despertaria las 64 derivaciones de direccion. El test negativo lo clava. 3) VALOR NO ES AFIRMACION. 33 de 37 componentes estampaban dir en su raiz con un valor siempre concreto, asi que una app que ponga html dir=rtl sin registrar la preferencia se encontraba 33 islas volteadas del reves. Investigadas las cinco librerias de referencia: MUI y react-aria no estampan nunca, Radix estampa siempre (sus mantenedores arrastran discussions/1405 por esto), y Zag estampa prop("dir") — presente si alguien lo pidio, ausente si no. Adoptado el modelo de Zag pero uniforme: el atributo NUNCA tiene defecto. La herencia la hace el navegador por AUSENCIA de atributo, sin ninguna lectura del DOM, asi que la cadena prop -> prefs -> 'ltr' se queda intacta. 4) HOMOGENEIDAD. Habia DIEZ formas distintas de resolver lo mismo en los wrappers y CINCO de declarar el tipo, con un cuarto escalon semantico —heredar del menu padre— enterrado en dos expresiones sueltas. Normalizado a activeDir(getter, soma) en los 36, un solo tipo Direction | undefined, y el escalon de los submenus declarado en la llamada, donde se ve. 5) LA SHELL. El toggle de direccion pasa de 2 a 3 estados: auto hace clearIntent, que es lo unico que deja ganar al derive. Y entra el arabe en el catalogo de idiomas, porque sin un idioma RTL la derivacion no era disparable. Con las dos cosas, elegir arabe voltea la direccion sin tocar el control — la cadena documentada funcionando de punta a punta por primera vez. Verificado: check 74 = linea base exacta, cero nuevos. 602/602 tests en 101 ficheros. En Chrome real: el topbar mueve al slider y al accordion en vivo, y el arabe voltea por derivacion. QUEDA: 48 demos siguen clavando dir='ltr'; el interrogante desplazado del accordion necesita dir="ltr" lang="en" en el contenedor de la demo (no es un fallo nuestro, es bidi correcto sobre texto ingles en parrafo RTL); html lang no sigue al idioma; smoke y perm:check sin ejecutar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// 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',
fix(direction): getDir() no era reactivo, y 33 componentes estampaban su valor congelado El slider no respondia al cambio rtl/ltr. Resultaron ser dos defectos independientes, y el segundo era del ecosistema entero. 1) EL SLIDER. slider.css emparejaba un inset LOGICO con un transform FISICO en las tres reglas verticales (inset-inline-start: 50% + translateX(-50%)). Los transforms no se voltean, asi que en RTL el rail y el relleno quedaban fuera de eje un ancho entero. Medido en Chrome: raiz/pulgar/ticks en x=961, rail y relleno en x=955. El Tick se libraba porque ya usaba margin-inline-start — ese era el idioma correcto del propio fichero y es el que aplico. Mismo defecto en accordion.css: text-align: left en el trigger, que era el UNICO text-align fisico de todo eidos; ahora quedan cero. 2) LA RAIZ. makeDimension().get() leia engine.snapshot() esquivando la celda $state, asi que prefs.<dim>.get() — y con ella el soma.prefs.getDir() que la guia manda usar — era una lectura sin tracking. Los ~64 componentes que resuelven su direccion desde prefs la congelaban al montarse. La proyeccion DOM se salvaba porque usa onChange: misma dimension, dos caminos de lectura, solo uno reactivo. Arreglado con un contador de cambios POR dimension, bombeado desde el effectiveDiff que el motor ya calcula, de modo que cubre tambien el cambio por DERIVACION (direction siguiendo a language) y no solo el intent. Un contador por clave y no una celda global: con una sola celda, tocar motion despertaria las 64 derivaciones de direccion. El test negativo lo clava. 3) VALOR NO ES AFIRMACION. 33 de 37 componentes estampaban dir en su raiz con un valor siempre concreto, asi que una app que ponga html dir=rtl sin registrar la preferencia se encontraba 33 islas volteadas del reves. Investigadas las cinco librerias de referencia: MUI y react-aria no estampan nunca, Radix estampa siempre (sus mantenedores arrastran discussions/1405 por esto), y Zag estampa prop("dir") — presente si alguien lo pidio, ausente si no. Adoptado el modelo de Zag pero uniforme: el atributo NUNCA tiene defecto. La herencia la hace el navegador por AUSENCIA de atributo, sin ninguna lectura del DOM, asi que la cadena prop -> prefs -> 'ltr' se queda intacta. 4) HOMOGENEIDAD. Habia DIEZ formas distintas de resolver lo mismo en los wrappers y CINCO de declarar el tipo, con un cuarto escalon semantico —heredar del menu padre— enterrado en dos expresiones sueltas. Normalizado a activeDir(getter, soma) en los 36, un solo tipo Direction | undefined, y el escalon de los submenus declarado en la llamada, donde se ve. 5) LA SHELL. El toggle de direccion pasa de 2 a 3 estados: auto hace clearIntent, que es lo unico que deja ganar al derive. Y entra el arabe en el catalogo de idiomas, porque sin un idioma RTL la derivacion no era disparable. Con las dos cosas, elegir arabe voltea la direccion sin tocar el control — la cadena documentada funcionando de punta a punta por primera vez. Verificado: check 74 = linea base exacta, cero nuevos. 602/602 tests en 101 ficheros. En Chrome real: el topbar mueve al slider y al accordion en vivo, y el arabe voltea por derivacion. QUEDA: 48 demos siguen clavando dir='ltr'; el interrogante desplazado del accordion necesita dir="ltr" lang="en" en el contenedor de la demo (no es un fallo nuestro, es bidi correcto sobre texto ingles en parrafo RTL); html lang no sigue al idioma; smoke y perm:check sin ejecutar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
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);
fix(direction): getDir() no era reactivo, y 33 componentes estampaban su valor congelado El slider no respondia al cambio rtl/ltr. Resultaron ser dos defectos independientes, y el segundo era del ecosistema entero. 1) EL SLIDER. slider.css emparejaba un inset LOGICO con un transform FISICO en las tres reglas verticales (inset-inline-start: 50% + translateX(-50%)). Los transforms no se voltean, asi que en RTL el rail y el relleno quedaban fuera de eje un ancho entero. Medido en Chrome: raiz/pulgar/ticks en x=961, rail y relleno en x=955. El Tick se libraba porque ya usaba margin-inline-start — ese era el idioma correcto del propio fichero y es el que aplico. Mismo defecto en accordion.css: text-align: left en el trigger, que era el UNICO text-align fisico de todo eidos; ahora quedan cero. 2) LA RAIZ. makeDimension().get() leia engine.snapshot() esquivando la celda $state, asi que prefs.<dim>.get() — y con ella el soma.prefs.getDir() que la guia manda usar — era una lectura sin tracking. Los ~64 componentes que resuelven su direccion desde prefs la congelaban al montarse. La proyeccion DOM se salvaba porque usa onChange: misma dimension, dos caminos de lectura, solo uno reactivo. Arreglado con un contador de cambios POR dimension, bombeado desde el effectiveDiff que el motor ya calcula, de modo que cubre tambien el cambio por DERIVACION (direction siguiendo a language) y no solo el intent. Un contador por clave y no una celda global: con una sola celda, tocar motion despertaria las 64 derivaciones de direccion. El test negativo lo clava. 3) VALOR NO ES AFIRMACION. 33 de 37 componentes estampaban dir en su raiz con un valor siempre concreto, asi que una app que ponga html dir=rtl sin registrar la preferencia se encontraba 33 islas volteadas del reves. Investigadas las cinco librerias de referencia: MUI y react-aria no estampan nunca, Radix estampa siempre (sus mantenedores arrastran discussions/1405 por esto), y Zag estampa prop("dir") — presente si alguien lo pidio, ausente si no. Adoptado el modelo de Zag pero uniforme: el atributo NUNCA tiene defecto. La herencia la hace el navegador por AUSENCIA de atributo, sin ninguna lectura del DOM, asi que la cadena prop -> prefs -> 'ltr' se queda intacta. 4) HOMOGENEIDAD. Habia DIEZ formas distintas de resolver lo mismo en los wrappers y CINCO de declarar el tipo, con un cuarto escalon semantico —heredar del menu padre— enterrado en dos expresiones sueltas. Normalizado a activeDir(getter, soma) en los 36, un solo tipo Direction | undefined, y el escalon de los submenus declarado en la llamada, donde se ve. 5) LA SHELL. El toggle de direccion pasa de 2 a 3 estados: auto hace clearIntent, que es lo unico que deja ganar al derive. Y entra el arabe en el catalogo de idiomas, porque sin un idioma RTL la derivacion no era disparable. Con las dos cosas, elegir arabe voltea la direccion sin tocar el control — la cadena documentada funcionando de punta a punta por primera vez. Verificado: check 74 = linea base exacta, cero nuevos. 602/602 tests en 101 ficheros. En Chrome real: el topbar mueve al slider y al accordion en vivo, y el arabe voltea por derivacion. QUEDA: 48 demos siguen clavando dir='ltr'; el interrogante desplazado del accordion necesita dir="ltr" lang="en" en el contenedor de la demo (no es un fallo nuestro, es bidi correcto sobre texto ingles en parrafo RTL); html lang no sigue al idioma; smoke y perm:check sin ejecutar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
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 = {
fix(direction): getDir() no era reactivo, y 33 componentes estampaban su valor congelado El slider no respondia al cambio rtl/ltr. Resultaron ser dos defectos independientes, y el segundo era del ecosistema entero. 1) EL SLIDER. slider.css emparejaba un inset LOGICO con un transform FISICO en las tres reglas verticales (inset-inline-start: 50% + translateX(-50%)). Los transforms no se voltean, asi que en RTL el rail y el relleno quedaban fuera de eje un ancho entero. Medido en Chrome: raiz/pulgar/ticks en x=961, rail y relleno en x=955. El Tick se libraba porque ya usaba margin-inline-start — ese era el idioma correcto del propio fichero y es el que aplico. Mismo defecto en accordion.css: text-align: left en el trigger, que era el UNICO text-align fisico de todo eidos; ahora quedan cero. 2) LA RAIZ. makeDimension().get() leia engine.snapshot() esquivando la celda $state, asi que prefs.<dim>.get() — y con ella el soma.prefs.getDir() que la guia manda usar — era una lectura sin tracking. Los ~64 componentes que resuelven su direccion desde prefs la congelaban al montarse. La proyeccion DOM se salvaba porque usa onChange: misma dimension, dos caminos de lectura, solo uno reactivo. Arreglado con un contador de cambios POR dimension, bombeado desde el effectiveDiff que el motor ya calcula, de modo que cubre tambien el cambio por DERIVACION (direction siguiendo a language) y no solo el intent. Un contador por clave y no una celda global: con una sola celda, tocar motion despertaria las 64 derivaciones de direccion. El test negativo lo clava. 3) VALOR NO ES AFIRMACION. 33 de 37 componentes estampaban dir en su raiz con un valor siempre concreto, asi que una app que ponga html dir=rtl sin registrar la preferencia se encontraba 33 islas volteadas del reves. Investigadas las cinco librerias de referencia: MUI y react-aria no estampan nunca, Radix estampa siempre (sus mantenedores arrastran discussions/1405 por esto), y Zag estampa prop("dir") — presente si alguien lo pidio, ausente si no. Adoptado el modelo de Zag pero uniforme: el atributo NUNCA tiene defecto. La herencia la hace el navegador por AUSENCIA de atributo, sin ninguna lectura del DOM, asi que la cadena prop -> prefs -> 'ltr' se queda intacta. 4) HOMOGENEIDAD. Habia DIEZ formas distintas de resolver lo mismo en los wrappers y CINCO de declarar el tipo, con un cuarto escalon semantico —heredar del menu padre— enterrado en dos expresiones sueltas. Normalizado a activeDir(getter, soma) en los 36, un solo tipo Direction | undefined, y el escalon de los submenus declarado en la llamada, donde se ve. 5) LA SHELL. El toggle de direccion pasa de 2 a 3 estados: auto hace clearIntent, que es lo unico que deja ganar al derive. Y entra el arabe en el catalogo de idiomas, porque sin un idioma RTL la derivacion no era disparable. Con las dos cosas, elegir arabe voltea la direccion sin tocar el control — la cadena documentada funcionando de punta a punta por primera vez. Verificado: check 74 = linea base exacta, cero nuevos. 602/602 tests en 101 ficheros. En Chrome real: el topbar mueve al slider y al accordion en vivo, y el arabe voltea por derivacion. QUEDA: 48 demos siguen clavando dir='ltr'; el interrogante desplazado del accordion necesita dir="ltr" lang="en" en el contenedor de la demo (no es un fallo nuestro, es bidi correcto sobre texto ingles en parrafo RTL); html lang no sigue al idioma; smoke y perm:check sin ejecutar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
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);
fix(direction): getDir() no era reactivo, y 33 componentes estampaban su valor congelado El slider no respondia al cambio rtl/ltr. Resultaron ser dos defectos independientes, y el segundo era del ecosistema entero. 1) EL SLIDER. slider.css emparejaba un inset LOGICO con un transform FISICO en las tres reglas verticales (inset-inline-start: 50% + translateX(-50%)). Los transforms no se voltean, asi que en RTL el rail y el relleno quedaban fuera de eje un ancho entero. Medido en Chrome: raiz/pulgar/ticks en x=961, rail y relleno en x=955. El Tick se libraba porque ya usaba margin-inline-start — ese era el idioma correcto del propio fichero y es el que aplico. Mismo defecto en accordion.css: text-align: left en el trigger, que era el UNICO text-align fisico de todo eidos; ahora quedan cero. 2) LA RAIZ. makeDimension().get() leia engine.snapshot() esquivando la celda $state, asi que prefs.<dim>.get() — y con ella el soma.prefs.getDir() que la guia manda usar — era una lectura sin tracking. Los ~64 componentes que resuelven su direccion desde prefs la congelaban al montarse. La proyeccion DOM se salvaba porque usa onChange: misma dimension, dos caminos de lectura, solo uno reactivo. Arreglado con un contador de cambios POR dimension, bombeado desde el effectiveDiff que el motor ya calcula, de modo que cubre tambien el cambio por DERIVACION (direction siguiendo a language) y no solo el intent. Un contador por clave y no una celda global: con una sola celda, tocar motion despertaria las 64 derivaciones de direccion. El test negativo lo clava. 3) VALOR NO ES AFIRMACION. 33 de 37 componentes estampaban dir en su raiz con un valor siempre concreto, asi que una app que ponga html dir=rtl sin registrar la preferencia se encontraba 33 islas volteadas del reves. Investigadas las cinco librerias de referencia: MUI y react-aria no estampan nunca, Radix estampa siempre (sus mantenedores arrastran discussions/1405 por esto), y Zag estampa prop("dir") — presente si alguien lo pidio, ausente si no. Adoptado el modelo de Zag pero uniforme: el atributo NUNCA tiene defecto. La herencia la hace el navegador por AUSENCIA de atributo, sin ninguna lectura del DOM, asi que la cadena prop -> prefs -> 'ltr' se queda intacta. 4) HOMOGENEIDAD. Habia DIEZ formas distintas de resolver lo mismo en los wrappers y CINCO de declarar el tipo, con un cuarto escalon semantico —heredar del menu padre— enterrado en dos expresiones sueltas. Normalizado a activeDir(getter, soma) en los 36, un solo tipo Direction | undefined, y el escalon de los submenus declarado en la llamada, donde se ve. 5) LA SHELL. El toggle de direccion pasa de 2 a 3 estados: auto hace clearIntent, que es lo unico que deja ganar al derive. Y entra el arabe en el catalogo de idiomas, porque sin un idioma RTL la derivacion no era disparable. Con las dos cosas, elegir arabe voltea la direccion sin tocar el control — la cadena documentada funcionando de punta a punta por primera vez. Verificado: check 74 = linea base exacta, cero nuevos. 602/602 tests en 101 ficheros. En Chrome real: el topbar mueve al slider y al accordion en vivo, y el arabe voltea por derivacion. QUEDA: 48 demos siguen clavando dir='ltr'; el interrogante desplazado del accordion necesita dir="ltr" lang="en" en el contenedor de la demo (no es un fallo nuestro, es bidi correcto sobre texto ingles en parrafo RTL); html lang no sigue al idioma; smoke y perm:check sin ejecutar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// `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');
});
// 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);
});
feat: build PasswordField (full stack) Dedicated password input component — matches the Ark UI / Chakra v3 / Mantine / PrimeReact / Ant Design pattern of treating PasswordField as its own first-class component (not a TextField variant). Includes one feature that NO surveyed library ships: a CapsLock indicator driven by KeyboardEvent.getModifierState('CapsLock'). Anatomy (5 parts, 4 events): - Provider (root, value + visibility state) - Input (the textbox; type flips between password / text) - VisibilityTrigger (eye toggle button; aria-pressed + aria-controls) - CapsLockIndicator (status live region; data-active toggles via soma listening to keydown/keyup on Input) - StrengthMeter (composable; consumer provides 0-4 score; UIX bundles no zxcvbn-style algorithm — dependency-free by design) Events: - commit-input (every keystroke) - commit-submit (Enter) - shift-toggle-visibility (eye click) - shift-caps-state (Caps Lock state flips while focused) Key behaviors: - `purpose: 'current' | 'new'` drives native autocomplete attribute ('current-password' for login / 'new-password' for registration). - Field integration: disabled/readonly/required/invalid OR-merged. - VisibilityTrigger gets tabindex=-1 so it doesn't break the form's natural tab order — mouse / programmatic focus only, matching Ark UI. - Caps Lock indicator is aria-live="polite" — announces on activation while focused, silent when inactive. - Letter-spacing increased on the input for legibility while masked. Reference library survey performed (10 libraries) before implementation — all consensus around dedicated component, all have built-in toggle, ZERO have caps lock warning. Documented in the demo's API tab with a feature comparison table. Demo follows DEMO_AUTHORING_GUIDE canonical 6-tab depth: Live (controls grouped by layer + code snippets at the bottom), API (props + reference comparison), Morfo (field/value + Parts + Events tables), Sema (events table with ▶ play), Recipe (selectors with morfo/eidos owner tags), A11y (concern/contract table). Also includes external work in words/ from a separate session (unrelated to PasswordField) — not touching Words is the standing rule, but the diff was already in the working tree. svelte-check: 0 errors in PasswordField files (Words errors are tracked by its own development branch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
const currentBreakpoint = $derived(uix.dom.currentBreakpoint.current);
const compactShell = $derived(
currentBreakpoint === 'base' || currentBreakpoint === 'sm' || currentBreakpoint === 'md'
);
const phoneShell = $derived(currentBreakpoint === 'base');
</script>
feat: build PasswordField (full stack) Dedicated password input component — matches the Ark UI / Chakra v3 / Mantine / PrimeReact / Ant Design pattern of treating PasswordField as its own first-class component (not a TextField variant). Includes one feature that NO surveyed library ships: a CapsLock indicator driven by KeyboardEvent.getModifierState('CapsLock'). Anatomy (5 parts, 4 events): - Provider (root, value + visibility state) - Input (the textbox; type flips between password / text) - VisibilityTrigger (eye toggle button; aria-pressed + aria-controls) - CapsLockIndicator (status live region; data-active toggles via soma listening to keydown/keyup on Input) - StrengthMeter (composable; consumer provides 0-4 score; UIX bundles no zxcvbn-style algorithm — dependency-free by design) Events: - commit-input (every keystroke) - commit-submit (Enter) - shift-toggle-visibility (eye click) - shift-caps-state (Caps Lock state flips while focused) Key behaviors: - `purpose: 'current' | 'new'` drives native autocomplete attribute ('current-password' for login / 'new-password' for registration). - Field integration: disabled/readonly/required/invalid OR-merged. - VisibilityTrigger gets tabindex=-1 so it doesn't break the form's natural tab order — mouse / programmatic focus only, matching Ark UI. - Caps Lock indicator is aria-live="polite" — announces on activation while focused, silent when inactive. - Letter-spacing increased on the input for legibility while masked. Reference library survey performed (10 libraries) before implementation — all consensus around dedicated component, all have built-in toggle, ZERO have caps lock warning. Documented in the demo's API tab with a feature comparison table. Demo follows DEMO_AUTHORING_GUIDE canonical 6-tab depth: Live (controls grouped by layer + code snippets at the bottom), API (props + reference comparison), Morfo (field/value + Parts + Events tables), Sema (events table with ▶ play), Recipe (selectors with morfo/eidos owner tags), A11y (concern/contract table). Also includes external work in words/ from a separate session (unrelated to PasswordField) — not touching Words is the standing rule, but the diff was already in the working tree. svelte-check: 0 errors in PasswordField files (Words errors are tracked by its own development branch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
<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>
uix: date-picker + date-range-picker components + component audit infra Two new Eidos components shipped end-to-end (wrapper + recipe + demo + Soma provider hardening) plus a checklist-driven audit pipeline that scores all 67 morfo components against doctrinal completion criteria. New components: - date-picker: full popover-anchored picker over date-field + calendar, with calendar/content/trigger parts and demo route. - date-range-picker: standalone wrapper with own calendar/grid/segment surface, demo route, and recipe CSS. - Both wrappers follow Option C disciplined (root + parts attached via explicit assignment, no Object.assign). Supporting Soma changes: - range-calendar provider tightened (211 LOC of behavior, 167 LOC of tests), README brought up to component doctrine. - date-field, date-picker, date-range-field, date-range-picker Soma providers + READMEs updated for new wrappers. - popover provider/close gain props needed by the picker wrappers. Morfo updates: - date-picker / date-range-picker / range-calendar morfos refined for the new APIs (parts, events, ARIA). Audit infrastructure (new): - src/uix/COMPONENT_COMPLETION_CHECKLIST.md — 81 doctrinal rules across morfo / eidos wrapper / recipe CSS / demo / README / cross-layer scripts. Each rule keyed to active_architecture.md, GUIA_IMPLEMENTACION and DEMO_AUTHORING_GUIDE. - scripts/component-audit.ts + `npm run component:audit` — regex parser over all 67 components, emits tmp/component-audit.md with summary scoreboard + per-component findings. Validates against canonical SEMA_FAMILIES / SEMA_VERBS / ARCHETYPE_VOCABULARY / INTENTS. - Initial baseline: 1 PASS, 64 NEEDS-WORK, 2 BROKEN (tooltip, date-range-picker). Top systemic gaps: translations.label (49), README Gaps/Comparativa/Baseline sections (87 combined), keyboard /event ratio under-declaration (15), apg URL absent (19). Misc: - src/uix/kimi-audit-eidos.md — supplementary audit notes. - .gitignore: ignore .codex-* agent scratch artifacts at repo root. - continue.md + READMEs updated through the migration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
<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. -->
fix(direction): getDir() no era reactivo, y 33 componentes estampaban su valor congelado El slider no respondia al cambio rtl/ltr. Resultaron ser dos defectos independientes, y el segundo era del ecosistema entero. 1) EL SLIDER. slider.css emparejaba un inset LOGICO con un transform FISICO en las tres reglas verticales (inset-inline-start: 50% + translateX(-50%)). Los transforms no se voltean, asi que en RTL el rail y el relleno quedaban fuera de eje un ancho entero. Medido en Chrome: raiz/pulgar/ticks en x=961, rail y relleno en x=955. El Tick se libraba porque ya usaba margin-inline-start — ese era el idioma correcto del propio fichero y es el que aplico. Mismo defecto en accordion.css: text-align: left en el trigger, que era el UNICO text-align fisico de todo eidos; ahora quedan cero. 2) LA RAIZ. makeDimension().get() leia engine.snapshot() esquivando la celda $state, asi que prefs.<dim>.get() — y con ella el soma.prefs.getDir() que la guia manda usar — era una lectura sin tracking. Los ~64 componentes que resuelven su direccion desde prefs la congelaban al montarse. La proyeccion DOM se salvaba porque usa onChange: misma dimension, dos caminos de lectura, solo uno reactivo. Arreglado con un contador de cambios POR dimension, bombeado desde el effectiveDiff que el motor ya calcula, de modo que cubre tambien el cambio por DERIVACION (direction siguiendo a language) y no solo el intent. Un contador por clave y no una celda global: con una sola celda, tocar motion despertaria las 64 derivaciones de direccion. El test negativo lo clava. 3) VALOR NO ES AFIRMACION. 33 de 37 componentes estampaban dir en su raiz con un valor siempre concreto, asi que una app que ponga html dir=rtl sin registrar la preferencia se encontraba 33 islas volteadas del reves. Investigadas las cinco librerias de referencia: MUI y react-aria no estampan nunca, Radix estampa siempre (sus mantenedores arrastran discussions/1405 por esto), y Zag estampa prop("dir") — presente si alguien lo pidio, ausente si no. Adoptado el modelo de Zag pero uniforme: el atributo NUNCA tiene defecto. La herencia la hace el navegador por AUSENCIA de atributo, sin ninguna lectura del DOM, asi que la cadena prop -> prefs -> 'ltr' se queda intacta. 4) HOMOGENEIDAD. Habia DIEZ formas distintas de resolver lo mismo en los wrappers y CINCO de declarar el tipo, con un cuarto escalon semantico —heredar del menu padre— enterrado en dos expresiones sueltas. Normalizado a activeDir(getter, soma) en los 36, un solo tipo Direction | undefined, y el escalon de los submenus declarado en la llamada, donde se ve. 5) LA SHELL. El toggle de direccion pasa de 2 a 3 estados: auto hace clearIntent, que es lo unico que deja ganar al derive. Y entra el arabe en el catalogo de idiomas, porque sin un idioma RTL la derivacion no era disparable. Con las dos cosas, elegir arabe voltea la direccion sin tocar el control — la cadena documentada funcionando de punta a punta por primera vez. Verificado: check 74 = linea base exacta, cero nuevos. 602/602 tests en 101 ficheros. En Chrome real: el topbar mueve al slider y al accordion en vivo, y el arabe voltea por derivacion. QUEDA: 48 demos siguen clavando dir='ltr'; el interrogante desplazado del accordion necesita dir="ltr" lang="en" en el contenedor de la demo (no es un fallo nuestro, es bidi correcto sobre texto ingles en parrafo RTL); html lang no sigue al idioma; smoke y perm:check sin ejecutar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
<!-- 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
fix(direction): getDir() no era reactivo, y 33 componentes estampaban su valor congelado El slider no respondia al cambio rtl/ltr. Resultaron ser dos defectos independientes, y el segundo era del ecosistema entero. 1) EL SLIDER. slider.css emparejaba un inset LOGICO con un transform FISICO en las tres reglas verticales (inset-inline-start: 50% + translateX(-50%)). Los transforms no se voltean, asi que en RTL el rail y el relleno quedaban fuera de eje un ancho entero. Medido en Chrome: raiz/pulgar/ticks en x=961, rail y relleno en x=955. El Tick se libraba porque ya usaba margin-inline-start — ese era el idioma correcto del propio fichero y es el que aplico. Mismo defecto en accordion.css: text-align: left en el trigger, que era el UNICO text-align fisico de todo eidos; ahora quedan cero. 2) LA RAIZ. makeDimension().get() leia engine.snapshot() esquivando la celda $state, asi que prefs.<dim>.get() — y con ella el soma.prefs.getDir() que la guia manda usar — era una lectura sin tracking. Los ~64 componentes que resuelven su direccion desde prefs la congelaban al montarse. La proyeccion DOM se salvaba porque usa onChange: misma dimension, dos caminos de lectura, solo uno reactivo. Arreglado con un contador de cambios POR dimension, bombeado desde el effectiveDiff que el motor ya calcula, de modo que cubre tambien el cambio por DERIVACION (direction siguiendo a language) y no solo el intent. Un contador por clave y no una celda global: con una sola celda, tocar motion despertaria las 64 derivaciones de direccion. El test negativo lo clava. 3) VALOR NO ES AFIRMACION. 33 de 37 componentes estampaban dir en su raiz con un valor siempre concreto, asi que una app que ponga html dir=rtl sin registrar la preferencia se encontraba 33 islas volteadas del reves. Investigadas las cinco librerias de referencia: MUI y react-aria no estampan nunca, Radix estampa siempre (sus mantenedores arrastran discussions/1405 por esto), y Zag estampa prop("dir") — presente si alguien lo pidio, ausente si no. Adoptado el modelo de Zag pero uniforme: el atributo NUNCA tiene defecto. La herencia la hace el navegador por AUSENCIA de atributo, sin ninguna lectura del DOM, asi que la cadena prop -> prefs -> 'ltr' se queda intacta. 4) HOMOGENEIDAD. Habia DIEZ formas distintas de resolver lo mismo en los wrappers y CINCO de declarar el tipo, con un cuarto escalon semantico —heredar del menu padre— enterrado en dos expresiones sueltas. Normalizado a activeDir(getter, soma) en los 36, un solo tipo Direction | undefined, y el escalon de los submenus declarado en la llamada, donde se ve. 5) LA SHELL. El toggle de direccion pasa de 2 a 3 estados: auto hace clearIntent, que es lo unico que deja ganar al derive. Y entra el arabe en el catalogo de idiomas, porque sin un idioma RTL la derivacion no era disparable. Con las dos cosas, elegir arabe voltea la direccion sin tocar el control — la cadena documentada funcionando de punta a punta por primera vez. Verificado: check 74 = linea base exacta, cero nuevos. 602/602 tests en 101 ficheros. En Chrome real: el topbar mueve al slider y al accordion en vivo, y el arabe voltea por derivacion. QUEDA: 48 demos siguen clavando dir='ltr'; el interrogante desplazado del accordion necesita dir="ltr" lang="en" en el contenedor de la demo (no es un fallo nuestro, es bidi correcto sobre texto ingles en parrafo RTL); html lang no sigue al idioma; smoke y perm:check sin ejecutar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
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"
fix(direction): getDir() no era reactivo, y 33 componentes estampaban su valor congelado El slider no respondia al cambio rtl/ltr. Resultaron ser dos defectos independientes, y el segundo era del ecosistema entero. 1) EL SLIDER. slider.css emparejaba un inset LOGICO con un transform FISICO en las tres reglas verticales (inset-inline-start: 50% + translateX(-50%)). Los transforms no se voltean, asi que en RTL el rail y el relleno quedaban fuera de eje un ancho entero. Medido en Chrome: raiz/pulgar/ticks en x=961, rail y relleno en x=955. El Tick se libraba porque ya usaba margin-inline-start — ese era el idioma correcto del propio fichero y es el que aplico. Mismo defecto en accordion.css: text-align: left en el trigger, que era el UNICO text-align fisico de todo eidos; ahora quedan cero. 2) LA RAIZ. makeDimension().get() leia engine.snapshot() esquivando la celda $state, asi que prefs.<dim>.get() — y con ella el soma.prefs.getDir() que la guia manda usar — era una lectura sin tracking. Los ~64 componentes que resuelven su direccion desde prefs la congelaban al montarse. La proyeccion DOM se salvaba porque usa onChange: misma dimension, dos caminos de lectura, solo uno reactivo. Arreglado con un contador de cambios POR dimension, bombeado desde el effectiveDiff que el motor ya calcula, de modo que cubre tambien el cambio por DERIVACION (direction siguiendo a language) y no solo el intent. Un contador por clave y no una celda global: con una sola celda, tocar motion despertaria las 64 derivaciones de direccion. El test negativo lo clava. 3) VALOR NO ES AFIRMACION. 33 de 37 componentes estampaban dir en su raiz con un valor siempre concreto, asi que una app que ponga html dir=rtl sin registrar la preferencia se encontraba 33 islas volteadas del reves. Investigadas las cinco librerias de referencia: MUI y react-aria no estampan nunca, Radix estampa siempre (sus mantenedores arrastran discussions/1405 por esto), y Zag estampa prop("dir") — presente si alguien lo pidio, ausente si no. Adoptado el modelo de Zag pero uniforme: el atributo NUNCA tiene defecto. La herencia la hace el navegador por AUSENCIA de atributo, sin ninguna lectura del DOM, asi que la cadena prop -> prefs -> 'ltr' se queda intacta. 4) HOMOGENEIDAD. Habia DIEZ formas distintas de resolver lo mismo en los wrappers y CINCO de declarar el tipo, con un cuarto escalon semantico —heredar del menu padre— enterrado en dos expresiones sueltas. Normalizado a activeDir(getter, soma) en los 36, un solo tipo Direction | undefined, y el escalon de los submenus declarado en la llamada, donde se ve. 5) LA SHELL. El toggle de direccion pasa de 2 a 3 estados: auto hace clearIntent, que es lo unico que deja ganar al derive. Y entra el arabe en el catalogo de idiomas, porque sin un idioma RTL la derivacion no era disparable. Con las dos cosas, elegir arabe voltea la direccion sin tocar el control — la cadena documentada funcionando de punta a punta por primera vez. Verificado: check 74 = linea base exacta, cero nuevos. 602/602 tests en 101 ficheros. En Chrome real: el topbar mueve al slider y al accordion en vivo, y el arabe voltea por derivacion. QUEDA: 48 demos siguen clavando dir='ltr'; el interrogante desplazado del accordion necesita dir="ltr" lang="en" en el contenedor de la demo (no es un fallo nuestro, es bidi correcto sobre texto ingles en parrafo RTL); html lang no sigue al idioma; smoke y perm:check sin ejecutar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
onclick={() =>
(dirMode = dirMode === 'auto' ? 'ltr' : dirMode === 'ltr' ? 'rtl' : 'auto')}
>
fix(direction): getDir() no era reactivo, y 33 componentes estampaban su valor congelado El slider no respondia al cambio rtl/ltr. Resultaron ser dos defectos independientes, y el segundo era del ecosistema entero. 1) EL SLIDER. slider.css emparejaba un inset LOGICO con un transform FISICO en las tres reglas verticales (inset-inline-start: 50% + translateX(-50%)). Los transforms no se voltean, asi que en RTL el rail y el relleno quedaban fuera de eje un ancho entero. Medido en Chrome: raiz/pulgar/ticks en x=961, rail y relleno en x=955. El Tick se libraba porque ya usaba margin-inline-start — ese era el idioma correcto del propio fichero y es el que aplico. Mismo defecto en accordion.css: text-align: left en el trigger, que era el UNICO text-align fisico de todo eidos; ahora quedan cero. 2) LA RAIZ. makeDimension().get() leia engine.snapshot() esquivando la celda $state, asi que prefs.<dim>.get() — y con ella el soma.prefs.getDir() que la guia manda usar — era una lectura sin tracking. Los ~64 componentes que resuelven su direccion desde prefs la congelaban al montarse. La proyeccion DOM se salvaba porque usa onChange: misma dimension, dos caminos de lectura, solo uno reactivo. Arreglado con un contador de cambios POR dimension, bombeado desde el effectiveDiff que el motor ya calcula, de modo que cubre tambien el cambio por DERIVACION (direction siguiendo a language) y no solo el intent. Un contador por clave y no una celda global: con una sola celda, tocar motion despertaria las 64 derivaciones de direccion. El test negativo lo clava. 3) VALOR NO ES AFIRMACION. 33 de 37 componentes estampaban dir en su raiz con un valor siempre concreto, asi que una app que ponga html dir=rtl sin registrar la preferencia se encontraba 33 islas volteadas del reves. Investigadas las cinco librerias de referencia: MUI y react-aria no estampan nunca, Radix estampa siempre (sus mantenedores arrastran discussions/1405 por esto), y Zag estampa prop("dir") — presente si alguien lo pidio, ausente si no. Adoptado el modelo de Zag pero uniforme: el atributo NUNCA tiene defecto. La herencia la hace el navegador por AUSENCIA de atributo, sin ninguna lectura del DOM, asi que la cadena prop -> prefs -> 'ltr' se queda intacta. 4) HOMOGENEIDAD. Habia DIEZ formas distintas de resolver lo mismo en los wrappers y CINCO de declarar el tipo, con un cuarto escalon semantico —heredar del menu padre— enterrado en dos expresiones sueltas. Normalizado a activeDir(getter, soma) en los 36, un solo tipo Direction | undefined, y el escalon de los submenus declarado en la llamada, donde se ve. 5) LA SHELL. El toggle de direccion pasa de 2 a 3 estados: auto hace clearIntent, que es lo unico que deja ganar al derive. Y entra el arabe en el catalogo de idiomas, porque sin un idioma RTL la derivacion no era disparable. Con las dos cosas, elegir arabe voltea la direccion sin tocar el control — la cadena documentada funcionando de punta a punta por primera vez. Verificado: check 74 = linea base exacta, cero nuevos. 602/602 tests en 101 ficheros. En Chrome real: el topbar mueve al slider y al accordion en vivo, y el arabe voltea por derivacion. QUEDA: 48 demos siguen clavando dir='ltr'; el interrogante desplazado del accordion necesita dir="ltr" lang="en" en el contenedor de la demo (no es un fallo nuestro, es bidi correcto sobre texto ingles en parrafo RTL); html lang no sigue al idioma; smoke y perm:check sin ejecutar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
{#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>
feat(direction): las demos dejan de clavar ltr, y la prosa inglesa se declara inglesa Cierra el eje de direccion abierto en 013ceac57. 1) LAS 49 DEMOS. Todas hacian `let dir = $state('ltr')` y se lo pasaban explicito al componente, asi que el interruptor del topbar no movia ninguna: la prop gana a prefs, que es el contrato. Pasan al patron de tres estados `auto | ltr | rtl` con `auto` por defecto, que es lo que recibe un consumidor real — sin prop, resolviendo por prefs. Elegir ltr/rtl sigue demostrando que la prop pisa. El arnes compartido SystemAxes va al mismo patron, y las 22 demos que lo bindean pasan a bindear el modo. El tooltip ya tenia tres estados con '' como "inherit" — mismo concepto, otro nombre; normalizado a `auto`. 2) LA PROSA. `<main data-uix-canvas dir="ltr" lang="en">`. La copia de la documentacion esta escrita en ingles, asi que se DECLARA inglesa. Sin eso, al voltear la shell a RTL el algoritmo bidi mueve la puntuacion neutra final al extremo visual izquierdo y "What is morfo?" se lee "?What is morfo" — bidi correcto sobre una declaracion falsa. `dir="ltr"` lo arregla y ademas AISLA la tirada en un solo atributo: la hoja de estilos de agente del WHATWG da `[dir=ltr i] { unicode-bidi: isolate }`, verificado en la fuente. NO `dir="auto"`: la spec llama a su heuristica de primer-caracter "very crude" y aqui ya sabemos la respuesta. LO QUE ESTO NO ARREGLA, y es correcto que no lo haga: el texto ingles DENTRO de un componente que declara rtl sigue mostrando el artefacto. El componente hace bien en estar en rtl —es lo que pidio el usuario— y el contenedor de arriba no entra en el. Eso solo se cierra traduciendo el contenido de la demo o marcandolo; es la parte que las cinco librerias de referencia dejan explicitamente al consumidor. Verificado: check 74 = linea base exacta. smoke: las 49 rutas tocadas PASAN; los 3 fallos de la pasada (/demos/motion, /temas, /uix/components/card) son inestables — ninguno esta en el diff, y la pasada sin mis cambios fallo en dos rutas DISTINTAS (/demos/cristal, /demos/heroscrolling). En Chrome: elegir arabe voltea calendar, tabs y slider por derivacion mientras la prosa se queda legible en ltr. NOTA DE METODO: un `prettier --write` sobre todas las demos genero 165 ficheros y +31k lineas de ruido, y rompio dos atributos normalizando `data-perm-mode='type="X"'` a comillas dobles. Revertido y reaplicado solo el cambio de direccion: 49 ficheros, +357/-161. QUEDA: `html lang` no sigue al idioma (el dir si); perm:check sin ejecutar —el topbar ya lleva data-perm-step, lo que hace que toda ruta tenga paso. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
<!-- The docs copy is written in English, so it is DECLARED as English.
Without this, flipping the shell to RTL leaves the prose in an RTL
paragraph and the Unicode bidi algorithm moves trailing neutral
punctuation to the visual left — "What is morfo?" renders as
"?What is morfo". That is correct bidi over a wrong declaration.
`dir="ltr"` fixes it and isolates the run in one attribute: the
WHATWG UA stylesheet gives `[dir=ltr i]` `unicode-bidi: isolate`.
NOT `dir="auto"` — the spec calls its first-strong-character
heuristic "very crude" and we already know the answer.
Components inside still follow prefs: they stamp their own `dir`
only when someone asserted one, so an RTL preference reaches them
through the prop/prefs chain, not through this container. -->
<main data-uix-canvas dir="ltr" lang="en">
{@render children?.()}
</main>
</div>
</div>

Powered by TurnKey Linux.