# soma Librería headless de componentes compuestos para Svelte 5. Capa de comportamiento dentro de UIX — emite los `data-*` y `aria-*` que el contrato declara en `morfo`, gestiona estado y eventos, y delega lo visual a `eidos` mediante el DOM. > **Doctrina del API**: soma mantiene la forma compound (`Toggle.Provider`, > `Tabs.Provider + Tabs.Trigger + ...`) por simetria con los multi-parte. > Eidos no inventa una API flat paralela: aplica la capa visual sobre la > anatomia declarada por morfo y materializada por soma. > **Cómo leer este README**: es la guía de entrada y de autoría — qué es soma, > qué componentes le pertenecen y cómo se construye uno. La referencia > arquitectónica profunda (runtime, layers internas, contratos `data-*`, > anti-patterns) vive en [`SOMA_ARCHITECTURE.md`](./SOMA_ARCHITECTURE.md); el > mapa de dónde está cada tema es §5. ## 1. Proposito soma resuelve **behavior, accesibilidad, composicion y estado** para componentes compuestos. No resuelve presentacion visual — eso es responsabilidad de eidos. soma existe para: - keyboard navigation entre partes de un componente - focus management (trap, scope, roving) - ARIA relationships entre partes (trigger↔content, tab↔panel) - floating/positioning de overlays - portal rendering - presence management (enter/exit animations) - dismiss on outside click / Escape - gesture tracking (drag, swipe, resize) - state machines para componentes con multiples estados - form integration (hidden inputs, validation context) soma NO existe para: - colores, tipografia, espaciado, animaciones visuales - tokens de tema - responsive design - iconografia - componentes de una sola parte sin behavior complejo --- ## 2. Criterio de pertenencia Un componente pertenece a soma si cumple **ambos** criterios: ### Composicion de partes El componente tiene 2 o mas subcomponentes que se comunican via context. Ejemplo: Accordion tiene Root, Item, Trigger, Content — cada parte lee estado del padre. ### Behavior complejo El componente implementa al menos uno de: - Keyboard navigation no trivial (roving focus, arrow keys, typeahead) - Focus management (trap, scope, restore) - Floating positioning (popover, tooltip, dropdown) - ARIA relationships que requieren IDs cruzados (aria-controls, aria-labelledby) - State machine con transiciones (open/closed, editing/preview) - Drag/gesture behavior (slider, splitter, drawer, toast) - Form integration via context (validation state, hidden inputs) Si un componente cumple solo uno de los criterios o ninguno, no necesita pasar por soma — su lógica puede vivir directamente en el wrapper de eidos. --- ## 3. Independencia soma solo depende de: - svelte (runes: $state, $derived, $effect) - runed (Context, watch) - clsx (class merging) - @floating-ui (positioning) - `$libs/reactive`, `$libs/days`, `$libs/datagrid`, `$libs/forms`, etc. — utilidades puras del repo (no façades) - `$uix/morfo` — el contrato cross-layer (compileMorfo + SomaRuntime) - `$uix/sema` — vocabulario semántico + EngineSemantic soma NO depende de eidos. La capa visual lee del DOM y de los tipos públicos del soma; la dirección del acoplamiento es eidos → soma, no al revés. Los motores reutilizables que no son comportamiento headless viven fuera de Soma: `src/libs/datagrid` para tablas, `src/libs/forms` para estado/validacion de formularios y `src/libs/strings` para scoring/fuzzy search. Soma no los reexporta: los consumidores importan esos motores desde `$libs/*`, que es su fuente canonica. ### Imports **Dentro de un componente/layer Soma**: usar paths relativos para piezas del mismo componente o de Soma. Para servicios/utilidades cross-layer usar el alias canonico (`$libs/*`, `$uix/morfo`, `$adom`) para dejar clara la frontera de ownership. El alias `$soma/*` es superficie publica para consumidores, no para imports internos del propio Soma. ```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'; ``` **Consumidores** (layouts, app code, test pages): usan el alias `$soma/` que configuran en su build. ```ts // Consumer code — alias import { Soma } from '$soma'; import * as Drawer from '$soma/components/drawer'; ``` soma **sí importa** del paquete hermano **morfo** (`$uix/morfo`), que es el contrato declarativo de la superficie DOM de cada componente (partes, data-attrs, ARIA, keyboard, focus). Ver §4. --- ## 4. Morfo — contrato declarativo cross-layer Cada componente tiene un archivo en `src/uix/morfo/components/{kebab}.ts` que declara, en un único objeto tipado, la **superficie DOM pública** del componente: - **parts** — el árbol de partes (name, kebab, kind, defaultElement, role, states, supportsNesting). - **data** — qué data-attrs emite cada parte, con valores enum cuando aplica y severity (`required` / `recommended` / `optional`). - **aria** — qué atributos ARIA emite cada parte, con la fuente del valor tipada vía tagged union (`v.literal`, `v.stateRef`, `v.partRef`, `v.propRef`, `v.translationRef`) y condición de emisión opcional. - **keyboard** — los atajos de teclado relevantes por parte. - **focus** — política de foco para overlays (`initial`, `trap`, `return`, `restore`). - **translations** — catálogo de traducciones propio del componente, registrado bajo `components.{kebab}`. - **apg** — URL al patrón WAI-ARIA APG cuando aplica. - **scope** — las capas que implementan el componente: `['soma']`, `['soma', 'eidos']`, etc. El morfo es la **única fuente de verdad** del contrato público: Soma, Eidos, Sema y la docs auto-generada lo consumen todos. El dev guide completo —por qué existe morfo, archetypes, la regla 2-de-3, validación y qué NO va en morfo— vive en [`src/uix/morfo/README.md`](../morfo/README.md). ### Cómo soma consume un morfo Cada provider raíz crea un runtime con su morfo. Ese paso registra el contrato `data-*`, compila la declaración y publica `morfo.translations` en los `ActiveLangs` conectados por `ActiveUix`: ```ts import { dialogMorfo } from '../../../morfo/components/dialog'; this.soma = Soma.require(); this.runtime = this.soma.runtime(dialogMorfo, sources); ``` Cuando un provider necesita nombres de selector DOM usa `createAttrs(morfo)` desde `$uix/morfo` — un helper tipado de nombres, no registra contrato ni escribe en el DOM: ```ts import { createAttrs } from '$uix/morfo'; const attrs = createAttrs(dialogMorfo); // { provider: 'data-dialog', trigger: 'data-dialog-trigger', ... } ``` **Requisito de autoría**: cada morfo se declara como `as const satisfies Morfo` para no perder los literales (un morfo tipado como `: Morfo` degrada `createAttrs` a `Record`): ```ts // ✅ Obligatorio export const dialogMorfo = { ... } as const satisfies Morfo; ``` El modelo de ejecución (cómo `SomaRuntime` transcribe el morfo en comportamiento) vive en [`SOMA_ARCHITECTURE.md`](./SOMA_ARCHITECTURE.md) §3.bis y §5. --- ## 5. Referencia profunda Este README cubre la entrada y la autoría. La **referencia arquitectónica** vive en [`SOMA_ARCHITECTURE.md`](./SOMA_ARCHITECTURE.md); la **guía paso a paso** de implementación, en [`COMPONENT_GUIDE.md`](./COMPONENT_GUIDE.md). | Tema | Documento | | ------------------------------------------------------------------- | ------------------------ | | Modelo de ejecución (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 | | `Soma` class, servicios y tipos date/time (`$libs/days`) | SOMA_ARCHITECTURE §7 | | Sistema reactivo (`state` / `readableActive` / `writableActive`) | SOMA_ARCHITECTURE §8 | | Helpers internos (mergeProps, KEYS, focus, scroll lock) | SOMA_ARCHITECTURE §8.bis | | Contratos `data-*` + CSS variables | SOMA_ARCHITECTURE §9 | | IDs, barrels, fronteras externas | SOMA_ARCHITECTURE §10–§12 | | Estructura de directorios + naming | SOMA_ARCHITECTURE §13 | | Anti-patterns + regla de estabilidad | SOMA_ARCHITECTURE §14, §16 | | Checklist de autoría (pasos 1–40 + reglas A1–A37) | COMPONENT_GUIDE.md | | Criterios de aceptación (machine-auditados) | COMPONENT_COMPLETION_CHECKLIST.md | --- ## 6. Patron de componente ### Provider ({name}-provider.svelte.ts) ```ts import { accordionMorfo } from '$uix/morfo/components/accordion'; // Canonical field shapes defined in types.ts, referenced here interface AccordionOpts extends WithRefOpts, StateProps, ActiveProps {} export class AccordionProvider { static readonly ctx = context('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) ); } ``` ### Wrapper svelte ({name}.svelte) ```svelte {#if child} {@render child({ props: mergedProps })} {:else}
{@render children?.()}
{/if} ``` --- ## 7. Patron de composicion Los componentes compuestos siguen el patron Provider → Parts con contexto: ```svelte Click me Content here ``` ### Flujo de datos ``` Provider ├── crea AccordionProvider ├── registra en context via runtime.part(..., { context, owner }) └── children ├── Item │ ├── crea AccordionItemProvider │ ├── lee AccordionProvider via AccordionProvider.require() │ └── children │ ├── Trigger → lee AccordionItemProvider.require() │ └── Content → lee AccordionItemProvider.require() └── Item └── ... ``` ### Regla de context - Root siempre registra en context al crear su `runtimePart` (`runtime.part(..., { owner: this, context: XProvider.ctx })`) - Sub-parts leen con `XProvider.require()` (obligatorio) o `XProvider.get()` (opcional) - Si una sub-part tiene hijos que necesitan su estado, crea su propio context (Item tiene ctx, Trigger lo lee) - El context es por componente instance — multiples Accordion en la misma pagina funcionan independientemente --- ## 8. Relacion con eidos ``` soma → headless behavior, accesibilidad, data-* contracts, context eidos → visual layer: tokens, CSS recipes, sizes, variants, event reactions sema → perception/events: hold, sound, haptic ``` Eidos consume Soma vía los `data-*` públicos y los subpaths públicos (`import { Accordion } from '$soma/components/accordion'`); responde a estados (`[data-accordion][data-state='open'] { ... }`), añade props visuales (`size`, `variant`, `color`) y reutiliza las translations. Nunca importa Provider classes internas, no depende de estructura DOM incidental ni duplica behavior que soma ya resuelve. El reparto estricto de responsabilidades entre las capas y la frontera `data-*` viven en [`SOMA_ARCHITECTURE.md`](./SOMA_ARCHITECTURE.md) §2. --- ## 9. Construir un componente nuevo Dos documentos cubren el ciclo, cada uno con un rol: - **Cómo construir** — el proceso de autoría ordenado (comparar con librerías de referencia, declarar el morfo, escribir provider + wrapper, demo interactivo, verificación) vive en [`COMPONENT_GUIDE.md`](./COMPONENT_GUIDE.md): checklist de 1 a 40 + las reglas A1–A37 con su rationale. - **Cuándo está terminado** — los criterios de **aceptación** a través de las cuatro capas (morfo · soma · sema · eidos + recipe CSS + demo), machine- auditados por `npm run component:audit`, viven en [`../COMPONENT_COMPLETION_CHECKLIST.md`](../COMPONENT_COMPLETION_CHECKLIST.md). Este README no reproduce ninguno de los dos — son la fuente única de su concern. --- ## 10. Inventory El catálogo vivo de componentes son los directorios bajo `src/uix/soma/components/`; cada uno declara su contrato en `src/uix/morfo/components/{kebab}.ts` con un campo `scope` (`['soma']`, `['soma', 'eidos']`, …). Hardcodear la lista aquí la deja desincronizada, así que la fuente de verdad es el árbol de directorios + los morfos. ### Criterio de admisión Nuevas piezas se aceptan solo si cumplen el criterio de pertenencia de §2 y declaran primero su morfo. ### Visual-native (no soma) Avatar, Icon y SVG son eidos-native hoy. Primitivas de una sola parte como Badge, Button, Label, Separator, Spinner, AspectRatio, Typography o Layout deben seguir eidos-native salvo que aparezca behavior compuesto real.