# Soma Architecture Documento de referencia arquitectonica para `src/uix/soma`. `soma` es la capa de primitives headless del sistema. Reemplaza a `terra` con un rediseño que elimina el boilerplate, resuelve los problemas de dispersion, y establece patrones mas claros para el desarrollador de componentes. ## 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 3 capas ``` soma → headless: behavior, accesibilidad, data-* contracts, context, servicios capa visual → apariencia: tokens, CSS, temas, motion, sound, 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 (translator, 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 y sound semanticos - 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: - `Provider` base class - `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 extiende `Provider` 2. Wrapper `.svelte` fino convierte props → Active/State 3. Props derivados via `$derived.by` + `assertProps` 4. Contexto para comunicacion padre-hijo No hay excepciones: components sin DOM usan `ProviderOpts` (ref opcional), components con DOM usan `WithRefOpts`. ### 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.baseProps, ...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.get(); const dir = this.soma?.presentation.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 exclusivamente `createAttrs({ component, parts })`. Cualquier `querySelector`, selector CSS, cadena en README o snippet debe coincidir exactamente con lo que `createAttrs` escribe en el DOM. 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 (close, cancel, etc.) se resuelve via `langs.ts()` con idlangref: `#?common.buttons.close|Close`. Cada componente define sus constantes en `langs.ts`. Las traducciones common viven en la raiz de langs, las de componente bajo `components.*`. ### 3.7 Props documentadas obligatoriamente Todas las props de todos los componentes llevan JSDoc en `types.ts`. Cada prop: descripcion, `@default`, notas de comportamiento. ### 3.8 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. ## 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): extiende `Provider`, renderiza `
` - **Sin DOM** (Dialog, Popover): extiende `Provider`, 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. ## 5. Provider base class ```ts abstract class Provider { readonly opts: S; readonly attachment: RefAttachment | undefined; protected constructor(opts, component, part, partAttr, ctx?, onRefChange?); protected get baseProps(): { id, [partAttr], ...attachment }; protected assertProps

(props: P): P; // valida data-* contract } ``` 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. | ### 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 App from context and exposes services to components. Components import from `$soma`, never from `$lib/ext/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 app: App; readonly portalTo: string | HTMLElement | undefined; // Service accessors (delegate to App) get langs(): AppLangs; get nums(): AppNums | undefined; get money(): AppMoney | undefined; get dates(): AppDates | undefined; get units(): AppUnits | undefined; get presentation(): AppPresentation; get logger(): AppLogger; } ``` ### 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?.presentation.getDir(); // 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 all date-related symbols through a single boundary: `$soma/external/dates`. That module re-exports from `$lib/util/dias`, the canonical date library. Components **never** import from `$lib/util/dates` (legacy) or `@internationalized/date` directly. - Value types: `CalendarDate`, `CalendarDateTime`, `Time`, `ZonedDateTime` - Types: `DateValue`, `DateRange`, `DateMatcher`, `Month`, `WeekStartsOn`, `HourCycle`, `DateOrder`, `Granularity` - Operations: `today`, `now`, `startOfMonth`, `endOfMonth`, `isSameDay`, `getDayOfWeek`, `getLastFirstDayOfWeek`, `getNextLastDayOfWeek`, etc. - Parsing: `parseDate`, `parseDateTime`, `parseTime` - Formatting: `DateFormatter`, `getCachedDateFormat`, `resolveDateOrder(locale)`, `resolveHourCycle(locale)` `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. ### 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 `Provider` subclasses. No standalone functions. No `from()`. No `ctx` exposed. ### Texto funcional Traducciones funcionales (close, cancel, etc.) se resuelven via `langs.ts()` con idlangref: ```ts // drawer/langs.ts export const DRAWER_LANGS = { TRIGGER: '#?components.drawer.trigger|Open drawer', CLOSE: '#?common.buttons.close|Close', } as const; // En el provider: 'aria-label': this.provider.soma?.langs.ts(DRAWER_LANGS.CLOSE), ``` Common keys (`close`, `open`, `cancel`, etc.) en `common.buttons.*`. Component-specific en `components.{name}.*`. soma exporta `componentLangs` desde `core/langs.ts` — el consumidor lo importa y extiende. soma no inyecta traducciones. ## 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 ├── core/ │ ├── soma.svelte.ts ← Soma class (root instance) │ └── langs.ts ← componentLangs (default translations) ├── reactive/ ← reactive system ├── provider/ ← Provider base class + context ├── props/ ← mergeProps, composeHandlers ├── attrs/ ← createAttrs, contracts, helpers ├── keyboard/ ← KEYS, directional ├── dom/ ← DOM utilities, focus ├── events/ ← addEventListener ├── 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/ ├── external/ │ └── dates/ ← boundary with date library ├── 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 ← idlangref constants (if has translations) │ │ ├── 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 subclasses - 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 ## 15. Improvements over terra | Aspect | Terra | Soma | | ------------- | -------------------------------- | ---------------------------------------------------- | | Layers | 5-level template nesting | Behaviors integrated in Provider | | Config | `config/` with loose functions | `Soma` class with services | | Resolvers | 6 specific functions | 1 generic `Soma.resolve()` | | Animations | Basic animationend detection | `getAnimations()` API + `data-starting/ending-style` | | Naming | Redundant prefixes (TerraDialog) | Hierarchical barrel (`Dialog.Provider`) | | Files | 28 files in 10 dirs (layers) | 13 files in 2 dirs | | Provider | No base class | `Provider` abstract with baseProps, assertProps | | Types | Scattered | `ProviderOpts` vs `WithRefOpts` | | IDs | Generic (`soma-c12`) | Descriptive (`soma-dialog-trigger-c13`) | | Format | 4 loose props in presentation | 1 namespace `SomaFormat` | | State files | `{name}.svelte.ts` (ambiguous) | `{name}-provider.svelte.ts` (explicit) | | Exports | Mixed Root/Provider | Always Provider | | Documentation | README only | Architecture + Component Guide + JSDoc | ## 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 test page funcional en `/test/soma/[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 (25 steps with A1-A22 rules). Summary: ``` [ ] 1. Compare with ark-ui, bits-ui, radix-ui — feature table [ ] 2. Verify membership criteria [ ] 3. Define parts + attrs + contract (registerContract) [ ] 4. Create types.ts (props + canonical field shapes) [ ] 5. Create langs.ts (idlangref constants, if has translations) [ ] 6. Create {name}-provider.svelte.ts (Provider subclasses) [ ] 7. Create wrapper .svelte files (thin) [ ] 8. Create exports.ts + index.ts [ ] 9. Create test page + link in index [ ] 10. svelte-check + test in browser ```