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

1210 lines
44 KiB

<script lang="ts">
import { onDestroy } from 'svelte';
import { createActiveUix, setActiveUix } from '$active-uix';
uix(skip-link): el tier decidió en F2.1 que los shells lo poseen, y no había nada que poseer Canon nuevo, eidos-native (1 parte, 0 eventos): la pieza de WCAG 2.4.1 que `site-header` difirió a los shells hace tres semanas y que `app-shell` (F3.1) necesita para existir. Nace de su fase 0, decisión Q4 firmada. Lo que decide, y por qué: - **Las palabras son suyas.** «Ir al contenido principal» no es copy: es el nombre de un contrato. La regla del tier (B-7 / D-BLK.5) apunta a esta puerta — si un string parece inevitable, lo posee el componente canónico vía `texts:` + langs. Por eso el prop es `region`, no `label`, y el vocabulario es la lista de landmarks de la APG. Cada clave es una FRASE ENTERA: en castellano el artículo se contrae con la región, así que un «Ir a» + sustantivo saldría mal en media catálogo. - **`region` no se estampa**: elige un texto y ninguna capa lo lee. Declararlo repetiría el error que `Affix` corrigió el 2026-08-15. - **No compone `Link`**: `Link` es tinta en línea y todo su API describe texto dentro de un párrafo. Esto es cromo que aparece de la nada, con su geometría y su suelo; envolverlo sería pisar todos sus props y aun así dejarlos en la API pública. Los componentes del canon renderizan sus nativos; son los blocks los que no pueden. - **El handler mueve el FOCO**, que es lo que el fragmento no hace: un `href` desplaza la vista y deja el foco en el enlace, y el siguiente Tab vuelve al cromo que el lector pidió saltar. El destino se hace enfocable sólo mientras dura el salto. El `href` se queda: es el camino sin JS. - **Peldaño propio de z (950), el más alto de la escalera estática.** No puede compartir el de `affix`: un empate lo rompe el orden del DOM y este enlace es por definición el primer elemento del documento, así que perdería contra cualquier aviso fijado debajo — el fallo medido en A-95. Medido con Playwright (el pane del navegador no tiene el foco del SO, así que `:focus` nunca casa ahí): en reposo 1x1 con clip-path; enfocado `position: fixed`, z 950, píldora de 181x36 sobre `primary-solid` con tinta blanca y anillo de foco, igual en claro y en oscuro; el salto deja el foco en `#demo-main` y el `tabindex` temporal se limpia en el blur; en RTL la píldora espeja al otro borde (x 338 -> 761) sin una segunda regla. Y la demo se corrigió a sí misma: decía «Tab desde el botón de abajo» y la medición enseñó que desde ahí el foco va hacia delante. El botón sube encima del marco, y de paso salió la otra mitad de la lección — por tabulación NUNCA se aterriza en el `main`, porque no tiene nada enfocable. Justo por eso el destino necesita el `tabindex` prestado. Guards: `component:audit --only skip-link` PASS · `morfo:check` PASS · `eidos-lint` 2 selectores morfo-backed, 0 inválidos · `docs:check` 0/0 · `svelte-check` 72, ninguno aquí. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
import { 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';
feat(web): /uix/docs/sound — la pagina que explica el canal del sonido Primera de las ocho del plan, en castellano y DERIVADA del codigo vivo: las tablas leen SEMA_MAP y el resolver real, nunca una transcripcion. Una pagina que copiara el catalogo envejeceria al primer gain que alguien tocara — que es exactamente la deriva que este subsistema se reconstruyo para quitar. SIETE SECCIONES, ordenadas para responder «no se como funciona esto despues de tantas correcciones»: (1) en una frase — un componente dice una palabra; (2) el catalogo, los 16 nombres sonando al pulsarlos, agrupados por la FORMA de cada entrada y no por una lista a mano; (3) el orden con los numeros de verdad, tabla viva de familia x nombre x intent capa a capa; (4) que puede llevar un .wav; (5) que no te toca escribir; (6) las correcciones y por que —la historia ES la explicacion, ocultarla es lo que dejo al autor sin saber como funciona—; (7) el tema. EL BLOQUEANTE, PRIMERO. El interruptor Sound del topbar no silenciaba NADA: el layout no pasaba `preferences`, asi que los canales se quedaban en 'full' y el switch solo movia el intent de prefs (que gobierna el lado visual). Publicar paginas que suenan con un mute que miente es peor que no publicarlas. Ahora pasa un objeto VIVO — ambos canales leen `preferences.{sound,haptic}` por emision, asi que mutarlo silencia al instante sin re-registrar nada. Cierra §3.3 de la cola. MEDIDO EN NAVEGADOR, no supuesto: 7 secciones, 17 chips, la rejilla y los chips con su CSS aplicado, y la tabla de capas moviendose de verdad — con `fulfill` el gain pasa de 0.05 a 0.1, el pitch de 700 a 1000 y el contour de flat a ascending. Es decir: la pagina DEMUESTRA en vivo que el intent sobrevive al nombre, que es la tesis del rediseño entero. ⚠️ NO he podido hacer captura: el panel del navegador no esta visible en esta sesion. Verificado por arbol de accesibilidad, consola y estilos computados — pero no lo he MIRADO. Queda pendiente tu ojo. ⚠️ Corregido de paso un fallo propio: la sonda del resolver usaba `document` en el render de servidor y la pagina daba 500. Ahora cae a un stub en SSR. El nav estrena seccion «El sistema perceptual». Quedan las otras siete paginas, y los seis enlaces muertos que Foundations ya declaraba (/uix/architecture, /uix/tokens, /uix/morfo, /uix/sema, /uix/eidos, /uix/getting-started) siguen sin pagina. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
import type { SemaPreferences } from '$uix/sema';
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'
}
});
feat(web): /uix/docs/sound — la pagina que explica el canal del sonido Primera de las ocho del plan, en castellano y DERIVADA del codigo vivo: las tablas leen SEMA_MAP y el resolver real, nunca una transcripcion. Una pagina que copiara el catalogo envejeceria al primer gain que alguien tocara — que es exactamente la deriva que este subsistema se reconstruyo para quitar. SIETE SECCIONES, ordenadas para responder «no se como funciona esto despues de tantas correcciones»: (1) en una frase — un componente dice una palabra; (2) el catalogo, los 16 nombres sonando al pulsarlos, agrupados por la FORMA de cada entrada y no por una lista a mano; (3) el orden con los numeros de verdad, tabla viva de familia x nombre x intent capa a capa; (4) que puede llevar un .wav; (5) que no te toca escribir; (6) las correcciones y por que —la historia ES la explicacion, ocultarla es lo que dejo al autor sin saber como funciona—; (7) el tema. EL BLOQUEANTE, PRIMERO. El interruptor Sound del topbar no silenciaba NADA: el layout no pasaba `preferences`, asi que los canales se quedaban en 'full' y el switch solo movia el intent de prefs (que gobierna el lado visual). Publicar paginas que suenan con un mute que miente es peor que no publicarlas. Ahora pasa un objeto VIVO — ambos canales leen `preferences.{sound,haptic}` por emision, asi que mutarlo silencia al instante sin re-registrar nada. Cierra §3.3 de la cola. MEDIDO EN NAVEGADOR, no supuesto: 7 secciones, 17 chips, la rejilla y los chips con su CSS aplicado, y la tabla de capas moviendose de verdad — con `fulfill` el gain pasa de 0.05 a 0.1, el pitch de 700 a 1000 y el contour de flat a ascending. Es decir: la pagina DEMUESTRA en vivo que el intent sobrevive al nombre, que es la tesis del rediseño entero. ⚠️ NO he podido hacer captura: el panel del navegador no esta visible en esta sesion. Verificado por arbol de accesibilidad, consola y estilos computados — pero no lo he MIRADO. Queda pendiente tu ojo. ⚠️ Corregido de paso un fallo propio: la sonda del resolver usaba `document` en el render de servidor y la pagina daba 500. Ahora cae a un stub en SSR. El nav estrena seccion «El sistema perceptual». Quedan las otras siete paginas, y los seis enlaces muertos que Foundations ya declaraba (/uix/architecture, /uix/tokens, /uix/morfo, /uix/sema, /uix/eidos, /uix/getting-started) siguen sin pagina. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
/**
* The perceptual reduction levels the sema channels read. A LIVE object:
* both channels read `preferences.{sound,haptic}` on every emit, so
* mutating it silences immediately with no channel re-registration.
*
* Until 2026-08-06 this was not passed at all, so the channels defaulted to
* `'full'` and the topbar's Sound switch silenced NOTHING — it set the prefs
* intent (which drives the visual/CSS side) and stopped there. A mute that
* does not mute is worse on a page that teaches sound than no mute at all.
*/
const semaPreferences: SemaPreferences = $state({ sound: 'full', haptic: 'full' });
const uix = createActiveUix({
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,
feat(web): /uix/docs/sound — la pagina que explica el canal del sonido Primera de las ocho del plan, en castellano y DERIVADA del codigo vivo: las tablas leen SEMA_MAP y el resolver real, nunca una transcripcion. Una pagina que copiara el catalogo envejeceria al primer gain que alguien tocara — que es exactamente la deriva que este subsistema se reconstruyo para quitar. SIETE SECCIONES, ordenadas para responder «no se como funciona esto despues de tantas correcciones»: (1) en una frase — un componente dice una palabra; (2) el catalogo, los 16 nombres sonando al pulsarlos, agrupados por la FORMA de cada entrada y no por una lista a mano; (3) el orden con los numeros de verdad, tabla viva de familia x nombre x intent capa a capa; (4) que puede llevar un .wav; (5) que no te toca escribir; (6) las correcciones y por que —la historia ES la explicacion, ocultarla es lo que dejo al autor sin saber como funciona—; (7) el tema. EL BLOQUEANTE, PRIMERO. El interruptor Sound del topbar no silenciaba NADA: el layout no pasaba `preferences`, asi que los canales se quedaban en 'full' y el switch solo movia el intent de prefs (que gobierna el lado visual). Publicar paginas que suenan con un mute que miente es peor que no publicarlas. Ahora pasa un objeto VIVO — ambos canales leen `preferences.{sound,haptic}` por emision, asi que mutarlo silencia al instante sin re-registrar nada. Cierra §3.3 de la cola. MEDIDO EN NAVEGADOR, no supuesto: 7 secciones, 17 chips, la rejilla y los chips con su CSS aplicado, y la tabla de capas moviendose de verdad — con `fulfill` el gain pasa de 0.05 a 0.1, el pitch de 700 a 1000 y el contour de flat a ascending. Es decir: la pagina DEMUESTRA en vivo que el intent sobrevive al nombre, que es la tesis del rediseño entero. ⚠️ NO he podido hacer captura: el panel del navegador no esta visible en esta sesion. Verificado por arbol de accesibilidad, consola y estilos computados — pero no lo he MIRADO. Queda pendiente tu ojo. ⚠️ Corregido de paso un fallo propio: la sonda del resolver usaba `document` en el render de servidor y la pagina daba 500. Ahora cae a un stub en SSR. El nav estrena seccion «El sistema perceptual». Quedan las otras siete paginas, y los seis enlaces muertos que Foundations ya declaraba (/uix/architecture, /uix/tokens, /uix/morfo, /uix/sema, /uix/eidos, /uix/getting-started) siguen sin pagina. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
preferences: semaPreferences,
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();
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' }
]
},
feat(web): /uix/docs/sound — la pagina que explica el canal del sonido Primera de las ocho del plan, en castellano y DERIVADA del codigo vivo: las tablas leen SEMA_MAP y el resolver real, nunca una transcripcion. Una pagina que copiara el catalogo envejeceria al primer gain que alguien tocara — que es exactamente la deriva que este subsistema se reconstruyo para quitar. SIETE SECCIONES, ordenadas para responder «no se como funciona esto despues de tantas correcciones»: (1) en una frase — un componente dice una palabra; (2) el catalogo, los 16 nombres sonando al pulsarlos, agrupados por la FORMA de cada entrada y no por una lista a mano; (3) el orden con los numeros de verdad, tabla viva de familia x nombre x intent capa a capa; (4) que puede llevar un .wav; (5) que no te toca escribir; (6) las correcciones y por que —la historia ES la explicacion, ocultarla es lo que dejo al autor sin saber como funciona—; (7) el tema. EL BLOQUEANTE, PRIMERO. El interruptor Sound del topbar no silenciaba NADA: el layout no pasaba `preferences`, asi que los canales se quedaban en 'full' y el switch solo movia el intent de prefs (que gobierna el lado visual). Publicar paginas que suenan con un mute que miente es peor que no publicarlas. Ahora pasa un objeto VIVO — ambos canales leen `preferences.{sound,haptic}` por emision, asi que mutarlo silencia al instante sin re-registrar nada. Cierra §3.3 de la cola. MEDIDO EN NAVEGADOR, no supuesto: 7 secciones, 17 chips, la rejilla y los chips con su CSS aplicado, y la tabla de capas moviendose de verdad — con `fulfill` el gain pasa de 0.05 a 0.1, el pitch de 700 a 1000 y el contour de flat a ascending. Es decir: la pagina DEMUESTRA en vivo que el intent sobrevive al nombre, que es la tesis del rediseño entero. ⚠️ NO he podido hacer captura: el panel del navegador no esta visible en esta sesion. Verificado por arbol de accesibilidad, consola y estilos computados — pero no lo he MIRADO. Queda pendiente tu ojo. ⚠️ Corregido de paso un fallo propio: la sonda del resolver usaba `document` en el render de servidor y la pagina daba 500. Ahora cae a un stub en SSR. El nav estrena seccion «El sistema perceptual». Quedan las otras siete paginas, y los seis enlaces muertos que Foundations ya declaraba (/uix/architecture, /uix/tokens, /uix/morfo, /uix/sema, /uix/eidos, /uix/getting-started) siguen sin pagina. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
{
kind: 'group',
docs(web): cuatro paginas del sonido, escritas para quien llega de nuevas Correccion del autor sobre la primera version: «la redaccion esta mal, empieza como si la gente supiera ya que es lo que has pasado, la redaccion tiene que ser didactica para un nuevo desarrollador». Tenia razon — estaban escritas como notas de version para quien vivio el rediseño: se abrian presumiendo el vocabulario y presumian de cuantas reglas se habian borrado, que es mi historia, no documentacion. Reescritas desde cero, en ese orden: primero la escena («guardas un documento y oyes un repique»), luego el vocabulario construido pieza a pieza (familia, verbo, intent) y solo entonces la maquinaria. Cada concepto se explica antes de usar su palabra. /uix/docs/sound como suena una interfaz — el modelo, con la resolucion en vivo (familia + verbo + intent) /uix/docs/sound/catalogo cada sonido que existe, audible, con la razon de por que suena asi y que significa cada eje /uix/docs/sound/packs traer tu propia voz: sintesis, wav, mp3, registrar nombres nuevos, cambiar en caliente /uix/docs/sound/gestos por que un arrastre suena por repeticion, con un simulador de tres velocidades para oirlo DERIVADAS DEL CODIGO: los sonidos, familias, intents y verbos salen de `SEMA_MAP` y del resolver real. Solo la prosa esta escrita a mano, asi que añadir una entrada al catalogo la hace aparecer con sus medidas. Lo historico no desaparece, cambia de sitio: donde aporta —por que los sonidos son tan distintos entre si, que se gana y que se pierde con el trinquete— va al final de su seccion como razon, nunca como apertura. VERIFICADO: las cuatro responden 200 en SSR con sus secciones · check sin errores nuevos en las paginas. ⚠️ Sigo sin poder mirarlas: el panel del navegador no compone en esta sesion, asi que la composicion visual queda pendiente de tu ojo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
heading: 'El sonido',
items: [
{ slug: '/uix/docs/sound', label: 'Cómo suena una interfaz' },
{ slug: '/uix/docs/sound/catalogo', label: 'El catálogo' },
{ slug: '/uix/docs/sound/packs', label: 'Sound packs' },
{ slug: '/uix/docs/sound/gestos', label: 'Gestos' }
]
feat(web): /uix/docs/sound — la pagina que explica el canal del sonido Primera de las ocho del plan, en castellano y DERIVADA del codigo vivo: las tablas leen SEMA_MAP y el resolver real, nunca una transcripcion. Una pagina que copiara el catalogo envejeceria al primer gain que alguien tocara — que es exactamente la deriva que este subsistema se reconstruyo para quitar. SIETE SECCIONES, ordenadas para responder «no se como funciona esto despues de tantas correcciones»: (1) en una frase — un componente dice una palabra; (2) el catalogo, los 16 nombres sonando al pulsarlos, agrupados por la FORMA de cada entrada y no por una lista a mano; (3) el orden con los numeros de verdad, tabla viva de familia x nombre x intent capa a capa; (4) que puede llevar un .wav; (5) que no te toca escribir; (6) las correcciones y por que —la historia ES la explicacion, ocultarla es lo que dejo al autor sin saber como funciona—; (7) el tema. EL BLOQUEANTE, PRIMERO. El interruptor Sound del topbar no silenciaba NADA: el layout no pasaba `preferences`, asi que los canales se quedaban en 'full' y el switch solo movia el intent de prefs (que gobierna el lado visual). Publicar paginas que suenan con un mute que miente es peor que no publicarlas. Ahora pasa un objeto VIVO — ambos canales leen `preferences.{sound,haptic}` por emision, asi que mutarlo silencia al instante sin re-registrar nada. Cierra §3.3 de la cola. MEDIDO EN NAVEGADOR, no supuesto: 7 secciones, 17 chips, la rejilla y los chips con su CSS aplicado, y la tabla de capas moviendose de verdad — con `fulfill` el gain pasa de 0.05 a 0.1, el pitch de 700 a 1000 y el contour de flat a ascending. Es decir: la pagina DEMUESTRA en vivo que el intent sobrevive al nombre, que es la tesis del rediseño entero. ⚠️ NO he podido hacer captura: el panel del navegador no esta visible en esta sesion. Verificado por arbol de accesibilidad, consola y estilos computados — pero no lo he MIRADO. Queda pendiente tu ojo. ⚠️ Corregido de paso un fallo propio: la sonda del resolver usaba `document` en el render de servidor y la pagina daba 500. Ahora cae a un stub en SSR. El nav estrena seccion «El sistema perceptual». Quedan las otras siete paginas, y los seis enlaces muertos que Foundations ya declaraba (/uix/architecture, /uix/tokens, /uix/morfo, /uix/sema, /uix/eidos, /uix/getting-started) siguen sin pagina. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
},
{
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' },
uix(skip-link): el tier decidió en F2.1 que los shells lo poseen, y no había nada que poseer Canon nuevo, eidos-native (1 parte, 0 eventos): la pieza de WCAG 2.4.1 que `site-header` difirió a los shells hace tres semanas y que `app-shell` (F3.1) necesita para existir. Nace de su fase 0, decisión Q4 firmada. Lo que decide, y por qué: - **Las palabras son suyas.** «Ir al contenido principal» no es copy: es el nombre de un contrato. La regla del tier (B-7 / D-BLK.5) apunta a esta puerta — si un string parece inevitable, lo posee el componente canónico vía `texts:` + langs. Por eso el prop es `region`, no `label`, y el vocabulario es la lista de landmarks de la APG. Cada clave es una FRASE ENTERA: en castellano el artículo se contrae con la región, así que un «Ir a» + sustantivo saldría mal en media catálogo. - **`region` no se estampa**: elige un texto y ninguna capa lo lee. Declararlo repetiría el error que `Affix` corrigió el 2026-08-15. - **No compone `Link`**: `Link` es tinta en línea y todo su API describe texto dentro de un párrafo. Esto es cromo que aparece de la nada, con su geometría y su suelo; envolverlo sería pisar todos sus props y aun así dejarlos en la API pública. Los componentes del canon renderizan sus nativos; son los blocks los que no pueden. - **El handler mueve el FOCO**, que es lo que el fragmento no hace: un `href` desplaza la vista y deja el foco en el enlace, y el siguiente Tab vuelve al cromo que el lector pidió saltar. El destino se hace enfocable sólo mientras dura el salto. El `href` se queda: es el camino sin JS. - **Peldaño propio de z (950), el más alto de la escalera estática.** No puede compartir el de `affix`: un empate lo rompe el orden del DOM y este enlace es por definición el primer elemento del documento, así que perdería contra cualquier aviso fijado debajo — el fallo medido en A-95. Medido con Playwright (el pane del navegador no tiene el foco del SO, así que `:focus` nunca casa ahí): en reposo 1x1 con clip-path; enfocado `position: fixed`, z 950, píldora de 181x36 sobre `primary-solid` con tinta blanca y anillo de foco, igual en claro y en oscuro; el salto deja el foco en `#demo-main` y el `tabindex` temporal se limpia en el blur; en RTL la píldora espeja al otro borde (x 338 -> 761) sin una segunda regla. Y la demo se corrigió a sí misma: decía «Tab desde el botón de abajo» y la medición enseñó que desde ahí el foco va hacia delante. El botón sube encima del marco, y de paso salió la otra mitad de la lección — por tabulación NUNCA se aterriza en el `main`, porque no tiene nada enfocable. Justo por eso el destino necesita el `tabindex` prestado. Guards: `component:audit --only skip-link` PASS · `morfo:check` PASS · `eidos-lint` 2 selectores morfo-backed, 0 inválidos · `docs:check` 0/0 · `svelte-check` 72, ninguno aquí. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
{ slug: '/uix/components/sidebar', label: 'Sidebar' },
{ slug: '/uix/components/skip-link', label: 'Skip link' }
]
},
{
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' },
refactor(skin-media-player): un cableado y cuatro pinturas, con el movimiento del sistema Reescritura completa de la versión A (`bf12b8a57`), que el autor valoró como duplicación. La medición le daba la razón: `fit()` idéntico en dos ficheros, `hold()` y `stop()` en otros dos, y `MediaPlayerProvider.require()` en los cuatro. Eran cuatro componentes compartiendo carpeta, no un skin. ## La forma Un componente cablea, los aparatos sólo pintan. - `skin-media-player.svelte` — EL punto de cableado: un `require()`, una derivación de todo, y las teclas generadas recorriendo un mapa. - `controls.ts` — datos por aparato: lienzo, teclas `[{id,x,y,w,h,action}]` y la ranura del volumen. - `actions.ts` — el vocabulario único (`play·pause·toggle·stop·seek-*·rpm·none`), con `stop()` y el veto por `preventDefault()` en un solo sitio. - `type-fit.svelte.ts` — el ajuste de `<text>`, una vez. - Los cuatro cuerpos: props entran, SVG sale. Cero provider, cero `$effect`, cero contexto. Añadir un quinto aparato es un SVG y una fila de datos. `<SkinMediaPlayer>` deja de montar player: es una PIEZA dentro de uno. No monta `Media`, no monta barra y no decide la forma — `inline/bar/row/card` es el eje `variant` de `AudioPlayer`, y la piel viaja dentro como hija. ## Movimiento: el del sistema, nada a mano La versión A traía tres `@keyframes` locales y siete transiciones con números crudos, ignorando `docs/theming/motion.md` (leída ahora, entera). - Todo giro continuo es el preset **`spin`**; el respirar de la cinta, **`pulse`**. El periodo va en la ranura del propio loop (`--motion-loop-spin`: 1.333s a 45 rpm, 1.8s a 33). - Las transiciones consumen `--duration-*` / `--ease-*` por una ranura `--_smp-*` por eje. - DEFECTO FUNCIONAL corregido: el generador apaga los loops por `[data-motion='reduce']` **y** por la media query; el bloque a mano sólo tenía la segunda, así que la preferencia UIX en `reduce` sin ajuste del SO no paraba el plato. Medido en navegador: ahora `animation-name: none`. - Queda un `@keyframes` local, la aguja del vúmetro, anotado `functional:` y con el periodo en token: ningún preset representa un instrumento analógico. ## Tres defectos que sólo se ven en movimiento Los tres pasaban toda comprobación de atributos y sólo aparecieron mirando la imagen moverse: 1. **La sombra rotaba con el disco.** El `feDropShadow` colgaba del grupo que gira, así que su desplazamiento orbitaba (se leía como la sombra subiendo y bajando) y la penumbra de la región del filtro barría las marcas del plato, que parecían parpadear. El filtro pasa a un padre quieto — un disco es un círculo, su silueta no cambia. Igual en el brazo. 2. **Las marcas del plato salían disparadas en arco.** Al rehacer se perdió la regla del estrobo; sin `transform-origin` el preset gira alrededor del centro del viewBox (500, 440) en vez del centro del plato (406, 468.5). 3. **El tamaño no llegaba.** `max-inline-size` sólo encoge, así que `lg` y `xl` eran inertes en cuanto la columna era más estrecha (medido: md/lg/xl los tres a 720px). Ahora `inline-size: min(100%, …)`, base por objeto y factor por tamaño, una sola fuente en el recipe — la base estaba declarada dos veces, inline y en CSS, y ganaba la inline. ## Volumen Primero se compuso el `Knob` del catálogo y era inmanejable: mapea el valor al ángulo ABSOLUTO del puntero alrededor del centro (`Gesture.rotate`), o sea orbitar el ratón en torno a un dial de 40px. Sustituido por el `VolumeSlider` del propio player corriendo en una guía pintada por la máquina. El re-tinte va sobre el MISMO descendiente que retinta el player (`media-player.css:420`) o su regla gana por estar más cerca del thumb. La banda inferior de la pletina se reordenó para alojarlo: vúmetro a media anchura con el dial recentrado (aguja intacta), deslizador a 42 unidades del bisel (20px a `md`), contador y piloto +20 a la derecha, la etiqueta REPROD del piloto fuera, y «MODELO CR-77» al mismo ancho que «Levante» por `textLength` + `lengthAdjust="spacing"`. ## Prop `labels` y la decisión de idioma `labels` (default `true`) apaga la serigrafía entera de la máquina. Los rótulos NO se traducen, y es decisión firmada: son parte del OBJETO, como la marca del badge. Los nombres accesibles sí siguen el idioma, porque salen del morfo de las partes compuestas. ## Demo canónica `web/routes/uix/components/skin-media-player/+page.svelte` con la plantilla v2 de 9 pestañas + su entrada en el sidebar. El banco provisional de `/otros/players` se borra: duplicaba la demo con DOS players independientes, que es lo que produjo dos lecturas falsas («no está sincronizado», «fondo negro»). Los cuatro bocetos del autor se quedan en la carpeta, ya sin ruta. `preload="none"` en la demo a propósito: un preload de metadata abre una petición de rango que el navegador aborta, y `smoke` cuenta ese aborto como fallo same-origin. ## Auditoría — el hueco que queda `component-guide.md` §5 dice que incluso un átomo pasivo es morfo-first, con un morfo mínimo `scope: ['eidos']` y `## Passive justification`. Este componente no lo tiene porque el alcance acordado excluía `src/uix/morfo/`, y la consecuencia no es cosmética: **tres máquinas no lo ven** — `component:audit` enumera por ficheros de morfo y no le da fila, `eidos-lint` lo salta («no morfo file») y `morfo:check` no visita su demo. Los guards transversales sí pasan; los específicos de componente no se han ejecutado sobre él ni una vez. Pendiente además: `data-perm-step` en la demo (A37), sin el cual `perm:check` la salta en silencio. Verificado: `npm run check` sin errores nuevos (base 72/59) · 77 tests de eidos (recipe-css-contract · component-api-contract · component-visual-attrs · motion) · `rtl:check` 0/179 · `docs:check` 0/0 · `layer:check` 0 · `smoke` PASS · `morfo:check` 159/159. Y en Chrome, con el ratón: transporte en los dos sentidos, brazo siguiendo el surco, bobinas por conservación de área, contador de cinta, y el volumen moviendo `audio.volume` de verdad. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
{ slug: '/uix/components/media-player', label: 'Media player' },
{ slug: '/uix/components/skin-media-player', label: 'Media player skin' }
]
},
{
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' },
uix(background): el fondo se mueve con el scroll, y con lo que el lector no pidió no F3 (parallax) + F4 (demo y registro documental) + las correcciones de sus dos auditorías, en un commit porque viven en los mismos ficheros: la demo enseña los ejes que F3 añade, y separarlas dejaría un estado que nunca se probó. Cinco maneras de que una capa deje de estarse quieta: `speed` (cuánto del token de travel cubre mientras el anfitrión cruza el viewport), `bleed` (crece más allá del anfitrión para que el viaje no arrastre su propio borde), `depth` (la deriva contra el puntero), `spotlight` y `attach='fixed'`. El travel es CSS: `animation-timeline: view()` lo gobierna desde la posición de scroll, sin listener ni rAF. Sólo donde el motor no lo trae, la pila arranca `ScrollProgress` y escribe `--background-progress`, que la rama `@supports not` mete en la MISMA declaración; los dos caminos no pueden estar vivos a la vez porque el JS comprueba la condición idéntica con `CSS.supports`, y ambos se paran bajo `prefers-reduced-motion` — el parallax es movimiento atado al scroll del propio lector, que es justo la clase que provoca síntomas vestibulares. **La decisión que no estaba prevista.** El scroll y el puntero quieren mover la MISMA capa, y una animación sobre `translate` gana a cualquier declaración estática: el puntero habría dejado de existir sin más. Así que el scroll anima una custom property REGISTRADA (`@property`, o interpolaría a saltos) y un único `translate` compone los dos términos. Medido: parallax solo → `0px 30px`; con el puntero arriba-derecha y `depth: 20px` → `20px 10px`. Es `translate` y nunca el shorthand `transform`, la misma ley que sigue el lift del draggable con `scale`. El precio, dicho porque en la primera redacción escribí lo contrario tres veces: una custom property NO se puede compositar, así que el navegador recalcula estilo cada frame. Para una decoración es el intercambio correcto —una property por capa que viaja, ninguna bajo reduced motion— pero «va en el compositor» era falso y ahora el código dice lo que ocurre. **Dos footguns cerrados por forma, no por disciplina:** - `attach='fixed'` se DECLARA desde la capa y la pila se recorta sola. Antes había que escribirlo también en la pila, y olvidarlo dejaba la capa `position: fixed` pintando a sangre por todo el viewport, detrás de todo y sin error (un hijo fijo se escapa de `overflow: clip`; sólo un `clip-path` lo trae de vuelta). La prop de la pila desaparece: no hay nada que olvidar. - Un `speed` negativo —una capa que se mueve contra el scroll— invertía el bleed: la capa ENCOGÍA y enseñaba justo los bordes que el bleed tapa. Ahora usa la magnitud. **Lo que costó medición**: el shorthand `animation` pone `duration: 0s` y una línea de tiempo de progreso necesita el `auto` inicial, así que con el shorthand la capa no se movía nunca (van longhands, con el porqué escrito) · mi listener de puntero pedía un frame y no lo liberaba si el rect salía degenerado, matando el puntero para el resto de la sesión (reescrito sin frame, con el rect cacheado e invalidado por `pointerenter` y `observeResize`) · las cuatro registraciones —`animated`, `pointer`, `scroll`, `fixed`— comparten un solo sitio, `declare.svelte.ts`, donde vive la regla A30 y su segunda mitad: registrar desde el init, y seguir el prop sin escribir en la primera pasada. **La demo** (`/uix/components/background`, v2, nueve pestañas) monta un ANFITRIÓN de verdad en el escenario, porque este componente es invisible por sí solo y sin padre no se puede enseñar lo único que importa: que el padre se adopta y el layout no se mueve. Los chips son uniones completas verificadas por el TIPO (`Record<Union, 0>`): un miembro que falte es error de compilación. Y fue la demo la que destapó que, con A30, encender `animate` en caliente no hacía aparecer el control de pausa — el registro era un hecho de montaje. Invisible en una sonda, obvio con un interruptor. Registro documental (D-BG.11): `next-features.md` §11 · la frase en `design-text-effects.md` (el mismo corte canon/pack leído desde el otro lado) · `PLAN-blocks-quality.md` Q0.3 → sucesor · `surface/README.md` §Gaps «scrim de autoría» CERRADO por `Background.Scrim` · glosario con entrada `Background` y `Aura` corregida (decía «Not built yet» y está construido) · y en `motion-guide.md` §8 + el RFC: el travel ligado al scroll no es un preset —un preset nombra una transición discreta CON duración, y esto es modulación continua sin ninguna— y sólo se replantea como dominio con un segundo consumidor. Verificado en Chrome real: el puntero mueve `depth` y `spotlight` con los valores exactos y vuelven al centro al salir · `attach='fixed'` estampa y retira el recorte de la pila · el bleed aguanta el speed negativo · RTL: el `translate` del puntero se mantiene FÍSICO y el bleed en el eje de bloque · cada control de la demo cambia algo (los de `spotlight` y `depth` no llegaban a tres de las cuatro clases de capa hasta la segunda auditoría). ⚠️ SIN VERIFICAR, y no lo doy por bueno: el travel real al hacer scroll, los 60 fps y el detector de reflow. El panel del navegador va oculto con viewport 0×0 y ahí las animaciones scroll-driven declaradas en CSS no se activan — comprobado que es del ENTORNO con un caso mínimo inyectado (un `div` pelado con `animation-timeline: view()` sale inactivo mientras una `ViewTimeline` creada por API sobre el mismo sujeto marca 68%). Necesita una pasada con Chrome visible. Gates: audit `--only background` PASS 0 errores · eidos-lint invalid 0 · `rtl:check` 0/180 · `docs:check` 0/0 en 634 docs · `blocks:check` 0/18 · `morfo:check` PASS · smoke PASS · 441/442 (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
{ slug: '/uix/components/background', label: 'Background' },
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/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' },
feat(eidos): Affix — la pieza que Sticky no puede ser, y la capa que Fab y MenuDial ya habian copiado dos veces Un aviso al principio del documento se va con la pagina: `position: sticky` se despega en cuanto termina su contenedor. Medido el 2026-08-10 sobre el block `banner` — `Sticky edge="bottom"` acaba en `top: -8` sin `data-stuck`. No es un cableado roto: es que un elemento anclado al viewport es `fixed`, y eso es otro componente. Pero no es un componente nuevo: la capacidad YA existia dos veces. `fab.css` fija 4 esquinas, `menu-dial.css` nueve, y las dos ya divergian (`--fab-offset` publico vs `--_menu-dial-offset` interno; `var(--fab-z)` vs `var(--z-index-sticky, 1100)`, un fallback de 1100 sobre un token que vale 100). Y antes de eso existio entera: `air/layout/float`, el primitivo de 9 zonas que el refactor retiro sin sustituto — lo dicen los README de `float` y de `Fab`. Asi que `affix.css` no es un tercer ejemplar: es la CAPA, enganchada en `data-affix-placement` y no en `data-affix`. El split es portante — `morfo-check` selecciona `[data-affix]` en toda la pagina y valida cada match contra `affixMorfo`, asi que si `Fab` estampara la identidad quedaria soldado a este contrato. Estampando solo el gancho de capa, no. Es el patron de `lib/list-surface.css`: una fuente, cero nodos extra. Lo que salio midiendo, no razonando: - `--z-index-affix: 150`, peldano nuevo. Con la tira en `top` empataba con `--sticky-z-index` (100 los dos) y el empate lo rompe el ORDEN DEL DOM: el aviso va antes que la cabecera en el fuente, luego perdia. 30,7px de solape con el cromo encima — el fallo original reproducido por su sustituto. Un peldano toca DOS sitios: `STATIC_Z_INDEX` y la lista cerrada `Z_INDEX_KEYS`. - NO compone `<Box>`, aunque sea el patron de los primitivos de layout: `box.css` declara `position: var(--box-position, revert-layer)` a la misma especificidad que el gancho, y eidos no usa `@layer`. En el orden de carga equivocado, `position: static` — un `fixed` que no hace nada. - `stretch` es solo de los bordes de bloque. El rail del eje inline se envio y se retiro el mismo dia: `Affix` no dimensiona a su hijo, asi que era una caja invisible de 380px con el hijo de 47px arriba, identica a `top-start` en pantalla. El marco medido decia otra cosa; la pintura mandaba. - `env(safe-area-inset-*)` es fisico y los anclajes logicos: remapeado bajo `:dir(rtl)`. Emparejar `inset-inline-start` con `safe-area-inset-left` despeja la muesca equivocada en RTL apaisado — que es lo que hacen hoy `fab` y `menu-dial`, registrado. `banner` recupera lo que su README daba por imposible: `affix="top" | "bottom"`. La disposicion anterior decia «app-land, el canon ya trae Sticky» y nombraba un componente que no puede hacerlo. Verificado con recorrido real (342px en la demo, 1247px en la preview del block): desplazamiento 0 en los dos bordes, nueve zonas exactas, RTL espejando, el ancestro transformado derivando -342px como esta documentado, y `elementFromPoint` sobre la tira devolviendo el aviso y no el cromo. component:audit PASS 0/0 · morfo:check PASS · rtl 0/178 · smoke 311/311. Queda: migrar `Fab` y `MenuDial` a la capa. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
{ slug: '/uix/components/sticky', label: 'Sticky' },
{ slug: '/uix/components/affix', label: 'Affix' }
]
}
];
const componentCount = $derived(
nav
.filter((s): s is Extract<Section, { kind: 'group' }> => s.kind === 'group')
.filter((g) => ['Controls', 'Media', 'Overlays'].includes(g.heading))
.reduce((acc, g) => acc + g.items.length, 0)
);
const current = $derived(page.url.pathname.replace(/\/$/, '') || '/uix');
// ── Theme ─────────────────────────────────────────────────────────────
let theme = $state<'light' | 'dark'>('light');
const eidosModeListeners = new Set<(value: 'light' | 'dark') => void>();
const activeEidos = ActiveEidos.create({
applyDom: true,
theme: 'base',
modeSource: {
get: () => theme,
onChange(handler) {
eidosModeListeners.add(handler);
return () => eidosModeListeners.delete(handler);
}
}
});
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(() => {
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');
feat(web): /uix/docs/sound — la pagina que explica el canal del sonido Primera de las ocho del plan, en castellano y DERIVADA del codigo vivo: las tablas leen SEMA_MAP y el resolver real, nunca una transcripcion. Una pagina que copiara el catalogo envejeceria al primer gain que alguien tocara — que es exactamente la deriva que este subsistema se reconstruyo para quitar. SIETE SECCIONES, ordenadas para responder «no se como funciona esto despues de tantas correcciones»: (1) en una frase — un componente dice una palabra; (2) el catalogo, los 16 nombres sonando al pulsarlos, agrupados por la FORMA de cada entrada y no por una lista a mano; (3) el orden con los numeros de verdad, tabla viva de familia x nombre x intent capa a capa; (4) que puede llevar un .wav; (5) que no te toca escribir; (6) las correcciones y por que —la historia ES la explicacion, ocultarla es lo que dejo al autor sin saber como funciona—; (7) el tema. EL BLOQUEANTE, PRIMERO. El interruptor Sound del topbar no silenciaba NADA: el layout no pasaba `preferences`, asi que los canales se quedaban en 'full' y el switch solo movia el intent de prefs (que gobierna el lado visual). Publicar paginas que suenan con un mute que miente es peor que no publicarlas. Ahora pasa un objeto VIVO — ambos canales leen `preferences.{sound,haptic}` por emision, asi que mutarlo silencia al instante sin re-registrar nada. Cierra §3.3 de la cola. MEDIDO EN NAVEGADOR, no supuesto: 7 secciones, 17 chips, la rejilla y los chips con su CSS aplicado, y la tabla de capas moviendose de verdad — con `fulfill` el gain pasa de 0.05 a 0.1, el pitch de 700 a 1000 y el contour de flat a ascending. Es decir: la pagina DEMUESTRA en vivo que el intent sobrevive al nombre, que es la tesis del rediseño entero. ⚠️ NO he podido hacer captura: el panel del navegador no esta visible en esta sesion. Verificado por arbol de accesibilidad, consola y estilos computados — pero no lo he MIRADO. Queda pendiente tu ojo. ⚠️ Corregido de paso un fallo propio: la sonda del resolver usaba `document` en el render de servidor y la pagina daba 500. Ahora cae a un stub en SSR. El nav estrena seccion «El sistema perceptual». Quedan las otras siete paginas, y los seis enlaces muertos que Foundations ya declaraba (/uix/architecture, /uix/tokens, /uix/morfo, /uix/sema, /uix/eidos, /uix/getting-started) siguen sin pagina. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// And the channels themselves: the prefs intent drives the visual side,
// this drives whether anything is heard or felt at all.
semaPreferences.sound = soundOn ? 'full' : 'off';
semaPreferences.haptic = hapticOn ? 'full' : 'off';
});
// Project the display axes onto :root through a managed <style> (id-keyed, so
// it replaces in place). :root — not the docs shell — so portaled overlays,
// which render outside the shell, inherit the same density/scaling/border.
$effect(() => {
const d = DENSITY_SCALES[density];
uix.dom.writeStyle(
'uix-docs-display',
`:root{--density-space-scale:${d.space};--density-control-scale:${d.control};--scaling:${scaling};--border-width:${borderWidth}px;}`
);
});
const filteredNav = $derived.by(() => {
const q = search.trim().toLowerCase();
if (!q) return nav;
return nav
.map((s) => {
if (s.kind !== 'group') return s;
const items = s.items.filter((i) => i.label.toLowerCase().includes(q));
return items.length ? { ...s, items } : null;
})
.filter((s): s is Section => s !== null);
});
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): un guard para el eje logico-fisico, y el <main> que congelaba las 51 demos Cierra la §2.1 del handoff. El guard nacio en rojo con 24 hallazgos y al arreglarlos aparecieron dos defectos mas grandes que el que buscaba. 1) EL GUARD RTL-1. Nada cruzaba un ancla LOGICA con un desplazamiento FISICO en la misma regla CSS: eidos-lint clasifica SELECTORES, no declaraciones, y la fila RTL del contrato de construccion decia "rule (LIVE)" — revision humana. Por eso el defecto del slider llego a produccion y lo cazo el ojo del usuario. Ahora `npm run rtl:check`, con la logica en src/uix/eidos/rtl-lint.ts (14 tests) para que sea testeable, calcando el par lint.ts / scripts/eidos-lint.ts. El test ancla el caso historico: slider.css ANTES de 013ceac57 sale rojo y el arreglo que se commiteo sale verde. Dos desviaciones del enunciado, deliberadas. AMPLIA a la propiedad independiente `translate:`, que se usa MAS que `transform: translate*` (87 frente a 36) y tiene el mismo defecto — ocho de los 24 hallazgos eran de esa forma. RECORTA `padding-inline*`: no mueve la caja contra su ancla, asi que no puede pelear con un translate; solo anadia ruido. 2) LA COSECHA, 24 -> 1. El unico que queda es palabras, auditado y no tocado. Los 23 caen en tres familias: CENTRADO (10) donde el resultado deseado es simetrico y basta dejar de descentrar; DIRECCIONAL (6) donde el signo debe voltear; y FISICO (7) donde lo que sobraba era el ancla logica — handles de brujula del cropper sobre una imagen que no se voltea, y el arco polar del menu-dial. Medido en Chrome el switch: el thumb salia a +24px en un carril de 38px, colgando fuera. Verificados tambien carousel, avatar, cropper, menu-dial y chat-log. 3) :dir() ES LA DOCTRINA, [dir='rtl'] QUEDA PROHIBIDO. Los 12 selectores que habia en eidos estaban TODOS rotos, en los dos sentidos opuestos del mismo error, y ambos medidos en Chrome. Los 5 descendientes se aplican DE MAS: el sidebar tenia direction:ltr de verdad y aun asi matcheaba, porque el selector capta cualquier ancestro RTL e ignora un dir mas cercano que redeclare. Los 7 de mismo-elemento NO se aplican NUNCA: el tree-view era rtl de verdad y no matcheaba, porque desde 013ceac57 el atributo solo se estampa si alguien lo afirmo. :dir() acierta los tres casos porque mira la direccion RESUELTA. Baseline desde 2023. 4) EL PIN DEL <main>. 5469e05df hizo dos cosas que se anulan: puso las demos en `auto` —sin prop dir, o sea el componente no estampa atributo y HEREDA del DOM— y a la vez clavo <main data-uix-canvas dir="ltr"> sobre ellas. Ese <main> era la herencia, asi que el toggle del topbar y el arabe llegaban a <html> y morian ahi: las 51 demos congeladas en LTR. Su comentario afirmaba que "los componentes siguen las prefs, este contenedor no les afecta" — cierto para la LOGICA, falso para el CSS, que lee el direction COMPUTADO. Medido: html dir="rtl" con el panel del sidebar todavia a la izquierda, y quitar ese atributo y nada mas lo movia. Se queda el lang="en". Coste aceptado por el usuario: vuelve la puntuacion desplazada en la prosa inglesa al elegir RTL, que es cosmetico y solo en un modo de inspeccion; demos que no pueden demostrar, no. Si hay que fijar la prosa otra vez, el pin va alrededor de la PROSA. Verificado: check 74 = linea base exacta en las tres pasadas. rtl-lint 14/14. Suite de eidos 375 pasan; el fallo de lint.test.ts por audio-player es PREEXISTENTE, comprobado con stash, y es del hilo del sonido. Los 9 CSS tocados ya fallaban prettier antes de tocarlos, asi que no se formatean. SIN VERIFICAR EN NAVEGADOR, anotado en el handoff: archetypes y proof-of-human (sus reglas viven en @media (pointer: coarse), invisible en escritorio) y sliding-indicator (la demo del radio-group segmented no llegaba a medir). QUEDA: hay wrappers estampando dir="" vacio en vez de omitir el atributo — visto en sidebar y tree-view; no rompe porque un valor invalido hereda, pero contradice la letra del contrato. 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 via
`lang` — but NOT via `dir`. A `dir="ltr"` here used to pin the prose
against the bidi artefact ("What is morfo?" rendering as "?What is
morfo"), and it worked, but it pinned the DEMOS too: a component in
`auto` mode passes no `dir` prop, so it stamps no attribute and
resolves its direction by INHERITANCE from the DOM. This container
was that inheritance, so the shell's RTL switch — and picking
Arabic — reached `<html>` and stopped here, leaving all 51 demos
frozen in LTR. Measured on the sidebar: `html dir="rtl"` with the
panel still on the left, and dropping this attribute alone moved it.
The prose artefact is cosmetic and only shows in RTL, which is an
inspection mode; demos that cannot demonstrate are not. If the
prose needs pinning again, it has to happen AROUND THE PROSE, not
around the canvas that also holds the live examples. -->
<main data-uix-canvas lang="en">
{@render children?.()}
</main>
</div>
</div>

Powered by TurnKey Linux.