# 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'`) ### 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 {content} ``` ### 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 `
` - **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; resolveProps(bindings?): Record; assert

>(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; ref?: State }` — para roots sin DOM - `WithRefOpts` — `{ id: Active; ref: State }` — 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 y navegación por `ActiveDom`. - `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. Pendiente: ampliar cobertura a los providers internos de fecha/hora de mayor tamano. `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 from `$soma`, never from `$active-app`. Nestable: child `` 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 `core/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(initial)` → `State` (mutable, `.current`) - `readableActive(() => value)` → `Active` (readonly derived) - `writableActive(getter, setter)` → `State` (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: ``` --soma-floating-transform-origin --soma-floating-available-width --soma-floating-available-height --soma-floating-anchor-width --soma-floating-anchor-height --soma-dialog-depth --soma-dialog-nested-count --drawer-progress → 0-1 drag progress --drawer-offset-x / y → drag offset in px --soma-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) │ └── langs.ts ← shared commonLangs defaults ├── 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, │ ├── {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 ← main barrel ``` ### 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/{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) ```