You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/architecture/soma.md

500 lines
18 KiB

---
title: Soma — the headless behavior layer
type: reference
audience: human + agent
authority: E1 architecture — soma's entry and authoring guide (deep reference stays in SOMA_ARCHITECTURE)
status: current
source: migrated from src/uix/soma/README.md (2026-07-02, docs-book F7.2)
---
# soma
A headless compound-component library for Svelte 5. The behavior layer inside
UIX — it emits the `data-*` and `aria-*` the contract declares in `morfo`,
manages state and events, and delegates the visual to `eidos` through the DOM.
> **API doctrine**: soma keeps the compound shape (`Toggle.Provider`,
> `Tabs.Provider + Tabs.Trigger + ...`) for symmetry with the multi-part
> components. Eidos does not invent a parallel flat API: it applies the visual
> layer over the anatomy declared by morfo and materialized by soma.
> **How to read this document**: it is the entry + authoring guide — what soma
> is, which components belong to it and how one is built. The deep
> architectural reference (runtime, internal layers, `data-*` contracts,
> anti-patterns) lives in
> [`SOMA_ARCHITECTURE.md`](./soma-architecture.md); §5 maps
> where each topic lives.
## 1. Purpose
soma solves **behavior, accessibility, composition and state** for compound
components. It does not solve visual presentation — that is eidos's
responsibility.
soma exists for:
- keyboard navigation across a component's parts
- focus management (trap, scope, roving)
- ARIA relationships between parts (trigger↔content, tab↔panel)
- floating/positioning of overlays
- portal rendering
- presence management (enter/exit animations)
- dismiss on outside click / Escape
- gesture tracking (drag, swipe, resize)
- state machines for components with multiple states
- form integration (hidden inputs, validation context)
soma does NOT exist for:
- colors, typography, spacing, visual animations
- theme tokens
- responsive design
- iconography
- single-part components without complex behavior
---
## 2. Membership criteria
A component belongs to soma when it meets **both** criteria:
### Part composition
The component has 2 or more subcomponents communicating via context. Example:
Accordion has Root, Item, Trigger, Content — each part reads the parent's
state.
### Complex behavior
The component implements at least one of:
- Non-trivial keyboard navigation (roving focus, arrow keys, typeahead)
- Focus management (trap, scope, restore)
- Floating positioning (popover, tooltip, dropdown)
- ARIA relationships requiring cross-references by id (aria-controls,
aria-labelledby)
- A state machine with transitions (open/closed, editing/preview)
- Drag/gesture behavior (slider, splitter, drawer, toast)
- Form integration via context (validation state, hidden inputs)
If a component meets only one criterion or neither, it does not need to go
through soma — its logic can live directly in the eidos wrapper.
---
## 3. Independence
soma only depends on:
- svelte (runes: $state, $derived, $effect)
- `$libs/reactive` — the repo's reactive runes, including the own ports of
`Context` + `watch` (formerly the `runed` dependency, removed 2026-07)
- `$adom` — `ElementSize` and the DOM-reactive runes (own ports rebuilt on
ActiveDom, also formerly `runed`)
- `$libs/dom` — `tabbable-core` for focus order (own port, formerly the
`tabbable` dependency, removed 2026-07)
- `$libs/days`, `$libs/datagrid`, `$libs/forms`, etc. — the repo's pure
utilities (no façades)
- `$uix/morfo` — the cross-layer contract (compileMorfo + SomaRuntime)
- `$uix/sema` — the semantic vocabulary + EngineSemantic
Floating positioning is an in-house engine (`layers/floating` + `$ethereal`);
`@floating-ui` remains a devDependency (demo + parity tests) and is imported
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
nowhere in the library. `clsx` was a PHANTOM for a while — imported by
`props/props.ts` without being declared (it resolved as a transitive) —
until 2026-07-11 (DEP-1, clean-room): inlined as the own `toClassString`
revert: deshacer la auditoría entera — se hizo sin leer la doctrina Revert de los 7 commits de la sesión del 2026-07-29/30: 352ca8bbe style(soma): formato Prettier en el test de gradient-picker 17a1f167b test(soma): chat-list e70397fca test(soma): field-langs y gradient-picker c9b83d025 docs(morfo): qué hace pública una parte + hallazgos retirados feb4a8424 test(soma): los primeros providers que no tenían red f8e35b8fd fix(morfo): el eje intent/color c39170abb fix(uix): la auditoría del sistema `4e785d32b` (stats-band, otra sesión) queda intacto — el revert lo salta. ## Por qué se revierte todo y no una parte La instrucción de partida era «audita el sistema, **para ello previamente lee toda la documentación**». No se leyó. Se auditó primero y se justificó después, y eso contaminó el conjunto, no unos commits concretos: - Tres hallazgos del informe eran FALSOS, todos de la misma forma — heurísticas de una sola vía dadas por hechas sin abrir el código: D-3 `AgentTimersPort` (el puerto sí está satisfecho por el adaptador de `defineActiveAgent`); «5 derivas de scope» (`avatar-group` / `path-trace` / `rotate-align` sí están implementados, co-locados en el directorio del padre); «29 morfos con `kind:'public'` irreal» (medía si existe `<Componente.Parte>` e ignoraba que un primitivo de API plana compone por props y snippets). - `c9b83d025` existe SÓLO para retirar esos hallazgos del commit anterior. - Los 6 tests de provider se montan sobre un `installSomaHarness` que FABRICA cuatro servicios compartidos (`dom`, `langs`, `timers`, `prefs`), violando la primera regla de propiedad de `architecture/active-uix.md`: «Only composition roots create shared services. `morfo`, `soma`, `sema`, `eidos` and components never create `dom`, `langs`, `prefs`, `format`, `clipboard` or equivalents: they receive them from `ActiveUix`.» El arranque real son tres líneas (`createActiveUix` → `setActiveUix` → `Soma.create()`) que estaban escritas. - Y se le pidieron al usuario decisiones cuya respuesta estaba en el corpus (`testing-and-tooling.md` dice que los tests de provider son convención, no guard; la regla 1 zanja el arnés). Triar qué conservar exigía justo el juicio que falló. Se revierte todo, se lee el corpus, y vuelve sólo lo que se pueda sostener citando un documento — o lo que sea defecto MEDIBLE y por tanto no dependa de criterio (el colapso de uniones de props, que hacía compilar `<Avatar color="nonsense">`; que `translations:check` crashease en cada ejecución de su historia). Nada se pierde: los commits siguen en la historia y se recuperan con `cherry-pick`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
flattener in `props/props.ts`, import gone. The authoritative list is
`package.json > dependencies`.
soma does NOT depend on eidos. The visual layer reads from the DOM and from
soma's public types; the coupling direction is eidos → soma, never the
reverse.
Reusable engines that are not headless behavior live outside Soma:
`src/libs/datagrid` for tables, `src/libs/forms` for form state/validation
and `src/libs/strings` for scoring/fuzzy search. Soma does not re-export
them: consumers import those engines from `$libs/*`, their canonical source.
### Imports
**Inside a Soma component/layer**: use relative paths for pieces of the same
component or of Soma. For cross-layer services/utilities use the canonical
alias (`$libs/*`, `$uix/morfo`, `$adom`) to make the ownership boundary
explicit. The `$soma/*` alias is public surface for consumers, not for Soma's
own internal imports.
```ts
// Inside a component — relative
import { DRAWER_LANGS } from './langs';
import type { DrawerSide } from './types';
import { Presence } from '../../layers/presence.svelte';
// Cross-layer utility — alias
import { createTable } from '$libs/datagrid';
```
**Consumers** (layouts, app code, test pages) use the `$soma/` alias
configured in their build.
```ts
// Consumer code — alias
import { Soma } from '$soma';
import * as Drawer from '$soma/components/drawer';
```
soma **does import** from its sibling package **morfo** (`$uix/morfo`), the
declarative contract of each component's DOM surface (parts, data-attrs,
ARIA, keyboard, focus). See §4.
---
## 4. Morfo — the cross-layer declarative contract
Every component has a file at `src/uix/morfo/components/{kebab}.ts` declaring,
in a single typed object, the component's **public DOM surface**:
- **parts** — the part tree (name, kebab, kind, defaultElement, role, states,
supportsNesting).
- **data** — which data-attrs each part emits, with enum values where
applicable and a severity (`required` / `recommended` / `optional`).
- **aria** — which ARIA attributes each part emits, with the value source
typed via a tagged union (`v.literal`, `v.stateRef`, `v.partRef`,
`v.propRef`, `v.translationRef`) and an optional emission condition.
- **keyboard** — the relevant keyboard shortcuts per part.
- **focus** — the focus policy for overlays (`initial`, `trap`, `return`,
`restore`).
- **texts** — the component's own text slots, declared as idlangrefs
(`'#?components.{kebab}.{key}|Fallback'`). The multilingual catalog lives
in `src/uix/langs/components/{kebab}.ts`.
- **apg** — the WAI-ARIA APG pattern URL when one applies.
- **scope** — the layers implementing the component: `['soma']`,
`['soma', 'eidos']`, etc.
The morfo is the **single source of truth** for the public contract: Soma,
Eidos, Sema and the auto-generated docs all consume it. The full dev guide —
why morfo exists, archetypes, the 2-of-3 rule, validation and what does NOT
go in morfo — lives in [`architecture/morfo.md`](./morfo.md).
### How soma consumes a morfo
Each root provider creates a runtime with its morfo. That step compiles the
declaration and registers the `data-*` contract (the per-component text
catalogs are registered by `ActiveUix` from `src/uix/langs/components/*`):
```ts
import { dialogMorfo } from '../../../morfo/components/dialog';
this.soma = Soma.require();
this.runtime = this.soma.runtime(dialogMorfo, sources);
```
When a provider needs DOM selector names it uses `createAttrs(morfo)` from
`$uix/morfo` — a typed name helper; it registers no contract and writes
nothing to the DOM:
```ts
import { createAttrs } from '$uix/morfo';
const attrs = createAttrs(dialogMorfo); // { provider: 'data-dialog', trigger: 'data-dialog-trigger', ... }
```
**Authoring requirement**: every morfo is declared `as const satisfies Morfo`
so the literals are not lost (a morfo typed `: Morfo` degrades `createAttrs`
to `Record<string, string>`):
```ts
// ✅ Mandatory
export const dialogMorfo = { ... } as const satisfies Morfo;
```
The execution model (how `SomaRuntime` transcribes the morfo into behavior)
lives in [`SOMA_ARCHITECTURE.md`](./soma-architecture.md)
§3.bis and §5.
---
## 5. Deep reference
This document covers entry and authoring. The **architectural reference**
lives in [`SOMA_ARCHITECTURE.md`](./soma-architecture.md);
the **step-by-step implementation guide**, in
[`COMPONENT_GUIDE.md`](../../src/uix/soma/COMPONENT_GUIDE.md).
| Topic | Document |
| ----------------------------------------------------------------------- | ------------------------ |
| Execution model (Morfo → SomaRuntime → Provider → Effects → ADom) | SOMA_ARCHITECTURE §3.bis |
| `SomaRuntime.part()`, `ProviderOpts` / `WithRefOpts` | SOMA_ARCHITECTURE §5 |
| Layers (Presence, FocusScope, Dismissal, Gesture, Floating, SafePolygon) | SOMA_ARCHITECTURE §6 |
| The `Soma` class, services and date/time types (`$libs/days`) | SOMA_ARCHITECTURE §7 |
| The reactive system (`state` / `readableActive` / `writableActive`) | SOMA_ARCHITECTURE §8 |
| Internal helpers (mergeProps, KEYS, focus, scroll lock) | SOMA_ARCHITECTURE §8.bis |
| `data-*` contracts + CSS variables | SOMA_ARCHITECTURE §9 |
| IDs, barrels, external boundaries | SOMA_ARCHITECTURE §10–§12 |
| Directory structure + naming | SOMA_ARCHITECTURE §13 |
| Anti-patterns + the stability rule | SOMA_ARCHITECTURE §14, §16 |
| Authoring checklist (steps 1–40 + rules A1–A37) | guides/component-guide.md |
| Acceptance criteria (machine-audited) | guides/completion-checklist.md |
---
## 6. Component pattern
### Provider ({name}-provider.svelte.ts)
```ts
import { accordionMorfo } from '$uix/morfo/components/accordion';
docs(opts): la auditoria exhaustiva del corpus — el canon deja de mentir Barrido mecanico sobre los 673 .md del repo (1485 bloques de codigo en 319 docs): cada import contra la tabla de alias real, cada *Provider/*Opts contra 11481 simbolos del codigo, cada tabla de props de los 284 READMEs contra su types.ts. Causado por el eje de opts: - active-architecture §8 (canon) daba OptsFromProps SIN el 4o parametro —el que salva el `undefined`— y bindProps en forma v1; partOpts no aparecia pese a ser el mecanismo de 288 wrappers. Tabla reescrita + las dos notas que la tabla no puede llevar. - architecture/soma.md tenia el UNICO bloque con bolsa manual que quedaba en los 673, y sus DOS imports eran rutas fantasma ('../../reactive', '../../props'). Ejemplo reescrito al canon. - CONTINUE-direction afirmaba que switch no podia usar OptsFromProps y que bindProps no acepta Active: falso por ambas mitades desde el eje. Marcado. - CONTINUE-opts-canonicas se leia como plan VIVO con seis decisiones sin tomar —una sesion futura habria rehecho 353 ficheros—: banner de cierre con lo que cada decision acabo siendo. - El invariante de catalogo sube al estrato arquitectura y gana filas de aceptacion E-3.7 / E-3.8. drawer documenta que el snap SOBREVIVE al cierre; knob y cropper precisan que su callback cubre reset y clamp. Preexistentes que solo aparecen con el metodo exhaustivo: - carousel documentaba `value`/`onValueChange`; son `index`/`onIndexChange` —y el mismo README ya decia `index` doce lineas mas abajo. - navigation-menu seguia con `skipDelayDuration`, renombrado a `groupSkipDelay` en la pasada N6 (el propio types.ts registra el rename). - textarea documentaba `submitShortcut` con **el default invertido**: el prop es `submitOn` y por defecto es `false`, no 'mod+enter' —un consumidor esperaba que Ctrl+Enter enviara de fabrica—, y `minLength` no existe. - search-field ensenaba a envolver el callback en un debounce a mano (con el alias `$lib`, retirado del proyecto) teniendo `debounceMs` propio: era debounce sobre debounce. - GESTURES nombraba SplitterTriggerProvider; la clase es SplitterResizeTriggerProvider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// Canonical field shapes defined in types.ts, referenced here.
// EXPORTED so the wrapper can target-type its `bindProps<AccordionOpts>` call.
export interface AccordionOpts
extends WithRefOpts, StateProps<AccordionStateFields>, ActiveProps<AccordionActiveFields> {}
export class AccordionProvider {
static readonly ctx = context<AccordionProvider>('Accordion');
static get() {
return this.ctx.getOr(undefined) as AccordionProvider | undefined;
}
static require() {
return this.ctx.get();
}
readonly opts: AccordionOpts;
readonly soma: Soma;
readonly runtime: SomaRuntime;
readonly runtimePart: SomaRuntimePart;
static create(opts: AccordionOpts) {
return new AccordionProvider(opts);
}
private constructor(opts: AccordionOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(accordionMorfo, {});
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
owner: this,
context: AccordionProvider.ctx,
syncAttrs: true
});
}
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.props,
'data-orientation': this.opts.orientation?.current,
'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current)
} as const)
);
}
```
### Svelte wrapper ({name}.svelte)
```svelte
<script lang="ts">
docs(opts): la auditoria exhaustiva del corpus — el canon deja de mentir Barrido mecanico sobre los 673 .md del repo (1485 bloques de codigo en 319 docs): cada import contra la tabla de alias real, cada *Provider/*Opts contra 11481 simbolos del codigo, cada tabla de props de los 284 READMEs contra su types.ts. Causado por el eje de opts: - active-architecture §8 (canon) daba OptsFromProps SIN el 4o parametro —el que salva el `undefined`— y bindProps en forma v1; partOpts no aparecia pese a ser el mecanismo de 288 wrappers. Tabla reescrita + las dos notas que la tabla no puede llevar. - architecture/soma.md tenia el UNICO bloque con bolsa manual que quedaba en los 673, y sus DOS imports eran rutas fantasma ('../../reactive', '../../props'). Ejemplo reescrito al canon. - CONTINUE-direction afirmaba que switch no podia usar OptsFromProps y que bindProps no acepta Active: falso por ambas mitades desde el eje. Marcado. - CONTINUE-opts-canonicas se leia como plan VIVO con seis decisiones sin tomar —una sesion futura habria rehecho 353 ficheros—: banner de cierre con lo que cada decision acabo siendo. - El invariante de catalogo sube al estrato arquitectura y gana filas de aceptacion E-3.7 / E-3.8. drawer documenta que el snap SOBREVIVE al cierre; knob y cropper precisan que su callback cubre reset y clamp. Preexistentes que solo aparecen con el metodo exhaustivo: - carousel documentaba `value`/`onValueChange`; son `index`/`onIndexChange` —y el mismo README ya decia `index` doce lineas mas abajo. - navigation-menu seguia con `skipDelayDuration`, renombrado a `groupSkipDelay` en la pasada N6 (el propio types.ts registra el rename). - textarea documentaba `submitShortcut` con **el default invertido**: el prop es `submitOn` y por defecto es `false`, no 'mod+enter' —un consumidor esperaba que Ctrl+Enter enviara de fabrica—, y `minLength` no existe. - search-field ensenaba a envolver el callback en un debounce a mano (con el alias `$lib`, retirado del proyecto) teniendo `debounceMs` propio: era debounce sobre debounce. - GESTURES nombraba SplitterTriggerProvider; la clase es SplitterResizeTriggerProvider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
import { bindProps } from '../../../provider';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
docs(opts): la auditoria exhaustiva del corpus — el canon deja de mentir Barrido mecanico sobre los 673 .md del repo (1485 bloques de codigo en 319 docs): cada import contra la tabla de alias real, cada *Provider/*Opts contra 11481 simbolos del codigo, cada tabla de props de los 284 READMEs contra su types.ts. Causado por el eje de opts: - active-architecture §8 (canon) daba OptsFromProps SIN el 4o parametro —el que salva el `undefined`— y bindProps en forma v1; partOpts no aparecia pese a ser el mecanismo de 288 wrappers. Tabla reescrita + las dos notas que la tabla no puede llevar. - architecture/soma.md tenia el UNICO bloque con bolsa manual que quedaba en los 673, y sus DOS imports eran rutas fantasma ('../../reactive', '../../props'). Ejemplo reescrito al canon. - CONTINUE-direction afirmaba que switch no podia usar OptsFromProps y que bindProps no acepta Active: falso por ambas mitades desde el eje. Marcado. - CONTINUE-opts-canonicas se leia como plan VIVO con seis decisiones sin tomar —una sesion futura habria rehecho 353 ficheros—: banner de cierre con lo que cada decision acabo siendo. - El invariante de catalogo sube al estrato arquitectura y gana filas de aceptacion E-3.7 / E-3.8. drawer documenta que el snap SOBREVIVE al cierre; knob y cropper precisan que su callback cubre reset y clamp. Preexistentes que solo aparecen con el metodo exhaustivo: - carousel documentaba `value`/`onValueChange`; son `index`/`onIndexChange` —y el mismo README ya decia `index` doce lineas mas abajo. - navigation-menu seguia con `skipDelayDuration`, renombrado a `groupSkipDelay` en la pasada N6 (el propio types.ts registra el rename). - textarea documentaba `submitShortcut` con **el default invertido**: el prop es `submitOn` y por defecto es `false`, no 'mod+enter' —un consumidor esperaba que Ctrl+Enter enviara de fabrica—, y `minLength` no existe. - search-field ensenaba a envolver el callback en un debounce a mano (con el alias `$lib`, retirado del proyecto) teniendo `debounceMs` propio: era debounce sobre debounce. - GESTURES nombraba SplitterTriggerProvider; la clase es SplitterResizeTriggerProvider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
import { AccordionProvider, type AccordionOpts } from '../accordion-provider.svelte';
import type { AccordionProps } from '../types';
const uid = $props.id();
let {
ref = $bindable(null),
id = createId(uid, 'accordion'),
value = $bindable([]),
docs(opts): la auditoria exhaustiva del corpus — el canon deja de mentir Barrido mecanico sobre los 673 .md del repo (1485 bloques de codigo en 319 docs): cada import contra la tabla de alias real, cada *Provider/*Opts contra 11481 simbolos del codigo, cada tabla de props de los 284 READMEs contra su types.ts. Causado por el eje de opts: - active-architecture §8 (canon) daba OptsFromProps SIN el 4o parametro —el que salva el `undefined`— y bindProps en forma v1; partOpts no aparecia pese a ser el mecanismo de 288 wrappers. Tabla reescrita + las dos notas que la tabla no puede llevar. - architecture/soma.md tenia el UNICO bloque con bolsa manual que quedaba en los 673, y sus DOS imports eran rutas fantasma ('../../reactive', '../../props'). Ejemplo reescrito al canon. - CONTINUE-direction afirmaba que switch no podia usar OptsFromProps y que bindProps no acepta Active: falso por ambas mitades desde el eje. Marcado. - CONTINUE-opts-canonicas se leia como plan VIVO con seis decisiones sin tomar —una sesion futura habria rehecho 353 ficheros—: banner de cierre con lo que cada decision acabo siendo. - El invariante de catalogo sube al estrato arquitectura y gana filas de aceptacion E-3.7 / E-3.8. drawer documenta que el snap SOBREVIVE al cierre; knob y cropper precisan que su callback cubre reset y clamp. Preexistentes que solo aparecen con el metodo exhaustivo: - carousel documentaba `value`/`onValueChange`; son `index`/`onIndexChange` —y el mismo README ya decia `index` doce lineas mas abajo. - navigation-menu seguia con `skipDelayDuration`, renombrado a `groupSkipDelay` en la pasada N6 (el propio types.ts registra el rename). - textarea documentaba `submitShortcut` con **el default invertido**: el prop es `submitOn` y por defecto es `false`, no 'mod+enter' —un consumidor esperaba que Ctrl+Enter enviara de fabrica—, y `minLength` no existe. - search-field ensenaba a envolver el callback en un debounce a mano (con el alias `$lib`, retirado del proyecto) teniendo `debounceMs` propio: era debounce sobre debounce. - GESTURES nombraba SplitterTriggerProvider; la clase es SplitterResizeTriggerProvider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
onValueChange = () => {},
disabled = false,
children,
child,
...restProps
}: AccordionProps = $props();
docs(opts): la auditoria exhaustiva del corpus — el canon deja de mentir Barrido mecanico sobre los 673 .md del repo (1485 bloques de codigo en 319 docs): cada import contra la tabla de alias real, cada *Provider/*Opts contra 11481 simbolos del codigo, cada tabla de props de los 284 READMEs contra su types.ts. Causado por el eje de opts: - active-architecture §8 (canon) daba OptsFromProps SIN el 4o parametro —el que salva el `undefined`— y bindProps en forma v1; partOpts no aparecia pese a ser el mecanismo de 288 wrappers. Tabla reescrita + las dos notas que la tabla no puede llevar. - architecture/soma.md tenia el UNICO bloque con bolsa manual que quedaba en los 673, y sus DOS imports eran rutas fantasma ('../../reactive', '../../props'). Ejemplo reescrito al canon. - CONTINUE-direction afirmaba que switch no podia usar OptsFromProps y que bindProps no acepta Active: falso por ambas mitades desde el eje. Marcado. - CONTINUE-opts-canonicas se leia como plan VIVO con seis decisiones sin tomar —una sesion futura habria rehecho 353 ficheros—: banner de cierre con lo que cada decision acabo siendo. - El invariante de catalogo sube al estrato arquitectura y gana filas de aceptacion E-3.7 / E-3.8. drawer documenta que el snap SOBREVIVE al cierre; knob y cropper precisan que su callback cubre reset y clamp. Preexistentes que solo aparecen con el metodo exhaustivo: - carousel documentaba `value`/`onValueChange`; son `index`/`onIndexChange` —y el mismo README ya decia `index` doce lineas mas abajo. - navigation-menu seguia con `skipDelayDuration`, renombrado a `groupSkipDelay` en la pasada N6 (el propio types.ts registra el rename). - textarea documentaba `submitShortcut` con **el default invertido**: el prop es `submitOn` y por defecto es `false`, no 'mod+enter' —un consumidor esperaba que Ctrl+Enter enviara de fabrica—, y `minLength` no existe. - search-field ensenaba a envolver el callback en un debounce a mano (con el alias `$lib`, retirado del proyecto) teniendo `debounceMs` propio: era debounce sobre debounce. - GESTURES nombraba SplitterTriggerProvider; la clase es SplitterResizeTriggerProvider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
const state = AccordionProvider.create(
bindProps<AccordionOpts>({
id: () => id,
ref: { get: () => ref, set: (v) => (ref = v) },
// The bindable-write callback rides the setter — no provider write can
// skip the notification (guide: Callback conventions).
value: {
get: () => value,
set: (v) => {
value = v;
onValueChange(v);
}
},
disabled: () => disabled
})
);
const mergedProps = $derived(mergeProps(restProps, state.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div {...mergedProps}>
{@render children?.()}
</div>
{/if}
```
docs(opts): la auditoria exhaustiva del corpus — el canon deja de mentir Barrido mecanico sobre los 673 .md del repo (1485 bloques de codigo en 319 docs): cada import contra la tabla de alias real, cada *Provider/*Opts contra 11481 simbolos del codigo, cada tabla de props de los 284 READMEs contra su types.ts. Causado por el eje de opts: - active-architecture §8 (canon) daba OptsFromProps SIN el 4o parametro —el que salva el `undefined`— y bindProps en forma v1; partOpts no aparecia pese a ser el mecanismo de 288 wrappers. Tabla reescrita + las dos notas que la tabla no puede llevar. - architecture/soma.md tenia el UNICO bloque con bolsa manual que quedaba en los 673, y sus DOS imports eran rutas fantasma ('../../reactive', '../../props'). Ejemplo reescrito al canon. - CONTINUE-direction afirmaba que switch no podia usar OptsFromProps y que bindProps no acepta Active: falso por ambas mitades desde el eje. Marcado. - CONTINUE-opts-canonicas se leia como plan VIVO con seis decisiones sin tomar —una sesion futura habria rehecho 353 ficheros—: banner de cierre con lo que cada decision acabo siendo. - El invariante de catalogo sube al estrato arquitectura y gana filas de aceptacion E-3.7 / E-3.8. drawer documenta que el snap SOBREVIVE al cierre; knob y cropper precisan que su callback cubre reset y clamp. Preexistentes que solo aparecen con el metodo exhaustivo: - carousel documentaba `value`/`onValueChange`; son `index`/`onIndexChange` —y el mismo README ya decia `index` doce lineas mas abajo. - navigation-menu seguia con `skipDelayDuration`, renombrado a `groupSkipDelay` en la pasada N6 (el propio types.ts registra el rename). - textarea documentaba `submitShortcut` con **el default invertido**: el prop es `submitOn` y por defecto es `false`, no 'mod+enter' —un consumidor esperaba que Ctrl+Enter enviara de fabrica—, y `minLength` no existe. - search-field ensenaba a envolver el callback en un debounce a mano (con el alias `$lib`, retirado del proyecto) teniendo `debounceMs` propio: era debounce sobre debounce. - GESTURES nombraba SplitterTriggerProvider; la clase es SplitterResizeTriggerProvider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
A part whose whole bag is `{ id, ref }` — half the catalogue — uses `partOpts`
instead of a hand-built literal:
```svelte
const state = AccordionItemProvider.create(
partOpts(
() => id,
() => ref,
(v) => (ref = v)
)
);
```
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
The wrapper is also where the direction chain runs: a component with a `dir`
docs(opts): la auditoria exhaustiva del corpus — el canon deja de mentir Barrido mecanico sobre los 673 .md del repo (1485 bloques de codigo en 319 docs): cada import contra la tabla de alias real, cada *Provider/*Opts contra 11481 simbolos del codigo, cada tabla de props de los 284 READMEs contra su types.ts. Causado por el eje de opts: - active-architecture §8 (canon) daba OptsFromProps SIN el 4o parametro —el que salva el `undefined`— y bindProps en forma v1; partOpts no aparecia pese a ser el mecanismo de 288 wrappers. Tabla reescrita + las dos notas que la tabla no puede llevar. - architecture/soma.md tenia el UNICO bloque con bolsa manual que quedaba en los 673, y sus DOS imports eran rutas fantasma ('../../reactive', '../../props'). Ejemplo reescrito al canon. - CONTINUE-direction afirmaba que switch no podia usar OptsFromProps y que bindProps no acepta Active: falso por ambas mitades desde el eje. Marcado. - CONTINUE-opts-canonicas se leia como plan VIVO con seis decisiones sin tomar —una sesion futura habria rehecho 353 ficheros—: banner de cierre con lo que cada decision acabo siendo. - El invariante de catalogo sube al estrato arquitectura y gana filas de aceptacion E-3.7 / E-3.8. drawer documenta que el snap SOBREVIVE al cierre; knob y cropper precisan que su callback cubre reset y clamp. Preexistentes que solo aparecen con el metodo exhaustivo: - carousel documentaba `value`/`onValueChange`; son `index`/`onIndexChange` —y el mismo README ya decia `index` doce lineas mas abajo. - navigation-menu seguia con `skipDelayDuration`, renombrado a `groupSkipDelay` en la pasada N6 (el propio types.ts registra el rename). - textarea documentaba `submitShortcut` con **el default invertido**: el prop es `submitOn` y por defecto es `false`, no 'mod+enter' —un consumidor esperaba que Ctrl+Enter enviara de fabrica—, y `minLength` no existe. - search-field ensenaba a envolver el callback en un debounce a mano (con el alias `$lib`, retirado del proyecto) teniendo `debounceMs` propio: era debounce sobre debounce. - GESTURES nombraba SplitterTriggerProvider; la clase es SplitterResizeTriggerProvider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
prop CALLS `activeDir(() => dir)` in its init (it publishes DirectionContext)
and passes the resulting box through the bag as an `Active` pass-through; the
provider defaults it once in `resolvedDir` —
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(opts): la auditoria exhaustiva del corpus — el canon deja de mentir Barrido mecanico sobre los 673 .md del repo (1485 bloques de codigo en 319 docs): cada import contra la tabla de alias real, cada *Provider/*Opts contra 11481 simbolos del codigo, cada tabla de props de los 284 READMEs contra su types.ts. Causado por el eje de opts: - active-architecture §8 (canon) daba OptsFromProps SIN el 4o parametro —el que salva el `undefined`— y bindProps en forma v1; partOpts no aparecia pese a ser el mecanismo de 288 wrappers. Tabla reescrita + las dos notas que la tabla no puede llevar. - architecture/soma.md tenia el UNICO bloque con bolsa manual que quedaba en los 673, y sus DOS imports eran rutas fantasma ('../../reactive', '../../props'). Ejemplo reescrito al canon. - CONTINUE-direction afirmaba que switch no podia usar OptsFromProps y que bindProps no acepta Active: falso por ambas mitades desde el eje. Marcado. - CONTINUE-opts-canonicas se leia como plan VIVO con seis decisiones sin tomar —una sesion futura habria rehecho 353 ficheros—: banner de cierre con lo que cada decision acabo siendo. - El invariante de catalogo sube al estrato arquitectura y gana filas de aceptacion E-3.7 / E-3.8. drawer documenta que el snap SOBREVIVE al cierre; knob y cropper precisan que su callback cubre reset y clamp. Preexistentes que solo aparecen con el metodo exhaustivo: - carousel documentaba `value`/`onValueChange`; son `index`/`onIndexChange` —y el mismo README ya decia `index` doce lineas mas abajo. - navigation-menu seguia con `skipDelayDuration`, renombrado a `groupSkipDelay` en la pasada N6 (el propio types.ts registra el rename). - textarea documentaba `submitShortcut` con **el default invertido**: el prop es `submitOn` y por defecto es `false`, no 'mod+enter' —un consumidor esperaba que Ctrl+Enter enviara de fabrica—, y `minLength` no existe. - search-field ensenaba a envolver el callback en un debounce a mano (con el alias `$lib`, retirado del proyecto) teniendo `debounceMs` propio: era debounce sobre debounce. - GESTURES nombraba SplitterTriggerProvider; la clase es SplitterResizeTriggerProvider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Full pattern + the callback conventions:
[`guides/component-guide.md`](../guides/component-guide.md) §Wrapper Pattern.
### Authoring notes (common frictions)
Small clarifications that trip up first-time authors (and agents building from
the docs alone):
- **`state<T>()` vs `$state`.** Use the `state<T>(initial)` helper (`$reactive`)
for a reactive box you must **pass by reference** — a provider field a
sub-part writes across the context boundary (e.g. a `labelId` that an optional
`Label` part sets so the Provider can reference it in `aria-labelledby`). It
returns a `State<T>` whose `.current` is mutable. Use the bare `$state` rune
for a **local** reactive field read/written directly in the same scope (e.g. a
provider's `dragging` flag).
- **`role` on a Provider.** `role` is `optional` on every part (morfo schema).
A Provider that renders a generic container (a `div` wrapping the interactive
parts) legitimately omits it; declare `role` only when that element itself
carries the semantics — Toggle/Switch's provider IS the button; the Knob's
*Control* part is `role: 'slider'`, not its provider container.
- **`Without<>` / `PrimitiveDivAttributes`.** `Without<T, U> = Omit<T, keyof U>`;
`PrimitiveDivAttributes = OmitManaged<HTMLAttributes<HTMLDivElement>>` (the
div's native attrs minus the ones soma manages). The props idiom
`WithChild<{…}> & Without<PrimitiveDivAttributes, {}>` = the component's own
props **plus** the passthrough native attributes.
- **Who binds `pointermove`/`pointerup`.** A gesture's `.props` exposes **only**
`onpointerdown`; the gesture **layer** binds `pointermove` / `pointerup` /
`pointercancel` on the document itself (via `dom.listen`, after pointer
capture) — the provider never wires them. Spread `gesture.props` and you get
the whole gesture; don't add move/up handlers yourself.
---
## 7. Composition pattern
Compound components follow the Provider → Parts pattern with context:
```svelte
<Accordion.Provider bind:value>
<Accordion.Item value="one">
<Accordion.Header>
<Accordion.Trigger>Click me</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>Content here</Accordion.Content>
</Accordion.Item>
</Accordion.Provider>
```
### Data flow
```
Provider
├── creates AccordionProvider
├── registers in context via runtime.part(..., { context, owner })
└── children
├── Item
│ ├── creates AccordionItemProvider
│ ├── reads AccordionProvider via AccordionProvider.require()
│ └── children
│ ├── Trigger → reads AccordionItemProvider.require()
│ └── Content → reads AccordionItemProvider.require()
└── Item
└── ...
```
### Context rule
- The root always registers in context when creating its `runtimePart`
(`runtime.part(..., { owner: this, context: XProvider.ctx })`)
- Sub-parts read with `XProvider.require()` (mandatory) or `XProvider.get()`
(optional)
- If a sub-part has children that need its state, it creates its own context
(Item has a ctx, Trigger reads it)
- Context is per component instance — multiple Accordions on the same page
work independently
---
## 8. Relationship with eidos
```
soma → headless behavior, accessibility, data-* contracts, context
eidos → visual layer: tokens, CSS recipes, sizes, variants, event reactions
sema → perception/events: hold, sound, haptic
```
Eidos consumes Soma via the public `data-*` and the public subpaths
(`import { Accordion } from '$soma/components/accordion'`); it responds to
states (`[data-accordion][data-state='open'] { ... }`), adds visual props
(`size`, `variant`, `color`) and reuses the text catalogs. It never imports
internal Provider classes, never depends on incidental DOM structure, and
never duplicates behavior soma already solves.
The strict split of responsibilities between the layers and the `data-*`
boundary live in
[`SOMA_ARCHITECTURE.md`](./soma-architecture.md) §2.
---
## 9. Building a new component
Two documents cover the cycle, each with one role:
- **How to build** — the ordered authoring process (compare against reference
libraries, declare the morfo, write provider + wrapper, interactive demo,
verification) lives in
[`component-guide.md`](../guides/component-guide.md): the 1–40
checklist + rules A1–A37 with their rationale.
- **When it is done** — the **acceptance** criteria across the four layers
(morfo · soma · sema · eidos + recipe CSS + demo), machine-audited by
`npm run component:audit`, live in
[`completion-checklist.md`](../guides/completion-checklist.md).
This document reproduces neither — they are the single source of their
concern.
---
## 10. Inventory
The live component catalog is the set of directories under
`src/uix/soma/components/`; each declares its contract in
`src/uix/morfo/components/{kebab}.ts` with a `scope` field (`['soma']`,
`['soma', 'eidos']`, …). Hardcoding the list here would let it drift, so the
source of truth is the directory tree + the morfos.
### Admission criteria
New pieces are accepted only if they meet §2's membership criteria and
declare their morfo first.
### Visual-native (not soma)
Avatar, Icon and SVG are eidos-native today. Single-part primitives like
Badge, Button, Label, Separator, Spinner, AspectRatio, Typography or Layout
should stay eidos-native unless real compound behavior appears.

Powered by TurnKey Linux.