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/src/uix/soma/README.md

15 KiB

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; 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).
  • texts — slots de texto propios del componente, declarados como idlangrefs ('#?components.{kebab}.{key}|Fallback'). El catálogo multilingüe vive en src/uix/langs/components/{kebab}.ts.
  • 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 compila la declaración y registra el contrato data-* (los catálogos de texto por componente los registra ActiveUix desde src/uix/langs/components/*):

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) 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 los catálogos de texto. 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.

Powered by TurnKey Linux.