# soma Librería headless de componentes compuestos para Svelte 5. Capa de comportamiento dentro de UIX — emite los `data-*` y `aria-*` que el contrato declara en `morfo`, gestiona estado y eventos, y delega lo visual a `eidos` mediante el DOM. > **Doctrina del API**: soma mantiene la forma compound (`Toggle.Provider`, > `Tabs.Root + Tabs.Trigger + ...`) por simetría con los multi-parte. La > forma flat (``) la expone `eidos` para componentes single-part. > Convención 10 en [`src/docs/sema-implementation-guide.md`](../../docs/sema-implementation-guide.md). --- ## 1. Proposito soma resuelve **behavior, accesibilidad, composicion y estado** para componentes compuestos. No resuelve presentacion visual — eso es responsabilidad de eidos. soma existe para: - keyboard navigation entre partes de un componente - focus management (trap, scope, roving) - ARIA relationships entre partes (trigger↔content, tab↔panel) - floating/positioning de overlays - portal rendering - presence management (enter/exit animations) - dismiss on outside click / Escape - gesture tracking (drag, swipe, resize) - state machines para componentes con multiples estados - form integration (hidden inputs, validation context) soma NO existe para: - colores, tipografia, espaciado, animaciones visuales - tokens de tema - responsive design - iconografia - componentes de una sola parte sin behavior complejo --- ## 2. Criterio de pertenencia Un componente pertenece a soma si cumple **ambos** criterios: ### Composicion de partes El componente tiene 2 o mas subcomponentes que se comunican via context. Ejemplo: Accordion tiene Root, Item, Trigger, Content — cada parte lee estado del padre. ### Behavior complejo El componente implementa al menos uno de: - Keyboard navigation no trivial (roving focus, arrow keys, typeahead) - Focus management (trap, scope, restore) - Floating positioning (popover, tooltip, dropdown) - ARIA relationships que requieren IDs cruzados (aria-controls, aria-labelledby) - State machine con transiciones (open/closed, editing/preview) - Drag/gesture behavior (slider, splitter, drawer, toast) - Form integration via context (validation state, hidden inputs) Si un componente cumple solo uno de los criterios o ninguno, no necesita pasar por soma — su lógica puede vivir directamente en el wrapper de eidos. --- ## 3. Independencia soma solo depende de: - svelte (runes: $state, $derived, $effect) - runed (Context, watch) - clsx (class merging) - @floating-ui (positioning) - `$libs/reactive`, `$libs/days`, etc. — utilidades puras del repo (no façades) - `$uix/morfo` — el contrato cross-layer (compileMorfo + SomaRuntime) - `$uix/sema` — vocabulario semántico + EngineSemantic soma NO depende de eidos. La capa visual lee del DOM y de los tipos públicos del soma; la dirección del acoplamiento es eidos → soma, no al revés. Todo lo demas vive dentro de `src/uix/soma/`. ### Imports **Dentro de soma** (provider files, layer files, utility files): usar **paths relativos**. Hace la libreria portable sin forzar aliases en el build del consumidor. ```ts // Inside soma — relative import { DRAWER_LANGS } from './langs'; import type { DrawerSide } from './types'; import { Presence } from '../../layers/presence.svelte'; ``` **Consumidores** (layouts, app code, test pages): usan el alias `$soma/` que configuran en su build. ```ts // Consumer code — alias import { componentLangs } from '$soma/core/langs'; import * as Drawer from '$soma/components/drawer'; ``` soma no importa de `$lib` — cualquier utilidad que necesite (e.g., funciones triviales) debe vivir dentro de soma. soma **sí importa** del paquete hermano **morfo** (`$uix/morfo`), que es el contrato declarativo de la superficie DOM de cada componente (partes, data-attrs, ARIA, keyboard, focus). Ver §4b. --- ## 4. Estructura ``` src/uix/soma/ │ ├── README.md │ ├── reactive/ ← sistema reactivo (propio, sin deps externas) │ ├── reactive.svelte.ts ← state, readableActive, writableActive, autoReset │ └── index.ts │ ├── provider/ ← Provider base class + context │ ├── provider.svelte.ts ← Provider base class │ ├── context.ts ← context(name) — wraps runed Context │ └── index.ts │ ├── props/ ← prop merging │ ├── merge-props.ts ← mergeProps(...sources) │ ├── compose-handlers.ts │ └── index.ts │ ├── attrs/ ← data-* system (consumes morfo — see §4b) │ ├── create-attrs.ts ← createAttrs(morfo) — literal-typed attr map │ ├── contracts.ts ← registerContract(morfo), assertContract │ ├── helpers.ts ← boolToStr, boolToEmptyStrOrUndef, etc. │ └── index.ts │ ├── keyboard/ ← keyboard system │ ├── keys.ts ← KEYS constant │ ├── directional.ts ← getDirectionalKeys │ ├── is-using-keyboard.svelte.ts │ └── index.ts │ ├── dom/ ← DOM utilities │ ├── core.ts ← isHTMLElement, contains, getDocument, etc. │ ├── env.ts ← isBrowser, isIOS, isTouch │ ├── context.svelte.ts ← DOMContext class │ ├── focus/ ← focus utilities │ │ ├── focus.ts ← focus(), focusFirst(), focusWithoutScroll() │ │ ├── tabbable.ts ← getTabbableCandidates(), edges │ │ ├── roving-focus.svelte.ts ← RovingFocusGroup class │ │ ├── arrow-nav.ts ← useArrowNavigation() │ │ └── guards.ts ← isElementHidden, isFocusVisible │ └── index.ts │ ├── events/ ← event system │ ├── types.ts ← EventCallback, typed helpers │ ├── add-event-listener.ts │ └── index.ts │ ├── css/ ← style utilities │ ├── parse.ts ← cssToStyleObj, styleToString │ ├── sr-only.ts ← srOnlyStyles │ └── index.ts │ ├── id/ ← ID generation │ ├── create-id.ts │ └── index.ts │ ├── types/ ← shared types │ ├── component.ts ← WithChild, WithRefOpts, Orientation, Direction │ ├── events.ts ← SomaEvent, SomaKeyboardEvent, SomaMouseEvent │ ├── html.ts ← PrimitiveDivAttributes, PrimitiveButtonAttributes │ ├── guards.ts ← isNull, isFunction, isNumberString │ └── index.ts │ ├── layers/ ← behavior layers (ver §10) │ ├── presence.svelte.ts ← animation-aware mount/unmount │ ├── focus-scope.svelte.ts ← focus trap, auto-focus, restore │ ├── dismissal.svelte.ts ← escape + interact-outside (merged) │ ├── text-selection.svelte.ts ← prevenir selection overflow │ ├── scroll-lock.svelte.ts ← body scroll lock con refcount │ ├── resize-observer.svelte.ts ← ResizeObserver con lifecycle Svelte │ ├── floating/ ← posicionamiento relativo a anchor (@floating-ui) │ │ ├── floating.svelte.ts │ │ ├── use-floating.svelte.ts │ │ ├── safe-polygon.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ └── index.ts │ ├── gesture/ ← drag/swipe/resize gesture tracking │ │ ├── gesture.svelte.ts ← Gesture.base(), Gesture.drag(), Gesture.resize() │ │ ├── velocity.ts ← ring buffer + velocity calculation (pure) │ │ ├── types.ts │ │ └── index.ts │ └── index.ts │ ├── core/ │ ├── soma.svelte.ts ← Soma class (root instance, service accessors) │ └── langs.ts ← legacy componentLangs catalog during morfo migration │ ├── components/ │ ├── internal/ ← componentes Svelte internos de soma │ │ ├── arrow.svelte │ │ ├── visually-hidden.svelte │ │ ├── portal.svelte │ │ ├── portal-consumer.svelte │ │ └── index.ts │ │ │ └── [componente]/ ← each headless component │ ├── [comp]-provider.svelte.ts ← Provider subclasses │ ├── types.ts ← public props + canonical field shapes │ ├── langs.ts ← optional idlangref constants for imperative strings │ ├── components/ ← svelte wrappers │ ├── exports.ts ← barrel (Provider, not Root) │ └── index.ts │ ├── exports.ts ← barrel principal └── index.ts ``` --- ## 4b. Morfo — contrato declarativo cross-layer Cada componente tiene un archivo en `src/uix/morfo/components/{kebab}.ts` que declara, en un único objeto tipado, la **superficie DOM pública** del componente: - **parts** — el árbol de partes (name, kebab, kind, defaultElement, role, states, supportsNesting). - **data** — qué data-attrs emite cada parte, con valores enum cuando aplica y severity (`required` / `recommended` / `optional`). - **aria** — qué atributos ARIA emite cada parte, con la fuente del valor tipada vía tagged union (`v.literal`, `v.stateRef`, `v.partRef`, `v.propRef`, `v.translationRef`) y condición de emisión opcional. - **keyboard** — los atajos de teclado relevantes por parte. - **focus** — política de foco para overlays (`initial`, `trap`, `return`, `restore`). - **langs** — catálogo de traducciones propio del componente, registrado bajo `components.{kebab}`. - **apg** — URL al patrón WAI-ARIA APG cuando aplica. - **scope** — las capas que implementan el componente: `['soma']`, `['soma', 'eidos']`, etc. El morfo es la **única fuente de verdad** del contrato público. soma, air, eidos, sema y la docs auto-generada lo consumen todos. ### ¿Por qué morfo existe? Antes de morfo, la información estructural de un componente vivía en seis sitios: 1. `createAttrs({ parts: [...] })` — nombres de partes dentro del provider. 2. `registerContract({ parts: {...} })` — enums de data-attrs en el provider. 3. ARIA hard-coded en cada `$derived.by(...)` de props. 4. Keyboard handlers distribuidos por el provider. 5. Prose en el README. 6. Selectores en CSS de air/eidos, `.csem` de sema y tablas de docs. Renombrar una parte (`content` → `panel`) tocaba 6+ sitios sin verificación automática. Drift cross-layer (soma emite `data-dialog-content`, eidos estiliza `data-dialog-panel`) era silencioso. Con morfo, **todo se declara una sola vez**. `createAttrs`, `compileMorfo`, `registerMorfo` y `SomaRuntime` consumen el morfo directamente. El script `npm run morfo:check` valida el DOM real contra la declaración en CI. ### Cómo soma consume un morfo Cada provider raíz del componente empieza así: ```ts import { createAttrs } from '$uix/morfo'; import { dialogMorfo } from '../../../morfo/components/dialog'; const attrs = createAttrs(dialogMorfo); // typed: { provider: 'data-dialog', trigger: 'data-dialog-trigger', ... } ``` `createAttrs` es **genérica con `const` type parameter** (TS 5.0+): infiere el tipo del objeto retornado a partir de la forma literal del morfo. Si un wrapper o provider escribe `attrs.trigerr`, es error de compilación. El soporte de autocomplete lista las partes válidas. El registro ejecutable ocurre al crear el runtime: `createSomaRuntime(morfo, sources)` y `uix.runtime(morfo, sources)` llaman internamente a `registerMorfo(morfo)`. Ese registro compila el morfo, registra su contrato `data-*` y publica `morfo.langs` en los `ActiveLang` conectados por `ActiveUix`. **Requisito de autoría**: cada morfo se declara como `as const satisfies Morfo`: ```ts // ✅ Obligatorio export const dialogMorfo = { ... } as const satisfies Morfo; // ❌ Pierde literales, degrada `createAttrs` a `Record` export const dialogMorfo: Morfo = { ... }; ``` Ver el dev guide completo en [`src/uix/morfo/README.md`](../morfo/README.md). ### Validación Tres capas atrapan tres clases de drift: - **Schema (build/dev)**: `validateMorfo(morfo)` en `src/uix/morfo/schema.ts` valida shape (sium) + invariantes cruzados (kebabs únicos, `partRef.target` resuelve, `stateRef.state` está en `states[]`, etc.). - **Strict mode en `assertContract` (dev runtime)**: valida que los data-attrs emitidos por el provider tienen valores declarados en el morfo. Logs warnings en consola cuando algo se desvía. - **CI**: `npm run morfo:check` navega a cada demo y valida el DOM real contra el morfo; `npm run morfo:vocabulary` detecta divergencias de vocabulario canónico (`open|closed`, `active|inactive`, etc.). ### Qué NO va en morfo | No | Va en | |----|-------| | Props del componente | `{component}/types.ts` con JSDoc | | Summary, comparativa, ejemplos | `{component}/README.md` | | Traducciones shared/common | app/lang catalog bajo `common.*` | | Constantes idlangref imperativas | `{component}/langs.ts`, opcional | | Event handlers, state machines | `{component}-provider.svelte.ts` | | Recetas visuales | `src/uix/eidos/` (futuro) | --- ## 5. Sistema reactivo Los runes de Svelte 5 (`$state`, `$derived`) son compiler magic — solo funcionan en archivos `.svelte` y `.svelte.ts`, y no se pueden pasar como valores entre clases o funciones en TypeScript puro. soma necesita exactamente eso: pasar estado reactivo como argumento de constructor para componer Providers, layers y gestures. La capa `reactive/` resuelve esto con contenedores (`Active`, `State`) que envuelven runes y exponen `.current` — el mismo patrón que React refs y Solid signals. Cuando Svelte ofrezca signals exportables de primera clase, esta capa se convierte en un alias thin migrable sin tocar los consumidores. ### Primitivas ```ts // Mutable container — backed by $state const count = state(0); count.current++; // Readonly derived — backed by $derived.by const double = readableActive(() => count.current * 2); double.current; // 2 // Writable derived — getter + setter (para two-way binding) const bound = writableActive( () => pressed, (v) => (pressed = v) ); ``` ### Tipos ```ts // Readonly reactive container type Active = { readonly current: T }; // Mutable reactive container type State = { current: T }; // Mapping helpers — wraps all properties type ActiveProps = { [K in keyof T]: Active }; type StateProps = { [K in keyof T]: State }; ``` ### Regla Los providers de soma reciben sus opciones como `Active` o `State`. Los wrappers svelte convierten props normales a Active/State con `readableActive`/`writableActive`. Esta conversion es la frontera entre el mundo de props de Svelte y el mundo de clases reactivas de soma. --- ## 6. Sistema de contexto Hay dos niveles: la funcion `context()` (interna) y los static methods del Provider (API publica). ### Nivel interno — `context()` ```ts import { context } from '../provider'; const ctx = context('Accordion'); ctx.set(instance); // registra en Svelte context ctx.get(); // lee — throws si no existe ctx.getOr(fallback); // lee con fallback ``` `context()` wraps runed's `Context` con mensajes de error descriptivos. ### Nivel publico — static methods Los consumidores (sub-parts, otros componentes) nunca tocan `ctx` directamente. Usan los static methods: ```ts // Root provider registra via create() static create(opts) { return new AccordionProvider(opts); // super() llama ctx.set(this) automaticamente } // Sub-parts leen via require() o get() this.provider = AccordionProvider.require(); // throws this.group = TooltipGroupProvider.get(); // undefined si no existe ``` | Method | Returns | Use | | ------------ | ---------------------- | ----------------------- | | `create()` | instance | Root crea + registra | | `get()` | instance \| undefined | Padre opcional | | `require()` | instance (throws) | Padre obligatorio | --- ## 7. Provider Base class de la que heredan todos los componentes headless de soma. ```ts import { Provider, context, type WithRefOpts } from '../provider'; export class AccordionProvider extends Provider { static readonly ctx = context('Accordion'); static get() { return this.ctx.getOr(undefined) as AccordionProvider | undefined; } static require() { return this.ctx.get(); } static create(opts: AccordionOpts) { return new AccordionProvider(opts); } private constructor(opts: AccordionOpts) { super(opts, 'Accordion', 'provider', attrs.provider, AccordionProvider.ctx); } readonly props = $derived.by(() => this.assertProps({ ...this.baseProps, 'data-orientation': this.opts.orientation?.current, 'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current) } as const) ); } ``` ### Lo que Provider proporciona ```ts abstract class Provider { readonly opts: TOpts; readonly attachment: RefAttachment; constructor(opts, component, part, partAttr, ctx?, onRefChange?); protected get baseProps(): { id, [partAttr], ...attachment }; protected assertProps

(props: P): P; // validates data-* contract } ``` ### Lo que cada componente define - `static ctx` — solo si usa context - `static create()` / `get()` / `require()` — static method convention - Constructor — llama `super()` con component/part info - `readonly props = $derived.by(...)` — ARIA, data-\*, events - Event handlers — logica de interaccion - Derived state — valores computados --- ## 8. Sistema de attrs ```ts import { accordionMorfo } from '$uix/morfo/components/accordion'; const attrs = createAttrs(accordionMorfo); attrs.provider; // "data-accordion" attrs.item; // "data-accordion-item" attrs.trigger; // "data-accordion-trigger" // Selector: `[${attrs.content}]` ``` ### Regla de naming - La parte `provider` genera `data-{component}` (sin sufijo `-provider`) - Las demas partes generan `data-{component}-{part}` - Estos attrs son API publica — cambiarlos es breaking change - air/eidos los usa como selectores CSS ### Contracts (via morfo) El morfo declara cada parte y sus `data-*` attrs (`required`/`recommended`/`optional`, con valores enumerados cuando aplica). `assertProps()` valida en desarrollo que los atributos emitidos cumplen el morfo. `scripts/morfo-check.ts` valida contra el DOM real. ### Boolean helpers ```ts boolToStr(true); // 'true' boolToEmptyStrOrUndef(true); // '' boolToEmptyStrOrUndef(false); // undefined boolToTrueOrUndef(true); // true boolToTrueOrUndef(false); // undefined ``` --- ## 9. mergeProps ```ts const merged = mergeProps(restProps, state.props); ``` Comportamiento: - Event handlers (`onclick`, `onfocus`, etc.) → compuestos con `composeHandlers` - `class` → merged con clsx - `style` → merged (object + string) - `hidden: false` / `disabled: false` → eliminados (fix Svelte) - Resto → last wins --- ## 10. Layers Los layers son infraestructura compartida que los componentes compuestos usan para resolver problemas transversales. ### Principio `layers/` contiene **solo clases de comportamiento** (`.svelte.ts`). Son infraestructura consumida por Providers. Los componentes Svelte internos de soma (Portal, Arrow, VisuallyHidden) viven en `components/internal/` — no son layers. ### Behavior layers — consumo en Provider Los behavior layers se instancian dentro del Provider y exponen `.props` para merge: ```ts class DrawerContentProvider extends Provider { readonly focusScope = FocusScope.use({ ref, trap, ... }); readonly dismissal = Dismissal.use({ ref, onEscapeKeydown, ... }); readonly scrollLock = new ScrollLock(); readonly gesture = Gesture.drag({ ref, direction, threshold, ... }); readonly props = $derived.by(() => this.assertProps({ ...this.baseProps, ...this.focusScope.props, ...this.dismissal.props, ...this.gesture.props, role: 'dialog', })); } ``` ### Convencion `.use()` vs `new` vs `Gesture.x()` - `X.use(opts)` — el layer gestiona su propio lifecycle via `watch`/`$effect`. Constructor privado. - `new X(opts)` — el consumidor controla el lifecycle. - `Gesture.base/drag/resize(opts)` — factory functions, tres especializaciones. | Layer | API | Props | Descripcion | | ----------------- | --------------------------------- | ----------------------------------- | ---------------------------------------------------- | | `Presence` | `new Presence(opts)` | — | Animation-aware mount/unmount. Expone `isPresent`. | | `FocusScope` | `FocusScope.use(opts)` | `{ tabindex: -1 }` | Focus trap, loop, auto-focus, restore. | | `Dismissal` | `Dismissal.use(opts)` | `{ onfocuscapture, onblurcapture }` | Escape + interact-outside. Global registry. | | `TextSelection` | `TextSelection.use(opts)` | — | Previene selection overflow durante drag. | | `ScrollLock` | `new ScrollLock()` | — | Body scroll lock con refcount. | | `ResizeObserver$` | `new ResizeObserver$(getter, cb)` | — | ResizeObserver con lifecycle Svelte. | | `Gesture.base` | `Gesture.base(opts)` | `{ onpointerdown }` | Pointer tracking + axis lock + velocity. | | `Gesture.drag` | `Gesture.drag(opts)` | `{ onpointerdown }` | Base + progress + snap points + dismiss. | | `Gesture.resize` | `Gesture.resize(opts)` | `{ onpointerdown }` | Base + delta + min/max constraints. | | `SafePolygon` | `new SafePolygon(opts)` | — | Hover-gap corridor between trigger↔content. | ### Gesture layer Three specializations, shared base. The layer measures — the component decides what it means. ``` Gesture.base() → pointer tracking + axis lock + velocity + cancel Gesture.drag() → base + offset + progress + snap points + threshold + dismiss Gesture.resize() → base + delta forwarding + min/max constraints ``` Key rules: - `setPointerCapture` deferred until moveBuffer exceeded for containers with child buttons (Drawer, Slider). Pure drag handles (Splitter) capture immediately — the handle IS the drag target - Cleanup on unmount: `$effect(() => { return () => { this.gesture.cancel(); }; })` - CSS vars (`--drawer-progress`, `--drawer-offset-x/y`) set by provider, consumed by visual layer - During drag: `transition: none` + inline `transform` for immediate feedback - Design: `src/uix/soma/layers/GESTURES.md` ### SafePolygon Handles pointer gap between trigger and floating content (DropdownMenu submenus, Tooltip). Calculates a corridor polygon — pointer can traverse the gap without closing. Used by any component where trigger and content have physical separation. ### Patterns **Registry over DOM queries**: Sub-parts register in a Map on mount, unregister on unmount. Keyboard nav iterates the registry instead of `querySelectorAll`. Used by Stepper triggers, Select labels. **Virtual focus (`aria-activedescendant`)**: For Select, Combobox — focus stays on trigger, items highlighted via `data-highlighted`. DOM focus (roving tabindex) for Menu, Toolbar, RadioGroup. **Exit animation via Presence**: Toast, Drawer — dismiss marks element as `dismissing`, Presence animates exit, `onComplete` removes from array. ### Naming - Archivo = concepto: `dismissal.svelte.ts`, `presence.svelte.ts` - Clase = concepto: `Dismissal`, `Presence`, `FocusScope`, `ScrollLock`, `Gesture` - Opts type: `{Concepto}Opts`: `DismissalOpts`, `GestureBaseOpts` - Sin sufijos redundantes: no `Layer`, no `State`, no `Body` --- ## 11. Keyboard ```ts KEYS.ENTER; // 'Enter' KEYS.ESCAPE; // 'Escape' KEYS.ARROW_DOWN; // 'ArrowDown' KEYS.SPACE; // ' ' const { nextKey, prevKey } = getDirectionalKeys('ltr', 'horizontal'); // nextKey: 'ArrowRight', prevKey: 'ArrowLeft' IsUsingKeyboard.current; // boolean — teclado vs pointer ``` --- ## 12. DOM utilities ### Focus ```ts focusWithoutScroll(element) focusFirst(candidates[]) getTabbableCandidates(container) getTabbableEdges(container) ``` ### Roving Focus ```ts const roving = new RovingFocusGroup({ candidateAttr: attrs.trigger, rootNode: ref, loop: true, orientation: 'horizontal' }); roving.handleKeydown(currentElement, event); ``` ### Scroll Lock ```ts const lock = new ScrollLock(); lock.locked.current = true; // locks body scroll lock.locked.current = false; // restores ``` --- ## 13. Soma (root instance) Soma es la identidad runtime del framework. Lee servicios de App via context. No inyecta traducciones — el consumidor las carga. ### Clase ```ts class Soma { static create(opts?: SomaOptions): Soma; // crea y registra en context static get(): Soma | undefined; // lee del context (safe) static require(): Soma; // throws si no existe readonly app: App; readonly portalTo: string | HTMLElement | undefined; // Service accessors (delegan a 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; } ``` ### Traducciones El catálogo nuevo de traducciones de componente vive en el morfo: ```ts export const drawerMorfo = { name: 'Drawer', kebab: 'drawer', langs: { trigger: { es: 'Abrir cajon', en: 'Open drawer' } }, // ... } as const satisfies Morfo; ``` `ActiveUix` conecta el registro de morfos con `ActiveLang`: cuando un provider crea `createSomaRuntime(morfo, sources)` o llama `uix.runtime(morfo, sources)`, el morfo se registra y sus `langs` se extienden bajo `components.{kebab}`. `commonLangs` en `core/langs.ts` aporta los defaults para `v.commonRef(...)` (`common.buttons.close`, `common.buttons.cancel`, etc.). El integrador puede predefinir sus propias claves en el schema de lang; `ActiveUix` solo rellena las que falten. `componentLangs` en `core/langs.ts` sigue existiendo como catálogo legado de transición. Mientras convive con `morfo.langs`, `ActiveUix` evita re-registrar desde morfo los componentes que ya están cubiertos por ese catálogo para no duplicar hojas. Namespace structure: ``` common.buttons.close ← shared (soma + eidos + app) common.buttons.open components.dialog.trigger ← component-specific components.drawer.trigger ``` ### Uso en Providers ```ts // drawer/langs.ts — optional constants for imperative strings export const DRAWER_LANGS = { CLOSE: '#?common.buttons.close|Close', } as const; // drawer-provider.svelte.ts import { DRAWER_LANGS } from './langs'; class DrawerCloseProvider { readonly props = $derived.by(() => ({ 'aria-label': this.provider.soma?.langs.ts(DRAWER_LANGS.CLOSE), })); } ``` Rules: - `langs.ts()` for simple strings, `langs.t()` only for interpolated templates - Fallback inline via idlangref (`#?path|fallback`), never `?? 'fallback'` - No `translate()` helpers in providers - Text owned by the component goes in `morfo.langs` and is referenced with `v.translationRef` - Common keys (`close`, `open`, `cancel`) use `common.*` / `v.commonRef`, not per-component duplicates ### Setup ```svelte ``` --- ## 14. Naming conventions ### Clases - `AccordionProvider` — root provider del componente - `AccordionItemProvider` — sub-parte - `AccordionTriggerProvider` — sub-parte - `Provider` — base class abstracta ### Files - `accordion-provider.svelte.ts` — Provider subclasses (NOT `accordion.svelte.ts`) - `accordion.svelte` — root wrapper (in `components/`) - `accordion-item.svelte` — sub-part wrapper - `types.ts` — public props + canonical field shapes - `langs.ts` — optional idlangref constants for imperative provider strings - `exports.ts` — barrel (exports `Provider`, not `Root`) ### Props - `AccordionProps` not `AccordionProviderProps` (root doesn't need "Provider" suffix in type name) - `AccordionItemProps` — sub-part - `AccordionTriggerProps` — sub-part ### Documentacion de props (norma obligatoria) Todas las props de todos los componentes deben documentarse con JSDoc en `types.ts`. Cada prop lleva: descripcion, `@default` si tiene valor por defecto, notas de comportamiento si aplica. ### Comparacion con librerias de referencia Cada componente headless debe compararse con su equivalente en ark-ui, bits-ui y radix-ui. Documentar: que props tienen ellos que soma no, y justificar si se omiten o se incluyen. ### Attrs - `data-accordion` — root (sin `-root`) - `data-accordion-item` — sub-parte - `data-state` — estado compartido (`open`/`closed`, `on`/`off`, `checked`/`unchecked`) - `data-disabled` — flag de disabled - `data-orientation` — orientacion - `data-dragging` — gesture activo ### Events - DOM handlers: `onclick`, `onkeydown`, `onfocus` (lowercase, Svelte convention) - User callbacks: `onOpenChange`, `onValueChange` (camelCase) ### Layers - Archivo = concepto: `dismissal.svelte.ts`, `presence.svelte.ts` - Clase = concepto: `Dismissal`, `Presence`, `FocusScope`, `ScrollLock`, `Gesture` - Opts = `{Concepto}Opts`: `DismissalOpts`, `GestureBaseOpts` - Sin sufijos redundantes --- ## 15. Patron de componente ### Provider ({name}-provider.svelte.ts) ```ts import { accordionMorfo } from '$uix/morfo/components/accordion'; const attrs = createAttrs(accordionMorfo); // Canonical field shapes defined in types.ts, referenced here interface AccordionOpts extends WithRefOpts, StateProps, ActiveProps {} export class AccordionProvider extends Provider { static readonly ctx = context('Accordion'); static get() { return this.ctx.getOr(undefined) as AccordionProvider | undefined; } static require() { return this.ctx.get(); } static create(opts: AccordionOpts) { return new AccordionProvider(opts); } private constructor(opts: AccordionOpts) { super(opts, 'Accordion', 'provider', attrs.provider, AccordionProvider.ctx); } readonly props = $derived.by(() => this.assertProps({ ...this.baseProps, 'data-orientation': this.opts.orientation?.current, 'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current) } as const) ); } ``` ### Wrapper svelte ({name}.svelte) ```svelte {#if child} {@render child({ props: mergedProps })} {:else}

{@render children?.()}
{/if} ``` --- ## 16. Patron de composicion Los componentes compuestos siguen el patron Provider → Parts con contexto: ```svelte Click me Content here ``` ### Flujo de datos ``` Provider ├── crea AccordionProvider ├── registra en context via super() → ctx.set(this) └── children ├── Item │ ├── crea AccordionItemProvider │ ├── lee AccordionProvider via AccordionProvider.require() │ └── children │ ├── Trigger → lee AccordionItemProvider.require() │ └── Content → lee AccordionItemProvider.require() └── Item └── ... ``` ### Regla de context - Root siempre registra en context via `super()` (pasa `ctx` al constructor de Provider) - Sub-parts leen con `XProvider.require()` (obligatorio) o `XProvider.get()` (opcional) - Si una sub-part tiene hijos que necesitan su estado, crea su propio context (Item tiene ctx, Trigger lo lee) - El context es por componente instance — multiples Accordion en la misma pagina funcionan independientemente --- ## 17. Relacion con air/eidos ``` soma → headless behavior, accesibilidad, data-* contracts, context air → visual layer: tokens, CSS recipes, sizes, variants eidos → enhanced visual layer: motion, sound, advanced interactions ``` air/eidos consume soma: - Importa componentes: `import { Accordion } from '$uix/soma'` - Responde a data-\*: `[data-accordion][data-state='open'] { ... }` - Añade props visuales: `size`, `variant`, `color` - Usa mismas translations: `#?common.buttons.close|Close`, `#?components.dialog.trigger|Open dialog` air/eidos NUNCA: - Importa Provider classes internas de soma - Depende de estructura DOM incidental - Accede a propiedades privadas - Duplica behavior que soma ya resuelve --- ## 18. Checklist de componente nuevo Referencia completa con todos los pasos en `COMPONENT_GUIDE.md`. Resumen: ``` [ ] 1. Comparar con ark-ui, bits-ui, radix-ui — feature table [ ] 2. Verificar criterio de pertenencia [ ] 3. Definir partes + attrs + contract [ ] 4. Crear types.ts (props + canonical field shapes) [ ] 5. Añadir `morfo.langs` para texto propio; `langs.ts` solo si hacen falta constantes imperativas [ ] 6. Crear {name}-provider.svelte.ts (Provider subclasses) [ ] 7. Crear wrappers .svelte (thin) [ ] 8. Crear exports.ts + index.ts [ ] 9. Crear test page + link en index [ ] 10. svelte-check + test in browser ``` --- ## 19. Inventory ### Implemented Accordion, Checkbox (Group), Collapsible, Combobox, ContextMenu, Dialog (AlertDialog variant), Drawer, DropdownMenu, Editable, LinkPreview, NumberField, Pagination, Popover, RadioGroup, ScrollArea, Select, Slider, Splitter, Stepper, Switch, Tabs, Table, TagsInput, Toast, Toggle (Group), Toolbar, Tooltip, TreeView. ### Planned **Tier 2 (menu + form):** Menubar, Command, Field/Form, FileUpload **Tier 3 (dates):** Calendar, RangeCalendar, DateField, DatePicker, DateRangeField, DateRangePicker **Tier 4 (time):** TimeField, TimePicker, TimeRangeField **Tier 5 (color):** ColorPicker, ColorField, ColorRangePicker, ColorRangeField **Tier 6 (sound):** SoundPicker, SoundRangePicker ### Visual-native (no soma) Avatar (eidos-native), Badge, Button, Label, Meter, Progress, Separator, Spinner, PinInput, RatingGroup, ColorPicker, AspectRatio, Typography, Layout, Icon.