|
|
4 months ago | |
|---|---|---|
| .. | ||
| components | 4 months ago | |
| core | 4 months ago | |
| css | 6 months ago | |
| datetime | 4 months ago | |
| keyboard | 6 months ago | |
| layers | 4 months ago | |
| props | 6 months ago | |
| provider | 5 months ago | |
| types | 5 months ago | |
| COMPONENT_GUIDE.md | 4 months ago | |
| README.md | 4 months ago | |
| SOMA_ARCHITECTURE.md | 4 months ago | |
| errors.ts | 5 months ago | |
| index.ts | 5 months ago | |
| runtime.svelte.test.ts | 5 months ago | |
| runtime.svelte.ts | 4 months ago | |
| soma-attr-audit.test.ts | 4 months ago | |
| typeahead.ts | 5 months ago | |
README.md
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 enSOMA_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.
// 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.
// 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.
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:
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:
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<string, string>):
// ✅ 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 §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; la guía paso a paso de
implementación, en 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)
import { accordionMorfo } from '$uix/morfo/components/accordion';
// Canonical field shapes defined in types.ts, referenced here
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)
);
}
Wrapper svelte ({name}.svelte)
<script lang="ts">
import { readableActive, writableActive } from '../../reactive';
import { mergeProps } from '../../props';
import { createId } from '$active-uix/id';
import { AccordionProvider } from '../accordion-provider.svelte';
import type { AccordionProps } from '../types';
const uid = $props.id();
let {
ref = $bindable(null),
id = createId(uid, 'accordion'),
value = $bindable([]),
disabled = false,
children,
child,
...restProps
}: AccordionProps = $props();
const state = AccordionProvider.create({
id: readableActive(() => id),
ref: writableActive(
() => ref,
(v) => (ref = v)
),
value: writableActive(
() => value,
(v) => (value = v)
),
disabled: readableActive(() => disabled)
});
const mergedProps = $derived(mergeProps(restProps, state.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div {...mergedProps}>
{@render children?.()}
</div>
{/if}
7. Patron de composicion
Los componentes compuestos siguen el patron Provider → Parts con contexto:
<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>
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) oXProvider.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 §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: 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.
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.