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/SOMA_ARCHITECTURE.md

963 lines
45 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Soma Architecture
Documento de referencia arquitectonica para `src/uix/soma`.
`soma` es la capa de primitives headless del sistema UIX. Implementa
behavior, accesibilidad y composicion de partes; la presentacion visual
es responsabilidad de eidos. Cada componente se describe primero como
**morfo** (contrato declarativo) y soma lo materializa mediante clases
concretas de estado que registran sus partes en `SomaRuntime`.
## 1. Proposito
`soma` existe para dar una base comun sobre la que construir interfaces complejas sin repetir la misma logica de:
- contexto compartido
- control de foco
- teclado y puntero
- `aria-*`
- `data-*`
- sincronizacion de estado
- integracion con servicios transversales
- animaciones de entrada/salida
- posicionamiento flotante
Su objetivo no es ser una capa visual ni de producto. `soma` define primitives reutilizables y predecibles; la capa visual decide el look and feel.
## 2. Arquitectura de capas
```
soma → headless: behavior, accesibilidad, data-* contracts, context, servicios
eidos → visual: tokens, CSS, temas, recipes, reacciones a data-event-*
events → percepcion: sound/haptic/hold y dispatch de ocurrencias semanticas
app → producto: composicion final, contenido, logica de negocio
```
Cada capa tiene responsabilidades estrictas:
### soma aporta
- comportamiento (keyboard, focus, dismiss, scroll lock)
- accesibilidad (ARIA, roles, live regions)
- contratos `data-*` estables y validados
- contexto y composicion de partes
- servicios de runtime (`langs`, `format`, `logger`)
- sistema de animaciones (presence, data-starting/ending-style, onComplete)
- posicionamiento flotante (@floating-ui)
### La capa visual aporta
- apariencia (tokens, colores, tipografia, spacing)
- tono visual (temas light/dark, variantes)
- decisiones de diseno opinionated (sizes, recipes)
- motion CSS y reacciones visuales a `data-event-*`
- responsive design
### La capa visual NUNCA
- importa state classes internas de soma
- depende de estructura DOM incidental
- accede a propiedades privadas
- duplica behavior que soma ya resuelve
- usa `data-*` fuera de los contratos publicados
La frontera es los `data-*` attrs y las CSS variables que soma expone.
## 3. Principios de diseno
### 3.1 El desarrollador no necesita conocer los internos
Los layers, el sistema reactivo, el floating engine — son implementacion interna. El desarrollador de componentes interactua con:
- clases concretas de estado + `SomaRuntime`
- `Soma` class para servicios
- Barrel imports jerárquicos (`import { Dialog } from '$soma/components'`)
- subpaths explicitos cuando necesita helpers publicos (`$soma/provider`, `$soma/keyboard`, `$soma/runtime.svelte`)
### 3.2 Un patron, no tres
Todo componente sigue el mismo patron:
1. State class concreta registra sus partes con `SomaRuntime.part(...)`
2. Wrapper `.svelte` fino convierte props → Active/State
3. Props derivados via `$derived.by` + `runtimePart.assert`
4. Contexto para comunicacion padre-hijo
Los roots sin DOM usan `ProviderOpts` (ref opcional), las partes con DOM usan
`WithRefOpts`. Las unicas excepciones son partes declarativas que pueden vivir
fuera de su provider (`AnnounceRegion`, `FeedSentinel`): si encuentran un
provider reutilizan su `runtime`; si no, crean un runtime propio desde el
`Soma` del scope actual.
### 3.3 Layers como behaviors, no como wrappers
Los layers se instancian en el constructor del Provider y exponen `.props` para merge. No hay nesting de componentes wrapper en template.
```ts
// Correcto: behaviors integrados
readonly focusScope = FocusScope.use({...});
readonly dismissal = Dismissal.use({...});
readonly props = $derived.by(() => this.runtimePart.assert({
...this.runtimePart.props,
...this.focusScope.props,
...this.dismissal.props,
}));
```
```svelte
<!-- Incorrecto: nesting de wrappers (patron terra) -->
<ScrollLock>
<FocusScope>
<DismissibleLayer>
{content}
</DismissibleLayer>
</FocusScope>
</ScrollLock>
```
### 3.4 Soma es una clase, no una configuracion
`Soma` es la identidad runtime del framework. No es un archivo de configuracion — es el objeto raiz que provee servicios via context.
```ts
// En un Provider:
readonly soma = Soma.require();
const dir = this.soma.prefs.getDir();
```
### 3.5 Los data-\* son contrato publico
Los `data-*` attrs son la frontera entre soma y la capa visual. Cambiarlos es breaking change.
Convencion (obligatoria, sin excepciones):
- provider: `data-{component}` (no `data-{component}-provider`, **no `data-soma-*`**)
- parte: `data-{component}-{part}`
- estado: `data-state`, `data-disabled`, `data-side`, `data-align`, `data-orientation`
- animacion: `data-starting-style`, `data-ending-style`
- nesting: `data-nested`, `data-nested-open`
Los nombres los emite el compilador de morfo que consume `SomaRuntime`.
`createAttrs(morfo)` queda como helper tipado para `querySelector` y tooling,
no como sistema de registro ni escritura DOM. Cualquier selector CSS, cadena
en README o snippet debe coincidir exactamente con esos nombres generados. El
validador de contratos (`assertContract`) solo verifica valores enumerados, no
nombres ni presencia — la consistencia de nombres es responsabilidad del autor
del componente (checklist item 27).
### 3.6 La accesibilidad base no se delega
soma resuelve ARIA por defecto. El consumidor no necesita añadir `role`, `aria-modal`, `aria-expanded`, `aria-controls`, etc. — el Provider los genera.
Texto funcional se resuelve via `langs.ts()` con idlangref. El catalogo propio del componente vive en `morfo.translations` y se referencia con `v.translationRef(...)`; texto compartido como close/cancel/save vive en `common.*` y se referencia con `v.commonRef(...)` o un idlangref absoluto. `langs.ts` por componente queda como comodidad opcional para constantes imperativas, no como catalogo canonico.
### 3.7 DOM global via ActiveDom
Soma usa el `ActiveDom` del scope para escrituras gestionadas por UIX,
listeners de `document/window`, queries globales, foco imperativo y scroll de
ventana. No existe fachada `soma/events`: `dom.listen(...)` es la superficie
canonica para registrar listeners con cleanup.
Las lecturas locales de un elemento propio (`contains`, `closest`,
`getBoundingClientRect`, `clientWidth`, `scrollTop`) no se envuelven en
`ActiveDom`; son parte del comportamiento local del componente.
### 3.8 Props documentadas obligatoriamente
Todas las props de todos los componentes llevan JSDoc en `types.ts`. Cada prop: descripcion, `@default`, notas de comportamiento.
### 3.9 Comparacion con referencias
Cada componente se compara con ark-ui, bits-ui y radix-ui antes de implementar. Se documentan las props que otros tienen y soma no, con justificacion.
## 3.bis Arquitectura cerrada (post-2026-04-25)
El reparto de responsabilidades entre Morfo, Soma, Sema y ADom esta cerrado en
seis piezas con responsabilidades disjuntas:
```
Morfo declara
SomaRuntime transcribe (vive en soma/)
Provider aporta sources, targets y handlers
Effects sincronizan attrs derivados
EngineSemantic despacha senales a canales perceptivos
VisualChannel materializa la senal en el DOM (data-event*, hold, cleanup)
ADom aplica mutaciones DOM (commit estructural)
```
### SomaRuntime — la pieza nueva
`SomaRuntime` es la pieza que faltaba entre `Morfo` (declaracion) y
`Provider` (ejecucion). Lee el morfo y produce el comportamiento.
Una instancia por componente:
```ts
readonly soma = Soma.require();
readonly runtime = this.soma.runtime(morfo, {
states: { open: () => this.opts.open.current },
props: { disabled: () => this.opts.disabled.current },
parts: { content: () => this.contentId.current },
events: {
open: () => {
this.opts.open.current = true;
},
'close-cancel': () => {
this.opts.open.current = false;
}
}
});
```
API V1:
- `runtime.part(part, opts)` — unica API publica para registrar una parte.
Devuelve el handle (`props`, `resolveProps`, `assert`) y, con
`syncAttrs: true`, sincroniza attrs derivados por morfo via `dom.apply`.
- `runtime.partProps(part)` — devuelve `{ id, ref, marker, data-archetype? }`. Solo identidad estatica (el `data-archetype` es classification cross-component, nunca cambia).
- `runtime.keydown(part, event)` — dispatch de teclas declaradas en `morfo.keyboard`.
- `runtime.trigger(eventName)` — orquesta la secuencia perceptiva + state.
### Provider en el modelo nuevo
El provider deja de tener helpers locales de resolucion de morfo. Solo
aporta:
- getters reactivos para `states`, `props`, `parts`
- handlers sincronos para los `events`
- glue de layers ortogonales (Presence, Dismissal, ScrollLock)
Cada part-provider conserva el handle devuelto por `runtime.part(...)`:
```ts
readonly runtimePart = runtime.part('trigger', {
id: opts.id,
ref: opts.ref,
owner: this,
syncAttrs: true
});
readonly props = $derived.by(() => this.runtimePart.props);
```
### Tres operaciones que cubren todos los escenarios
```ts
// Cambio estructural sin senal
provider.commitState(change);
// Cambio estructural con senal
provider.commitState(change, event);
// Senal sin cambio estructural
provider.emitEvent(event);
```
Internamente:
```ts
async commitState(change, event?) {
if (event) await this.soma.events?.emit(event);
this.soma.dom.apply(change);
}
emitEvent(event) {
void this.soma.events?.emit(event);
}
```
### La secuencia de `runtime.trigger(eventName)`
```
1. prewrite imperativo (transient markers como data-last-action)
2. await events.emit(event)
3. handler sincrono del provider muta state
4. effects derivan y aplican attrs estructurales (data-state, aria-*)
```
Los effects del runtime escuchan los sources reactivos y reaplican attrs cada
vez que el estado cambia. ADom es el unico escritor de attrs mutables.
### Reglas operativas
- `partProps(part)` solo emite identidad estatica. Lo mutable lo escribe ADom.
- Lo que `dom.apply` escribe, Svelte no lo renderiza desde `partProps`.
- Event handlers son sincronos. Async va antes del trigger.
- Guards (`if (disabled) return`) van en el call-site, no dentro del handler.
- `events`/`VisualChannel` pueden usar `ActiveDom` a traves del projector
inyectado; `ActiveDom` no conoce `events`.
- `morfo.events.commits` es descriptivo, no ejecutable. El smoke valida.
### Pilotaje (orden incremental)
1. **Toggle** — primer caso, solo `partProps`.
2. **Collapsible** — anade `keydown`.
3. **Toast** — primer test real de `trigger()` con `intent`.
4. **Dialog** — al final, cuando layers + portal ya esten validados.
Ver tambien:
- [src/uix/morfo/README.md](../morfo/README.md) — declaracion, archetypes, regla 2-de-3
- [src/uix/sema/README.md](../sema/README.md) — contrato de `emit`, vocabulario de verbs
- [src/uix/adom/README.md](../adom/README.md) — `dom.apply`
- [src/uix/eidos/README.md](../eidos/README.md) — qué consume eidos del DOM
- [src/uix/README.md](../README.md) §2.bis — vista cross-layer
### Cross-layer hooks que soma emite por la regla 2-de-3
Soma escribe al DOM no solo lo que necesita; escribe también lo que sema y
eidos van a consumir. La regla "2-de-3" justifica qué entra al morfo y por
tanto qué emite el runtime:
- **`data-archetype`** — emitido por `partProps` cuando la parte declara
archetype. Eidos lo usa para selectores transversales
(`[data-archetype=trigger] { ... }`); sema puede asociar verbs por
archetype.
- **`data-event*`** — emitido por `events.emit` (a través del VisualChannel)
durante un hold configurable (240ms emerge/commit/handle, 600ms alert/sustain
por defecto). Eidos lo usa para tintar transiciones de eventos
(`[data-event^=dismiss]`).
- **`data-{component}` / `data-{component}-{part}`** — los markers
estructurales clásicos. Eidos los usa para selectores per-componente.
## 4. Modelo de componente
La forma base de soma es `Componente.Parte`:
```ts
import { Dialog } from '$soma/components';
Dialog.Provider; // root — crea contexto
Dialog.Trigger; // accion — abre/cierra
Dialog.Content; // contenido — layers integrados
Dialog.Overlay; // fondo — presence
Dialog.Title; // metadata ARIA
Dialog.Description; // metadata ARIA
Dialog.Close; // accion — cierra
```
### Provider (root)
Crea el estado central, lo registra en context, gestiona presence para content y overlay. Puede o no renderizar DOM:
- **Con DOM** (Collapsible, Accordion): usa `WithRefOpts`, renderiza `<div>`
- **Sin DOM** (Dialog, Popover): usa `ProviderOpts`, solo renderiza children
### Subcomponentes
Leen estado del root via `.require()`. No reimplementan logica — derivan props, ARIA, data-\*, events del estado del padre.
### Portal
Componente interno (`components/internal/portal.svelte`). Renderiza hijos en otro nodo DOM. El contexto de Svelte se preserva.
### Picker composition (shared state across providers)
Pickers (`DatePicker`, `DateRangePicker`, `TimePicker`, y futuros `TimeRangePicker`) no reimplementan Popover / Field / Calendar — los **componen** con estado compartido. El root wrapper crea tres (o más) Providers que apuntan a los mismos `writableActive` refs:
```ts
// Root wrapper
const sharedValue = writableActive(() => value, (v) => (value = v));
const sharedPlaceholder = writableActive(() => placeholder, (v) => (placeholder = v));
const sharedOpen = writableActive(() => open, (v) => (open = v));
{Name}PickerProvider.create({ value: sharedValue, placeholder: sharedPlaceholder, open: sharedOpen, ...config });
PopoverProvider.create({ open: sharedOpen, ... });
{Base}FieldProvider.create({ value: sharedValue, placeholder: sharedPlaceholder, ...config });
// Calendar/RangeCalendar/slider providers are created in their own wrapper (DatePicker.Calendar, TimePicker.HourSlider, …)
```
El picker expone **solo** wrappers únicos para `Provider`, `Trigger`, y el puente al calendario/slider. Los demás exports re-exportan desde los componentes compuestos — sus `data-*` nativos (`data-popover-*`, `data-date-field-*`, `data-calendar-*`, `data-slider-*`) siguen siendo la API de estilo autoritativa. El picker sólo añade atributos de identidad (`data-{picker}-trigger`, `data-{picker}-calendar`) en sus propios wrappers.
**Auto-close / auto-anchor**: el PickerProvider expone `handleSelect()` que el wrapper del calendario llama al completarse una selección. Los range pickers re-anclan `placeholder` para que el mes final caiga en la columna más a la derecha visible, evitando mostrar al usuario un mes que no contiene su selección.
Ver A27 en `COMPONENT_GUIDE.md` para el checklist completo.
## 5. Runtime parts
```ts
readonly soma = Soma.require();
readonly runtime = this.soma.runtime(accordionMorfo, {});
const runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
owner: this,
context: AccordionProvider.ctx
});
readonly props = $derived.by(() =>
runtimePart.assert({
...runtimePart.props,
'data-state': this.state
})
);
```
`SomaRuntime.part()` centraliza lo mecanico de cada parte:
```ts
interface SomaRuntimePart {
readonly attachment: RefAttachment | undefined;
readonly props: Record<string, unknown>;
resolveProps(bindings?): Record<string, unknown>;
assert<P extends Record<string, unknown>>(props: P): P;
}
```
`syncAttrs: true` activa la escritura imperativa por `uix.dom` para partes
cuyos sources ya estan declarados en el runtime. Si un provider sigue
componiendo attrs en render props, no activa `syncAttrs`.
Dos interfaces de opts:
- `ProviderOpts` — `{ id: Active<string>; ref?: State<HTMLElement | null> }` — para roots sin DOM
- `WithRefOpts` — `{ id: Active<string>; ref: State<HTMLElement | null> }` — para parts con DOM
## 6. Layers
`layers/` contiene solo clases de comportamiento (`.svelte.ts`). Son infraestructura consumida por Providers, nunca directamente por el consumidor.
### Inventario
| Layer | API | Responsabilidad |
| ----------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `Presence` | `new Presence(opts)` | Mount/unmount con animaciones. `isPresent`, `transitionAttrs`, `onComplete`. |
| `FocusScope` | `FocusScope.use(opts)` | Focus trap, loop, auto-focus, restore. Singleton manager con stack. |
| `Dismissal` | `Dismissal.use(opts)` | Escape + click-outside. Registry global. Behaviors: close, ignore, defer. |
| `TextSelection` | `TextSelection.use(opts)` | Previene selection overflow durante drag. |
| `ScrollLock` | `new ScrollLock(initial?, delay?)` | Body scroll lock con refcount. Soporta delay para animaciones. |
| `ResizeObserver$` | `new ResizeObserver$(getter, cb)` | ResizeObserver con lifecycle Svelte. |
| `Floating*` | `FloatingProvider.create()`, `FloatingContent.create(opts)`, etc. | Posicionamiento relativo a anchor via @floating-ui. |
| `Gesture.base` | `Gesture.base(opts)` | Pointer tracking + axis lock + velocity. |
| `Gesture.drag` | `Gesture.drag(opts)` | Base + progress + snap points + dismiss. |
| `Gesture.resize` | `Gesture.resize(opts)` | Base + delta + min/max constraints. |
| `SafePolygon` | `new SafePolygon(opts)` | Hover-gap corridor between trigger↔content. |
`layers/floating/placement.ts` es la fuente unica para `Side`, `Align`,
`Boundary`, `SIDE_OPTIONS` y `ALIGN_OPTIONS`. `floating/types.ts` consume esa
fuente y no importa del runtime `floating.svelte.ts`, evitando ciclos entre
tipos y clases.
### Cobertura P0 actual
Los tests por componente estan creciendo desde las piezas de mayor riesgo. A
2026-05-15 existen tests directos para:
- `$libs/datagrid/table-core.svelte.test.ts` — motor de data grid.
- `$libs/forms/form-core.svelte.test.ts` — motor de formularios + Standard Schema.
- `dialog/dialog-provider.svelte.test.ts` — open/close, fallback target y
eventos morfo.
- `drawer/drawer-provider.svelte.test.ts` — direccion logica, dismiss y modo
persistent.
- `command/command-provider.svelte.test.ts` — filtro, visibilidad, navegación
y selección.
- `combobox/combobox-provider.svelte.test.ts` — selección, `inputValue` y
navegación sin items disabled.
- `select/select-provider.svelte.test.ts` — selección single/multiple y
navegación sin items disabled.
- `popover/popover-provider.svelte.test.ts` — toggle, hover timers y señales
runtime.
- `toast/toaster.svelte.test.ts` — overflow, dismiss/remove y promesas.
- `calendar/calendar-provider.svelte.test.ts` — placeholder, meses, selección
y flags.
- `range-calendar/range-calendar-provider.svelte.test.ts` — placeholder,
orden de endpoints y limites min/max.
- `date-field/date-field-provider.svelte.test.ts` — helpers UI de segmentos,
lectura DOM, navegación por `ActiveDom`, validación y commit segmentado.
- `date-picker/date-picker-provider.svelte.test.ts` — registro runtime y
cierre controlado por `closeOnDateSelect`.
- `date-range-picker/date-range-picker-provider.svelte.test.ts` — registro
runtime y cierre controlado por `closeOnRangeSelect`.
- `time-picker/time-picker-provider.svelte.test.ts` — registro runtime,
valores de placeholder y escritura de sliders al valor compartido.
- `time-range-picker/time-range-picker-provider.svelte.test.ts` — valores por
endpoint, escritura de sliders y cierre al completar rango.
- `time-range-field/time-range-field-provider.svelte.test.ts` — validación de
orden/min/max, validación custom y foco de label via `ActiveDom`.
- `time-field/time-field-provider.svelte.test.ts` — validación, sincronización
de segmentos 12h y commit solo cuando los segmentos renderizados estan
completos.
- `date-range-field/date-range-field-provider.svelte.test.ts` — validación de
orden/min/max, validación custom y foco de label via `ActiveDom`.
- `virtual-list/virtual-list-provider.svelte.test.ts` — cálculo de ventana,
`scrollToIndex` y compensación anti-jump via `ActiveDom`.
- `virtual-grid/virtual-grid-provider.svelte.test.ts` — cálculo de ventana 2D
y `scrollToCell` via `ActiveDom`.
- `number-field/number-field-provider.svelte.test.ts` — parsing localizable,
teclado spinbutton, triggers, props ARIA y scrubber.
- `file-upload/file-upload-provider.svelte.test.ts` — aceptación/rechazo,
dropzone, input oculto, items, progress y acciones remove/clear.
- `color-field/color-field-provider.svelte.test.ts` — commit por segmentos,
edición hex por teclado, selector de formato, input oculto y foco via
`ActiveDom`.
- `color-picker/color-picker-provider.svelte.test.ts` — helpers de canales,
trigger/value/hidden, area 2D, slider de canal y swatches.
- `navigation-menu/navigation-menu-provider.svelte.test.ts` — timers UIX,
enlace item/trigger/content, foco por teclado y props de list/link.
- `tree-grid/tree-grid-provider.svelte.test.ts` — expansión, selección/rango,
filas visibles, navegación y props de row/cell/header/expand trigger.
- `dropdown-menu/dropdown-menu-provider.svelte.test.ts` — trigger open/close,
scoping de items, selección, checkbox/radio groups, submenu y group/separator.
- `context-menu/context-menu-provider.svelte.test.ts` — apertura por
`contextmenu`, anchor virtual, scoping de items, selección, checkbox/radio,
submenu y group/separator.
- `menubar/menubar-provider.svelte.test.ts` — coordinación de menús hermanos,
hover-follow, navegación horizontal y cambio de menú desde content.
- `listbox/listbox-provider.svelte.test.ts` — props ARIA root, selección,
navegación, typeahead, indicador de item y grupos.
- `tree-view/tree-view-provider.svelte.test.ts` — expansión/selección,
navegación root, typeahead, props de ramas/hojas y partes auxiliares.
- `accordion/accordion-provider.svelte.test.ts` — modos single/multiple,
eventos `open/close` por item, navegación de triggers y header/content.
- `checkbox/checkbox-provider.svelte.test.ts` — commits check/uncheck,
indeterminate, hidden input y sincronización con `CheckboxGroup`.
- `radio-group/radio-group-provider.svelte.test.ts` — roving tabindex,
selección síncrona, navegación con auto-select, hidden input y guardas.
- `switch/switch-provider.svelte.test.ts` — `commit-toggle` runtime,
keyboard, hidden input, integración con `FieldProvider` y thumb.
- `toggle/toggle-provider.svelte.test.ts` — `commit-toggle` runtime, attrs DOM
via `ActiveDom`, integración con `FieldProvider` y guardas.
- `toggle-group/toggle-group-provider.svelte.test.ts` — modos single/multiple,
roving tabindex y navegación de items saltando disabled.
- `tabs/tabs-provider.svelte.test.ts` — registros trigger/content, activacion
automatic/manual, navegacion roving, fallback tab stop y partes auxiliares.
- `slider/slider-provider.svelte.test.ts` — contrato provider/range/thumb/tick,
snapping/clamping, drag pointer confirmado y teclado de thumb.
- `progress/progress-provider.svelte.test.ts` y
`meter/meter-provider.svelte.test.ts` — `data-value/data-min/data-max`
emitidos por morfo/runtime, estados derivados e indicador sincronizado.
- `collapsible/collapsible-provider.svelte.test.ts` — refs cruzadas
trigger/content, eventos `expand/collapse` y guardas disabled.
- `clipboard/clipboard-provider.svelte.test.ts` — copia via `uix.clipboard`,
reset con `uix.timers`, labels traducidos/override y errores `onError`.
- `tooltip/tooltip-provider.svelte.test.ts` — delay/close con `uix.timers`,
skip-delay de grupo, focus/blur, disabled y partes trigger/content/arrow.
- `field/field-provider.svelte.test.ts` — wiring de partes/ARIA, input
guardado por disabled/readonly e integración con parent Form.
- `search-field/search-field-provider.svelte.test.ts` — props root/input/clear,
input/submit/clear, foco y OR-merge con Field.
- `pagination/pagination-provider.svelte.test.ts` — rangos/ellipsis, clamp
reactivo, slice, triggers prev/next, item selected y disabled global.
- `breadcrumb/breadcrumb-provider.svelte.test.ts` — label/labelledby,
separator, link current con mirror al item y ellipsis decorativo/interactivo.
- `toolbar/toolbar-provider.svelte.test.ts` — roving focus, focusin, group
single/multiple y separator.
- `stepper/stepper-provider.svelte.test.ts` — conteo/estado de steps,
attrs runtime, selección lineal/no lineal, teclado y prev/next/completed.
- `scroll-area/scroll-area-provider.svelte.test.ts` — medición de overflow,
viewport, visibilidad/timers de scrollbar, track click, thumb y corner.
- `splitter/splitter-provider.svelte.test.ts` — registro de paneles, clamp,
collapse/expand y resize por pointer/teclado con attrs ARIA/data.
- `pin-input/pin-input-provider.svelte.test.ts` — props del input oculto,
merge con Field, patrón/paste, completion y estado de celdas/caret.
- `rating-group/rating-group-provider.svelte.test.ts` — slider attrs,
merge con Field, half hover/click, clearable y teclado LTR/RTL.
- `tag-group/tag-group-provider.svelte.test.ts` — labels/root grid, roving
por DOM renderizado, selección/removal, link y remove button.
- `announce/announce-provider.svelte.test.ts` — alternancia polite/assertive,
timers UIX, regiones ARIA declarativas y uso standalone.
- `feed/feed-provider.svelte.test.ts` — attrs APG feed/article/title,
navegación PageUp/PageDown, threads anidados y sentinel IntersectionObserver.
- `tags-input/tags-input-provider.svelte.test.ts` — add/paste/blur,
navegación de tags, delete/clear triggers y props de input/control.
- `editable/editable-provider.svelte.test.ts` — activación preview/trigger,
foco via `ActiveDom`, commit/cancel por teclado/blur y guards.
- `alert-dialog/alert-dialog-provider.svelte.test.ts` — contratos
action/cancel, labels traducidos/override y cierre delegado a Dialog.
- `carousel/carousel-provider.svelte.test.ts` — detección de slides,
navegación trigger/teclado, props de partes y autoplay via `uix.timers`.
- `grid-list/grid-list-provider.svelte.test.ts` — merge con Field,
selección, navegación/typeahead, celdas focusables y checkbox de fila.
- `link-preview/link-preview-provider.svelte.test.ts` — delays open/close
con `uix.timers`, touch guard, content hover y props Floating/Arrow.
- `table/table-provider.svelte.test.ts` — root/section/header/cell props
desde `$libs/datagrid`, selección de fila y disclosure `RowDetail`.
- `form/form-provider.svelte.test.ts` — submit inválido con foco via
`ActiveDom`, attrs runtime y partes Submit/Reset/ErrorSummary.
- `toast/toast-provider.svelte.test.ts` — viewport hotkey via `ActiveDom`,
runtime por item, ARIA/parts, auto-dismiss y close action.
- `drag-drop/drag-drop-provider.svelte.test.ts` — ruta de teclado
start/nav/drop, filtros `accept`, prevención de drag, cancel y preview.
La guardia `*-provider.svelte.ts` sin test directo devuelve
`NO_MISSING_PROVIDER_TESTS`: todos los providers Soma activos tienen cobertura
directa. `table-core`, `form-core` y el scorer de Command ya no viven dentro
de Soma: se consumen desde `$libs/datagrid`, `$libs/forms` y `$libs/strings`.
### Convencion
- `.use(opts)` → lifecycle auto-gestionado (watch/$effect internos). Constructor privado.
- `new X(opts)` → lifecycle manual. El consumidor controla.
- `.props` → objeto para spread en el Provider.
### Animaciones (Presence)
Lifecycle:
```
OPENING:
open=true → shouldRender=true + data-starting-style
→ next rAF: data-starting-style removed (triggers CSS transition)
→ getAnimations().finished → onComplete(true)
CLOSING:
open=false → data-ending-style (element stays in DOM!)
→ getAnimations().finished
→ shouldRender=false + data-ending-style removed → onComplete(false)
```
- `forceMount` mantiene el elemento en DOM siempre (para transiciones CSS)
- `onComplete` usa `getAnimations()` API, no eventos `transitionend`/`animationend`
- Run ID cancellation previene callbacks stale en toggle rapido
## 7. Soma class (component runtime scope)
Soma reads `ActiveUix` from context and exposes services to components.
Components import Soma internals through relative paths, never from
`$active-app` and never through their own `$soma/*` public alias. Nestable:
child `<Soma portalTo="#modals">` overrides parent.
```ts
class Soma {
static create(opts?: SomaOptions): Soma; // factory + context set
static get(): Soma | undefined; // safe read
static require(): Soma; // throws if not found
readonly uix: ActiveUix;
readonly portalTo: string | HTMLElement | undefined;
// Service accessors (delegate to ActiveUix)
get langs(): ActiveLangs;
get nums(): ActiveNumbers | undefined;
get money(): ActiveCurrency | undefined;
get dates(): ActiveDates | undefined;
get units(): ActiveUnits | undefined;
get prefs(): ActiveUixPrefsView;
get logger(): EngineLogger;
}
```
### Service access from components
Components access services through Soma, never through App directly:
```ts
const soma = Soma.get();
soma?.langs.ts('#?common.buttons.close|Close'); // translation via idlangref
soma?.prefs.getDir(); // effective direction from uix.prefs.direction
soma?.money?.format(1099); // currency formatting
soma?.dates?.getDateOrder(); // DMY / MDY / YMD
soma?.dates?.getHourCycle(); // 12 | 24 (numeric — not '12h' / '24h')
soma?.portalTo; // portal target
```
### Date / time types and formatting
Soma imports date-related symbols from `$libs/days`, the canonical date library. Components **never** import from `$lib/util/dates` (legacy) or `@internationalized/date` directly. There is no Soma re-export façade for the date domain.
- Value types: `CalendarDate`, `CalendarDateTime`, `Time`, `ZonedDateTime`
- Types: `DateValue`, `TimeValue`, `DateRange`, `DateMatcher`, `Month`, `WeekStartsOn`, `HourCycle`, `TimeGranularity`, `DateOrder`, `Granularity`, `SegmentPart`, `EditableTimeSegmentPart`, `TimeSegmentObj`, `SegmentValueObj`, `DayPeriod`, …
- Queries: `isSameDay`, `hasTime`, `isZonedDateTime`, `isTimeBefore`, `isTimeAfter`, `today`, `now`, `startOfMonth`, `endOfMonth`, `getLastFirstDayOfWeek`, `getNextLastDayOfWeek`, …
- Operations: `dateValueToDate`, `convertTimeValueToDateValue`, `convertTimeValueToTime`, `toCalendarDate`, `toZoned`, …
- Parsing: `parseDate`, `parseDateTime`, `parseTime`
- Formatting: `DateFormatter`, `getCachedDateFormat`, `getPlaceholder`, `getDefaultDate`, `getDefaultTime`, `inferGranularity`, `inferTimeGranularity`, `getDefaultHourCycle`, `resolveDateOrder(locale)`, `resolveHourCycle(locale)`
- **Segments (dias/segments.ts)**: constants (`DATE_SEGMENT_PARTS`, `EDITABLE_TIME_SEGMENT_PARTS`, …), type guards (`isDateSegmentPart`, `isEditableTimeSegmentPart`, `isDateAndTimeSegmentObj`, …), pure helpers (`initializeSegmentValues`, `initializeTimeSegmentValues`, `getValueFromSegments`, `getTimeValueFromSegments`, `areAllSegmentsFilled`, `createSegmentContent`, `createTimeSegmentContent`, `getOptsByGranularity`, `getOptsByTimeGranularity`).
`HourCycle` is canonically the numeric form `12 | 24` across the whole framework, matching `Intl.DateTimeFormat`'s `hour12` resolved option. String forms like `'12h'`/`'24h'` are legacy and must not appear in new code.
**`soma/datetime/` holds only UI-level helpers** (screen-reader announcer, DOM segment navigation, `SegmentState` shape with `lastKeyZero`/`hasLeftFocus`/`updating`, `isAcceptableSegmentKey` using KEYS, description-element DOM writers). It must not re-export `$libs/days` symbols — consumers import from `$libs/days` directly. Extending `$libs/days` is the default for new date/time helpers; adding to `soma/datetime/` is only correct when the helper is genuinely UI-specific.
### Static method convention (project-wide)
All classes that use Svelte context follow the same pattern:
| Method | Returns | Use when |
| ---------------- | ----------------------- | --------------------------------- |
| `X.create(opts)` | instance | Creating + registering in context |
| `X.get()` | instance or `undefined` | Parent/context is optional |
| `X.require()` | instance (throws) | Parent/context is required |
This applies to `App`, `Soma`, and all state classes that use context. No
standalone functions. No `from()`. No `ctx` exposed.
### Texto funcional
Las traducciones propias del componente se declaran en el morfo:
```ts
export const drawerMorfo = {
name: 'Drawer',
kebab: 'drawer',
translations: {
trigger: { es: 'Abrir cajon', en: 'Open drawer' }
},
parts: [
{
name: 'Trigger',
kebab: 'trigger',
aria: [{ attr: 'aria-label', value: v.translationRef('trigger', 'Open drawer') }]
}
]
} as const satisfies Morfo;
```
Las traducciones compartidas no se duplican por componente:
```ts
value: v.commonRef('buttons.close', 'Close'); // #?common.buttons.close|Close
```
`ActiveUix` conecta el registro de morfos con `ActiveLangs`. Cuando el
provider crea `createSomaRuntime(morfo, sources)` o `soma.runtime(morfo,
sources)`, `registerMorfo(morfo)` registra el contrato `data-*` y publica
`morfo.translations` bajo `components.{kebab}`.
`commonLangs` en `src/uix/langs.ts` aporta los defaults de `common.*`.
`ActiveUix` los registra sin pisar hojas existentes, de modo que el
integrador puede pasar sus propias traducciones y UIX solo completa lo que
falte.
No existe catálogo global por componente. `ActiveUix` conecta el registro de
morfos; cada componente publica sus textos cuando su morfo se registra.
## 8. Sistema reactivo
Capa fina sobre runes de Svelte 5 que permite pasar estado reactivo por referencia entre clases.
- `state<T>(initial)` → `State<T>` (mutable, `.current`)
- `readableActive(() => value)` → `Active<T>` (readonly derived)
- `writableActive(getter, setter)` → `State<T>` (two-way binding)
Los wrappers `.svelte` convierten props normales a `Active`/`State` con estas funciones. Esta conversion es la frontera entre el mundo de props de Svelte y el mundo de clases reactivas de soma.
## 9. Contratos data-\*
Los `data-*` son API publica formal, validados con `assertContract()`.
Convencion:
```
data-dialog → provider (sin -provider, sin -root)
data-dialog-trigger → parte
data-dialog-content → parte
data-state="open|closed" → estado
data-disabled → flag
data-side="top|right|bottom|left" → posicion flotante
data-align="start|center|end" → alineacion
data-starting-style → animacion de entrada (1 frame)
data-ending-style → animacion de salida (persiste)
data-nested → es hijo de otro del mismo tipo
data-nested-open → tiene un hijo abierto
data-dragging → gesture drag activo
data-highlighted → item con virtual focus (aria-activedescendant)
data-resizing → splitter resize activo
```
CSS variables expuestas:
```
--floating-transform-origin
--floating-available-width
--floating-available-height
--floating-anchor-width
--floating-anchor-height
--dialog-depth
--dialog-nested-count
--drawer-progress → 0-1 drag progress
--drawer-offset-x / y → drag offset in px
--toast-swipe-move-x / y → toast swipe offset
```
## 10. IDs
Los IDs se generan con contexto de componente:
```
soma-dialog-c12
soma-dialog-trigger-c13
soma-dialog-content-c14
```
Pattern: `soma-{component}-{part}-{uid}`. Descriptivos e inspeccionables.
## 11. Barrel exports
### Componentes (jerárquico)
```ts
// $soma/components/index.ts
export * as Collapsible from './collapsible';
export * as Dialog from './dialog';
export * as Popover from './popover';
```
Consumo:
```ts
import { Dialog, Popover } from '$soma/components';
Dialog.Provider; // no SomaDialogProvider, no TerraDialogProvider
Dialog.Trigger;
```
### Internal
```ts
import { Portal, Arrow, VisuallyHidden, Soma } from '$soma/components/internal';
```
## 12. Fronteras externas
`soma` distingue entre:
- internos: `layers/`, `reactive/`, `dom/`, `provider/` — helpers propios
- externos: `@floating-ui/dom`, `runed`, `tabbable` — dependencias npm
Si una dependencia tiene API inestable o podria cambiar, se accede a traves de una frontera formal (como `layers/floating/` wrappea @floating-ui). Las dependencias estables (runed, svelte) se importan directamente.
## 13. Estructura del directorio
```
src/uix/soma/
├── SOMA_ARCHITECTURE.md ← this document
├── COMPONENT_GUIDE.md ← step-by-step implementation guide
├── README.md ← API reference
├── runtime.svelte.ts ← SomaRuntime (morfo interpreter)
├── errors.ts ← typed runtime/context errors
├── core/
│ └── soma.svelte.ts ← Soma class (root instance)
├── reactive/ ← reactive system
├── provider/ ← context + opts bridge
├── props/ ← mergeProps, composeHandlers
├── keyboard/ ← KEYS, directional
├── dom/ ← DOM utilities, focus
├── css/ ← styleToString, cssToStyleObj
├── id/ ← createId, useId
├── types/ ← shared types + service interfaces
├── layers/ ← behavior layers (classes only)
│ ├── presence.svelte.ts
│ ├── focus-scope.svelte.ts
│ ├── dismissal.svelte.ts
│ ├── text-selection.svelte.ts
│ ├── scroll-lock.svelte.ts
│ ├── resize-observer.svelte.ts
│ └── floating/
├── datetime/ ← UI-only helpers (announcer, segment DOM nav,
│ segment UI-state shapes, segment-key predicates,
│ description-element writers). NO date math,
│ NO re-exports of days — import `$libs/days`
│ directly.
├── components/
│ ├── internal/ ← Portal, Arrow, VisuallyHidden, <Soma>
│ ├── {name}/ ← each headless component
│ │ ├── {name}-provider.svelte.ts ← state classes (NOT {name}.svelte.ts)
│ │ ├── types.ts ← public props + canonical field shapes
│ │ ├── langs.ts ← optional idlangref constants for imperative strings
│ │ ├── exports.ts
│ │ ├── index.ts
│ │ └── components/
│ │ ├── {name}.svelte ← root wrapper
│ │ ├── {name}-trigger.svelte
│ │ └── ...
│ └── index.ts ← hierarchical barrel
└── index.ts ← root scope only (`Soma`)
```
### File naming convention
- State class: `{name}-provider.svelte.ts` — NOT `{name}.svelte.ts`
- Avoids Vite module resolution ambiguity with `{name}.svelte` wrapper
- Reflects what's inside: provider/state classes
- Root wrapper: `{name}.svelte` in `components/` subdirectory
- Export name: always `Provider`, never `Root`
## 14. Anti-patterns
Avoid in soma:
- Complex logic inside wrapper `.svelte` — belongs in Provider
- Props drilling when context is the correct pattern
- `data-*` attrs outside of contract
- Inventing part names without checking reference library anatomies (ark-ui, bits-ui, radix-ui)
- Nesting layers as component wrappers in templates
- Inline `z-index: auto` that overrides CSS
- Coupling primitives to app libraries
- Product copy inside the primitive
- Speculative abstractions ("just in case")
- One-line files that only re-export (merge into parent)
- Redundant naming prefixes (SomaDialog, DialogLayerState)
- Dummy refs to satisfy a type — use `ProviderOpts` for no-DOM roots
- **State class file named same as wrapper** — `select.svelte.ts` + `components/select.svelte` causes Vite module duplication. Always use `{name}-provider.svelte.ts`
- **Event handlers not in props** — defining onclick as a class method but not including it in the props derived object
- **getContext in event handlers** — getContext only works during initialization. Capture references in constructor
- **Exporting as Root** — always `Provider`, never `Root`
- **Skipping reference library comparison** — mandatory step, no exceptions
- **Comments in Spanish** — all code comments in English
- **Standalone context functions** — no `createX()`, `getX()`, `useX()` as loose functions. Use `X.create()`, `X.get()`, `X.require()` static methods
- **Importing from `$lib/ext/app`** in components — components access services through `Soma`, never App directly
- **`from()` as factory name** — use `create()` consistently
- **Re-implementing date/time helpers inside soma** — extend `$libs/days` (A23). Importing from `$lib/util/dates` (legacy vendored) or `@internationalized/date` directly is forbidden; use `$libs/days`.
- **Re-export façades over days** — a soma module whose only job is to forward `$libs/days` symbols is dead weight. Consumers import from `$libs/days` directly.
- **UI-level helpers in dias, or date math in `soma/datetime/`** — dias is pure (no DOM, no Svelte, no KEYS); `soma/datetime/` is UI-only (screen-reader announcer, DOM segment navigation, `SegmentState` shapes, KEYS-based predicates). No crossover
- **`readonlySegments` without a concrete value anchor** — A24: warn via `soma?.logger.warn` when `value` is undefined. Range components split into `startReadonlySegments` / `endReadonlySegments` (A25)
- **`keydown.preventDefault()` as the only guard on contenteditable segments** — IME/paste/drop bypass keydown. Always add `onbeforeinput: e => e.preventDefault()` (A26)
- **Time placeholders as `'––'`** — use `createSegmentContent` / `createTimeSegmentContent` from `dias/segments.ts`; time parts render as `hh`/`mm`/`ss` (A28)
- **Pickers that reimplement field/calendar/popover** — compose via shared `writableActive` refs (A27). Only `Provider`, `Trigger`, and the calendar/slider bridge are unique parts
- **Demo pages as galleries of canned snippets** — every soma demo must be an interactive testbed wiring every public prop to a live control, including a Field-integration section (A29)
- **`HourCycle` as `'12h' \| '24h'`** — canonical form is numeric `12 \| 24` (matches `Intl.DateTimeFormat.hour12`). String forms are legacy
## 15. Estado actual y deuda histórica
Soma ha pasado por varias fases. La forma actual (rama `active-uix`,
post-2026-05-08):
- **Provider inheritance dropped** — los providers ya no heredan de un
base abstracto; son clases concretas. La mecanica DOM comun vive en
`SomaRuntime.part(...)`, y los casos que necesitan eventos semanticos
usan el mismo `SomaRuntime` para `trigger`/`keydown`.
- **SomaRuntime cachea** la compilación del morfo (`compileMorfo` por
WeakMap) y registra los `effects` que sincronizan `state → attrs` vía
`dom.apply`.
- **Naming**: `Provider` (nunca `Root`); child providers referencian al
padre como `provider`, nunca `root`. El export de un componente
multi-parte sigue la forma compound `Toggle.Provider + Toggle.Trigger
- ...`.
- **Data-attr naming**: `data-{component}` (provider) y
`data-{component}-{kebab}` (sub-parts). Sin prefijo `data-soma-*`. El
compiler emite estos via `compiled.parts.attrs`.
- **State files**: `{name}-provider.svelte.ts` (explicit, no ambiguity).
- **IDs**: descriptive (`soma-dialog-trigger-c13`).
## 16. Regla de estabilidad
Un componente de soma se considera estable cuando:
- su API publica esta clara y documentada con JSDoc
- sus `data-*` estan registrados y validados con `assertContract`
- el wrapper y el Provider siguen el patron general
- su accesibilidad base esta resuelta (ARIA, roles, keyboard)
- sus props se han comparado con ark-ui, bits-ui y radix-ui
- tiene demo page funcional en `web/routes/uix/components/{componente}/`
- no depende de hacks locales, z-index hardcodeados, ni demo CSS para sostenerse
- compila con 0 errores (`svelte-check`)
## 17. New component checklist
See `COMPONENT_GUIDE.md` for the full step-by-step process (27 general steps + 4 date/time specific, with rules A1–A29). Summary:
```
[ ] 1. Compare with ark-ui, bits-ui, radix-ui — feature table
[ ] 2. Verify membership criteria
[ ] 3. Define parts + attrs + morfo.translations when the component owns text
[ ] 4. Create types.ts (props + canonical field shapes)
[ ] 5. Create langs.ts only for imperative idlangref constants, not as the catalog
[ ] 6. Create {name}-provider.svelte.ts (concrete state classes, no Provider inheritance)
[ ] 7. Create wrapper .svelte files (thin)
[ ] 8. Create exports.ts + index.ts
[ ] 9. Create interactive demo page + link in index (A29)
[ ] 10. README.md with anatomy, ARIA, data-attrs, comparison table
[ ] 11. svelte-check + test in browser
[ ] 12. Date/time components: only consume date/time domain via `$libs/days`,
`onbeforeinput` on contenteditable, readonly-without-value warning,
picker composition pattern (A23–A28)
```

Powered by TurnKey Linux.