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

14 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).
  • 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) 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 §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.