docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
---
title: Component Implementation Guide
type: guide
audience: human + agent
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo
El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en
`CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source
of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`.
Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es
normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon;
por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que
`:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook
incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las
trampas que el guard no ve; que espeja y que no; y la mitad global de prefs.
EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal:
- `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones,
mandando al wrapper a leer prefs directamente. Eso excluye la prop.
- `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()`
dentro del provider) como LA forma de obtener la direccion — justo lo que el eje
retiro del catalogo.
- `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la
regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una
clave HTML) es correcto y sobrevive; solo cambia el ejemplo.
- `active-architecture.md:416` no listaba `lang` en la proyeccion, contra
`contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`.
Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`,
`data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER
guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`,
`building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la
matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura,
que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`.
El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en
la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en
E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de
estampar `data-dir`, que `:dir()` no puede ver.
DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y
resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del
catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse.
Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos:
RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca
lee al padre» era absoluto y borraba la composicion sancionada en el punto de
llamada · el estampado se afirmaba incondicional en un sitio y condicional en
otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni
`component-audit.ts` conocen.
`docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
authority: canonical — the ordered build process (steps 1– 41 + rules A1– A37)
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
status: current
source: migrated from src/uix/soma/COMPONENT_GUIDE.md (2026-07-02, docs-book F7.5)
---
# Component Implementation Guide
Step-by-step guide for building soma headless components.
fix(cleanroom): F2+F3+F4-C+SEM-4s1 — lote mecánico, censos con guard, corpus documental y el close polimórfico de los pickers VIVO
F2 — lote mecánico (13 ítems):
- DEP-2 ogl eliminado (0 imports) · DEP-1 clsx inlineado como toClassString
propio + suite de contrato (props.test.ts; soma.md §12 cerrado).
- THM-7: los 5 selectores manuales de sema.md reescritos con semaSelector
(los ejemplos [data-toast-root] apuntaban a un part INEXISTENTE — la
deriva que el builder previene, demostrada en el propio doc).
- MOR-1 escape isomorfo + validación de attr-names en semaSelector + 9
tests (selectors.test.ts, matches() real con comillas/corchetes) ·
MOR-2 partMarkerAttr = única fuente compilador↔builder + test de paridad ·
MOR-3 _resetCompileCache borrado (0 usos).
- SOM-2 keydown continue en match sin handler + keyboardFixtureMorfo ·
SOM-1 no-await de handlers (censo async = 0; contrato V1 cumplido) + pin.
- SEM-2 trigger pre-attacha catch con logger (void trigger sin unhandled
rejection; throw intacto para awaiters) + pin · SEM-3 fallback muerto de
applyDominance → skip defensivo + timer tope de awaitExpression cancelado ·
SEC-1 adjudicado YA implementado (assertCssVariableValue desde 2026-05-11)
+ pin del path de VALOR.
- accordion → outline (§32; su outline:none dejaba CERO anillo en HCM) —
verificado en vivo · THM-6 radius-full 9999px · EID-4 recuentos 33.
F3 — censos con guard:
- SOM-3 cerrado: announcer + image-provider migrados a scheduler-preferred
(consumidores cableados: date/time-field vía soma.uix.timers; avatar/image
vía eidos.timers — verificado en vivo); guard de timers ENSANCHADO de
soma/components a TODO soma y pasado a EVIDENCIA (setTimeout exige
.schedule( en el fichero — layers/ y datetime/ escapaban del ámbito viejo).
- THM-5: R-4.7 nueva (válvula same-line /* important: <razón> */, escaneo
comment-blanked) + las 15 declaraciones anotadas con su razón + canon
recipe-contract §3/§4.
- SOM-4 adjudicado: el censo/guard YA existían (49 pins); knob/mask-field/
timeline pinneados (overrides documentados en call-site); media-player
Batch-4 (35 hits, cero renderProps) = único batch restante, registrado.
- THM-4 doctrinado en eidos.md §unused (comportamiento/composición =
legítimo; deuda = eje visual sin consumidor; hotspots por lotes).
F4-C — corpus documental (decisiones de usuario aplicadas):
- DOC-3: los 15 enlaces muertos resueltos (repoint a la edición FINAL
trackeada / des-link históricos) · docs:check I6-links WARN→ERROR.
- DOC-1: tabla «Build contract» MIGRADA a component-guide con estados
modernizados (A3–A5 → LIVE + guards de hoy); banners reapuntados; citas
de CANON/sema.md historificadas; lápida-redirect en el §13 del fósil.
- DOC-4: hold chain → holds.ts · FAQ event:* SUPERSEDED por signatures ·
gradient añadido a los DOS capstones (sextet real) · nota de paleta de
demo-authoring corregida (universalPaletteDecls + decisión THM-2 =
mecanismo universal como sucesor del tracker borrado).
- DOC-5/6: recuentos anti-frágiles datados · §4.11 dup → §4.12 · Known gaps
historificado · N-6/N-7 recuperadas de git (d68d2c45^) y canonizadas en
eidos.md §pickers · authoring E2 → canon/tsc.md · air-old des-linkado ·
EID-3 (placement) en la fila RTL · AUX-2 disabledDom documentado.
SEM-4 sesión 1 — el close polimórfico de los pickers, VIVO (D.11):
- Reconciliación: los morfos ya no declaran close (delegated al Popover,
de-dialoged 06-27); el agujero real era el cierre programático bypaseando
dismissWith → save/cancel/select eran perceptualmente SILENCIOSOS.
- Fix: PickerShellHandle.setPopoverDismiss + closeWith(cause) en los 5
providers (14 sitios; select/commit → 'save' = commit.save+fulfill,
cancel → 'cancel' = emerge; fallback raw para headless) + UN inyector en
el eidos PickerShell root (norma N-8). Picker genérico fuera a propósito
(ya suena commit-set/cancel por diseño S9).
- Verificado en vivo (date-picker): Done → close·commit·fulfill·active ·
Cancel → close·emerge · cierre real.
Gates: matriz 141/141 (los 6 morfos nuevos de la pista de texto paralela
también PASS) · contracts 38/38 · eidos 314 · sema 178 · morfo 94 ·
docs:check 0/0 con I6 en error · baseline propio 57.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> **⚠️ Build contract — read before building.** The canon table of WHAT every
> component must consume to avoid drift lives **below, in this guide**
> ([§ Build contract](#build-contract-the-canon-table)). Historical origin:
> the 2026-06-19 archetype-coherence audit (its §13 seeded this table; the
> audit is history now, not the source — DOC-1, 2026-07-11).
## Build contract (the canon table)
**This is what EVERY component consumes to stay faithful to the eidos
design.** All axes are LIVE — the phased rollout the 2026-06-19 audit
planned (A3– A5) landed during 2026-06/07; each axis names the guard that
defends it today.
docs(eidos): la doctrina alcanza a la jornada — dos guards nuevos, el patron de capa, y EID-3 deja de estar pendiente
Barrido de lo que la sesion cambio y la documentacion todavia no decia.
`testing-and-tooling.md` — los dos guards nuevos entran en la tabla de «que
atrapa cada script», con su reparto explicito: `layer:check` mira el valor
computado (quien gana la cascada) y declara su hueco (la geometria, porque
`getComputedStyle` da el valor USADO y un `inset: auto` se lee como pixeles);
`shared-layer-contract.test.ts` mira el texto, y existe por lo que el navegador
no puede ver — un `env()` ya sustituido devuelve `"0px"` en escritorio.
`component-guide.md` fila RTL — deja de remitir a «EID-3 exception» y enuncia la
regla: dos rejillas NOMBRADAS, `Position` fisica y `LogicalPosition` logica, y la
pregunta que decide entre ellas («¿tiene que voltearse para un lector de derecha
a izquierda?»). Estrechar con `Extract<>`, nunca redeclarar.
`canon/recipe-contract.md` — la fila de z-index flotante distinguia mal: la banda
`--z-index-overlay-*` es de overlays PORTALED. El cromo fijado al viewport que no
portala es otra cosa y tiene su peldano (`affix`, 150). Y el item 9 del checklist
de recetas decia «si flota → una rung de overlay», que era incompleto.
`eidos/components/README.md` — el patron de CAPA COMPARTIDA, que no estaba
escrito en ningun sitio pese a tener dos ejemplares vivos (`list-surface` y
`affix`). Sus cuatro reglas, tres de ellas aprendidas rompiendose: enganchar en
el attr de capa y no en la identidad (o `morfo-check` suelda al consumidor a un
contrato ajeno), un eje = token publico + ranura, el puente reafirma `position`
si el primitivo compuesto declara uno, y se guarda con dos redes porque ninguna
basta sola.
`audit-active-uix.md` — EID-3 pasa a RESUELTO, conservando el hallazgo original
debajo. Era un P3 de julio cerrado «como excepcion» pero marcado en su propia
tabla resumen como «pendiente de doctrina explicita». Ya no lo esta.
`PLAN-affix.md` §7 — lo que vino DESPUES de cerrar el plan, que es casi todo lo
interesante: las dos migraciones y sus dos lecciones, el peldano de z, el patron
de tokens, la canonizacion de la rejilla y los dos guards. Ninguna estaba
prevista en el plan; todas salieron de auditar lo construido.
docs:check 0/627.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Axis | Canon — WHAT to consume | NOT this (drift) | Guard |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| **Surface / elevation** | `data-depth='overlay'\|'modal'\|…` → the full bundle (surface·shadow·halo·border·blur·z) | hand-picked `surface-raised` /`-default`; own `--{c}-overlay-z` ; arbitrary frost | `elevation-plane.test.ts` · THEME-SYS-1 |
| **Radius** | `--radius-default` / global factor + `[data-shape-nest]` concentric | `calc(--radius-md − space)` by hand; fixed px | R-2.x + shape engine |
| **State (hover/active)** | `--state-{hover,press,selected}` layer (neutral tier; per-variant accent stays in the recipe) | ad-hoc `color-mix` ; per-component `--x-hover-bg` | R-4.3 |
| **Focus** | `outline` + `--focus-ring-*` (§32 — ONE model, HCM-safe; the foundation fallback is `:where()` -wrapped so recipes win) | own focus tokens; box-shadow rings (die in HCM) | R-1.5 + forced-colors floor |
| **Field label** | the canonical label role (size-relative, one step below the input; unified weight/color) | redefining `--{c}-label-*` | Field doctrine 2026-07-05 |
| **Size (controls)** | the `--size-{k}-*` bundle (height·font·padding·gap·radius·icon) | re-deriving size→font; consuming none of the bundle | size-bundle test (recipe-css-contract) |
| **Touch hit-area** | §37: `--touch-target` (44px) under `pointer: coarse` only — AREA ≠ VISUAL (`::before` slop for bare markers) | targets < 44 on touch without slop ; growing the visual | archetypes . css coarse rules |
| **Portal typography** | anchor `font-family` +`line-height`+`color` on the portaled content root | inheriting (falls to serif in the portal) | rule (LIVE) |
| **RTL** | **logical** properties (`inline/block`, `inset-inline` ) for flow; physical `left/right` ONLY where the geometry itself is physical (compass handles, polar arcs, a JS-measured offset) or as a **placement grid that must NOT mirror** — and that is now a named choice, not an exception: `Position` (physical) vs `LogicalPosition` (`start`/`end`, mirrors), both consts in `eidos/lib/types.ts` , both in [`canon/vocabularies.md` ](../canon/vocabularies.md ) §Placement grids. **Ask: must it flip for a right-to-left reader?** A strip pinned to `bottom-end` belongs on the trailing edge in both directions; a panel that opens to the physical right because that is where the space is does not. Narrow with `Extract<>` , never re-declare a grid (the logical one was hand-written five times until 2026-08-15). This closes EID-3, which recorded the physical exception in July 2026 and left its doctrine pending; branch on direction with ** `:dir(rtl)` ** — and then the provider MUST stamp the raw `dir` , or `:dir()` only ever sees the inherited direction ([direction contract](../canon/direction-contract.md)) | physical `padding-left` /… in content flow; a logical anchor paired with a physical `translateX` — the anchor flips, the transform does not; `[dir='rtl'] …` , which misses the common no-attribute case and ignores any nearer re-declaration; accepting the prop, running the chain and never stamping — the maths moves, the paint stays behind; a `:dir()` rule that turns one logical face off and repaints the other — the property had ALREADY mirrored, so that cancels it | RTL-1 · RTL-2 · `npm run rtl:check` |
| **RTL · SVG** | a graphic with a READING axis mirrors (invert the scale's pixel range); a RADIAL one does not. `text-anchor` is LOGICAL: leave it alone when the composition mirrors, force the physical one when it does not — see `eidos/components/chart/README.md` §Direction | mirroring _and_ flipping the anchor (they cancel); flipping the anchor on a gutter that never moves (the label walks across the graphic); mirroring y values | eye, in RTL — RTL-1 reads CSS text and cannot see SVG attrs or JS-written inline geometry |
| **i18n** | `eidos.langs.ts('#?key\|fallback')` + key in the catalog | hardcoded strings / `aria-label` s | rule (LIVE) |
| **Color (values)** | role tokens `--color-*` / recipe tokens | raw hex/rgb/hsl/oklch | R-2.1/2.6 · R-4.6 |
| **Density / spacing** | `--space-*` · `--control-height-*` | fixed px (bypasses density/scaling) | R-2.x |
| **Composition** | compose the existing `Button` /`Field`/`Icon`/`Select` | re-implementing primitives inline | §4 + review |
fix(cleanroom): F2+F3+F4-C+SEM-4s1 — lote mecánico, censos con guard, corpus documental y el close polimórfico de los pickers VIVO
F2 — lote mecánico (13 ítems):
- DEP-2 ogl eliminado (0 imports) · DEP-1 clsx inlineado como toClassString
propio + suite de contrato (props.test.ts; soma.md §12 cerrado).
- THM-7: los 5 selectores manuales de sema.md reescritos con semaSelector
(los ejemplos [data-toast-root] apuntaban a un part INEXISTENTE — la
deriva que el builder previene, demostrada en el propio doc).
- MOR-1 escape isomorfo + validación de attr-names en semaSelector + 9
tests (selectors.test.ts, matches() real con comillas/corchetes) ·
MOR-2 partMarkerAttr = única fuente compilador↔builder + test de paridad ·
MOR-3 _resetCompileCache borrado (0 usos).
- SOM-2 keydown continue en match sin handler + keyboardFixtureMorfo ·
SOM-1 no-await de handlers (censo async = 0; contrato V1 cumplido) + pin.
- SEM-2 trigger pre-attacha catch con logger (void trigger sin unhandled
rejection; throw intacto para awaiters) + pin · SEM-3 fallback muerto de
applyDominance → skip defensivo + timer tope de awaitExpression cancelado ·
SEC-1 adjudicado YA implementado (assertCssVariableValue desde 2026-05-11)
+ pin del path de VALOR.
- accordion → outline (§32; su outline:none dejaba CERO anillo en HCM) —
verificado en vivo · THM-6 radius-full 9999px · EID-4 recuentos 33.
F3 — censos con guard:
- SOM-3 cerrado: announcer + image-provider migrados a scheduler-preferred
(consumidores cableados: date/time-field vía soma.uix.timers; avatar/image
vía eidos.timers — verificado en vivo); guard de timers ENSANCHADO de
soma/components a TODO soma y pasado a EVIDENCIA (setTimeout exige
.schedule( en el fichero — layers/ y datetime/ escapaban del ámbito viejo).
- THM-5: R-4.7 nueva (válvula same-line /* important: <razón> */, escaneo
comment-blanked) + las 15 declaraciones anotadas con su razón + canon
recipe-contract §3/§4.
- SOM-4 adjudicado: el censo/guard YA existían (49 pins); knob/mask-field/
timeline pinneados (overrides documentados en call-site); media-player
Batch-4 (35 hits, cero renderProps) = único batch restante, registrado.
- THM-4 doctrinado en eidos.md §unused (comportamiento/composición =
legítimo; deuda = eje visual sin consumidor; hotspots por lotes).
F4-C — corpus documental (decisiones de usuario aplicadas):
- DOC-3: los 15 enlaces muertos resueltos (repoint a la edición FINAL
trackeada / des-link históricos) · docs:check I6-links WARN→ERROR.
- DOC-1: tabla «Build contract» MIGRADA a component-guide con estados
modernizados (A3–A5 → LIVE + guards de hoy); banners reapuntados; citas
de CANON/sema.md historificadas; lápida-redirect en el §13 del fósil.
- DOC-4: hold chain → holds.ts · FAQ event:* SUPERSEDED por signatures ·
gradient añadido a los DOS capstones (sextet real) · nota de paleta de
demo-authoring corregida (universalPaletteDecls + decisión THM-2 =
mecanismo universal como sucesor del tracker borrado).
- DOC-5/6: recuentos anti-frágiles datados · §4.11 dup → §4.12 · Known gaps
historificado · N-6/N-7 recuperadas de git (d68d2c45^) y canonizadas en
eidos.md §pickers · authoring E2 → canon/tsc.md · air-old des-linkado ·
EID-3 (placement) en la fila RTL · AUX-2 disabledDom documentado.
SEM-4 sesión 1 — el close polimórfico de los pickers, VIVO (D.11):
- Reconciliación: los morfos ya no declaran close (delegated al Popover,
de-dialoged 06-27); el agujero real era el cierre programático bypaseando
dismissWith → save/cancel/select eran perceptualmente SILENCIOSOS.
- Fix: PickerShellHandle.setPopoverDismiss + closeWith(cause) en los 5
providers (14 sitios; select/commit → 'save' = commit.save+fulfill,
cancel → 'cancel' = emerge; fallback raw para headless) + UN inyector en
el eidos PickerShell root (norma N-8). Picker genérico fuera a propósito
(ya suena commit-set/cancel por diseño S9).
- Verificado en vivo (date-picker): Done → close·commit·fulfill·active ·
Cancel → close·emerge · cierre real.
Gates: matriz 141/141 (los 6 morfos nuevos de la pista de texto paralela
también PASS) · contracts 38/38 · eidos 314 · sema 178 · morfo 94 ·
docs:check 0/0 con I6 en error · baseline propio 57.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
**Update rule (so the guide can never reference a nonexistent token):** a new
axis enters this table WITH its guard in the same pass — the table, the
how-to-consume section and the lint advance coupled to the implementation,
never ahead of it.
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## Before You Start
### 1. Compare with reference libraries
**This step is mandatory. Do not skip it.**
Search ark-ui, bits-ui, and radix-ui for the same component. Create a feature table:
| Feature | Radix | Ark | Bits | Soma | Decision |
| ----------- | ----- | --- | ---- | ---- | ------------- |
| (each prop) | ... | ... | ... | ✓/✗ | justification |
Document what soma includes and what it skips (with reason).
### 2. Audit Morfo/Sema events
**This step is mandatory for every component, including existing morfos.**
Do not treat an empty `events` array as correct by default. Classify the
component first:
docs(eidos): la doctrina alcanza a la jornada — dos guards nuevos, el patron de capa, y EID-3 deja de estar pendiente
Barrido de lo que la sesion cambio y la documentacion todavia no decia.
`testing-and-tooling.md` — los dos guards nuevos entran en la tabla de «que
atrapa cada script», con su reparto explicito: `layer:check` mira el valor
computado (quien gana la cascada) y declara su hueco (la geometria, porque
`getComputedStyle` da el valor USADO y un `inset: auto` se lee como pixeles);
`shared-layer-contract.test.ts` mira el texto, y existe por lo que el navegador
no puede ver — un `env()` ya sustituido devuelve `"0px"` en escritorio.
`component-guide.md` fila RTL — deja de remitir a «EID-3 exception» y enuncia la
regla: dos rejillas NOMBRADAS, `Position` fisica y `LogicalPosition` logica, y la
pregunta que decide entre ellas («¿tiene que voltearse para un lector de derecha
a izquierda?»). Estrechar con `Extract<>`, nunca redeclarar.
`canon/recipe-contract.md` — la fila de z-index flotante distinguia mal: la banda
`--z-index-overlay-*` es de overlays PORTALED. El cromo fijado al viewport que no
portala es otra cosa y tiene su peldano (`affix`, 150). Y el item 9 del checklist
de recetas decia «si flota → una rung de overlay», que era incompleto.
`eidos/components/README.md` — el patron de CAPA COMPARTIDA, que no estaba
escrito en ningun sitio pese a tener dos ejemplares vivos (`list-surface` y
`affix`). Sus cuatro reglas, tres de ellas aprendidas rompiendose: enganchar en
el attr de capa y no en la identidad (o `morfo-check` suelda al consumidor a un
contrato ajeno), un eje = token publico + ranura, el puente reafirma `position`
si el primitivo compuesto declara uno, y se guarda con dos redes porque ninguna
basta sola.
`audit-active-uix.md` — EID-3 pasa a RESUELTO, conservando el hallazgo original
debajo. Era un P3 de julio cerrado «como excepcion» pero marcado en su propia
tabla resumen como «pendiente de doctrina explicita». Ya no lo esta.
`PLAN-affix.md` §7 — lo que vino DESPUES de cerrar el plan, que es casi todo lo
interesante: las dos migraciones y sus dos lecciones, el peldano de z, el patron
de tokens, la canonizacion de la rejilla y los dos guards. Ninguna estaba
prevista en el plan; todas salieron de auditar lo construido.
docs:check 0/627.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Shape | Sema expectation |
| ----------- | ---------------------------------------------------------------------------- |
| Passive | `0 events` is valid when the component only projects external state. |
| Interactive | User decisions usually need discrete events. |
| Continuous | Do not emit every frame/pixel; model start/confirmed drag/commit boundaries. |
| Mixed | Passive display may stay silent, but user actions still need events. |
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
For every real user action decide:
- `family` and `verb` from the canonical Sema vocabulary.
- `sequence` (`pre`, `post` , `coincident` ) based on whether the perceptual
event must precede, follow, or accompany the state change.
- `intent` only when the occurrence is evaluative. Neutral UI mechanics can be
non-evaluative or default to `neutral` .
- `target` part. Prefer the part the user perceives as acting; use provider only
when the event is component-wide.
- `prewrite` / `commit` only when the DOM must expose state before/after the
semantic occurrence.
The provider must route semantic actions through `runtime.trigger(...)` . Local
callbacks such as `onValueChange` /`onValueCommit` are not a substitute for Sema
when the action is perceptual.
### 3. Verify membership criteria
The component must meet ALL of these:
- **WAI-ARIA pattern or semantic role** — the component implements a pattern from the [ARIA Authoring Practices Guide ](https://www.w3.org/WAI/ARIA/apg/patterns/ ) (Dialog, Combobox, Treegrid, Feed, Tabs, Toolbar, …) OR a canonical ARIA role (`role="status"`, `role="meter"` , `role="progressbar"` , `role="searchbox"` , …). If the browser's native HTML gives you the right role + keyboard model with no extra behavior required (e.g. `<a>` for Link, `<hr>` for Separator, `<img>` for Image), the primitive belongs in **Eidos** , not Soma.
- **Composition of parts** — 2+ sub-components communicating via context (Provider + Trigger + Content, Provider + Row + Cell, etc.). A single-DOM wrapper is Eidos-level styling, not headless behavior.
- **Complex behavior** — keyboard navigation, focus management, floating, ARIA relationships, state machines, drag, form integration, or live-region coordination. Adding `role="…"` + `aria-label` to a single element is not enough.
If it fails any of these → it's Eidos-native, not Soma.
Accepted exceptions (composition criterion waived when WAI-ARIA defines a tight contract):
- `Announce` — a live-region primitive per WAI-ARIA 1.2 live regions; meets complex-behavior via dual-region A/B dispatch + auto-clear + priority routing, even though its surface is a single region per priority.
- `Progress` / `Meter` — canonical single-element roles with computed ARIA values and CSS custom properties for the decorative indicator; shipped with an `Indicator` part so consumers have two slots (the role host and the fill), crossing the composition threshold.
### 4. Compose existing components; flag gaps
**Dogfood the framework.** When a new component — or its demo, or any UI you
build — needs a building block the framework already provides (`Button`, `Field` ,
`Popover` , `Dialog` , `Icon` , `Calendar` , `Select` , …), **compose the existing
soma/eidos component**. Never re-implement a primitive inline or hand-roll a
one-off. The picker family is the canonical example: pickers compose `Popover` +
`Field` + `Calendar` /`Slider` with shared state instead of reinventing any of
them (A27).
If a needed building block **does not exist** as a framework component, do **not**
silently inline a bespoke version. **Flag the gap** — report that component `X`
is missing — so it can be built as a proper, reusable component (its own morfo +
soma + eidos) and then composed. A missing component is a signal to create it (or
record the need), never an excuse for an ad-hoc reinvention that drifts from the
system.
uix(toolbar): la acción principal de un cluster ya puede decir su rango — y el gesto ya suena
`Toolbar.Button` tipaba contra el ButtonProps de SOMA ({id, disabled}): sin
variant, sin color, sin size. Un cluster de acciones donde UNA es la principal
no tenía cómo decirlo desde dentro, y la demo del app-shell dejaba «Nueva»
FUERA del toolbar como workaround documentado (A-112).
La ficha ofrecía dos salidas y mi primera recomendación fue la equivocada —
declarar el toolbar deliberadamente uniforme y consagrar el workaround. El
autor la tumbó, con razón: el eidos toolbar-button.svelte era un PASSTHROUGH
que dejaba el <button> nativo de soma, exactamente la clase de bug que
dialog-trigger (2026-06-19) y card-group-title (2026-06-27) ya pagaron, y el
precedente correcto no es menubar-trigger sino Form.Submit. Dos datos que el
primer análisis no tenía:
- El botón era MUDO. El morfo del toolbar sólo declara commit-toggle (el
group-item), así que pulsar una acción no emitía nada. Medido tras el
cambio: click → contact-activate estampado en el nodo + 8 nodos de audio,
donde antes 0.
- La uniformidad la dan los DEFAULTS, no la ausencia del eje: ghost/neutral y
el size del toolbar por contexto de eidos (toolbar/context.ts, calcado del
precedente ButtonGroup y con su misma razón: la receta del Button lee
data-size EN el nodo, un descendant selector no llega). El tipo estrecha a
SelectionVariant (solid | outline | ghost).
La forma es el Button consumer pattern entero: composición vía child de soma
(con el rename outerChild contra la recursión), y la receta CEDE el nodo —
cero cromo de botón en toolbar.css (regla 5); Link y GroupItem siguen siendo
superficie de la barra y los pinta ella. Antes de ceder, las dos recetas sobre
un mismo nodo producían un híbrido medido en el que solid/primary NO pintaba.
La doctrina que faltaba queda escrita en guides/component-guide.md §4: dos
clases de parte con forma de botón en una barra. ACCIÓN en barra (Toolbar.
Button, Form.Submit, Dialog.Trigger) compone el Button; control de SUPERFICIE
de barra (Menubar.Trigger: File, Edit) lo pinta la barra. El test: ¿significa
lo mismo fuera de la barra? Esa distinción vivía en un comentario de
menubar-trigger; el comentario ahora apunta a la doctrina.
Verificado en navegador (Playwright, servidor limpio): el nodo ES el Button
del canon (data-button + data-variant), solid/primary pinta (fondo primary,
tinta blanca), paridad exacta con el GroupItem por construcción — mismo bundle
--size-*: 36px/36px en md, 30px/30px en sm, con la herencia del size del
toolbar medida en vivo al cambiarlo. La demo del app-shell mueve «Nueva» AL
INTERIOR del cluster (workaround retirado, gap del block cerrado en su README)
y la demo del toolbar gana la acción «New» con controles vivos action variant
/ action color.
Gates: svelte-check 72/62 (base 73/62, sin regresión) · vitest eidos+blocks
470/471 (el rojo es skin-media-player, ajeno y documentado en el handoff) ·
blocks:check 0 errores / 19 blocks · docs:check 0/0. Los avisos de prettier de
los dos README ya fallaban en HEAD.
⚠️ El árbol compartido tiene ya encima trabajo NUEVO de otra sesión
(navigation-menu, PLAN-sidebar, book-deviations): este commit lleva sólo los
14 ficheros del eje A-112, verificados por lista.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
**Button-shaped parts in a bar: two classes, not one rule.** An eidos part that
renders a `<button>` composes the canon `<Button>` via soma's `child` snippet
(the Button consumer pattern) — UNLESS it is the bar's own chrome:
- **Action in a bar** (`Toolbar.Button`, `Form.Submit` , `Dialog.Trigger` ): a
standalone action that happens to be clustered. It composes `<Button>` ;
uniformity comes from DEFAULTS (`ghost`/`neutral`/the bar's size via
context), so the one principal action can say its rank from inside
(`variant="solid" color="primary"`) — and the part gains Button's
`contact-activate` , where the native soma fallback is perceptually mute.
The bar's recipe paints NO chrome on that node (consumer pattern rule 5).
- **Bar-surface control** (`Menubar.Trigger` — "File", "Edit"): part of the
bar's chrome, meaningless outside it, never a principal action. The bar's
recipe paints it directly; no Button composition.
The test: could the control stand alone outside the bar and still mean the
same thing? Yes → action, compose `<Button>` . No → surface, the bar paints it.
Leaving an action as the native soma fallback is the documented passthrough
bug (dialog-trigger 2026-06-19 · card-group-title 2026-06-27 · toolbar-button
2026-08-19, A-112).
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
### 5. Classify the piece: component · shared layer · passive atom (2026-07-07)
Formal classes, canonized at the component-audit checkpoint (verdict S7,
[`docs/audit/components/_veredictos.md` ](../audit/components/_veredictos.md )).
Every piece declares which one it is — the audit machine classifies by marker,
never by guessing:
- **Full component** — public compound with its own morfo (`scope` per the
2-of-3 rule), soma provider(s) and/or eidos recipe. The default.
- **Shared layer** — infrastructure several components consume; no public
component of its own (`spin-field`, `list-surface` , `picker-shell` ,
`field-segment-state` ). Requires a **layer README** documenting the contract
its consumers rely on. Layers carry no demo and no pack; their tests live
with their consumers unless behavior is layer-owned.
- **Passive atom / composition** — display-only piece or thin composition
over existing components. STILL morfo-first: a minimal morfo with
`scope: ['eidos']` and 0 events, plus the `## Passive justification`
section in its README (machine rule F-1.5). The reference exemplars:
`color-swatch` (minimal eidos-scope morfo) and `radio-cards` (composition
docs(eidos): la doctrina alcanza a la jornada — dos guards nuevos, el patron de capa, y EID-3 deja de estar pendiente
Barrido de lo que la sesion cambio y la documentacion todavia no decia.
`testing-and-tooling.md` — los dos guards nuevos entran en la tabla de «que
atrapa cada script», con su reparto explicito: `layer:check` mira el valor
computado (quien gana la cascada) y declara su hueco (la geometria, porque
`getComputedStyle` da el valor USADO y un `inset: auto` se lee como pixeles);
`shared-layer-contract.test.ts` mira el texto, y existe por lo que el navegador
no puede ver — un `env()` ya sustituido devuelve `"0px"` en escritorio.
`component-guide.md` fila RTL — deja de remitir a «EID-3 exception» y enuncia la
regla: dos rejillas NOMBRADAS, `Position` fisica y `LogicalPosition` logica, y la
pregunta que decide entre ellas («¿tiene que voltearse para un lector de derecha
a izquierda?»). Estrechar con `Extract<>`, nunca redeclarar.
`canon/recipe-contract.md` — la fila de z-index flotante distinguia mal: la banda
`--z-index-overlay-*` es de overlays PORTALED. El cromo fijado al viewport que no
portala es otra cosa y tiene su peldano (`affix`, 150). Y el item 9 del checklist
de recetas decia «si flota → una rung de overlay», que era incompleto.
`eidos/components/README.md` — el patron de CAPA COMPARTIDA, que no estaba
escrito en ningun sitio pese a tener dos ejemplares vivos (`list-surface` y
`affix`). Sus cuatro reglas, tres de ellas aprendidas rompiendose: enganchar en
el attr de capa y no en la identidad (o `morfo-check` suelda al consumidor a un
contrato ajeno), un eje = token publico + ranura, el puente reafirma `position`
si el primitivo compuesto declara uno, y se guarda con dos redes porque ninguna
basta sola.
`audit-active-uix.md` — EID-3 pasa a RESUELTO, conservando el hallazgo original
debajo. Era un P3 de julio cerrado «como excepcion» pero marcado en su propia
tabla resumen como «pendiente de doctrina explicita». Ya no lo esta.
`PLAN-affix.md` §7 — lo que vino DESPUES de cerrar el plan, que es casi todo lo
interesante: las dos migraciones y sus dos lecciones, el peldano de z, el patron
de tokens, la canonizacion de la rejilla y los dos guards. Ninguna estaba
prevista en el plan; todas salieron de auditar lo construido.
docs:check 0/627.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
whose morfo header explains the delegation: _"declaring them here would
duplicate the contract"_).
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
A composite that delegates behavior to embedded components documents that
delegation in its morfo header and, when sema-scoped, declares
`expression: 'delegated'` (see the participation doctrine in
[`architecture/sema.md` ](../architecture/sema.md )).
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## File Structure
```
components/{name}/
├── {name}-provider.svelte.ts ← ALL concrete provider/state classes
├── types.ts ← ALL prop types with JSDoc + canonical field shapes
├── langs.ts ← optional idlangref constants for imperative strings
├── exports.ts ← barrel (Provider, Trigger, Content, etc.)
├── index.ts ← re-exports from exports.ts
└── components/
├── {name}.svelte ← root wrapper
├── {name}-trigger.svelte ← trigger wrapper
├── {name}-content.svelte ← content wrapper
└── ...
```
### File naming rules
- State class file: `{name}-provider.svelte.ts` (NOT `{name}.svelte.ts` )
- Why: avoids Vite module resolution ambiguity with `{name}.svelte` wrapper
- Contains ALL provider/state classes for the component
- Wrapper files: `{name}.svelte` , `{name}-trigger.svelte` , etc.
- Types file: `types.ts` — public props + canonical field shapes for Opts
- Langs file: optional `langs.ts` — idlangref constants only when provider code needs imperative refs (see A3)
- Barrel: `exports.ts` + `index.ts`
## State Class Pattern ({name}-provider.svelte.ts)
```ts
import { context, type ProviderOpts, type WithRefOpts } from '../../provider';
import { Soma } from '../../core/soma.svelte';
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
import { createAttrs } from '$uix/morfo';
// Parts + data contract live in the component's morfo (see src/uix/morfo/README.md):
import { {name}Morfo } from '../../../morfo/components/{name}';
// Selector helper only. Registration and DOM writes happen through SomaRuntime.part().
const attrs = createAttrs({name}Morfo);
// Root provider (with or without DOM)
interface {Name}Opts extends ProviderOpts, ... {} // ProviderOpts if no DOM
interface {Name}Opts extends WithRefOpts, ... {} // WithRefOpts if renders element
export class {Name}Provider {
static readonly ctx = context< {Name}Provider>('{Name}');
static get() { return this.ctx.getOr(undefined) as {Name}Provider | undefined; }
static require() { return this.ctx.get(); }
readonly opts: {Name}Opts;
readonly runtime: SomaRuntime;
readonly runtimePart: SomaRuntimePart;
static create(opts: {Name}Opts) {
return new {Name}Provider(opts);
}
private constructor(opts: {Name}Opts) {
this.opts = opts;
this.runtime = Soma.require().runtime({name}Morfo, {});
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
owner: this,
docs(uix): la doctrina alcanza a la bolsa única — el corpus deja de enseñar la API muerta
La clase exacta que la auditoría flagueó (doc↔código), producida esta vez
por NUESTROS propios cambios de P0 fase C y sin corregir hasta ahora:
- component-guide §«two sanctioned ways»: reescrita a UNA vía (la bolsa
.props resuelve el contrato entero; extras solo lo que el morfo no puede
expresar; valores CRUDOS; la inversa «morfo sin value ⇒ soma escribe»).
Los 4 snippets con syncAttrs: true, limpiados.
- soma-architecture: partProps re-descrito como LA bolsa (identidad +
contrato completo, SSR incluido); el párrafo del flag sustituido por «el
único escritor imperativo es el prewrite»; snippet limpiado.
- soma.md: snippet de accordion sin el flag.
- morfo.md §naming: el «(SSR included)» dejó de ser contraste — desde la
tubería única TODO plan viaja en la bolsa; lo que distingue a los naming
es la PRECEDENCIA (consumer-first via mergeProps).
- testing-and-tooling: la postura SSR gana el canario ssr-contract.test, y
nace «The gate» (check:gate + ledger menguante, gate, hook — los
instrumentos de fase B no estaban documentados).
- CLAUDE.md: el paso 4 de trigger ya no dice «effect-driven» (prettier
normalizó de paso el fichero entero — solo whitespace, contenido intacto,
declarado aquí para que el diff ancho no sea silencioso).
Lo histórico (changelog, old-deprecated, audit de julio, process/) se queda
como historia. Verificación: docs:check 0 errores sobre 817 docs; cero
menciones normativas de la API muerta fuera de referencias históricas.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
context: {Name}Provider.ctx
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
});
}
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.props,
// component-specific props
} as const),
);
}
// Sub-parts read parent context
export class {Name}TriggerProvider {
static create(opts) { return new {Name}TriggerProvider(opts); }
readonly opts: {Name}TriggerOpts;
readonly runtimePart: SomaRuntimePart;
readonly provider: {Name}Provider;
private constructor(opts) {
this.opts = opts;
this.provider = {Name}Provider.require();
this.runtimePart = this.provider.runtime.part('trigger', {
id: opts.id,
ref: opts.ref,
docs(uix): la doctrina alcanza a la bolsa única — el corpus deja de enseñar la API muerta
La clase exacta que la auditoría flagueó (doc↔código), producida esta vez
por NUESTROS propios cambios de P0 fase C y sin corregir hasta ahora:
- component-guide §«two sanctioned ways»: reescrita a UNA vía (la bolsa
.props resuelve el contrato entero; extras solo lo que el morfo no puede
expresar; valores CRUDOS; la inversa «morfo sin value ⇒ soma escribe»).
Los 4 snippets con syncAttrs: true, limpiados.
- soma-architecture: partProps re-descrito como LA bolsa (identidad +
contrato completo, SSR incluido); el párrafo del flag sustituido por «el
único escritor imperativo es el prewrite»; snippet limpiado.
- soma.md: snippet de accordion sin el flag.
- morfo.md §naming: el «(SSR included)» dejó de ser contraste — desde la
tubería única TODO plan viaja en la bolsa; lo que distingue a los naming
es la PRECEDENCIA (consumer-first via mergeProps).
- testing-and-tooling: la postura SSR gana el canario ssr-contract.test, y
nace «The gate» (check:gate + ledger menguante, gate, hook — los
instrumentos de fase B no estaban documentados).
- CLAUDE.md: el paso 4 de trigger ya no dice «effect-driven» (prettier
normalizó de paso el fichero entero — solo whitespace, contenido intacto,
declarado aquí para que el diff ancho no sea silencioso).
Lo histórico (changelog, old-deprecated, audit de julio, process/) se queda
como historia. Verificación: docs:check 0 errores sobre 817 docs; cero
menciones normativas de la API muerta fuera de referencias históricas.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
owner: this
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
});
}
}
```
### Part props: read the morfo, don't re-declare it
"Morfo declares, soma executes" — a part's `role` / `aria-*` / `data-*` live in
the morfo. A provider must NEVER re-declare them as literals in its `props`
docs(uix): la doctrina alcanza a la bolsa única — el corpus deja de enseñar la API muerta
La clase exacta que la auditoría flagueó (doc↔código), producida esta vez
por NUESTROS propios cambios de P0 fase C y sin corregir hasta ahora:
- component-guide §«two sanctioned ways»: reescrita a UNA vía (la bolsa
.props resuelve el contrato entero; extras solo lo que el morfo no puede
expresar; valores CRUDOS; la inversa «morfo sin value ⇒ soma escribe»).
Los 4 snippets con syncAttrs: true, limpiados.
- soma-architecture: partProps re-descrito como LA bolsa (identidad +
contrato completo, SSR incluido); el párrafo del flag sustituido por «el
único escritor imperativo es el prewrite»; snippet limpiado.
- soma.md: snippet de accordion sin el flag.
- morfo.md §naming: el «(SSR included)» dejó de ser contraste — desde la
tubería única TODO plan viaja en la bolsa; lo que distingue a los naming
es la PRECEDENCIA (consumer-first via mergeProps).
- testing-and-tooling: la postura SSR gana el canario ssr-contract.test, y
nace «The gate» (check:gate + ledger menguante, gate, hook — los
instrumentos de fase B no estaban documentados).
- CLAUDE.md: el paso 4 de trigger ya no dice «effect-driven» (prettier
normalizó de paso el fichero entero — solo whitespace, contenido intacto,
declarado aquí para que el diff ancho no sea silencioso).
Lo histórico (changelog, old-deprecated, audit de julio, process/) se queda
como historia. Verificación: docs:check 0 errores sobre 817 docs; cero
menciones normativas de la API muerta fuera de referencias históricas.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
getter (that is duplication: the same attr in two sources, which drift). There
is exactly ONE way to apply them (the render bag became the single attr
pipeline in P0 fase C, audit 2026-08-26 — the old `syncAttrs` effect and
`renderProps()` split is gone):
**Spread `...this.runtimePart.props` ** — the render bag: static identity plus
every morfo-declared attr, resolved against this part's registered
`props` /`states` sources — then add ONLY what the morfo can't express (event
handlers, a locale-formatted value, a native form attr). The same values
server-render and re-derive through Svelte's reactivity. Register the value
sources at the `runtime.part(...)` call:
```ts
this.runtimePart = provider.runtime.part('input', {
id, ref, owner: this,
props: { value: () => provider.value, min: () => provider.min }
});
readonly props = $derived.by(() => this.runtimePart.assert({
...this.runtimePart.props, // role, aria-valuenow/min, data-*
oninput: this.oninput, // handler (morfo can't model)
'aria-valuetext': this.formatValue(...) // formatted (overrides raw morfo)
}));
```
Override a morfo attr only when soma genuinely owns the _value_ (formatting —
note the bag hands RAW resolved values: a numeric source arrives as `20` , not
`'20'` ; Svelte stringifies at render). A morfo entry declared WITHOUT a value
source is the inverse rule: soma owns that value and hand-writes it (the
combobox trigger's `data-state` idiom). Never override just to repeat.
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> **Anti-pattern**: `{ ...this.runtimePart.props, role: 'spinbutton', 'aria-disabled': ... }`
docs(uix): la doctrina alcanza a la bolsa única — el corpus deja de enseñar la API muerta
La clase exacta que la auditoría flagueó (doc↔código), producida esta vez
por NUESTROS propios cambios de P0 fase C y sin corregir hasta ahora:
- component-guide §«two sanctioned ways»: reescrita a UNA vía (la bolsa
.props resuelve el contrato entero; extras solo lo que el morfo no puede
expresar; valores CRUDOS; la inversa «morfo sin value ⇒ soma escribe»).
Los 4 snippets con syncAttrs: true, limpiados.
- soma-architecture: partProps re-descrito como LA bolsa (identidad +
contrato completo, SSR incluido); el párrafo del flag sustituido por «el
único escritor imperativo es el prewrite»; snippet limpiado.
- soma.md: snippet de accordion sin el flag.
- morfo.md §naming: el «(SSR included)» dejó de ser contraste — desde la
tubería única TODO plan viaja en la bolsa; lo que distingue a los naming
es la PRECEDENCIA (consumer-first via mergeProps).
- testing-and-tooling: la postura SSR gana el canario ssr-contract.test, y
nace «The gate» (check:gate + ledger menguante, gate, hook — los
instrumentos de fase B no estaban documentados).
- CLAUDE.md: el paso 4 de trigger ya no dice «effect-driven» (prettier
normalizó de paso el fichero entero — solo whitespace, contenido intacto,
declarado aquí para que el diff ancho no sea silencioso).
Lo histórico (changelog, old-deprecated, audit de julio, process/) se queda
como historia. Verificación: docs:check 0 errores sobre 817 docs; cero
menciones normativas de la API muerta fuera de referencias históricas.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
> — `role`/`aria-disabled` are morfo-declared; the bag already supplies them.
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
### Static Method Convention
All classes that use Svelte context follow the same 3-method pattern:
| Method | Behavior | When to use |
| -------------- | ------------------------------- | ------------------------------------------------------- |
| `create(opts)` | Factory + context set | Root wrapper creates the provider |
| `get()` | Returns instance or `undefined` | Optional parent (e.g., Checkbox inside optional Group) |
| `require()` | Throws if not found | Required parent (e.g., Trigger must be inside Provider) |
This applies consistently to:
- **Provider classes**: `XProvider.create()` , `XProvider.get()` , `XProvider.require()`
- **Soma**: `Soma.create()` , `Soma.get()` , `Soma.require()`
- **App**: `App.create()` , `App.get()` , `App.require()`
No standalone functions (`createApp`, `getApp` , `useApp` ). No `from()` . The `ctx` field is `readonly` on the class but not exposed as public API — consumers use the static methods.
### Rules
- `getContext()` only works during component initialization (constructor called from script block). NEVER in event handlers, timeouts, or callbacks.
- If a handler needs a context reference, capture it in the constructor.
- Event handlers (onclick, onkeydown) must be included in `props` . Defining them as class methods without spreading them into props means they won't reach the DOM.
- Layers (Presence, FocusScope, Dismissal, Gesture, etc.) are instantiated in the constructor and their `.props` are spread into the component's `props` .
- For DropdownMenu / ContextMenu: `interactOutsideBehavior` defaults to `'close'` . Menus are intentionally non-modal — for blocking semantics use Dialog/Drawer/AlertDialog.
### Types: define once, reference in Opts
Canonical field shapes are defined once in `types.ts` and referenced by provider Opts via `StateProps<>` / `ActiveProps<>` :
```ts
// types.ts
export type DrawerStateFields = { open: boolean; activeSnapPoint: SnapPoint | null; };
export type DrawerActiveFields = { disabled: boolean; modal: boolean; direction: Direction; ... };
// provider
interface DrawerOpts extends ProviderOpts, StateProps< DrawerStateFields > , ActiveProps< DrawerActiveFields > {}
```
Do NOT redeclare field types in both `types.ts` and the Opts interface.
Alternatively, derive the whole Opts from the public Props —
`OptsFromProps<Props, Managed, StateKey, Preserve>` (`soma/provider/opts.ts`).
`Preserve` lists the keys whose `undefined` is meaningful (no wrapper
default, or `dir` ): those keep `T | undefined` ; the rest are stripped
because the wrapper's destructure default resolves them. Either way, EXPORT
the Opts type — the wrapper targets it via `bindProps<XOpts>` .
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## Wrapper Pattern (components/{name}.svelte)
The wrapper builds the provider's reactive bag with the TARGET-TYPED bridge:
`bindProps<{Name}Opts>` . The provider EXPORTS its `Opts` type, which computes
the expected config shape — the literal is fully checked (keys, getter types,
setter bodies) and the return IS the opts, so there is never a cast.
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
```svelte
< script lang = "ts" >
import { bindProps } from '../../../provider';
import { mergeProps } from '../../../props';
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
import { createId } from '$active-uix/id';
import { activeDir } from '../../../direction';
import { {Name}Provider, type {Name}Opts } from '../{name}-provider.svelte';
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
import type { {Name}Props } from '../types';
const uid = $props.id();
let {
ref = $bindable(null),
id = createId(uid, '{name}'),
open = $bindable(false),
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
onOpenChange = () => {},
// ... props with defaults ...
dir,
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
children,
child,
...restProps
}: {Name}Props = $props();
const state = {Name}Provider.create(
bindProps< {Name}Opts>({
id: () => id,
ref: { get: () => ref, set: (v) => (ref = v) },
// Bindable-write callback rides the setter (see Callback conventions).
open: {
get: () => open,
set: (v) => {
open = v;
onOpenChange(v);
}
},
// ... a bare getter per remaining prop: disabled: () => disabled, ...
// Active pass-through — the activeDir CALL stays visible in the init.
dir: activeDir(() => dir)
})
);
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
const mergedProps = $derived(mergeProps(restProps, state.props));
< / script >
{#if child}
{@render child({ props: mergedProps })}
{:else}
< div { . . . mergedProps } >
{@render children?.()}
< / div >
{/if}
```
For the trivial part — a bag of exactly `{ id, ref }` , which is half the
catalogue's parts — use `partOpts` instead of a hand-built literal:
```svelte
< script lang = "ts" >
import { partOpts } from '../../../provider';
// ...
const state = {Name}TitleProvider.create(
partOpts(
() => id,
() => ref,
(v) => (ref = v)
)
);
< / script >
```
`partOpts<E>` narrows for element-specific refs (`HTMLInputElement`…): the
getter fixes `E` , the setter's parameter is contextually typed from it, and
the ONE narrowing cast lives inside the helper — call sites neither cast nor
annotate.
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
### Rules
- Wrappers are thin: props → `bindProps` / `partOpts` → Provider.create() → mergeProps → render
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- No logic in wrappers. If you're writing more than prop conversion, the logic belongs in the Provider.
- IDs: `createId(uid, '{component}-{part}')` — descriptive and inspectable
- Callbacks default to `() => {}` inline — no `noop` import, soma does not depend on `$lib`
- Direction: the wrapper CALLS `activeDir(() => dir)` in its init (prop → prefs, and it
publishes DirectionContext) and passes the resulting box through the bag as an Active
pass-through; the provider defaults once in `resolvedDir` , and stamps the raw
`opts.dir.current` as `dir` whenever the recipe branches with `:dir()` —
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo
El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en
`CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source
of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`.
Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es
normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon;
por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que
`:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook
incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las
trampas que el guard no ve; que espeja y que no; y la mitad global de prefs.
EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal:
- `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones,
mandando al wrapper a leer prefs directamente. Eso excluye la prop.
- `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()`
dentro del provider) como LA forma de obtener la direccion — justo lo que el eje
retiro del catalogo.
- `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la
regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una
clave HTML) es correcto y sobrevive; solo cambia el ejemplo.
- `active-architecture.md:416` no listaba `lang` en la proyeccion, contra
`contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`.
Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`,
`data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER
guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`,
`building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la
matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura,
que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`.
El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en
la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en
E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de
estampar `data-dir`, que `:dir()` no puede ver.
DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y
resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del
catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse.
Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos:
RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca
lee al padre» era absoluto y borraba la composicion sancionada en el punto de
llamada · el estampado se afirmaba incondicional en un sitio y condicional en
otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni
`component-audit.ts` conocen.
`docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
[`canon/direction-contract.md` ](../canon/direction-contract.md )
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
### Callback conventions (two concepts, one form each)
- **Bindable-write callback** — a callback that reports writes to a bindable
(`onOpenChange` beside `open` ): it fires INSIDE the `{ get, set }` setter,
right after the local write. The setter is the ONLY write path of the
`State` box, so no provider write can skip the notification — by
construction, not by discipline. The provider just writes
`opts.open.current = v` and never invokes the callback itself.
- **Pure event callback** — no write to couple to (`onValueCommit`,
`onPress` , `onPlaced` ): it travels as its own `Active` entry (a bare
getter) and the provider invokes it at the event site.
- **Debounced change callback** — the one named exception. The write stays
immediate (the binding always tells the truth) but the NOTIFICATION is
coalesced by a `debounceMs` prop. It is never lost: clear / submit /
unmount flush the pending one. A component in this shape emits from a
SINGLE private method (`search-field`'s `emitValueChange` ) so the
guarantee lives at one point rather than at every call site.
> **Catalogue invariant — no silent internal write.** Every internal write to
> a bindable notifies, by construction, with exactly one named exception: the
> coalesced (debounced) notification above. A path that writes without
> notifying is a defect: the `bind:` consumer and the callback consumer would
> see different histories of the same component. When a reset feels like it
> needs to write silently (drawer's snap point once did), the real question is
> whether it should write at all — drawer's answer was that the snap survives
> the close, like a scroll position.
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## Exports Pattern (exports.ts)
```ts
export { default as Provider } from './components/{name}.svelte'; // ALWAYS "Provider", never "Root"
export { default as Trigger } from './components/{name}-trigger.svelte';
export { default as Content } from './components/{name}-content.svelte';
export type {
{Name}Props as ProviderProps, // ALWAYS "ProviderProps", never "RootProps"
{Name}TriggerProps as TriggerProps,
{Name}ContentProps as ContentProps,
} from './types';
```
## Types Pattern (types.ts)
ALL props documented with JSDoc. No exceptions.
```ts
export type {Name}Props = WithChild< {
/** Unique identifier. Auto-generated if omitted. */
id?: string;
/** Whether open. Bindable. @default false */
open?: boolean;
/** Callback on open change. */
onOpenChange?: OnChangeFn< boolean > ;
/**
* Multi-line for complex behavior.
* Inherits from X when not set.
* @default 'close'
*/
escapeKeydownBehavior?: DismissalBehavior;
}> & Without< PrimitiveDivAttributes , { } > ;
```
### Rules
- One-line JSDoc for simple props
- Multi-line when there's conditional behavior or prop relationships
- `@default` on every prop that has a default in the wrapper
- Callbacks: document what `e.preventDefault()` does if applicable
- `value: string` for required props (no `?` )
### Primitive HTML attributes
Soma prop types use primitive HTML aliases from `src/uix/soma/types/html.ts`
instead of raw `svelte/elements` attributes. Those aliases omit fields managed
by `WithChild` : `id` , `style` and `children` .
Use the primitive matching the rendered element:
```ts
import type { PrimitiveFormAttributes } from '../../types';
export type FormProviderProps = WithChild<
{
id?: string;
// state/config props…
},
FormProviderSnippetProps
> &
Without< Omit < PrimitiveFormAttributes , ' onsubmit ' > , {}>;
```
Do not intersect `WithChild<..., SnippetProps>` with raw
`HTMLFormAttributes` , `HTMLAttributes` , etc. Raw Svelte HTML types carry their
own `children?: Snippet<[]>` ; that collides with argumented snippets like
`children(snippetProps)` and breaks Eidos wrappers that forward provider state.
## ID Generation
```ts
createId(uid, 'dialog'); // → "soma-dialog-c12"
createId(uid, 'dialog-trigger'); // → "soma-dialog-trigger-c13"
createId(uid, 'dialog-content'); // → "soma-dialog-content-c14"
```
Pattern: `soma-{component}-{part}-{uid}` . Always descriptive.
## Data Attributes
- Provider: `data-{component}` (no `-provider` suffix)
- Parts: `data-{component}-{part}`
- State: `data-state="open|closed"` , `data-state="checked|unchecked|indeterminate"`
- Flags: `data-disabled` , `data-readonly` , `data-checked`
- Floating: `data-side` , `data-align`
- Animation: `data-starting-style` , `data-ending-style`
- Nesting: `data-nested` , `data-nested-open`
- Drag: `data-dragging` (present during active gesture)
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
Enum'd data attributes declare their closed set in the morfo
(`{ attr: 'data-type', values: ['single', 'multiple'] }`) whenever soma holds
a closed union — the compiler validates values and eidos can select per value.
Do not emit aggregate state attrs nobody consumes: one representation per
concept (checkpoint verdict S10 pruned field's dead 5-value `data-state` in
favor of its consumed flags).
## Documentation — the two READMEs (2026-07-07)
Canonized at the component-audit checkpoint (verdict S4). A full component
documents itself at BOTH levels, each with its own audience — `date-field` is
the reference pair:
- **`eidos/components/{name}/README.md` — the consumer's door.** Visual API:
variants, sizes, the recipe's public tokens (`--{name}-*`), composition
examples. This is the level the machine requires (rule E-2.3).
- **`soma/components/{name}/README.md` — the headless contract.** Provider
API, snippet props, keyboard, the `## Sema events` section, Field/Form
participation.
Shared layers document their contract in a layer README; passive atoms carry
the `## Passive justification` section (see "Classify the piece" above).
## API naming conventions (2026-07-07)
The prop style guide, ratified at the component-audit checkpoint (verdicts
N1– N10; census and evidence in
[`docs/audit/components/_naming.md` ](../audit/components/_naming.md )):
1. ** `value` + `onValueChange: OnChangeFn<T>` ** is the primary-value pair —
except where a universal domain word exists (`page`/`onPageChange`,
`files` /`onFilesChange`), which then follows the same `on{Word}Change`
shape.
2. **Binaries speak their ARIA** : `checked` /`onCheckedChange`,
`pressed` /`onPressedChange`, `indeterminate` — never `value: boolean` .
3. **Overlays** : `open` /`onOpenChange`/`onOpenChangeComplete` (post-animation)
docs(eidos): la doctrina alcanza a la jornada — dos guards nuevos, el patron de capa, y EID-3 deja de estar pendiente
Barrido de lo que la sesion cambio y la documentacion todavia no decia.
`testing-and-tooling.md` — los dos guards nuevos entran en la tabla de «que
atrapa cada script», con su reparto explicito: `layer:check` mira el valor
computado (quien gana la cascada) y declara su hueco (la geometria, porque
`getComputedStyle` da el valor USADO y un `inset: auto` se lee como pixeles);
`shared-layer-contract.test.ts` mira el texto, y existe por lo que el navegador
no puede ver — un `env()` ya sustituido devuelve `"0px"` en escritorio.
`component-guide.md` fila RTL — deja de remitir a «EID-3 exception» y enuncia la
regla: dos rejillas NOMBRADAS, `Position` fisica y `LogicalPosition` logica, y la
pregunta que decide entre ellas («¿tiene que voltearse para un lector de derecha
a izquierda?»). Estrechar con `Extract<>`, nunca redeclarar.
`canon/recipe-contract.md` — la fila de z-index flotante distinguia mal: la banda
`--z-index-overlay-*` es de overlays PORTALED. El cromo fijado al viewport que no
portala es otra cosa y tiene su peldano (`affix`, 150). Y el item 9 del checklist
de recetas decia «si flota → una rung de overlay», que era incompleto.
`eidos/components/README.md` — el patron de CAPA COMPARTIDA, que no estaba
escrito en ningun sitio pese a tener dos ejemplares vivos (`list-surface` y
`affix`). Sus cuatro reglas, tres de ellas aprendidas rompiendose: enganchar en
el attr de capa y no en la identidad (o `morfo-check` suelda al consumidor a un
contrato ajeno), un eje = token publico + ranura, el puente reafirma `position`
si el primitivo compuesto declara uno, y se guarda con dos redes porque ninguna
basta sola.
`audit-active-uix.md` — EID-3 pasa a RESUELTO, conservando el hallazgo original
debajo. Era un P3 de julio cerrado «como excepcion» pero marcado en su propia
tabla resumen como «pendiente de doctrina explicita». Ya no lo esta.
`PLAN-affix.md` §7 — lo que vino DESPUES de cerrar el plan, que es casi todo lo
interesante: las dos migraciones y sus dos lecciones, el peldano de z, el patron
de tokens, la canonizacion de la rejilla y los dos guards. Ninguna estaba
prevista en el plan; todas salieron de auditar lo construido.
docs:check 0/627.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- `side` /`align`/`forceMount`/`modal` + `onInteractOutside` /
`onFocusOutside` . Hover timing: `openDelay` /`closeDelay` (+
`groupSkipDelay` for tooltip groups).
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
4. **Capability booleans** : plain positive adjective first (`deselectable`,
`dismissible` , `loop` ); `allowX` only when no natural adjective exists
(`allowHalf`, `allowCustomValue` ); never `allowsX` .
5. **Callbacks** : `on{Noun}Change` for state; `on{Verb}` for gestures and
diagnostics (`onPress`, `onResize` ); a raw `() => void` only for
payload-less signals (`onValueRevert`). The terminal-commit callback is
** `onValueCommit: OnChangeFn<T>` ** — the single name catalog-wide.
6. ** `is*` is forbidden in props** — reserved for derived snippet props
(`isFocused`, `isPlaying` ). `pending` is the form-transaction word
(derived); `loading` is the consumer-set busy prop — two concepts, both
legitimate.
7. **Multi-axis values** suffix the axis (`selectedValue`, `expandedValue` ) —
only when ≥2 value axes coexist; single-axis components use plain `value` .
8. **Selection multiplicity** is `selectionMode: 'single' | 'multiple'`
(native-attribute mirrors like file-upload's `multiple` stay, documented
as such). **No `defaultValue`** — Svelte 5's `$bindable(initial)` covers
the uncontrolled-initial case. **Validation** is `validate` returning a
typed reason + `onInvalid(reason)` . Numbers with units carry the unit in
the name (`debounceMs`).
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## Checklist
> This is the **build checklist** — the ordered authoring steps to take a
docs(eidos): la doctrina alcanza a la jornada — dos guards nuevos, el patron de capa, y EID-3 deja de estar pendiente
Barrido de lo que la sesion cambio y la documentacion todavia no decia.
`testing-and-tooling.md` — los dos guards nuevos entran en la tabla de «que
atrapa cada script», con su reparto explicito: `layer:check` mira el valor
computado (quien gana la cascada) y declara su hueco (la geometria, porque
`getComputedStyle` da el valor USADO y un `inset: auto` se lee como pixeles);
`shared-layer-contract.test.ts` mira el texto, y existe por lo que el navegador
no puede ver — un `env()` ya sustituido devuelve `"0px"` en escritorio.
`component-guide.md` fila RTL — deja de remitir a «EID-3 exception» y enuncia la
regla: dos rejillas NOMBRADAS, `Position` fisica y `LogicalPosition` logica, y la
pregunta que decide entre ellas («¿tiene que voltearse para un lector de derecha
a izquierda?»). Estrechar con `Extract<>`, nunca redeclarar.
`canon/recipe-contract.md` — la fila de z-index flotante distinguia mal: la banda
`--z-index-overlay-*` es de overlays PORTALED. El cromo fijado al viewport que no
portala es otra cosa y tiene su peldano (`affix`, 150). Y el item 9 del checklist
de recetas decia «si flota → una rung de overlay», que era incompleto.
`eidos/components/README.md` — el patron de CAPA COMPARTIDA, que no estaba
escrito en ningun sitio pese a tener dos ejemplares vivos (`list-surface` y
`affix`). Sus cuatro reglas, tres de ellas aprendidas rompiendose: enganchar en
el attr de capa y no en la identidad (o `morfo-check` suelda al consumidor a un
contrato ajeno), un eje = token publico + ranura, el puente reafirma `position`
si el primitivo compuesto declara uno, y se guarda con dos redes porque ninguna
basta sola.
`audit-active-uix.md` — EID-3 pasa a RESUELTO, conservando el hallazgo original
debajo. Era un P3 de julio cerrado «como excepcion» pero marcado en su propia
tabla resumen como «pendiente de doctrina explicita». Ya no lo esta.
`PLAN-affix.md` §7 — lo que vino DESPUES de cerrar el plan, que es casi todo lo
interesante: las dos migraciones y sus dos lecciones, el peldano de z, el patron
de tokens, la canonizacion de la rejilla y los dos guards. Ninguna estaba
prevista en el plan; todas salieron de auditar lo construido.
docs:check 0/627.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> component from nothing to shipped. For the _acceptance_ criteria (the
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> machine-audited rules that decide when a component counts as done across all
> four layers + recipe CSS + demo), see
> [`completion-checklist.md`](./completion-checklist.md).
> The two are a complementary pair — build process vs done-criteria — not
> duplicate checklists. [`architecture/soma.md`](../architecture/soma.md) §9
> only points at both; it keeps no copy of either.
```
[ ] 1. Compare with ark-ui, bits-ui, radix-ui — feature table with decisions
[ ] 2. Verify membership criteria (composition + complex behavior)
[ ] 3. Define parts: Provider + sub-parts (root part uses 'provider', not 'root' — A2)
[ ] 4. Create {name}-provider.svelte.ts with concrete provider/state classes
[ ] 5. Use `soma.runtime()` inside providers (or `createSomaRuntime()` in tests/tools) so `registerMorfo()` runs — A1
[ ] 6. Create types.ts with JSDoc on ALL props + canonical field shapes
[ ] 7. Add `morfo.texts` for component-owned text (catalog in `langs/components/{kebab}.ts` ); create langs.ts only for imperative constants — A3
[ ] 8. Create wrapper .svelte files (thin: props → Active → Provider → mergeProps → render — A11)
[ ] 9. Verify: event handlers included in props (not just class methods)
[ ] 10. Verify: context captured in constructor, not in handlers
[ ] 11. Verify: ARIA relationships complete (aria-controls, aria-labelledby, aria-expanded — A4)
[ ] 12. Verify: accessible name via morfo `translationRef` / `commonRef` / idlangref constant, not hardcoded string — A3
[ ] 13. Verify: keyboard navigation respects RTL via getDirectionalKeys() — A12
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo
El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en
`CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source
of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`.
Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es
normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon;
por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que
`:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook
incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las
trampas que el guard no ve; que espeja y que no; y la mitad global de prefs.
EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal:
- `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones,
mandando al wrapper a leer prefs directamente. Eso excluye la prop.
- `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()`
dentro del provider) como LA forma de obtener la direccion — justo lo que el eje
retiro del catalogo.
- `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la
regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una
clave HTML) es correcto y sobrevive; solo cambia el ejemplo.
- `active-architecture.md:416` no listaba `lang` en la proyeccion, contra
`contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`.
Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`,
`data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER
guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`,
`building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la
matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura,
que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`.
El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en
la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en
E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de
estampar `data-dir`, que `:dir()` no puede ver.
DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y
resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del
catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse.
Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos:
RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca
lee al padre» era absoluto y borraba la composicion sancionada en el punto de
llamada · el estampado se afirmaba incondicional en un sitio y condicional en
otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni
`component-audit.ts` conocen.
`docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
[ ] 41. Verify: direction contract — `dir?: Direction` declared with the alias,
the wrapper runs `activeDir(() => dir, soma)` , and the raw value is
stamped as `dir` whenever the recipe branches with `:dir()` .
See `docs/canon/direction-contract.md` .
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
[ ] 14. Verify: timers/listeners cleaned up in $effect return — A6
[ ] 15. Verify: gesture cleanup on unmount if using Gesture layer — A6, A15
[ ] 16. Verify: .get() for optional parents, .require() for required — A7
[ ] 17. Verify: no visual styles in provider (A8), static methods follow create/get/require pattern
[ ] 18. Verify: roving tabindex has exactly one tabindex=0 item — A14
[ ] 19. Create exports.ts (compound public parts; Provider only for the root headless entry)
[ ] 20. Add to components/index.ts barrel
[ ] 21. Create demo page in web/routes/uix/components/{name} as an INTERACTIVE TESTBED (A29)
— every public prop wired to a live control, Field integration section,
state readout. Not a gallery of canned snippets.
[ ] 22. Add link to web/routes/uix/+layout@.svelte sidebar nav
[ ] 23. svelte-check: 0 errors
[ ] 24. Run `npm run smoke` — all routes pass, including the new one.
Smoke script catches runtime errors that svelte-check + HTTP 200 miss:
`pageerror` (uncaught throws during hydration), `console.error` ,
translation-key-not-found, and `Context "X" not found` . HTTP 200 alone
is SSR — it does NOT exercise client hydration. Interactively exercise
every control in DevTools afterwards.
[ ] 25. Document gaps vs reference libraries
[ ] 26. Create README.md in the component folder — anatomy, props, data-attrs,
keyboard, ARIA, and at least one composition example. Follow the
format used by dialog/README.md and accordion/README.md.
[ ] 27. Verify data-attr naming is consistent across code, CSS, and docs.
The morfo compiler emits `data-{component}` for root and
`data-{component}-{part}` for sub-parts — NEVER `data-soma-*` .
`createAttrs(morfo)` is only the typed selector helper for those
names; it does not register the contract or write to the DOM.
Grep the component folder for `data-soma-` and any other
prefix: if any querySelector, CSS selector, README, or inline
string uses a name that does not match the morfo-generated
attrs, the reference is broken (selectors return null, CSS
matches nothing) and the contract registration lies about
what is on the DOM.
# Date / time specific
[ ] 28. Import date types/utilities from `$libs/days` directly — never from
`$lib/util/dates` (legacy) and never from `@internationalized/date` .
Extend `$libs/days` when a reusable helper is missing; never
re-implement inside soma (A23). `soma/datetime/` only holds UI-level
helpers.
[ ] 29. Segmented inputs emit `onbeforeinput: e => e.preventDefault()` on the
contenteditable segment (A26). `keydown.preventDefault` does not
stop IME / paste / drop.
[ ] 30. `readonlySegments` in a single-value component warns via
`soma?.logger.warn` when `value` is undefined (A24). Range components
split into `startReadonlySegments` / `endReadonlySegments` (A25).
[ ] 31. Pickers follow the shared-state composition pattern (A27): root
wrapper creates the picker Provider + PopoverProvider + underlying
Field/Calendar/Slider providers pointing at the same `writableActive`
refs. Unique parts only for `Provider` / `Trigger` / calendar-or-slider
bridge; everything else re-exports from the composed components.
# Reactivity hazards — mandatory
[ ] 32. Registering a child id with a parent provider's state is a **direct
assignment in the constructor** (A30). Never use `$effect` for this.
`$effect(() => parent.inputId.current = opts.id.current)` creates a
reactive edge child → parent that can loop when any downstream
consumer feeds back. Symptom: the page "freezes" / "blocks" on
mount.
[ ] 33. Per-entity `$derived` MUST NOT call a provider method that reads
global state (value array, items list, version counter) (A31). Lift
the computation to a single `$derived` on the provider; per-entity
derivations compare against the lifted result with O(1) operations.
Symptom: works with 1– 5 items, hangs with 30+.
[ ] 36. Reactive collections: use ** `SvelteMap` / `SvelteSet` ** from
`svelte/reactivity` whenever readers index per-entry (`.get(k)`,
`.has(k)` , iteration, `.size` ) inside `$derived` / `$effect` /
templates (A33). `$state(new Map())` only tracks field reassignment;
`.set(k, v)` on the existing Map silently fails to notify readers.
Symptom: cache updates but derivations that read it never re-run.
[ ] 38. Per-item `$effect` MUST NOT read `opts.ref.current` / tracked inputs
AND write provider state that per-item `props` $derived read back
(A35). The attachment reapply loop triggers
`effect_update_depth_exceeded` . Register in the constructor; put
only the cleanup in `$effect` . If a per-item method walks the full
DOM / item set, wrap the walk in `untrack(...)` so the caller's
`$derived` depends on one reactive field, not every sibling's ref.
Symptom: demo page throws `effect_update_depth_exceeded` on mount;
`morfo:check` reports "Execution context was destroyed" for that
route.
[ ] 39. An `$effect` that kicks off an async side-effect (`.then` /
microtask / `setTimeout` ) which eventually WRITES a reactive var
MUST NOT read that same var back — directly or via any helper it
calls — without `untrack` (A36). The write will re-trigger the
effect via the tracked read, spawn another async side-effect, and
keep looping through the microtask queue. Svelte's synchronous
effect-depth guard does not fire; the browser tab simply freezes.
Symptom: `npm run smoke` passes (500 ms settle doesn't catch the
build-up), component demo hangs on mount when reading a derived
whose body triggers the effect. Wrap the fallback read in
`untrack(() => ({ errors, issues }))` or similar.
[ ] 40. Instrument the component's demo page with `data-perm-step="N"`
annotations on every interactive control that drives a distinct
state transition (A37). Run `npm run perm:check` before shipping
and confirm every permutation passes — this is the validation
layer that catches reactivity loops (A35 / A36) and transition-
time morfo drift that `morfo:check` misses. At minimum cover:
open / dismiss for overlays, toggle for toggleables, first-to-
second-item for composite roving, empty→invalid→valid for forms.
See `src/uix/morfo/PERMUTATION_RUNNER.md` for the full authoring
convention and the opt-in modifiers (`data-perm-mode`,
`data-perm-settle` , `data-perm-skip-validate` ).
# Scope approval — mandatory
[ ] 34. Before declaring the component done, **present the comparison table
to the user in the conversation message** (A32). Not just in the
README — in the reply. Every `❌` and `⚠️` row gets an explicit
decision: (a) implement now, (b) defer to v2 with written
justification and cost estimate in the README, or (c) drop because
it's not a real gap. The user approves scope — the programmer
does not.
[ ] 35. Deferred features land in an ** "Out of scope (v2 roadmap)"** section
in the component's README (A32). Each entry: what it is, the
reference libraries that ship it, why it's deferred, and a cost
estimate. This becomes the PR backlog — no feature dies in a
footnote.
# Translation + topology audits — mandatory (A34)
[ ] 37. **Translation namespace grep.** After touching any lang-related
code in a component or demo, grep the repo for `soma\.` inside
quoted string literals outside `.md` files:
grep -n "['\"]soma\.[a-z-]" src --include=!*.md
soma's translation namespace is ALWAYS `components.{kebab-name}.*`
(or `common.*` for shared strings). Any `langs.t('soma.…')` /
`langs.ts('soma.…')` is a bug and will log `Translation key not
found` at runtime. Component-owned paths should come from
`morfo.texts` + `v.translationRef` ; shared paths should use
`v.commonRef` or an explicit idlangref constant (A3).
[ ] 38. **DOM topology vs `.require()` audit.** For every `X.require()`
call in the provider file, answer: "is the required provider's
component a DOM ancestor of the consumer of my component?". If
the answer is NO, `.require()` WILL throw at runtime — context
only flows to descendants. The classic trap is HTML constraints:
`<tr>` cannot nest `<tr>` , so `Table.RowDetail` (a sibling `<tr>` )
cannot `TableRowProvider.require()` even though it "belongs" to a
row conceptually. Fix by: (a) receive the object via prop, (b) use
`.get()` + fallback, or (c) restructure the DOM. Svelte-check
never catches this — smoke does.
[ ] 39. **Smoke script is part of done.** A component is not done until
`npm run smoke` (with `npm run dev` running) reports PASS for
its new route AND all existing routes. Regressions in unrelated
components caused by translation-table edits, lang-key typos, or
core context changes must be caught here before declaring the
work complete.
```
## Common Mistakes
1. **Event handlers not in props** — defining `onclick` as a class method but forgetting to include it in the `props` derived object. The handler exists but never reaches the DOM.
2. **getContext in event handler** — calling `ctx.get()` inside onclick/onkeydown. getContext only works during component initialization. Capture the reference in the constructor.
3. **Naming Root instead of Provider** — the export name is always `Provider` , never `Root` .
4. **State class file named same as wrapper** — `select.svelte.ts` + `components/select.svelte` can cause Vite module resolution issues. Always use `{name}-provider.svelte.ts` .
5. **Missing readonly prop on form components** — Switch, Checkbox, RadioGroup should have `readonly` alongside `disabled` . readonly prevents interaction but keeps the element focusable.
6. **Comments in Spanish** — all code comments must be in English.
7. **Skipping reference library comparison** — mandatory before implementation. No exceptions.
8. **Redeclaring field types** — defining prop types in both `types.ts` and the provider Opts interface. Define canonical shapes once in `types.ts` , reference with `StateProps<>` / `ActiveProps<>` .
9. **Hardcoding aria strings** — declare component-owned text slots in `morfo.texts` and reference them with `v.translationRef` ; use `v.commonRef` / idlangref constants for shared imperative labels. Never inline strings in providers.
10. **Gesture capturing child clicks** — `setPointerCapture` must be deferred until moveBuffer is exceeded. Immediate capture on pointerdown steals click events from buttons inside the draggable area.
11. ** `data-soma-*` prefix** — the framework never emits `data-soma-{component}-*` . The morfo compiler produces `data-{component}` for the root part and `data-{component}-{part}` for children; `createAttrs(morfo)` only exposes those names as typed strings for selectors. Writing a querySelector like `[data-soma-calendar-day]` returns `null` silently and the contract validator does NOT catch it (it only checks enum values, not attribute presence). Always grep the component folder for any `data-soma-` reference before completing the work — checklist item 27.
12. **Date types from the wrong module** — `DateValue` , `CalendarDate` , `CalendarDateTime` , `Time` , `ZonedDateTime` , `DateRange` , `Month` , etc. come from `$libs/days` . Never import from `$lib/util/dates` (the legacy vendored copy) or directly from `@internationalized/date` .
13. **Using `HourCycle` as `'12h' \| '24h'`** — the canonical form is numeric `12 \| 24` , matching `Intl.DateTimeFormat` 's `hour12` resolved option. The App-layer `ext/dates` service, `ext/app/types` , and the `dias` library all share this form. String forms are legacy.
## Audit-Derived Rules (mandatory for all components)
fix(cleanroom): F2+F3+F4-C+SEM-4s1 — lote mecánico, censos con guard, corpus documental y el close polimórfico de los pickers VIVO
F2 — lote mecánico (13 ítems):
- DEP-2 ogl eliminado (0 imports) · DEP-1 clsx inlineado como toClassString
propio + suite de contrato (props.test.ts; soma.md §12 cerrado).
- THM-7: los 5 selectores manuales de sema.md reescritos con semaSelector
(los ejemplos [data-toast-root] apuntaban a un part INEXISTENTE — la
deriva que el builder previene, demostrada en el propio doc).
- MOR-1 escape isomorfo + validación de attr-names en semaSelector + 9
tests (selectors.test.ts, matches() real con comillas/corchetes) ·
MOR-2 partMarkerAttr = única fuente compilador↔builder + test de paridad ·
MOR-3 _resetCompileCache borrado (0 usos).
- SOM-2 keydown continue en match sin handler + keyboardFixtureMorfo ·
SOM-1 no-await de handlers (censo async = 0; contrato V1 cumplido) + pin.
- SEM-2 trigger pre-attacha catch con logger (void trigger sin unhandled
rejection; throw intacto para awaiters) + pin · SEM-3 fallback muerto de
applyDominance → skip defensivo + timer tope de awaitExpression cancelado ·
SEC-1 adjudicado YA implementado (assertCssVariableValue desde 2026-05-11)
+ pin del path de VALOR.
- accordion → outline (§32; su outline:none dejaba CERO anillo en HCM) —
verificado en vivo · THM-6 radius-full 9999px · EID-4 recuentos 33.
F3 — censos con guard:
- SOM-3 cerrado: announcer + image-provider migrados a scheduler-preferred
(consumidores cableados: date/time-field vía soma.uix.timers; avatar/image
vía eidos.timers — verificado en vivo); guard de timers ENSANCHADO de
soma/components a TODO soma y pasado a EVIDENCIA (setTimeout exige
.schedule( en el fichero — layers/ y datetime/ escapaban del ámbito viejo).
- THM-5: R-4.7 nueva (válvula same-line /* important: <razón> */, escaneo
comment-blanked) + las 15 declaraciones anotadas con su razón + canon
recipe-contract §3/§4.
- SOM-4 adjudicado: el censo/guard YA existían (49 pins); knob/mask-field/
timeline pinneados (overrides documentados en call-site); media-player
Batch-4 (35 hits, cero renderProps) = único batch restante, registrado.
- THM-4 doctrinado en eidos.md §unused (comportamiento/composición =
legítimo; deuda = eje visual sin consumidor; hotspots por lotes).
F4-C — corpus documental (decisiones de usuario aplicadas):
- DOC-3: los 15 enlaces muertos resueltos (repoint a la edición FINAL
trackeada / des-link históricos) · docs:check I6-links WARN→ERROR.
- DOC-1: tabla «Build contract» MIGRADA a component-guide con estados
modernizados (A3–A5 → LIVE + guards de hoy); banners reapuntados; citas
de CANON/sema.md historificadas; lápida-redirect en el §13 del fósil.
- DOC-4: hold chain → holds.ts · FAQ event:* SUPERSEDED por signatures ·
gradient añadido a los DOS capstones (sextet real) · nota de paleta de
demo-authoring corregida (universalPaletteDecls + decisión THM-2 =
mecanismo universal como sucesor del tracker borrado).
- DOC-5/6: recuentos anti-frágiles datados · §4.11 dup → §4.12 · Known gaps
historificado · N-6/N-7 recuperadas de git (d68d2c45^) y canonizadas en
eidos.md §pickers · authoring E2 → canon/tsc.md · air-old des-linkado ·
EID-3 (placement) en la fila RTL · AUX-2 disabledDom documentado.
SEM-4 sesión 1 — el close polimórfico de los pickers, VIVO (D.11):
- Reconciliación: los morfos ya no declaran close (delegated al Popover,
de-dialoged 06-27); el agujero real era el cierre programático bypaseando
dismissWith → save/cancel/select eran perceptualmente SILENCIOSOS.
- Fix: PickerShellHandle.setPopoverDismiss + closeWith(cause) en los 5
providers (14 sitios; select/commit → 'save' = commit.save+fulfill,
cancel → 'cancel' = emerge; fallback raw para headless) + UN inyector en
el eidos PickerShell root (norma N-8). Picker genérico fuera a propósito
(ya suena commit-set/cancel por diseño S9).
- Verificado en vivo (date-picker): Done → close·commit·fulfill·active ·
Cancel → close·emerge · cierre real.
Gates: matriz 141/141 (los 6 morfos nuevos de la pista de texto paralela
también PASS) · contracts 38/38 · eidos 314 · sema 178 · morfo 94 ·
docs:check 0/0 con I6 en error · baseline propio 57.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
These rules were extracted from a full audit of the soma catalog (25 components at the time, 2026-05; they held through the 2026-07 re-audit of the full ≈140-component matrix). Every issue below was found in multiple components. Follow these to avoid repeating them.
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
### A1. Register the morfo through the runtime
Every component MUST register its morfo before it relies on `assertContract` .
The canonical path is to create a runtime:
```ts
const runtime = this.soma.runtime({name}Morfo, { states, props, parts, events });
```
feat(motion)!: una fuente de motion, y los cuatro ejes visuales SIEMPRE en prefs — eidos no lee medios, nadie en el árbol uix pregunta al SO, ningún esquema del app deja a eidos sin motor (P2 #5)
La preferencia efectiva de motion la decide prefs UNA vez (`resolveMotion`: la
intención `allow|reduce` gana al hint del SO, `system` deriva) y la proyección la
estampa en `<html data-motion>` — desde e3c0899dd antes del primer pintado. Aun
así el árbol la re-derivaba por su cuenta leyendo el SO en crudo en dos capas:
A) CSS de eidos (§61 del changelog): 65 at-rules `@media (prefers-reduced-motion:
reduce)` en 63 ficheros + 29 emitidas por el generador, conviviendo con 14 + 52
selectores `[data-motion='reduce']` («with or without the JS projection»). Dos
fuentes ⇒ un `allow` explícito no llegaba al CSS. Cada bloque pasa a
`[data-motion='reduce'] SEL` con la MISMA declaración y orden (paridad a
máquina: 63 ficheros, 65 bloques, 89 reglas, 178 pares selector-declaraciones,
0 discrepancias); los 3 ficheros con gemelo idéntico se unifican; ningún bloque
usaba `:root`. Los 4 `!important` dentro de bloques migrados QUEDAN con su rival
nombrado (navigation-menu ×2 contra el swap direccional (0,3,0); text-focus
contra un `style:transition` inline; events.css empate (0,3,0) con las firmas
generadas en otra hoja). Generador: los 4 emisores dejan de emitir el media, ink
marks gana su gemelo; `generated/base.css` 29 → 0 media, `--nombres` 8034 /
5751 únicos INTACTOS. GUARD nuevo `reduced-motion-media.test.ts` (at-rule, no
prosa; anti-vacío > 60 ficheros; mordido por mutación). Cambio de veredicto de
cascada MEDIDO en Chrome real por el adversarial: el prefijo suma (0,1,0), una
veintena de variantes más específicas que ganaban al media hoy pierden — y bajo
la media del SO la forma de HEAD NO paraba el anillo del spinner. `data-motion`
tiene TRES dueños (reduce · navigation-menu from/to · tabs fade|slide), valores
disjuntos, forma ancestro obligatoria. Prosa que afirmaba «both» barrida por la
AFIRMACIÓN (tooltip.css, card.css, recipes/base.ts, skin-media-player, events,
float-panel).
B) JS (§62): el motor de motion (`engine-motion.ts:131`), el motor de escena
(`engine-scene.ts:99`), la háptica de sema (`chans/haptic.ts:129`) y OCHO
lecturas en componentes de eidos preguntaban a `ActiveDom.prefersReducedMotion`
(el media en crudo). El puerto correcto YA EXISTÍA sin consumidores:
`MotionSource = Source<MotionEffective>` (`$libs/motion`). La fuente nace en
prefs (`createMotionSourceFromPrefs`, `src/arts/prefs/motion-source.ts`,
precedente `createLocaleSourceFromPrefs`) y la construyen UNA vez las tres
raíces: `createActiveUix`, `attachActiveUix` (fallbacks desde `app.prefs`) y las
fábricas `defineEngineMotion` / `defineEngineScene` / `defineEngineSemantic`
(`coreDependencies: ['prefs']`, forma de format y langs). MUEREN de los puertos
`MotionDom.prefersReducedMotion`, `SceneDom.prefersReducedMotion` y el interface
`HapticChannelDom` entero (un puerto que contesta política es la puerta por
donde vuelve el defecto); `MotionRunOptions.reduced` se queda como PIN por
ejecución. `ActiveEidos.reducedMotion` con dos puertas: con prefs el efectivo;
standalone sigue al SO con acta (sin primer motor no hay segundo, §60). Los
ocho sitios de componentes leen `eidos.reducedMotion`. Hallazgo del lote: DOS
motores de escena por superficie DOM (aura-indicator, pack Ambient) que se
habrían quedado ciegos a la policy `reduce` obligatoria (P-1) EN SILENCIO —
reciben la fuente vía `eidos.reducedMotion`. Deuda nombrada: escena lee la
fuente en el montaje (paridad con la lectura del media que sustituye).
`ActiveDom.prefersReducedMotion` no se toca: es un hecho del SO que alimenta
el ENTORNO de prefs y nada más.
Soma (adenda firmada «a en B»): el último lector de política que preguntaba al
SO, `soma/runtime.svelte.ts:1280` (migración a11y del morfo: `'state'` ⇒
`channels: []` por S5, `'text'` ⇒ región viva, `'focus'` ⇒ foco), lee la
MISMA fuente: `SomaRuntimeBaseSources.motion: MotionSource` OBLIGATORIO como
`dom` (un runtime que no puede responder «¿reducido?» no puede honrar S5 — lo
garantiza el tipo), llenado por `Soma.runtime()` desde `uix.prefs` (esquema sin
`motion` ⇒ permitir, como los motores). La decisión mayor (b) — que el MOTOR
aplique `'state'` en su pasada de reducción y que un `emit()` directo reciba
tratamiento a11y — queda ABIERTA como fila §3.9 de CONTINUE-sema-audit.md, a
ejecutar junto a D-full. `ActiveEidos.reducedMotion` distingue «sin prefs»
(standalone ⇒ SO, §60) de «prefs sin ranura motion» (⇒ permitir, como los
motores): las dos mitades de una UI ya no discrepan bajo un esquema sin la
dimensión. Hallazgo de la adenda: la bolsa `sources` se construye en 96
sitios (3 de producción — menu-dial, metrics, onion-menu — que reciben la
fuente vía `eidos.reducedMotion`, y 93 harnesses cuyo `as unknown as Soma`
CEGABA la comprobación de miembros: 60 tests rojos hasta declarar el `Omit`
real; el tipo hizo su trabajo en el código de producción y era ciego justo
donde se suponía que bastaba). Test real nuevo
`soma/test/reduced-motion-source.svelte.test.ts` (chromium: `Soma.create()`
lee contexto Svelte) con `createActiveUix` + `EngineSemantic` reales;
mutación (el trigger vuelve al dom) ROJA 3/3. Adversarial dirigido de la
adenda: siete defectos cerrados — `src/uix/contracts.ts` declaraba
`requires: ['dom']` para la bolsa de soma y el test no lo asertaba (un guard
que no inspecciona nada pasa) → `['dom', 'motion']` + aserto; la bolsa #97
(`bag-census.test.ts`) sin `motion` bajo un casteo; el camino `'text'` —el
ÚNICO que declaran 9 morfos de producción— sin test (añadido); dos snippets
de docs que ya no compilaban (component-guide A1, morfo.md); cifras del §62.
C) La raíz GARANTIZA los cuatro ejes visuales (cazado por el autor en el docs
site: texto invisible en oscuro). Cuatro layouts congelados componen su propio
esquema de prefs SIN `mode/theme/density/scaling`; un esquema del app
SUSTITUÍA al de la raíz entero y eidos «degradaba defensivamente» a
`mode = 'light'` CONSTANTE ignorando el SO, mientras el shell estampaba su
wrapper en oscuro: tinta de tema claro sobre superficies oscuras. Forma:
`uixVisualPrefsDimensions()` (pura, en `prefs-schema.ts`; el boot compila la
misma) y `createActiveUix` fusiona SIEMPRE `{ ...ejesVisuales, ...esquemaDelApp }`
— el app puede REDEFINIR un eje, nunca omitirlo; sin un solo cast. Eidos deja
de degradar en silencio: `createPrefsPreferenceSource(prefs, fallbacks,
onMissing)` avisa por `uix.logger.warn` por cada ranura ausente (queda solo
para attach con prefs ajenas; docs de attach: el app compone
`uixVisualPrefsDimensions()`). Tests reales (esquema sin ejes + SO oscuro ⇒
`data-mode="dark"`; app que redefine `theme` gana; prefs ajenas ⇒ 4 avisos por
el logger REAL); el viejo test «degrades to the fallbacks» estaba verde POR
COINCIDENCIA (defaults = fallbacks) y se sustituye; mutación (sin fusión) 2
rojos y restaurada. Boot regenerado (15134 B), delta cero ×5 intacto.
`web/routes/uix/+layout@.svelte` (descongelado por orden del autor, +14/−29):
muere el `modeSource` muerto y el `$state` local; el shell LEE la ranura
`mode` y el toggle escribe `uix.prefs.setIntent('mode', …)`; migración única
de `uix-docs-theme` al sobre de prefs (clave borrada). Ledger 95 → 93 (la
entrada de ese fichero desaparece). VERIFICADO EN CHROME por el coordinador:
SO oscuro sin clave ⇒ `<html data-mode="dark">`, wrapper dark, tinta
`oklch(0.95)`; clave vieja `dark` ⇒ intent `dark`; toggle mueve html, wrapper,
tinta e intent en los dos sentidos. Nombrado, no arreglado: el boot compila
siempre el esquema por defecto (un app que REDEFINE un eje resuelve distinto
que el boot; hoy nadie en web/ usa el boot) · los otros tres layouts congelados
recuperan los ejes pero su wrapper sigue en `$state` local · SSR del docs sirve
`light` y la hidratación corrige (previo).
Verificación: A — vitest eidos+value-channels 43/483, eidos:lint 0, paridad 0
discrepancias, mutación del guard 3/3 y 4/4 (adversarial), prettier solo avisos
preexistentes; B — vitest del scope 90/990 y, con la adenda, 242/2395; +26 tests
(motor 5 · escena 2 · háptica 2 · raíz real jsdom 7 · ActiveEidos 7 · fábrica 1 ·
soma 3 — cifras verificadas una a una por el adversarial), 11 dobles re-firmados,
93 harnesses de soma tipados con el `Omit` real, cuatro mutaciones ROJAS (3/2/3 +
la de soma 3/3) y restauradas por sha256, más la mutación de TIPO del adversarial
(la fuente devuelve la intención ⇒ 2 errores nuevos en src/: el puerto es gate de
tipo, no prosa); C — vitest active-uix+eidos+prefs 57/591, boot 8/8 con delta
cero, mutación 2 rojos, verificación en Chrome por el coordinador; todos —
check src/ 0 (ledger 95 → 93: MENGUA), check:gate OK, docs:check OK,
arts:check OK, packs:check OK. Suite completa 457/5336 verde (adversarial A). Adversariales Opus
independientes por lote (informes en el handoff). Constructores + adversariales
Opus 5; la sesión coordina. ⚠ Lección: la cuenta del brief de A («94 en fuentes»)
era un fallo de medida del coordinador (`grep -rh | grep -v generated` filtra
LÍNEAS con esa palabra, no el directorio); el constructor la re-midió porque el
brief lo exigía. ⚠ Dos constructores cortados por límite de sesión y reanudados
tras releer su diff entero (ley de la casa).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
For tests or a tool that does not have a `Soma` scope, use the lower-level
factory. `dom` and `motion` are both REQUIRED there: inside a scope the root
injects them, here the caller passes the DOM service and a `MotionSource` (the
effective `prefs.motion` ; a constant `{ get: () => 'allow' }` is the right
answer for a harness that is not about the preference). See changelog §62 §7.
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
```ts
const runtime = createSomaRuntime({name}Morfo, {
dom,
feat(motion)!: una fuente de motion, y los cuatro ejes visuales SIEMPRE en prefs — eidos no lee medios, nadie en el árbol uix pregunta al SO, ningún esquema del app deja a eidos sin motor (P2 #5)
La preferencia efectiva de motion la decide prefs UNA vez (`resolveMotion`: la
intención `allow|reduce` gana al hint del SO, `system` deriva) y la proyección la
estampa en `<html data-motion>` — desde e3c0899dd antes del primer pintado. Aun
así el árbol la re-derivaba por su cuenta leyendo el SO en crudo en dos capas:
A) CSS de eidos (§61 del changelog): 65 at-rules `@media (prefers-reduced-motion:
reduce)` en 63 ficheros + 29 emitidas por el generador, conviviendo con 14 + 52
selectores `[data-motion='reduce']` («with or without the JS projection»). Dos
fuentes ⇒ un `allow` explícito no llegaba al CSS. Cada bloque pasa a
`[data-motion='reduce'] SEL` con la MISMA declaración y orden (paridad a
máquina: 63 ficheros, 65 bloques, 89 reglas, 178 pares selector-declaraciones,
0 discrepancias); los 3 ficheros con gemelo idéntico se unifican; ningún bloque
usaba `:root`. Los 4 `!important` dentro de bloques migrados QUEDAN con su rival
nombrado (navigation-menu ×2 contra el swap direccional (0,3,0); text-focus
contra un `style:transition` inline; events.css empate (0,3,0) con las firmas
generadas en otra hoja). Generador: los 4 emisores dejan de emitir el media, ink
marks gana su gemelo; `generated/base.css` 29 → 0 media, `--nombres` 8034 /
5751 únicos INTACTOS. GUARD nuevo `reduced-motion-media.test.ts` (at-rule, no
prosa; anti-vacío > 60 ficheros; mordido por mutación). Cambio de veredicto de
cascada MEDIDO en Chrome real por el adversarial: el prefijo suma (0,1,0), una
veintena de variantes más específicas que ganaban al media hoy pierden — y bajo
la media del SO la forma de HEAD NO paraba el anillo del spinner. `data-motion`
tiene TRES dueños (reduce · navigation-menu from/to · tabs fade|slide), valores
disjuntos, forma ancestro obligatoria. Prosa que afirmaba «both» barrida por la
AFIRMACIÓN (tooltip.css, card.css, recipes/base.ts, skin-media-player, events,
float-panel).
B) JS (§62): el motor de motion (`engine-motion.ts:131`), el motor de escena
(`engine-scene.ts:99`), la háptica de sema (`chans/haptic.ts:129`) y OCHO
lecturas en componentes de eidos preguntaban a `ActiveDom.prefersReducedMotion`
(el media en crudo). El puerto correcto YA EXISTÍA sin consumidores:
`MotionSource = Source<MotionEffective>` (`$libs/motion`). La fuente nace en
prefs (`createMotionSourceFromPrefs`, `src/arts/prefs/motion-source.ts`,
precedente `createLocaleSourceFromPrefs`) y la construyen UNA vez las tres
raíces: `createActiveUix`, `attachActiveUix` (fallbacks desde `app.prefs`) y las
fábricas `defineEngineMotion` / `defineEngineScene` / `defineEngineSemantic`
(`coreDependencies: ['prefs']`, forma de format y langs). MUEREN de los puertos
`MotionDom.prefersReducedMotion`, `SceneDom.prefersReducedMotion` y el interface
`HapticChannelDom` entero (un puerto que contesta política es la puerta por
donde vuelve el defecto); `MotionRunOptions.reduced` se queda como PIN por
ejecución. `ActiveEidos.reducedMotion` con dos puertas: con prefs el efectivo;
standalone sigue al SO con acta (sin primer motor no hay segundo, §60). Los
ocho sitios de componentes leen `eidos.reducedMotion`. Hallazgo del lote: DOS
motores de escena por superficie DOM (aura-indicator, pack Ambient) que se
habrían quedado ciegos a la policy `reduce` obligatoria (P-1) EN SILENCIO —
reciben la fuente vía `eidos.reducedMotion`. Deuda nombrada: escena lee la
fuente en el montaje (paridad con la lectura del media que sustituye).
`ActiveDom.prefersReducedMotion` no se toca: es un hecho del SO que alimenta
el ENTORNO de prefs y nada más.
Soma (adenda firmada «a en B»): el último lector de política que preguntaba al
SO, `soma/runtime.svelte.ts:1280` (migración a11y del morfo: `'state'` ⇒
`channels: []` por S5, `'text'` ⇒ región viva, `'focus'` ⇒ foco), lee la
MISMA fuente: `SomaRuntimeBaseSources.motion: MotionSource` OBLIGATORIO como
`dom` (un runtime que no puede responder «¿reducido?» no puede honrar S5 — lo
garantiza el tipo), llenado por `Soma.runtime()` desde `uix.prefs` (esquema sin
`motion` ⇒ permitir, como los motores). La decisión mayor (b) — que el MOTOR
aplique `'state'` en su pasada de reducción y que un `emit()` directo reciba
tratamiento a11y — queda ABIERTA como fila §3.9 de CONTINUE-sema-audit.md, a
ejecutar junto a D-full. `ActiveEidos.reducedMotion` distingue «sin prefs»
(standalone ⇒ SO, §60) de «prefs sin ranura motion» (⇒ permitir, como los
motores): las dos mitades de una UI ya no discrepan bajo un esquema sin la
dimensión. Hallazgo de la adenda: la bolsa `sources` se construye en 96
sitios (3 de producción — menu-dial, metrics, onion-menu — que reciben la
fuente vía `eidos.reducedMotion`, y 93 harnesses cuyo `as unknown as Soma`
CEGABA la comprobación de miembros: 60 tests rojos hasta declarar el `Omit`
real; el tipo hizo su trabajo en el código de producción y era ciego justo
donde se suponía que bastaba). Test real nuevo
`soma/test/reduced-motion-source.svelte.test.ts` (chromium: `Soma.create()`
lee contexto Svelte) con `createActiveUix` + `EngineSemantic` reales;
mutación (el trigger vuelve al dom) ROJA 3/3. Adversarial dirigido de la
adenda: siete defectos cerrados — `src/uix/contracts.ts` declaraba
`requires: ['dom']` para la bolsa de soma y el test no lo asertaba (un guard
que no inspecciona nada pasa) → `['dom', 'motion']` + aserto; la bolsa #97
(`bag-census.test.ts`) sin `motion` bajo un casteo; el camino `'text'` —el
ÚNICO que declaran 9 morfos de producción— sin test (añadido); dos snippets
de docs que ya no compilaban (component-guide A1, morfo.md); cifras del §62.
C) La raíz GARANTIZA los cuatro ejes visuales (cazado por el autor en el docs
site: texto invisible en oscuro). Cuatro layouts congelados componen su propio
esquema de prefs SIN `mode/theme/density/scaling`; un esquema del app
SUSTITUÍA al de la raíz entero y eidos «degradaba defensivamente» a
`mode = 'light'` CONSTANTE ignorando el SO, mientras el shell estampaba su
wrapper en oscuro: tinta de tema claro sobre superficies oscuras. Forma:
`uixVisualPrefsDimensions()` (pura, en `prefs-schema.ts`; el boot compila la
misma) y `createActiveUix` fusiona SIEMPRE `{ ...ejesVisuales, ...esquemaDelApp }`
— el app puede REDEFINIR un eje, nunca omitirlo; sin un solo cast. Eidos deja
de degradar en silencio: `createPrefsPreferenceSource(prefs, fallbacks,
onMissing)` avisa por `uix.logger.warn` por cada ranura ausente (queda solo
para attach con prefs ajenas; docs de attach: el app compone
`uixVisualPrefsDimensions()`). Tests reales (esquema sin ejes + SO oscuro ⇒
`data-mode="dark"`; app que redefine `theme` gana; prefs ajenas ⇒ 4 avisos por
el logger REAL); el viejo test «degrades to the fallbacks» estaba verde POR
COINCIDENCIA (defaults = fallbacks) y se sustituye; mutación (sin fusión) 2
rojos y restaurada. Boot regenerado (15134 B), delta cero ×5 intacto.
`web/routes/uix/+layout@.svelte` (descongelado por orden del autor, +14/−29):
muere el `modeSource` muerto y el `$state` local; el shell LEE la ranura
`mode` y el toggle escribe `uix.prefs.setIntent('mode', …)`; migración única
de `uix-docs-theme` al sobre de prefs (clave borrada). Ledger 95 → 93 (la
entrada de ese fichero desaparece). VERIFICADO EN CHROME por el coordinador:
SO oscuro sin clave ⇒ `<html data-mode="dark">`, wrapper dark, tinta
`oklch(0.95)`; clave vieja `dark` ⇒ intent `dark`; toggle mueve html, wrapper,
tinta e intent en los dos sentidos. Nombrado, no arreglado: el boot compila
siempre el esquema por defecto (un app que REDEFINE un eje resuelve distinto
que el boot; hoy nadie en web/ usa el boot) · los otros tres layouts congelados
recuperan los ejes pero su wrapper sigue en `$state` local · SSR del docs sirve
`light` y la hidratación corrige (previo).
Verificación: A — vitest eidos+value-channels 43/483, eidos:lint 0, paridad 0
discrepancias, mutación del guard 3/3 y 4/4 (adversarial), prettier solo avisos
preexistentes; B — vitest del scope 90/990 y, con la adenda, 242/2395; +26 tests
(motor 5 · escena 2 · háptica 2 · raíz real jsdom 7 · ActiveEidos 7 · fábrica 1 ·
soma 3 — cifras verificadas una a una por el adversarial), 11 dobles re-firmados,
93 harnesses de soma tipados con el `Omit` real, cuatro mutaciones ROJAS (3/2/3 +
la de soma 3/3) y restauradas por sha256, más la mutación de TIPO del adversarial
(la fuente devuelve la intención ⇒ 2 errores nuevos en src/: el puerto es gate de
tipo, no prosa); C — vitest active-uix+eidos+prefs 57/591, boot 8/8 con delta
cero, mutación 2 rojos, verificación en Chrome por el coordinador; todos —
check src/ 0 (ledger 95 → 93: MENGUA), check:gate OK, docs:check OK,
arts:check OK, packs:check OK. Suite completa 457/5336 verde (adversarial A). Adversariales Opus
independientes por lote (informes en el handoff). Constructores + adversariales
Opus 5; la sesión coordina. ⚠ Lección: la cuenta del brief de A («94 en fuentes»)
era un fallo de medida del coordinador (`grep -rh | grep -v generated` filtra
LÍNEAS con esa palabra, no el directorio); el constructor la re-midió porque el
brief lo exigía. ⚠ Dos constructores cortados por límite de sesión y reanudados
tras releer su diff entero (ley de la casa).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
motion,
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
eventEngine,
states,
props,
parts,
events
});
```
Both paths call `registerMorfo(morfo)` internally. That compiles the morfo
and registers its `data-*` contract (component text catalogs are registered
separately by `ActiveUix` from `src/uix/langs/components/*` ). Only call
`registerMorfo(morfo)` manually for a legacy provider or tool that needs the
registry side effect without creating a runtime.
### A2. Root part must use 'provider', not 'root'
The orchestrator / context-creator part uses `kebab: 'provider'` in the morfo. The morfo compiler special-cases `'provider'` to strip the suffix, so the emitted attribute is `data-{component}` (bare, no suffix). Using any other name produces `data-{component}-{name}` .
**Naming coherence**: morfo's `name: 'Provider'` field (the consumer-facing export) and `kebab: 'provider'` field (the DOM role) match — one name for the same part across both axes.
```ts
// Correct — in the morfo file:
{ name: 'Provider', kebab: 'provider', /* ... */ }
// The provider registers the same kebab through the runtime:
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
owner: this,
docs(uix): la doctrina alcanza a la bolsa única — el corpus deja de enseñar la API muerta
La clase exacta que la auditoría flagueó (doc↔código), producida esta vez
por NUESTROS propios cambios de P0 fase C y sin corregir hasta ahora:
- component-guide §«two sanctioned ways»: reescrita a UNA vía (la bolsa
.props resuelve el contrato entero; extras solo lo que el morfo no puede
expresar; valores CRUDOS; la inversa «morfo sin value ⇒ soma escribe»).
Los 4 snippets con syncAttrs: true, limpiados.
- soma-architecture: partProps re-descrito como LA bolsa (identidad +
contrato completo, SSR incluido); el párrafo del flag sustituido por «el
único escritor imperativo es el prewrite»; snippet limpiado.
- soma.md: snippet de accordion sin el flag.
- morfo.md §naming: el «(SSR included)» dejó de ser contraste — desde la
tubería única TODO plan viaja en la bolsa; lo que distingue a los naming
es la PRECEDENCIA (consumer-first via mergeProps).
- testing-and-tooling: la postura SSR gana el canario ssr-contract.test, y
nace «The gate» (check:gate + ledger menguante, gate, hook — los
instrumentos de fase B no estaban documentados).
- CLAUDE.md: el paso 4 de trigger ya no dice «effect-driven» (prettier
normalizó de paso el fichero entero — solo whitespace, contenido intacto,
declarado aquí para que el diff ancho no sea silencioso).
Lo histórico (changelog, old-deprecated, audit de julio, process/) se queda
como historia. Verificación: docs:check 0 errores sobre 817 docs; cero
menciones normativas de la API muerta fuera de referencias históricas.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
context: DialogProvider.ctx
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
});
// runtimePart.props includes data-dialog
// Wrong
{ name: 'Provider', kebab: 'root' }
// runtimePart.props would include data-dialog-root instead of data-dialog
```
### A3. Soma access, translations, and imports
**Soma access:** Declare `readonly soma = Soma.get()` in the root provider when the component needs prefs-derived services or imperative translations. Sub-parts access soma via `this.provider.soma` .
**Texts:** The morfo declares component-owned text slots as idlangrefs; the
multilingual catalog lives in `src/uix/langs/components/{kebab}.ts` :
```ts
export const drawerMorfo = {
name: 'Drawer',
kebab: 'drawer',
texts: {
trigger: '#?components.drawer.trigger|Open drawer'
},
parts: [
{
name: 'Trigger',
kebab: 'trigger',
aria: [{ attr: 'aria-label', value: v.translationRef('trigger', 'Open drawer') }]
}
]
} as const satisfies Morfo;
```
Shared strings use common refs:
```ts
value: v.commonRef('buttons.close', 'Close');
```
`langs.ts` is still allowed, but only as a small constants file when provider
code needs an imperative idlangref:
```ts
// drawer/langs.ts
export const DRAWER_LANGS = {
CLOSE: '#?common.buttons.close|Close'
} as const;
// provider
'aria-label': this.provider.soma?.langs.ts(DRAWER_LANGS.CLOSE)
```
**Translation namespace structure:**
```
common.buttons.close ← project-wide, shared by soma + eidos + app
common.buttons.open
common.labels.*
components.drawer.trigger ← component-specific
components.dialog.trigger
```
- Common keys live under `common.*` at the lang root — not under soma
- Component keys live under `components.{name}.*`
- `ActiveUix` registers `commonLangs` defaults without overwriting user-provided leaves
- `ActiveUix` registers the per-component catalogs from `src/uix/langs/components/*` under `components.{kebab}.*`
- The morfo only declares slots (`morfo.texts`, idlangrefs); the multilingual records live in `langs/components/{kebab}.ts` . Shared strings live in `common.*` .
- `langs.ts()` with idlangref for simple strings. `langs.t()` only for interpolated templates (e.g., `Page {{value}}` )
Do NOT:
- Create `translate()` helper methods in providers
- Use `?? 'fallback'` — the fallback belongs inside the langref (`#?path|fallback`)
- Hardcode aria strings — use `morfo.texts` + `v.translationRef` , `v.commonRef` , or an explicit idlangref constant
- Put common keys (close, open, cancel) under component namespaces — they belong in `common.*`
**Imports within soma:** Use relative paths, not `$soma/` aliases. Relative paths make the library portable without requiring alias configuration in the consumer's build. soma does not import from `$lib` — trivial utilities (like empty callbacks) are inline (`() => {}`).
```ts
// Inside soma — relative
import { DRAWER_LANGS } from './langs';
import type { DrawerSide } from './types';
import { Presence } from '../../layers/presence.svelte';
// Wrong — alias
import { DRAWER_LANGS } from '$soma/components/drawer/langs';
// Wrong — app-layer dependency: soma depends on NOBODY above it
// (the `$lib` alias this example used no longer exists — it was removed in the
// cleanup phase, and code that resurfaces it is a regression on its own)
import { noop } from '$app/util/funcs';
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
```
Consumer code (layouts, test pages, app) uses `$soma/` alias — that's their build config, not soma's concern.
### A4. ARIA relationships are mandatory
Every component with trigger→content pattern MUST emit:
- **Trigger**: `aria-controls={contentId}` , `aria-expanded`
- **Content**: `aria-labelledby={triggerId}` (for dialogs, popovers, selects, drawers)
- **Form controls**: `aria-labelledby={labelId}` when a Label part exists
- **Groups**: `aria-label` or `aria-labelledby` on `role="group"` , `role="tablist"` , `role="toolbar"` , `role="radiogroup"` , `role="tree"`
Missing ARIA relationships = the component is broken for screen readers.
### A5. Feature flags are opt-in (`=== true`)
Per-item feature flags (Table columns, tree nodes) default to `false` . A feature only activates when the consumer explicitly sets it to `true` .
```ts
// Correct — opt-in
return col?.def.enableSorting === true;
// Wrong — opt-out (active by default)
return col?.def.enableSorting !== false;
```
Applies to Table: `enableSorting` , `enableFiltering` , `enableResizing` , `enablePinning` , `enableHiding` (exception: `enableHiding` defaults to `true` — documented in ColumnDef JSDoc).
### A6. Clean up timers, listeners, and observers
Every `setTimeout` , `setInterval` , listener, `ResizeObserver` , or `MutationObserver` created in a provider MUST have cleanup in `$effect` return or explicit dispose. Uncleaned resources cause memory leaks.
Global or transversal listeners use `this.soma.dom.listen(...)` / `this.provider.soma.dom.listen(...)` . Local Svelte handlers stay in props (`onclick`, `onkeydown` , etc.).
```ts
// Correct
$effect(() => {
const timer = setTimeout(fn, delay);
return () => clearTimeout(timer);
});
// Wrong — leak
constructor() {
setTimeout(fn, delay); // never cleared
window.addEventListener('keydown', fn); // bypasses ActiveDom and is never removed
}
```
### A7. Use `.get()` for optional parents, `.require()` for required
| Method | Returns | Use when |
| ---------------- | ---------------------------- | --------------------------------- |
| `X.create(opts)` | instance | Creating + registering in context |
| `X.get()` | instance or `undefined` | Parent is optional |
| `X.require()` | instance (throws if missing) | Parent is required |
```ts
// Tooltip can work without Group — use get()
this.group = TooltipGroupProvider.get();
// Accordion Item MUST be inside Accordion — use require()
this.provider = AccordionProvider.require();
```
This convention applies to App, Soma, and all Provider classes. No standalone functions (`createApp`, `getSoma` ). No `from()` .
### A8. No visual styles in headless providers
Headless providers MUST NOT emit visual CSS properties (`border-radius`, `background` , `color` , `overflow: auto` ). Only functional CSS is allowed:
- `touch-action: none` — required for gesture drag
- `pointer-events: auto` — required for overlays and fixed-position content
- `transition: none` — required during active drag to disable CSS transitions
- `transform: translate3d(...)` — required during active drag for visual feedback
feat(theming)!: SS16 - el guard vive donde NACE el valor, y el velo del drawer vuelve a pintar
Firma SS16, quinta aplicacion de la doctrina "la capa sostiene la pluma, no es
la dueña". El censo, el ledger y la ley del espacio cerrado auditaban SOLO
quien LEE (recetas de eidos); quien ESCRIBE la custom property por instancia
vive en soma/arts y ningun instrumento del eje lo habia enumerado jamas.
EL BUG VIVO QUE ESTO DESTAPO — y no se parcheo, se diagnostico
--drawer-overlay-opacity era DOS especies bajo un nombre: el knob de tema (el
contrato dice 62%) y el canal del arrastre (0..1). soma escribia el canal SIN
UNIDAD sobre el nombre del knob; color-mix() exige porcentaje, la funcion caia
invalida y el velo computaba rgba(0,0,0,0): NO PINTABA. Y el inline dejaba el
knob inalcanzable para cualquier tema. Se separan: el publico se queda como
knob (nadie lo escribe en runtime), el arrastre pasa a
--_drawer-overlay-progress (sin unidad, que para un multiplicador es lo
correcto) y la receta los COMPONE - calc(knob * progress) -, asi que el
arrastre ATENUA el tema en vez de destruirlo.
Medido en Chrome: reposo 0.408471 (antes rgba 0,0,0,0) - el tema LLEGA (20%
-> 0.1318, 62% -> 0.4085, 100% -> 0.6588; antes ninguno movia nada) - el
arrastre sigue (Escape real -> alpha 0; 0/0.25/0.5/1 lineal) - control
negativo: reinyectando la escritura vieja el velo vuelve a caer, o sea que la
unidad era el SINTOMA y las dos especies la enfermedad.
⚠ Estaba adjudicado EN PROSA en theming-sentinel-exceptions.ts:815 desde hacia
dias: la excepcion se trago el bug.
LOS 18 NOMBRES A SU SITIO
Clase B (11 en command/dialog/scroll-area/toast + 4 del drawer): forma publica
sin contrato y sin UN SOLO lector en el repo - una API publicada que el
framework no consume. A --_{c}-*, y los README de soma dejan de enseñarlos
como API del consumidor: ahora enseñan LA PARTE, con la formulacion verbatim
que SS15 ya habia verificado. Clase D (tree-view, gradient-picker): tenian
lector, verificados en navegador. --scrollbar-width NO se renombro por
inercia: es una escritura sobre el unico <body>, la doctrina no le llega, y
queda REGISTRADA con su razon en vez de inventarle un dueño.
LA AGUJA - src/uix/value-channels.test.ts (fichero propio, 6 tests)
Vitest y no script, porque el gate termina en la suite y eso es lo que
convierte la doctrina en ley. Deriva el vocabulario de sistema RESTANDO el
contrato a lo que emite el generador (lista derivada, nunca a mano). Barre
NUEVE raices - las siete nuevas verificadas a cero ANTES de asertarlas - y el
quinto test asserta que cada raiz declarada se anduvo de verdad: una raiz que
resuelve a cero ficheros es la puerta que nadie habria visto.
Dos correcciones al dimensionado, por medida: el arbol tiene SEIS formas de
escribir, no cuatro, y una de las que faltaban era LA CANONICA (la
--_${component}-... que SS14 y SS15 firmaron) - un guard ciego a ella habria
dado verde sobre su propio destino. Y un barrido mas ancho marcaba en rojo un
anchor-name, que en gramatica es identico a un nombre de propiedad: probado y
REVERTIDO. El instrumento miente primero.
Mutaciones: cinco, con el arbol byte a byte identico. Incluyen las dos que
prueban lo que las correcciones añaden (una clave _ DEL contrato pasa de verde
a rojo; un --_ legitimo en blocks pasa de rojo a verde).
LO QUE EL ADVERSARIAL CORRIGIO DE MI PROPIA LEY
Dictamen: "es LEY sobre la mitad que barre, y sigue siendo PROSA sobre el
absoluto que enuncia". Cierto: decia "toda escritura por instancia" y la aguja
no mira eidos, donde viven ONCE escrituras de nombres contratados. Corregido -
el enunciado nombra sus nueve raices y DECLARA sus dos fronteras (eidos, con
su expediente abierto; web/routes, congelado); las salidas son CUATRO y no
tres (la cuarta, sistema, es verde); y el registro se queda con cuatro campos
por entrada (since, reason, destination, heldBecause) con un test que exige
los cuatro - la diferencia entre un registro y un cajon.
Y la relectura con ojo de abogado encontro DOS absolutos mas, en direccion
contraria, escritos bajo "WHAT THE GUARD DOES NOT CHECK": "por instancia" no
es decidible estaticamente (el antecedente del guard es MAS ANCHO que el de la
doctrina), y el {c} de --_{c}-* no lo comprueba nadie, solo el guion bajo.
SS18 ABIERTO, y es la respuesta medida a "¿puede volver a nacer un nombre sin
dueño sin que nadie se entere?": SI, desde eidos. 11 contratadas + 18 sin
dueño, partidas en dos especies (la fundacion generando su vocabulario, que es
legitima, y las props ergonomicas del consumidor, que no es lo mismo).
dialog-overlay-opacity es el GEMELO EXACTO del velo del drawer y sigue vivo.
No es "añadir el root": distinguir las dos especies EXIGE FIRMA.
Guards: value-channels 6/6 - recipe-css-contract + reach-floor + generated-css
55/55 - docs:check 0/0 sobre 819 - --debt 1148/0/0 - tsc 0 propios - prettier
limpio.
BREAKING: los 18 nombres publicos ya no existen; el canal se lee --_{c}-* y
sigue sin ser de nadie para fijarlo. Y consumir un --{c}-* del contrato desde
un provider es ROJO desde hoy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
- CSS custom properties (`--_drawer-progress`, `--_drawer-offset-*` ) — data for the visual layer
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
The visual layer (Eidos) owns appearance. The provider owns behavior.
### A9. Dismissal behavior for drawers vs dialogs
A drawer is NOT a popover. `interactOutsideBehavior` should be `'ignore'` for drawers:
- **Modal drawer**: overlay `onclick` handles close. Dismissal layer only handles Escape.
- **Non-modal drawer**: background is interactive by definition. Dismissal layer disabled entirely. Escape handled via `onkeydown` on the content element.
- **Non-dismissible**: Dismissal layer fully disabled. Close button uses `forceClose()` (bypasses the `dismissible` guard).
### A10. DOM queries in providers must handle dynamism
`getItems()` patterns using `querySelectorAll` are fragile — they capture a snapshot, not a live reference. If items are added/removed dynamically, the query must re-run. For nested components (e.g., nested Accordion), filter results to only include elements whose closest root is the current root:
```ts
getTriggers(): HTMLButtonElement[] {
const root = this.opts.ref?.current;
if (!root) return [];
const all = Array.from(root.querySelectorAll< HTMLButtonElement > (selector));
return all.filter((el) => el.closest(`[${attrs.provider}]`) === root);
}
```
### A11. Wrapper must be thin
Complex logic (effects, DOM manipulation, state machines, event coordination) belongs in the Provider, not the wrapper. The wrapper's job is: destructure props → wrap in Active/State → create Provider → mergeProps → render. If a wrapper has `$effect` blocks, `onMount` , or branching logic, the code belongs in the Provider.
### A12. Keyboard navigation must respect RTL
docs(direction): el contrato pasa a ser canon, y el corpus deja de contradecirlo
El eje estaba cerrado en CODIGO y no en DOCUMENTACION. El contrato vivia solo en
`CONTINUE-direction.md`, un handoff que `docs/README.md` declara «never a source
of truth». Un capitulo E2 lo fija ahora: `docs/canon/direction-contract.md`.
Va a canon y no a arquitectura por la misma razon que `recipe-contract.md`: es
normativo y tiene guard ejecutable. Cubre la cadena y donde corre cada eslabon;
por que `undefined` no es `'ltr'`; las DOS atributos —`dir` crudo (nativo, lo que
`:dir()` mira) y `data-dir` resuelto (opt-in, para recetas que necesitan un hook
incondicional)—; cuando el estampado es OBLIGATORIO; la doctrina de selector; las
trampas que el guard no ve; que espeja y que no; y la mitad global de prefs.
EL CORPUS SE CONTRADECIA en cuatro sitios, y dos de ellos ENSENABAN mal:
- `component-guide.md:372` daba la cadena como `prefs → 'ltr'`, DOS eslabones,
mandando al wrapper a leer prefs directamente. Eso excluye la prop.
- `soma-architecture.md` §3.4 presentaba el resolutor a pelo (`soma.prefs.getDir()`
dentro del provider) como LA forma de obtener la direccion — justo lo que el eje
retiro del catalogo.
- `html.ts` ensenaba en su JSDoc `dir?: 'ltr' | 'rtl'`, el union a mano que la
regla E-2.5 ahora prohibe. El mecanismo que ensena (extender para estrechar una
clave HTML) es correcto y sobrevive; solo cambia el ejemplo.
- `active-architecture.md:416` no listaba `lang` en la proyeccion, contra
`contracts.ts` y contra su propio test de frontera. Igual `prefs/README.md`.
Y no lo mencionaba en absoluto: el glosario (`activeDir`, `resolvedDir`,
`data-dir`, RTL-1, el escape `rtl-physical:`), `eidos.md` (RTL-1 es el TERCER
guard de deriva y faltaba junto al builder tipado y eidos-lint), `soma.md`,
`building-a-component.md` (§Known traps es exactamente donde va «la prop mueve la
matematica y deja la pintura atras»), y la tabla de atributos de la arquitectura,
que afirma «todo lo que viaja entre capas viaja por atributos» y omitia `dir`.
El checklist gana cuatro reglas y el guard que faltaba (`rtl:check` no estaba en
la matriz de aceptacion pese a que la tabla canon lo nombra), mas un recorte en
E-3.5: «todas las props visuales mapean a `data-{prop}`» leia como mandato de
estampar `data-dir`, que `:dir()` no puede ver.
DIVERGENCIA DECLARADA (§7): la familia `chart` no tiene prop ni provider y
resuelve leyendo `getComputedStyle(node).direction`. Es el unico sitio del
catalogo que hace lo que §1 prohibe. Se registra en vez de esconderse.
Tres verificadores adversariales sobre el barrido; sus hallazgos, corregidos:
RTL-1 estaba anunciado como guard de TODO el capitulo (solo cubre §4) · «nunca
lee al padre» era absoluto y borraba la composicion sancionada en el punto de
llamada · el estampado se afirmaba incondicional en un sitio y condicional en
otro · las cuatro filas nuevas usaban una aplicabilidad que ni la leyenda ni
`component-audit.ts` conocen.
`docs:check` 0 errores sobre 564 docs. `check` 77 = la linea base.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Components with arrow-key navigation MUST check `dir` and swap left/right keys. That `dir` is the provider's `resolvedDir` — the resolved end of the chain, never a DOM read ([`canon/direction-contract.md`](../canon/direction-contract.md)). Use `getDirectionalKeys(dir, orientation)` .
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
```ts
const { nextKey, prevKey } = getDirectionalKeys(dir, orientation);
```
Do NOT hardcode `KEYS.ARROW_LEFT` / `KEYS.ARROW_RIGHT` for directional navigation.
### A13. Form components need hidden inputs
Components that participate in forms (Checkbox, RadioGroup, Switch, Select, TagsInput, NumberField) should render a hidden `<input>` with the current value and `name` prop. The `name` from a parent Group must propagate to children.
### A14. Roving tabindex: exactly one item gets tabindex=0
In roving tabindex patterns (RadioGroup, Toolbar, Tabs, ToggleGroup), exactly one item must have `tabindex=0` at all times — either the focused/selected item, or the first item when nothing is selected. Never all `-1` (group unreachable) and never multiple `0` (breaks single-tab-stop pattern).
### A15. Gesture layer integration
Components with drag behavior (Drawer, Slider, Splitter, ScrollArea, Toast) use the Gesture layer from `layers/gesture/` . Three specializations:
- `Gesture.base()` — pointer tracking + axis lock + velocity (Slider, ScrollArea)
- `Gesture.drag()` — base + progress + snap points + dismiss (Drawer, Toast)
- `Gesture.resize()` — base + delta + min/max constraints (Splitter)
Rules:
- `setPointerCapture` deferred until moveBuffer exceeded for containers with child buttons (Drawer, Slider). **Exception** : pure drag handles (Splitter resize trigger) capture immediately — the handle IS the drag target, no children to protect
- Gesture `.props` (only `onpointerdown` ) must be spread into the component's `props`
- Gesture cleanup on unmount: `$effect(() => { return () => { this.gesture.cancel(); }; })`
feat(theming)!: SS16 - el guard vive donde NACE el valor, y el velo del drawer vuelve a pintar
Firma SS16, quinta aplicacion de la doctrina "la capa sostiene la pluma, no es
la dueña". El censo, el ledger y la ley del espacio cerrado auditaban SOLO
quien LEE (recetas de eidos); quien ESCRIBE la custom property por instancia
vive en soma/arts y ningun instrumento del eje lo habia enumerado jamas.
EL BUG VIVO QUE ESTO DESTAPO — y no se parcheo, se diagnostico
--drawer-overlay-opacity era DOS especies bajo un nombre: el knob de tema (el
contrato dice 62%) y el canal del arrastre (0..1). soma escribia el canal SIN
UNIDAD sobre el nombre del knob; color-mix() exige porcentaje, la funcion caia
invalida y el velo computaba rgba(0,0,0,0): NO PINTABA. Y el inline dejaba el
knob inalcanzable para cualquier tema. Se separan: el publico se queda como
knob (nadie lo escribe en runtime), el arrastre pasa a
--_drawer-overlay-progress (sin unidad, que para un multiplicador es lo
correcto) y la receta los COMPONE - calc(knob * progress) -, asi que el
arrastre ATENUA el tema en vez de destruirlo.
Medido en Chrome: reposo 0.408471 (antes rgba 0,0,0,0) - el tema LLEGA (20%
-> 0.1318, 62% -> 0.4085, 100% -> 0.6588; antes ninguno movia nada) - el
arrastre sigue (Escape real -> alpha 0; 0/0.25/0.5/1 lineal) - control
negativo: reinyectando la escritura vieja el velo vuelve a caer, o sea que la
unidad era el SINTOMA y las dos especies la enfermedad.
⚠ Estaba adjudicado EN PROSA en theming-sentinel-exceptions.ts:815 desde hacia
dias: la excepcion se trago el bug.
LOS 18 NOMBRES A SU SITIO
Clase B (11 en command/dialog/scroll-area/toast + 4 del drawer): forma publica
sin contrato y sin UN SOLO lector en el repo - una API publicada que el
framework no consume. A --_{c}-*, y los README de soma dejan de enseñarlos
como API del consumidor: ahora enseñan LA PARTE, con la formulacion verbatim
que SS15 ya habia verificado. Clase D (tree-view, gradient-picker): tenian
lector, verificados en navegador. --scrollbar-width NO se renombro por
inercia: es una escritura sobre el unico <body>, la doctrina no le llega, y
queda REGISTRADA con su razon en vez de inventarle un dueño.
LA AGUJA - src/uix/value-channels.test.ts (fichero propio, 6 tests)
Vitest y no script, porque el gate termina en la suite y eso es lo que
convierte la doctrina en ley. Deriva el vocabulario de sistema RESTANDO el
contrato a lo que emite el generador (lista derivada, nunca a mano). Barre
NUEVE raices - las siete nuevas verificadas a cero ANTES de asertarlas - y el
quinto test asserta que cada raiz declarada se anduvo de verdad: una raiz que
resuelve a cero ficheros es la puerta que nadie habria visto.
Dos correcciones al dimensionado, por medida: el arbol tiene SEIS formas de
escribir, no cuatro, y una de las que faltaban era LA CANONICA (la
--_${component}-... que SS14 y SS15 firmaron) - un guard ciego a ella habria
dado verde sobre su propio destino. Y un barrido mas ancho marcaba en rojo un
anchor-name, que en gramatica es identico a un nombre de propiedad: probado y
REVERTIDO. El instrumento miente primero.
Mutaciones: cinco, con el arbol byte a byte identico. Incluyen las dos que
prueban lo que las correcciones añaden (una clave _ DEL contrato pasa de verde
a rojo; un --_ legitimo en blocks pasa de rojo a verde).
LO QUE EL ADVERSARIAL CORRIGIO DE MI PROPIA LEY
Dictamen: "es LEY sobre la mitad que barre, y sigue siendo PROSA sobre el
absoluto que enuncia". Cierto: decia "toda escritura por instancia" y la aguja
no mira eidos, donde viven ONCE escrituras de nombres contratados. Corregido -
el enunciado nombra sus nueve raices y DECLARA sus dos fronteras (eidos, con
su expediente abierto; web/routes, congelado); las salidas son CUATRO y no
tres (la cuarta, sistema, es verde); y el registro se queda con cuatro campos
por entrada (since, reason, destination, heldBecause) con un test que exige
los cuatro - la diferencia entre un registro y un cajon.
Y la relectura con ojo de abogado encontro DOS absolutos mas, en direccion
contraria, escritos bajo "WHAT THE GUARD DOES NOT CHECK": "por instancia" no
es decidible estaticamente (el antecedente del guard es MAS ANCHO que el de la
doctrina), y el {c} de --_{c}-* no lo comprueba nadie, solo el guion bajo.
SS18 ABIERTO, y es la respuesta medida a "¿puede volver a nacer un nombre sin
dueño sin que nadie se entere?": SI, desde eidos. 11 contratadas + 18 sin
dueño, partidas en dos especies (la fundacion generando su vocabulario, que es
legitima, y las props ergonomicas del consumidor, que no es lo mismo).
dialog-overlay-opacity es el GEMELO EXACTO del velo del drawer y sigue vivo.
No es "añadir el root": distinguir las dos especies EXIGE FIRMA.
Guards: value-channels 6/6 - recipe-css-contract + reach-floor + generated-css
55/55 - docs:check 0/0 sobre 819 - --debt 1148/0/0 - tsc 0 propios - prettier
limpio.
BREAKING: los 18 nombres publicos ya no existen; el canal se lee --_{c}-* y
sigue sin ser de nadie para fijarlo. Y consumir un --{c}-* del contrato desde
un provider es ROJO desde hoy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
- CSS vars (`--_drawer-progress`, `--_drawer-offset-x/y` ) set by provider for visual layer
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- During active drag: `transition: none` + inline `transform` for immediate feedback
- Scroll-drag guard: disable gesture via `enabled` , not by suppressing callback
- The gesture layer measures — the component decides what it means (dismiss, value, resize)
### A16. Non-modal overlay components
Components with `modal` prop (Dialog, Drawer) must adjust behavior when `modal=false` :
- **FocusScope**: `trap=false` , `enabled=false` — background must stay interactive
- **ScrollLock**: disabled — background scrolling must work
- **Dismissal**: disabled — no interactOutside, no focusOutside
- **Escape**: handled via `onkeydown` on the content element, not via Dismissal layer
- **Auto-focus**: prevented (`e.preventDefault()` in `onOpenAutoFocus` )
- **Content**: needs `tabindex=-1` to receive keyboard events without focus trap
- **Overlay**: not rendered (consumer should not include `<Overlay>` for non-modal)
- **Focus return**: non-modal close must return focus to trigger manually (FocusScope doesn't handle it)
### A17. Focus strategy: virtual vs DOM
Two focus strategies exist. Choose based on component type:
- **`aria-activedescendant` (virtual focus)**: focus stays on trigger/input, items highlighted via CSS `[data-highlighted]` . Used for **Select** , **Combobox** — the trigger owns keyboard, items are options.
- **Roving tabindex (DOM focus)**: items receive real DOM focus. Used for **DropdownMenu** , **RadioGroup** , **Toolbar** , **Tabs** — items are independent interactive elements.
Never mix both in the same component. If the trigger has `aria-activedescendant` , items must NOT call `.focus()` .
### A18. Registry pattern over DOM queries
Prefer registering sub-parts in a Map on mount/unmount over `querySelectorAll` for keyboard navigation:
```ts
// In root provider:
private triggerRegistry = new Map< number , HTMLElement > ();
registerTrigger(index: number, el: HTMLElement) { this.triggerRegistry.set(index, el); }
unregisterTrigger(index: number) { this.triggerRegistry.delete(index); }
getRegisteredTriggers(): HTMLElement[] {
return [...this.triggerRegistry.entries()]
.sort((a, b) => a[0] - b[0])
.map(([, el]) => el)
.filter(el => el.getAttribute('aria-disabled') !== 'true');
}
// In sub-part constructor:
$effect(() => {
const el = opts.ref.current;
if (el) this.provider.registerTrigger(this.index, el);
return () => this.provider.unregisterTrigger(this.index);
});
```
Benefits: no DOM queries, works with portaled/lazy-mounted elements, O(1) lookup.
Same pattern for value-to-label registries (Select, Combobox):
```ts
private labelRegistry = new Map< string , string > ();
registerLabel(value: string, label: string) { this.labelRegistry.set(value, label); }
```
### A19. SafePolygon for hover-gap components
Components where pointer must traverse a gap between trigger and content (DropdownMenu submenus, Tooltip) integrate `SafePolygon` from `layers/floating/safe-polygon` :
```ts
import { SafePolygon } from '../../layers/floating/safe-polygon';
// In the provider that owns both trigger and content refs:
new SafePolygon({
enabled: () => opts.open.current,
triggerNode: () => this.triggerRef.current,
contentNode: () => this.contentRef.current,
onPointerExit: () => this.handleClose(),
buffer: 2,
transitIntentTimeout: 300
});
```
SafePolygon calculates a corridor polygon between trigger and content. The pointer can traverse the gap without closing. `onPointerExit` fires only when the pointer leaves the safe zone.
### A20. Exit animation via Presence
Components that dismiss/remove elements (Toast, Drawer) must integrate `Presence` for exit animations:
1. `dismiss()` marks the element as dismissing (state change, not removal)
2. `data-state` transitions from `'open'` to `'closed'`
3. Presence emits `data-ending-style` for CSS exit animation
4. Animation completes → Presence fires `onComplete(false)` → element removed from array
```ts
// In Toaster:
dismiss(id) { this.toasts = this.toasts.map(t => t.id === id ? { ...t, dismissing: true } : t); }
remove(id) { this.toasts = this.toasts.filter(t => t.id !== id); /* + onDismiss callback */ }
// In provider:
this.presence = new Presence({
open: readableActive(() => this.isOpen),
ref: opts.ref,
onComplete: (open) => { if (!open) this.provider.toaster.remove(id); }
});
```
### A21. Contract case normalization
`registerMorfo()` and `assertContract()` normalize names to lowercase. Provider code should use morfo kebabs (`'provider'`, `'trigger'` , `'content'` ) when calling `runtime.part(...)` ; component names in contracts remain normalized internally. No manual case matching needed.
### A22. Dismissal isValidEvent for complex widgets
Components with multiple interactive zones (Combobox, Select) must exclude their own elements from interact-outside detection. The `isValidEvent` callback should return `false` for clicks on trigger, input, and content:
```ts
isValidEvent: readableActive(() => (e: PointerEvent | FocusEvent) => {
const target = e.target;
if (!(target instanceof Node)) return true;
if (inputEl?.contains(target)) return false;
if (triggerEl?.contains(target)) return false;
if (contentEl?.contains(target)) return false;
return true;
});
```
Without this, clicking scrollbars inside the content, or clicking the trigger to close, triggers interact-outside and causes race conditions.
### A23. Date/time utilities — never re-implement, extend `dias`
The canonical date library is `$libs/days` . Soma consumes it directly via the alias — **no Soma façade** . Before porting any date helper or writing a new one:
1. Read `$libs/days/*.ts` (types, queries, operations, parse, format, segments) fully.
2. If the helper already exists in days → import it via `$libs/days` . Never duplicate.
3. If it is missing **and** reusable outside soma (pure, no DOM, no KEYS/Svelte deps) → add it to days. Don't proxy.
4. Only when the helper is UI-specific (DOM navigation, KEYS-based predicates, screen-reader announcer, segment UI-state shapes with `hasLeftFocus` /`lastKeyZero`) does it live in `soma/datetime/` .
**Never** create a soma module whose only job is to re-export days symbols — consumers import from `$libs/days` directly. Dead re-export façades hide the real dependency.
### A24. Readonly segments without a concrete value must log a warning
`readonlySegments` (or `startReadonlySegments` /`endReadonlySegments` in range components) fixes specific segments so the user cannot change them. The lock needs a concrete anchor:
- **Valid**: `value` is set → locked segments preserve their values from `value` .
- **Invalid**: `value` is `undefined` → the lock falls back to `placeholder` (empty-state display), which is not a commitment. Log a warning:
```ts
this.soma?.logger.warn(
'{Name}Field',
'`readonlySegments` is set but `value` is undefined — lock has no concrete anchor; falling back to placeholder. Supply an initial `value` so the locked segment has a defined meaning.',
{ readonlySegments: [...segs] }
);
```
Guard against spam: track the last warned segment set and only re-warn when it changes, reset when the config becomes valid.
### A25. Range components split readonly per-endpoint
`DateRangeField` , `DateRangePicker` (and future `TimeRangeField` ) expose two lists:
- `startReadonlySegments?: EditableTimeSegmentPart[] | EditableSegmentPart[]` — locks segments on the start input.
- `endReadonlySegments?: ...` — locks segments on the end input.
A single `readonlySegments` applied symmetrically is wrong because the user may legitimately want one endpoint fixed (e.g., start's year) while the other remains editable.
For range pickers whose calendar is shared between endpoints: the calendar's navigation for a segment is blocked **only** when **both** endpoints have that segment in their readonly list. Blocking when only one side is locked would prevent navigating to pick the other endpoint's value. Per-cell selection constraints are not applied from outside because `RangeCalendar` picks the endpoint based on its own anchor state.
### A26. Block direct contenteditable mutations with `onbeforeinput`
Segmented inputs (DateField, TimeField) use `contenteditable="true"` to get `role="spinbutton"` keyboard behaviour. The contenteditable surface must **never** accept direct mutations — all content is driven by the provider's `segmentValues` :
```ts
readonly sharedSegmentAttrs = {
// …
onbeforeinput: (e: Event) => e.preventDefault()
};
```
`keydown.preventDefault()` alone is not enough: IME/composition paths, paste, drag-and-drop, and mobile autocomplete bypass keydown. `beforeinput` fires before the browser mutates the element and blocks every insertion path in one line.
### A27. Picker composition pattern (shared state)
Pickers (`DatePicker`, `DateRangePicker` , `TimePicker` , future `TimeRangePicker` ) compose `Popover` + one of (`DateField`/`DateRangeField`/`TimeField`) + one of (`Calendar`/`RangeCalendar`/slider group). The picker's own Provider owns the shared reactive state, and the root wrapper creates **three** providers pointing at the same `writableActive` refs:
```ts
// Root wrapper script
const sharedValue = writableActive(/* getter */, /* setter */);
const sharedPlaceholder = writableActive(…);
const sharedOpen = writableActive(…);
{Name}PickerProvider.create({ value: sharedValue, placeholder: sharedPlaceholder, open: sharedOpen, …config });
PopoverProvider.create({ open: sharedOpen, … });
{Base}FieldProvider.create({ value: sharedValue, placeholder: sharedPlaceholder, …config });
// Calendar/RangeCalendar/slider providers are created inside their own wrapper
// (DatePicker.Calendar, TimePicker.HourSlider, …) reading from the picker context.
```
**Parts**: unique wrappers for `Provider` , `Trigger` , and the calendar/slider bridge. Everything else re-exports from the composed components — their native data-attrs (`data-popover-*`, `data-date-field-*` , `data-calendar-*` , `data-slider-*` ) remain authoritative for styling. The picker only adds identity attrs (`data-{picker}-trigger`, `data-{picker}-calendar` ) on the unique wrappers.
**Auto-close / auto-anchor**: the picker's Provider exposes a `handleSelect()` method that the calendar wrapper calls when a selection completes. Range pickers also re-anchor the placeholder so the end month lands in the rightmost visible slot — the user sees their selection, not the month they last scrolled past.
### A28. Time placeholders are `hh`/`mm`/`ss`, not `--`
Dias' `getPlaceholder('hour'|'minute'|'second', …)` returns `'– – '` (two en-dashes). Unreadable in most fonts and not self-describing. `dias/segments.ts` exposes `createSegmentContent` / `createTimeSegmentContent` which use a local `getSegmentPlaceholder` that returns `'hh'` / `'mm'` / `'ss'` for time parts (while delegating to dias for date parts). Time-segmented components automatically benefit — no extra code needed.
### A29. Demo pages are interactive testbeds
Every soma component demo at `web/routes/uix/components/{name}` must expose **every public prop** of the Provider as a live control plus a Field-integration section when applicable. Not six static code snippets. Required coverage:
1. Each boolean → switch/checkbox. Each enum → radio or chip group. Each number → input. Arrays (e.g. `readonlySegments` ) → one toggle per valid value.
2. All format/locale/direction variants switchable (granularity, hourCycle, locale, dateOrder, dir).
3. Field integration section with toggles for parent `Field` 's `disabled` /`readonly`/`required`/`invalid` to verify inheritance.
4. Live state readout — bindable value + placeholder + last `onInvalid` message visible.
5. Edge cases — empty value, readonly-without-value (triggers A24 warning), disabled, required, form submission.
The demo page is how a consumer evaluates the component; a gallery of canned examples does not satisfy that.
### A30. Register child ids with direct assignment — never `$effect`
A child provider that publishes its `id` to a parent provider's state (typical for `Field.inputId` , `Dialog.triggerId` , group `labelId` , etc.) assigns **directly in the constructor** . Wrapping the write in `$effect` creates a reactive edge child → parent that can loop when any downstream consumer feeds back into the child's derivations — the page appears "frozen" / "bloqueada" on mount.
```ts
// Wrong — $effect tracks opts.id + writes parent state; any downstream
// chain that reads back into this child loops through Svelte's scheduler.
$effect(() => {
if (this.field) this.field.inputId.current = opts.id.current;
});
// Right — one-shot assignment at construction, matching Dialog, Combobox,
// Command, NumberField, DateField, TimeField, ColorField.
if (this.field) this.field.inputId.current = opts.id.current;
```
The rule is specifically for **boilerplate identity writes** (id, labelId, triggerId, contentId, descriptionId). Genuine side effects that must react to dep changes — DOM observers, timers, external subscriptions — still use `$effect` . `ids` almost never change after mount; there's nothing to react to.
**How to recognise a violation:** grep the provider file for `$effect` blocks whose body writes to a parent's `Id.current` / `labelId.current` / similar bookkeeping. Replace with a direct assignment after `Provider.require()` or after the parent reference is captured.
Incident: Listbox and PinInput both shipped with `$effect` wrappers for id registration. Listbox froze the page on mount.
### A31. Per-entity `$derived` must NOT read global state through the provider
When each item / row / cell owns a `$derived` that calls a provider method which reads a shared `$state` (selection array, items registry, version counter, expanded map), every mutation of that shared state invalidates **every** entity's derivation — and each re-runs the provider method. Classic O(N²) cascade. Works with 1– 5 items; hangs at 30+.
```ts
// Wrong — O(N²): value change invalidates N isRovingTarget derivations,
// each re-runs a full DOM query + Set construction.
readonly isRovingTarget = $derived.by(() => {
return this.provider.rovingTarget() === this.opts.ref.current;
});
rovingTarget(): HTMLElement | undefined {
const selected = new Set(this.opts.value.current);
return this.getItems().find((el) => selected.has(el.dataset.value)) ?? this.getItems()[0];
}
// Right — O(N): lift the expensive computation to a single $derived on
// the provider. Per-entity derivations only pointer-compare.
// Provider:
readonly rovingTargetEl = $derived.by(() => {
const items = this.getItems();
if (items.length === 0) return undefined;
const selected = new Set(this.opts.value.current);
return items.find((el) => selected.has(el.dataset.value)) ?? items[0];
});
// Item:
readonly isRovingTarget = $derived.by(() => {
return this.opts.ref.current === this.provider.rovingTargetEl;
});
```
**Patterns that commonly hit this:**
- `provider.isVisible(value)` / `provider.isSelected(value)` / `provider.isExpanded(id)` called from N per-item derivations → lift a `visibleSet: Set<string>` / `selectedSet` / `expandedSet` on the provider.
- `provider.getItems()` (DOM query or registry read) called from N per-item derivations → lift `rovingTargetEl` / `firstVisibleIndex` / whatever the real answer is to a single provider derived.
**Recognise it:** works with a handful of entities, freezes with a larger list. `isX` method called from N derivations is the signature. Fix before shipping — do not mask with `untrack` , microtask batching, or version-counter reads.
Incidents: Command component (2026-04-17, external diagnosis required), Listbox rovingTarget (2026-04-18).
### A32. Explicit gap sign-off — the user approves scope, the programmer doesn't
The comparison table (`## Comparison` in every component README) is a **contract** , not a footnote. Before saying "component done":
1. **Fill the table** — every feature that at least one of Radix / Ark / bits / React Aria implements is a row. Mark each cell `✅` / `⚠️` / `❌` — don't omit rows to hide a gap.
2. **Present the table in the conversation** — paste the rows where at least one `⚠️` or `❌` exists (or the full table) into the reply that finalises the component. The user sees the gaps before approving.
3. **Decide each `❌` / `⚠️` explicitly** — for every non-`✅`, the user approves one of:
- **Implement now** — the gap is strategic or blocks a WAI-ARIA / reference expectation. Bring it into scope and finish the component with the feature.
- **Defer to v2** — the gap exists but isn't blocking. Add it to the component's `## Out of scope (v2 roadmap)` section with: what it is, reference libraries that ship it, why deferred, cost estimate in lines. This becomes the PR backlog.
- **Drop** — the feature isn't a real gap for Soma (e.g. a competitor's framework-specific quirk, or something Eidos should own). Document the reasoning and remove the row from the table.
4. **No silent gaps** — if a feature appears only as a footnote and nowhere else, that's a failure mode. The reader of the README should see `❌` and know it's a deliberate decision.
**Why this exists:** during the 2026-04-19 session, AlertDialog / Listbox / Carousel / NavigationMenu all shipped with strategic gaps (Escape default, range-select, multi-slide, Viewport, Sub, data-motion, skipDelayDuration) hidden inside comparison tables the user never saw in conversation. AlertDialog in particular inherited Dialog's `escapeKeydownBehavior='ignore'` default — a WAI-ARIA regression disguised as a `⚠️` row. The rule is: if the gap isn't argued explicitly, it doesn't get to ship.
**How to apply:** the checklist items 34– 35 are the mechanism. Item 34 says "present the table in the conversation and get sign-off"; item 35 says "deferred features become their own README section, not a table footnote".
### A33. Reactive collections: `SvelteMap` / `SvelteSet`, not `$state(new Map())`
In Svelte 5 runes, plain `Map` / `Set` are **not** deeply reactive. `$state(new Map())` only tracks **reassignment** of the field — writing to the map via `.set(k, v)` / `.delete(k)` does not notify readers of `.get(k)` , `.has(k)` , `.size` , or iteration.
Use `SvelteMap` / `SvelteSet` from `'svelte/reactivity'` when:
- Readers index **per-entry** (`.get(k)`, `.has(k)` , iteration, `.size` ) inside a `$derived` , `$effect` , or template expression.
- Mutations happen via `.set(k, v)` / `.delete(k)` on the existing collection (the common ergonomic case).
- You want **per-entry invalidation** — changing key `A` shouldn't invalidate readers of key `B` .
```ts
// ❌ Wrong — .set() updates don't propagate to readers of .get() in $derived.
import { ... } from '...';
class ExampleProvider {
private cache = $state(new Map< string , number > ());
measure(k: string, v: number) {
this.cache.set(k, v); // silently non-reactive
}
readonly computed = $derived.by(() => this.cache.get('x') ?? 0);
// ^^^^^ never re-runs after .set()
}
// ✅ Right — per-entry reactive, clean .set().
import { SvelteMap } from 'svelte/reactivity';
class ExampleProvider {
private cache = new SvelteMap< string , number > ();
measure(k: string, v: number) {
this.cache.set(k, v); // notifies readers of .get(k) etc.
}
readonly computed = $derived.by(() => this.cache.get('x') ?? 0);
// re-runs when .set('x', ...) fires
}
```
**The "reassign-the-whole-Map" workaround** — some code does this to force reactivity with plain `$state(Map)` :
```ts
// Works but fragile:
registerLabel(value: string, label: string) {
const next = new Map(this.labels);
next.set(value, label);
this.labels = next; // field reassignment triggers tracking
}
```
This is O(N) per mutation (copies the whole Map), looks like a bug to future readers ("why clone?"), and breaks silently if anyone refactors to `this.labels.set(...)` direct. `SvelteMap` removes both problems — `.set` is reactive and O(1).
**Detection:** grep for `$state(new Map` / `$state(new Set` in the providers folder. For each hit, audit: are readers using `.get` / `.has` / `.size` / iteration inside `$derived` ? If yes, migrate to `SvelteMap` /`SvelteSet`. The reassignment-clone workaround should be rewritten too.
**Incidents:**
- VirtualList dynamic heights (2026-04-19) — `ResizeObserver` wrote sizes to `$state(Map)` cache, `offsets` derived read `.get(key)` and never re-ran. Every row stayed at the 60 px estimate.
- Form `touched` + `registry` Maps — `setFieldTouched` / `registerField` use `.set` direct, but `isTouched` / `isDirty` / `firstInvalidField` derivations read `.values()` / `.keys()` / `.has()` . Same bug, harder to notice because `values` + `errors` state cover most user-visible flows.
- Combobox `labelRegistry` — used the "clone-and-reassign" workaround. Works today but fragile.
### A34. Verification before "done": translation namespace + DOM topology + smoke
Three classes of bug cannot be caught by `svelte-check` or HTTP 200 — they all require either a runtime grep or a real browser. They must be run every time a component, demo, or lang entry is touched.
1. **Translation namespace grep.** soma's namespace is `components.{kebab-name}.*` (or `common.*` for shared strings). Any `soma.…` or other prefix inside a quoted translation path is a bug that logs `[langs] Translation key not found` at runtime. Check with:
```sh
grep -rn "['\"]soma\.[a-z-]" src --include=!*.md
```
Fixes: route component-owned text through `morfo.texts` + `v.translationRef` ; route shared text through `v.commonRef` or an explicit idlangref constant. Don't build translation paths via template strings in demos or providers. If a demo needs a dynamic path helper (like `tt('columns', 'Columns')` ), hard-code the namespace prefix `components.{name}.` correctly.
2. **DOM topology vs `.require()` audit.** Svelte's context (via `getContext` ) flows only to descendants. Every `X.require()` call must be reachable from a descendant of the component that set the context. The trap is HTML: `<tr>` cannot nest `<tr>` , so `Table.RowDetail` (rendered as a sibling `<tr>` of `Table.Row` ) cannot `TableRowProvider.require()` . Use one of:
- Receive the object via a prop (consumer passes `{row}` or similar explicitly). This is consistent with `<Table.Row {row}>` / `<Table.Cell {cell}>` — Table already requires explicit objects.
- Use `.get()` + a fallback for truly optional context (e.g. `FeedProvider.get()` inside `Feed.Sentinel` , which can live outside a Feed).
- Restructure so the child actually lives inside the parent's subtree.
Svelte-check never catches this — the error is thrown on mount. Smoke catches it.
3. ** `npm run smoke` .** The smoke script (`scripts/smoke-check.mjs`) walks every
concrete `+page.svelte` route under `web/routes` with Playwright and surfaces:
- `pageerror` (uncaught throw during hydration — e.g. `Context "X" not found` )
- `console.error` (runtime exceptions caught by the framework)
- same-origin request failures
- Translation key missing warnings
- `[soma]` context-not-found warnings
- rendered `__uix_lang_missing__` fallback markers
It waits for `domcontentloaded` plus a short settle instead of
`networkidle` , because icon/gallery-heavy docs pages can keep network work
alive without being broken. Run it before declaring a component done.
Regressions in unrelated components caused by lang-table edits or core changes surface here too. `npm run smoke` requires `npm run dev` running in another terminal and auto-detects the port on 5173– 5180.
Use `SMOKE_SCOPE=/uix npm run smoke` when you only need the UIX shell.
**Incidents:**
- Table demo (2026-04-19) — `tt('columns')` built `soma.table.columns` instead of `components.table.columns` . Dozens of `Translation key not found` logs, silent in svelte-check.
- Pagination item `aria-label` (pre-existing) — provider called `langs.t('soma.pagination.page')` directly. Same class of bug; fixed by migrating to an idlangref constant (`PAGINATION_LANGS.PAGE`).
- `Table.RowDetail` (2026-04-19) — first version called `TableRowProvider.require()` . Threw `Context "TableRow" not found` because `<tr>` cannot nest and the Detail is a DOM sibling, not descendant. Fixed by taking `{row}` as prop + deriving the `aria-controls` id deterministically from `row.id` .
### A35. `$effect` reading `ref.current` + writing provider state is a loop trap
A30 forbids using `$effect` for _id registration_ . A35 extends the ban to **any** per-item `$effect` that reads `opts.ref.current` (or similar reactive input) and writes to provider state that the per-item `props` $derived reads back through the attachment system.
**Recognise the shape:**
```ts
// ❌ Wrong — mounts the component and immediately loops.
$effect(() => {
void opts.ref.current; // tracked
void opts.disabled.current; // tracked
this.provider.notifyItemsChanged(); // writes itemsVersion
return () => this.provider.notifyItemsChanged();
});
// Provider:
readonly firstTabStop = $derived.by(() => {
void this.itemsVersion; // tracks the counter
return this.getItems()[0];
});
// Item props:
tabindex: this.provider.isTabStop(this.opts.ref.current) ? 0 : -1
// isTabStop reads firstTabStop → tabindex depends on itemsVersion
```
**Why it loops:** the item `$effect` writes `itemsVersion` → invalidates `firstTabStop` → invalidates every item's `props` $derived → Svelte re-spreads `{...mergedProps}` including the ref attachment → attachment re-runs → `ref.current = node` (same node, but the internal write still notifies tracked subscribers) → item `$effect` re-runs → back to step 1. Svelte terminates with`effect_update_depth_exceeded`.
**Fix:**
- **Don't use `$effect` with reactive deps to notify the provider.** Register in the constructor (A30 pattern) or via explicit method calls from handlers. `$effect` is for the cleanup function only: `$effect(() => () => provider.unregister(...))` .
- When a per-item `$derived` needs to consult the full item set (e.g. "am I the first tab stop?"), wrap the set-walking read in `untrack(...)` so the derivation depends only on the single reactive field it actually cares about (`lastFocusedElement`), not on every sibling's ref or a shared counter.
```ts
// ✅ Right — no counter, no feedback edge.
isTabStop(el: HTMLElement | null): boolean {
if (!el) return false;
if (this.lastFocusedElement) return this.lastFocusedElement === el;
return untrack(() => this.getItems()[0] === el);
}
```
**Recognise it:** demo page freezes or logs `effect_update_depth_exceeded` on mount. The stacktrace names the per-item `$effect` and the provider setter it calls (e.g. `set itemsVersion` ). `npm run smoke` passes (HTTP 200) but `scripts/morfo-check.ts` fails with `page.$$eval: Execution context was destroyed, most likely because of a navigation` — Playwright sees the page's error handler trip and the document effectively dies mid-query.
**Incident:** Toolbar (2026-04-19) — Button / Link / GroupItem each carried a mount `$effect` that called `notifyItemsChanged()` ; `firstTabStop` $derived read `itemsVersion` ; per-item `props` read `firstTabStop` via `isTabStop` . Loop tripped on every page load, hiding behind an `ERROR toolbar ... Execution context destroyed` in `morfo:check` (not obviously a reactivity bug until probed in the browser console). Fix: removed the counter + three effects; `isTabStop` uses `untrack` .
### A36. Async side-effect + reactive read-back = microtask-mediated loop
A35 covers **synchronous** `$effect` feedback cycles. A36 covers the
**asynchronous** variant — the one Svelte's effect-depth guard does NOT
catch because each re-entry happens in a separate scheduler tick, mediated
by the microtask queue.
**Shape:**
```ts
// ❌ Wrong — microtask loop.
$effect(() => {
JSON.stringify(values); // tracks values
const res = schema['~standard'].validate(values);
if (isPromiseLike(res)) {
// Fires later in a microtask:
res.then((r) => {
errors = groupIssues(r.issues); // writes errors
issues = groupIssues(r.issues); // writes issues
});
// Synchronous fallback value — reads errors/issues REACTIVELY:
return { errors, issues }; // ← tracks errors + issues
}
return res.issues ? groupIssues(res.issues) : { errors: {}, issues: {} };
});
```
**Why it loops:**
1. Effect runs. `JSON.stringify(values)` tracks `values` . The fallback
`return { errors, issues }` tracks `errors` and `issues` as deps.
2. `.then(...)` is scheduled as a microtask.
3. Effect returns.
4. Microtask fires: writes `errors = …` and `issues = …` .
5. Those writes invalidate the effect (it depends on `errors` /`issues`).
6. Effect re-runs. New `.then` scheduled. Goto 4.
Each iteration enqueues another microtask. The microtask queue starves the
event loop — the tab freezes. **No `effect_update_depth_exceeded` fires**
because the guard only counts depth inside a single synchronous tick.
**Fix:** wrap the reactive read in `untrack` so the effect doesn't
subscribe to the state the async callback writes.
```ts
// ✅ Right — `untrack` breaks the feedback edge.
if (isPromiseLike(res)) {
res.then((r) => {
errors = groupIssues(r.issues);
issues = groupIssues(r.issues);
});
return untrack(() => ({ errors, issues }));
}
```
**Recognise it:**
- **Browser tab freezes on mount** of a specific component variant. No
Svelte error in the console.
- `npm run smoke` **passes** because its 500 ms post-load settle is
shorter than the microtask storm's ramp-up.
- `npm run morfo:check` may pass too — the DOM exists, validation just
never reaches a steady state.
- Bisect by stripping the effect body to `read + empty-write → add
validate alone → add the real writes back`. The combination where the
async helper reads state the effect writes is the trigger.
Why this is especially sneaky with **Standard Schema v1** adapters: some
libraries (sium included) declare their adapter's `validate` as
`async (...)` unconditionally, so the `Promise` branch fires even for
schemas whose underlying validation is synchronous. The soma `Form` has
to live with that until the adapter exposes a sync path — `untrack`
around the fallback is the durable fix.
**Incident:** Form `onChange` / `onBlur` hang (2026-04-21) — kitchen-sink
at `/test/sium/kitchen-sink` froze on mount with a 12-field nested schema
because `runValidate` 's fallback read `errors` /`issues` reactively inside
the validation `$effect` . Fixed in `form-core.svelte.ts` by wrapping the
fallback return in `untrack` . Regression locked by two new tests in
`form-auto-fields.svelte.test.ts` with 5 s vitest timeouts. See
`src/uix/soma/components/form/BUG-onchange-onblur-hang.md` for the full
diagnostic transcript.
### A37. Instrument demos with `data-perm-step` for the permutation runner
Single-state validation (`morfo:check`) passes even when a state transition
would loop or emit an undeclared attr. The permutation runner
(`scripts/permutation-check.ts`) cycles components through their declared
state space and re-validates morfo after every transition. This is the
layer that would have caught the toolbar A35 loop, the form A36 microtask
loop, and the slider RTL transform bug the same day they shipped — each of
them passed `morfo:check` but failed the moment state changed.
Demos opt in by tagging interactive controls with `data-perm-step="N"` :
```svelte
<!-- Open → close → re - open cycle -->
< Dialog.Trigger data-perm-step = "0" data-perm-label = "open via trigger" > Open< / Dialog.Trigger >
{#if open}
< Dialog.Content >
< Dialog.Close data-perm-step = "1" data-perm-label = "close via Close button" > Close< / Dialog.Close >
< / Dialog.Content >
{/if}
```
Extra modifiers:
- `data-perm-mode='key="Escape"'` — dispatch a keydown instead of clicking.
- `data-perm-mode='type="ada@example.com"'` — type a string.
- `data-perm-settle="800"` — longer wait before re-validation (for animations or async validation).
- `data-perm-skip-validate` — click but don't re-validate (intermediate action).
- `data-perm-label="..."` — override the log label.
**What to exercise:**
- **Overlays** (Dialog, Popover, Drawer, Tooltip, NavigationMenu, DropdownMenu, ContextMenu, Menubar) — open / dismiss via every declared path (trigger click, Escape, outside click when applicable).
- **Toggleable items** (Checkbox, Switch, Toggle, ToggleGroup.Item, Tabs.Trigger, RadioGroup.Item, Accordion.Trigger) — click to flip state; for multi-value pickers, advance through at least three values.
- **Composite roving** (Listbox, Menu, Tree, Toolbar) — focus first item, arrow-key to next, arrow-key past the loop boundary.
- **Forms** — empty → invalid input → valid input, to catch any validation-effect loops under change / blur modes.
- **RTL** — if the component has `dir` semantics, include a `data-perm-step` that swaps `dir="rtl"` on the root and validates arrow keys flip.
**What to skip:**
- Alerts / confirmations / anything that triggers `window.alert()` or `window.confirm()` — Playwright hangs on those by default. If the demo has them, use a non-alert callback for the perm-step path.
- File uploads — native file picker is browser-modal and not scriptable from Playwright without `setInputFiles` .
**Pragmatic coverage target:** every component with a non-trivial state
fix(cleanroom): F2+F3+F4-C+SEM-4s1 — lote mecánico, censos con guard, corpus documental y el close polimórfico de los pickers VIVO
F2 — lote mecánico (13 ítems):
- DEP-2 ogl eliminado (0 imports) · DEP-1 clsx inlineado como toClassString
propio + suite de contrato (props.test.ts; soma.md §12 cerrado).
- THM-7: los 5 selectores manuales de sema.md reescritos con semaSelector
(los ejemplos [data-toast-root] apuntaban a un part INEXISTENTE — la
deriva que el builder previene, demostrada en el propio doc).
- MOR-1 escape isomorfo + validación de attr-names en semaSelector + 9
tests (selectors.test.ts, matches() real con comillas/corchetes) ·
MOR-2 partMarkerAttr = única fuente compilador↔builder + test de paridad ·
MOR-3 _resetCompileCache borrado (0 usos).
- SOM-2 keydown continue en match sin handler + keyboardFixtureMorfo ·
SOM-1 no-await de handlers (censo async = 0; contrato V1 cumplido) + pin.
- SEM-2 trigger pre-attacha catch con logger (void trigger sin unhandled
rejection; throw intacto para awaiters) + pin · SEM-3 fallback muerto de
applyDominance → skip defensivo + timer tope de awaitExpression cancelado ·
SEC-1 adjudicado YA implementado (assertCssVariableValue desde 2026-05-11)
+ pin del path de VALOR.
- accordion → outline (§32; su outline:none dejaba CERO anillo en HCM) —
verificado en vivo · THM-6 radius-full 9999px · EID-4 recuentos 33.
F3 — censos con guard:
- SOM-3 cerrado: announcer + image-provider migrados a scheduler-preferred
(consumidores cableados: date/time-field vía soma.uix.timers; avatar/image
vía eidos.timers — verificado en vivo); guard de timers ENSANCHADO de
soma/components a TODO soma y pasado a EVIDENCIA (setTimeout exige
.schedule( en el fichero — layers/ y datetime/ escapaban del ámbito viejo).
- THM-5: R-4.7 nueva (válvula same-line /* important: <razón> */, escaneo
comment-blanked) + las 15 declaraciones anotadas con su razón + canon
recipe-contract §3/§4.
- SOM-4 adjudicado: el censo/guard YA existían (49 pins); knob/mask-field/
timeline pinneados (overrides documentados en call-site); media-player
Batch-4 (35 hits, cero renderProps) = único batch restante, registrado.
- THM-4 doctrinado en eidos.md §unused (comportamiento/composición =
legítimo; deuda = eje visual sin consumidor; hotspots por lotes).
F4-C — corpus documental (decisiones de usuario aplicadas):
- DOC-3: los 15 enlaces muertos resueltos (repoint a la edición FINAL
trackeada / des-link históricos) · docs:check I6-links WARN→ERROR.
- DOC-1: tabla «Build contract» MIGRADA a component-guide con estados
modernizados (A3–A5 → LIVE + guards de hoy); banners reapuntados; citas
de CANON/sema.md historificadas; lápida-redirect en el §13 del fósil.
- DOC-4: hold chain → holds.ts · FAQ event:* SUPERSEDED por signatures ·
gradient añadido a los DOS capstones (sextet real) · nota de paleta de
demo-authoring corregida (universalPaletteDecls + decisión THM-2 =
mecanismo universal como sucesor del tracker borrado).
- DOC-5/6: recuentos anti-frágiles datados · §4.11 dup → §4.12 · Known gaps
historificado · N-6/N-7 recuperadas de git (d68d2c45^) y canonizadas en
eidos.md §pickers · authoring E2 → canon/tsc.md · air-old des-linkado ·
EID-3 (placement) en la fila RTL · AUX-2 disabledDom documentado.
SEM-4 sesión 1 — el close polimórfico de los pickers, VIVO (D.11):
- Reconciliación: los morfos ya no declaran close (delegated al Popover,
de-dialoged 06-27); el agujero real era el cierre programático bypaseando
dismissWith → save/cancel/select eran perceptualmente SILENCIOSOS.
- Fix: PickerShellHandle.setPopoverDismiss + closeWith(cause) en los 5
providers (14 sitios; select/commit → 'save' = commit.save+fulfill,
cancel → 'cancel' = emerge; fallback raw para headless) + UN inyector en
el eidos PickerShell root (norma N-8). Picker genérico fuera a propósito
(ya suena commit-set/cancel por diseño S9).
- Verificado en vivo (date-picker): Done → close·commit·fulfill·active ·
Cancel → close·emerge · cierre real.
Gates: matriz 141/141 (los 6 morfos nuevos de la pista de texto paralela
también PASS) · contracts 38/38 · eidos 314 · sema 178 · morfo 94 ·
docs:check 0/0 con I6 en error · baseline propio 57.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
machine (roughly a third of the catalog's morfos — ≈40 of the then-66 at
the 2026-06 count; see `src/uix/morfo/components/` for today's) should have
≥2 permutation steps. Plain
docs(book): F7.5 (2/2) — COMPONENT_GUIDE moved; guides/ batch COMPLETE
src/uix/soma/COMPONENT_GUIDE.md (1449 L, English body) moved to
docs/guides/component-guide.md. The only Spanish passage — the
archetype-canon banner (2026-06-19) — translated in the move; the rest
is verbatim. Banner's ARCHETYPE_COHERENCE_AUDIT link reaches into src
(audit fossil, stays with the code); the checklist cross-link is now
the sibling ./completion-checklist.md and the 'soma/README s9' mention
now names architecture/soma.md (its post-F7.2 home). Corpus swept:
authoring + README E4 stratum rows (also stale THEMING_GUIDE name),
building-a-component phases 0/2, architecture/{soma-architecture s4/
s13-tree/s17, overview, morfo, active-architecture}.
F7.5 complete: docs/guides/ = component-guide, completion-checklist,
demo-authoring, component-audit. docs:check 0 errors, warns back at
the 11 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
leaf components (Progress, Meter, Announce) don't need any.
Run `npm run perm:check` before shipping a new component; the runner SKIPs
annotations-less demos without failing, so onboarding is incremental.
**Reference:** `src/uix/morfo/PERMUTATION_RUNNER.md` has the full authoring
convention and roadmap (v2 URL-driven states, v3 morfo-inferred cycles,
v4 MutationObserver ordering for Sema).