# UIX — Active Architecture > Documento vivo de la arquitectura activa de UIX: motivaciones, las cuatro > capas, cómo se articulan, qué problema resuelven, qué dejan deliberadamente > fuera. Este doc es la visión de conjunto; los READMEs por capa son la > referencia operativa. > > Última revisión arquitectónica: 2026-05-15 (rama `active-uix`). Cambios > mayores desde la revisión anterior: > > - **Auditoría P0/P1 aplicada** — `SoundChannel` usa la superficie DOM > inyectada para listeners globales, los ciclos type-only de > `sema-map/types` y `floating/types` quedan rotos, y hay tests iniciales > para los providers/motores de mayor riesgo: Dialog, Drawer, Command, > Calendar, DateField, Select, Popover, Toast y RangeCalendar. Los motores > de Table/Form/Command scoring salen de Soma hacia `libs`. > - **Pausa de coherencia arquitectónica** — contratos mínimos por módulo > auditados y fijados en `src/uix/contracts.ts`: qué necesita cada capa, > qué instancia si no se le pasa, cuándo degrada y cuándo debe fallar. > - **Provider inheritance dropped** — los providers ya no heredan de un > base compartido; son clases concretas y centralizan la mecanica > DOM con `SomaRuntime.part(...)`. > - **Eidos entro en produccion como runtime visual aplanado** — > `ActiveEidos` gestiona primitivas/themes, validacion, contrato CSS, > persistencia e inyeccion runtime cuando la app lo pide. > - **Doctrina del API cerrada** — convenciones 1-13 en > [`src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md`](../docs/GUIA_IMPLEMENTACION_SEMAUIX.md) > (single-event para operaciones instantáneas, 8 color tokens, > intent ↔ color resolution, subset por componente, root visual con > partes attached en eidos, sound prepare-time priming, etc.). > - **SoundChannel prepare-time priming** — el `AudioContext` se crea + > resume en `prepare()` cuando una señal sonora nace dentro del gesto de > usuario, evitando side-effects globales en el constructor sin reabrir > la race con la autoplay policy. > - **Sema sin slices visuales** — `motion`, `color` y `presence` salen del > mapa perceptivo; Sema resuelve `hold`, `sound` y `haptic`, y Eidos > materializa la respuesta visual desde CSS/token contract. > - **Ownership DOM P1 cerrado** — live regions, descripciones ocultas, > bloqueo de seleccion de texto, observers y acciones imperativas de foco / > scroll pasan por `ActiveDom`. --- ## 0. Contratos mínimos por módulo Qué requiere cada módulo, qué es opcional, cómo degrada y cuándo falla. Las reglas de ownership y degradación se enuncian, atemporales, en [`active-uix/README.md`](./active-uix/README.md) §"Reglas de ownership y degradación". > Fuente ejecutable: `src/uix/contracts.ts`. Test de frontera: > `src/uix/contracts.test.ts`. ```text Modulo Requiere Opcional Si falta Error active-uix langs,prefs,dom* clipboard,format,events,portal standalone disabledDom falta langs/dom en attach morfo ninguno translations no registra traducciones no soma dom events,langs,format,clipboard disabledDom desde uix morfo/event/part invalido; servicio opcional ausente sema projector/dom* sound,haptic,visual:false ninguno SemaConfigError sin dom/projector eidos dom* langs,format,prefs,mode/density sources applyDom:false falta dom con applyDom activo adom surface ActiveDom target/window/breakpoints disabledDom solo explicito errores ADom sin DOM real * `dom` significa superficie `ActiveDom`, no necesariamente DOM real. Puede ser `disabledDom` solo en standalone cuando el integrador pide `dom:false`. En attach debe venir de `ActiveApp`. * Fuera de `ActiveUix`, `EngineSemantic` con canal visual activo debe recibir `dom` o `projector`; `visual:false` es la degradacion explicita. ``` Los siguientes cambios deben derivar de esta tabla, no de constructores inventados desde capas inferiores. Correccion aplicada: `ActiveUix` no importa ni instancia `Soma`/`Eidos`. `portal` queda como setting generico de UIX; `Soma` lo consume como default para `portalTo`, y `ActiveEidos.create(...)` crea el scope visual cuando la app necesita Eidos. --- ## 0.1 Naming canonico La arquitectura puede mantener los nombres historicos de carpetas (`morfo`, `soma`, `sema`, `eidos`), pero la superficie publica debe usar una gramatica consistente. Regla general: un nombre representa un unico concepto; si un termino es alias historico, debe quedar marcado y con ruta de retirada. | Concepto | Nombre canonico | Evitar / retirar | | ------------------------------------ | ----------------------------------------- | --------------------------------------------------------- | | Servicio runtime de traducciones | `langs` | `lang` como servicio | | Idioma activo | `prefs.language` | `locale` para idioma de traducciones | | Locale / formatos regionales | `prefs.locale` | `language` para formatos | | Catalogos declarativos de texto | `translations` | `langs` dentro de `morfo`; tablas globales por componente | | Preferencias UIX | `prefs` | `settings`, `presentation` como nombre nuevo | | Eventos perceptivos UIX | `events` | `semantic` como servicio publico | | Ocurrencia en vuelo | `signal` | usarlo para la capa completa | | Payload semantico de un evento morfo | `semantic` | mezclarlo con el servicio runtime | | Contrato declarativo TS | `morfo` | `contract` como API TS duplicada | | Contrato CSS/data exportado | `contract` | `morfo` para CSS externo | | Bridge CSS runtime | `ActiveEidos` | runtime visual obligatorio de componentes | | Engine puro independiente | `EngineX` solo si vive fuera de `ActiveX` | engines decorativos | | Root visual de Eidos | `DrawerProps`, `DialogProps` | `DrawerProviderProps` en API visual | Decisiones ya aplicadas: - `ActiveUix.events` es el nombre canonico del motor perceptivo. En attach mode lee `app.events`; `defineUixServices(...)` declara ese servicio con el mismo nombre. - No existe `ActiveUix.semantic` como servicio publico. `semantic` sobrevive solo como nombre del payload en `morfo.events[].semantic`. - `morfo.texts` es el campo declarativo para idlangrefs owned por el componente — la nomenclatura `morfo.translations` se renombró a `texts` durante la migración 2026-05 (ver `langs/components/*.ts` para los catálogos por componente). `langs` queda para el servicio runtime. - `prefs` es el unico nombre para preferencias. `ActiveUix` expone el `ActivePrefs` bruto; Soma/Eidos consumen vistas acotadas. No se introduce `settings`. - `ActiveUix.motion` (`EngineMotion`, `arts/motion`) es el motor de animación, consumido por Soma (`soma.motion`) y Eidos (`eidos.motion`). Vive en `arts/`, no en Eidos, para que Soma anime (spring) sin dependencia soma→eidos. En attach lee `app.motion`. Orden de retirada: 1. Mantener `assertContract` como validador de data-contract y no como registry paralelo. `registerContract` queda para tooling/tests directos; Soma registra contratos via `registerMorfo()`. 2. Recién despues limpiar nombres de props en componentes visuales. --- ## 1. La tesis en una frase > UIX trata un componente como **cuatro capas con contratos explícitos**, no > como un bloque monolítico que mezcla estructura, comportamiento, semántica > y presentación. Las cuatro capas son **Morfo · Soma · Sema · Eidos**. Cada una hace un trabajo nítido y se comunica con las otras únicamente a través del DOM y de un contrato declarativo compartido. Ninguna invade a la siguiente. --- ## 2. El problema que resuelve En la mayoría de frameworks de UI, un componente acumula: - el **contrato público** del DOM (atributos, parts, ARIA) - el **comportamiento headless** (estado, teclado, foco, eventos) - la **semántica** del evento (qué significa "abrir un dialog" más allá del cambio de un atributo) - la **capa visual** (CSS, animaciones, theming) - los **motores modales** (sonido, haptic y reacciones CSS vía eventos DOM) - la **integración con servicios de app** (i18n, dates, theme, etc.) Todo eso vive mezclado. Renombrar una `part` toca seis sitios sin verificación automática. La semántica de un evento se entierra en strings hardcodeados que solo el componente conoce. El CSS se acopla a estructura DOM incidental. Los motores de sonido reescriben mapeos por componente. Cuando quieres cambiar una decisión transversal — "todos los triggers deben tener un hover dim común" — tienes que enumerar los 30 componentes que tienen un trigger. UIX rompe ese bloque en cuatro capas con responsabilidades disjuntas y un canal de comunicación común: **el DOM con atributos declarados por el contrato cross-layer**. --- ## 3. Las cuatro capas ### Morfo — el contrato cross-layer `Morfo` declara la genética del componente: sus partes, los `data-*` que emite, los ARIA que aporta, los roles, los estados, los eventos semánticos que puede disparar, y las teclas que dispatcha. Una declaración por componente, en TypeScript, validada por sium. Morfo **no ejecuta nada**. Es DNA, no proteína. ```ts // src/uix/morfo/components/dialog.ts (extracto) export const dialogMorfo = { name: 'Dialog', kebab: 'dialog', scope: ['soma', 'sema'], events: [{ name: 'close-cancel', semantic: { family: 'emerge', verb: 'close', target: v.partRef('content'), sequence: 'pre' }, prewrite: [{ part: v.partRef('content'), attr: 'data-last-action', value: 'cancelled' }], commits: { part: v.partRef('content'), attr: 'data-state', value: 'closed' } }], parts: [ { name: 'Trigger', kebab: 'trigger', archetype: 'trigger', role: 'button', ... }, { name: 'Content', kebab: 'content', archetype: 'content', role: 'dialog', ... }, // ... ] } as const satisfies Morfo ``` Morfo es **el único punto de articulación cross-layer**. Cualquier dato que las otras capas necesitan compartir entre sí pasa por aquí. Es la regla estructural más importante: si dos capas necesitan saber lo mismo, ese "lo mismo" vive en morfo. ### Soma — el comportamiento headless `Soma` consume morfo y lo transcribe a comportamiento ejecutable. Lee `morfo.events`, `morfo.keyboard`, `morfo.parts[].data` y `aria`, y los materializa: dispatcha teclas, aplica atributos al DOM, gestiona estado, integra con context (Field, Form, Soma). Soma **no decide visualidad**. No sabe colores. No sabe transiciones. No sabe sonidos. Solo sabe estados, eventos, foco, teclado y cómo materializar todo eso en el DOM. La pieza central de soma es `SomaRuntime`: un intérprete del morfo que recibe del provider las fuentes reactivas (states, props, parts, events, actions) y se encarga de: - emitir los attrs estáticos (`partProps`) - aplicar los attrs derivados de state via `dom.apply` (effects) - dispatchear teclas via `keydown(part, event)` - ejecutar eventos via `trigger(eventName)` con la cadena perceptiva completa El provider aporta **lo que morfo no puede inferir**: getters reactivos sobre el estado interno, handlers concretos, glue de layers ortogonales (Presence, Dismissal, ScrollLock). ### Sema — vocabulario + canales perceptivos `Sema` define el vocabulario canónico del framework y orquesta el **dispatch de señales perceptivas** a un conjunto de canales modulares. Vocabulario canónico (`SEMA_MAP` en `src/uix/sema/sema-map.ts`): - **8 familias** — `contact`, `commit`, `signal`, `handle`, `emerge`, `shift`, `sustain`, `delegate`. Cada una declara `hold`, base para canales reales (`sound`, `haptic`) y el set de canales activos. - **6 intents** — `neutral`, `affirm`, `fulfill`, `risk`, `threat`, `loss`. Cada intent declara `deltas` per canal que se aplican sobre el base familiar cuando la familia es valenced. - **Action verbs** (`SEMA_VERBS` en `src/uix/sema/verbs.ts`) — `present`, `dismiss`, `commit`, `cancel`, `announce`, `warn`, … — son los nombres canónicos de los `morfo.events[].name`. Sema **no decide qué evento ocurrió** — eso lo decide el provider. El `EngineSemantic` solo: - mantiene un registry de canales que implementan `Channel` - genera el `id` de cada ocurrencia - resuelve la `EffectiveSignature` per canal (base × intent deltas) - despacha cada signal a todos los canales registrados - bloquea al caller solo el tiempo que el canal visual necesite ``` src/uix/sema/ ├── engine.ts registry + channel prepare/dispatch ├── resolver.ts resolveSignature(signal): EffectiveSignature ├── sema-map.ts tabla per-family base + per-intent deltas (typed) ├── verbs.ts SEMA_VERBS catalog └── chans/ ├── types.ts interfaz Channel ├── visual.ts VisualChannel (built-in, data-event projection + hold) ├── sound.ts SoundChannel (Web Audio, prepare-time priming) └── haptic.ts HapticChannel ``` El **canal visual** (built-in) es el único que comparte plano DOM con el commit estructural posterior, y por tanto el único que bloquea al caller. `EngineSemantic` ejecuta hooks genéricos de canales; el `VisualChannel.prepare()` proyecta `data-event` + `data-event-id` + `data-event-phase` (y opcionalmente `data-event-family` y `data-event-intent`) al target mediante un `SignalProjector`. En `ActiveUix` ese proyector recibe `uix.dom`, por lo que la escritura de attrs entra por el mismo dueño DOM que usa soma. `VisualChannel` mantiene el `hold` configurable y el cleanup retira la proyección antes de resolver la Promise (semántica secuencial estricta). > **Namespace discipline**: la proyección semántica escribe **solo** > atributos bajo el prefijo `data-event-*`. Nunca toca `data-state`, > `data-intent`, > `data-disabled` u otros state attrs — esos pertenecen al runtime/morfo. > Eidos lee `data-event-intent` para reacciones a la señal transitoria > y `data-intent` (cuando lo emita el morfo) para el estado persistente. Los hold defaults internos al canal visual vienen de `SEMA_MAP.families[*].hold` y se resuelven sobre la escala perceptiva `SEMA_DURATIONS`: `glimpse`, `brief`, `noticed`, etc. El integrador puede sobreescribir per signal (`signal.hold`) o globalmente vía `new EngineSemantic({ dom, visual: { defaultHold } })`. El **SoundChannel** está implementado: sintetiza earcons cortos vía Web Audio (dos osciladores → biquad lowpass → envelope ADSR-lite, parametrizado por `effective.sound.{pitch, centroid, gain, contour, roughness, duration}`). Hace **prepare-time priming**: crea + resume el `AudioContext` en el `prepare()` de una señal sonora, síncronamente dentro del gesto de usuario. Solo despues registra el listener capture-phase en `document` para re-resume posteriores. Es opt-in: `new EngineSemantic({ sound: true })`. Las firmas sonoras reutilizables viven en `src/uix/sema/sounds.ts`. Los packs de componente referencian nombres (`sound('handle.pickup.air')`, `sound('notification.ping')`) o recetas dinámicas, no constantes sueltas. Una entrada del repositorio puede ser sintética o un `.wav` externo con fallback sintético; `SoundChannel` reproduce `sampleUrl` y cae a síntesis si fetch/decode falla. El **HapticChannel** sigue como canal opt-in; el registry ya lo acepta para que cuando se implemente encaje sin cambios estructurales. Cualquier canal no-visual es fire-and-forget: gestiona su propio timing en su plano sin afectar al caller. ### Eidos — la capa visual `Eidos` es la capa visual. Su acceso al sistema es **el DOM**: lee parts, data-attrs, ARIA, archetypes y event signals que las otras capas escriben. No importa internals de soma; no pregunta a sema. Eidos **no es solo CSS**. Cubre lo que en la rama muerta `air/` era el "runtime visual" más el sistema de tokens — sin heredar código. Su estructura actual: ``` src/uix/eidos/ ├── active-eidos.svelte.ts ActiveEidos: runtime/contexto visual creado por ActiveEidos.create ├── archetypes.css reglas comunes a [data-archetype=*] ├── events.css reacciones a [data-event-*] (sema visual) ├── generated/base.css foundation CSS generado desde EidosConfig base ├── themes/fonts.css font faces usados por el base generado ├── lib/ soporte de config, recipes, contrato CSS y tipos compartidos └── components/{x}/ recipe + wrapper Svelte + tipos por componente ├── {x}.css recipe (selectores [data-{x}], variants) ├── {x}.svelte wrapper sobre el provider headless de soma ├── types.ts Props visuales + props publicas de soma └── index.ts default root + partes attached ``` `ActiveEidos` es la fuente de verdad nueva del theming: primitivas (color + alpha scales, size map, espacios, control height, radius, borde, opacidad, z-index, focus ring, layout, tipografia, shadow, motion, icon), roles semanticos y themes. Tambien resuelve el theme activo desde sus fuentes visuales (`theme`, `modeSource`, `densitySource` o defaults) e inyecta CSS runtime cuando la app no lo precompila. Los themes externos pueden venir solo por CSS si respetan el contrato de custom properties (`themeSource: 'auto' | 'config' | 'css'`); `getCssContract()` publica ese contrato como datos typed y `renderContractCss()` lo materializa como CSS vacío desde la config. Los aliases de recipe por componente (`--toast-*`, `--dialog-*`, etc.) viven en `EidosConfig.recipes` y se generan dentro de `generated/base.css`; las recipes CSS permanecen como selectores/estados, no como fuente paralela de tokens. `ActiveEidos.listRecipes()` y `getRecipeTokens(component)` son la superficie de consulta para editores de theme; devuelven nombres y copias defensivas, no handles mutables al config interno. `ActiveEidos` tambien puede escribir variables runtime en un style block propio, validandolas contra el contrato para que un editor de theme no tenga que mutar CSS a mano variable por variable. La persistencia de configuracion completa usa `EidosConfigDocument` (`kind + version + options`), de modo que `EidosConfig` queda como objeto puro de authoring y el versionado vive en el borde de almacenamiento/intercambio. `ActiveEidos` es tambien el contexto que consumen los wrappers Svelte: `ActiveEidos.require()` expone solo la superficie visual (`dom`, `langs`, `format`, `prefs` y helpers como `resolve(...)`, `breakpoint(...)` e `isBelow(...)`). Los wrappers no importan `getActiveUix()` directamente. El wrapper Svelte publico sigue la opcion C disciplinada: un root visual `` / `` / `` y partes attached ``, ``, etc. No hay `Provider` publico y no hay API flat con snippets como forma principal. Reglas de selección (eidos lee, no escribe): ```css /* Estilo común a todos los triggers, independiente del componente */ [data-archetype='trigger'] { cursor: pointer; } /* Tinta exit anim según la causa (saved/cancelled/dismissed) */ [data-state='closed'][data-last-action='cancelled'] { animation: ...; } /* Reacción a una señal perceptiva durante el hold (200–260ms según familia) */ [data-event-family='commit'][data-event-phase='active'] { animation: eidos-commit-settle 260ms var(--ease-out); } /* Variante por intent transitorio (de la señal, no del estado) */ [data-event-family='commit'][data-event-intent='threat'][data-event-phase='active'] { animation: eidos-announce-pulse-threat 400ms var(--ease-spring); } ``` El DOM es el canal entre events/sema y eidos. El `VisualChannel` proyecta la ocurrencia mediante `SignalProjector` + `ActiveDom`; Eidos reacciona. --- ## 3.bis ActiveUix sin `frontend` (cerrado) `frontend` ya no existe como artefacto activo. La fuente transversal de preferencias es `ActivePrefs`, siguiendo el mismo patron que usa `ActiveApp`; la proyeccion DOM es explicita y vive fuera de `ActiveUix`. La particion actual: - `uix.langs` — idioma y traducciones; se sincroniza desde `prefs.language`. - `uix.format` — formatos regionales; consume `prefs.locale` como `LocaleSource`. - `uix.clipboard` — capacidad de escritura al portapapeles; en standalone se crea salvo `clipboard:false`, en attach se consume de `app.clipboard` cuando un componente lo pide. - `uix.dom` — unico escritor de attrs globales mediante `dom.apply`. - `uix.motion` — motor de animación (`EngineMotion`, `arts/motion`): registra + corre presets del momento `--state` (CSS settle / drivers JS spring/waapi/rect). Lo consumen Soma (`Presence` vía `soma.motion`) y Eidos (`eidos.motion`: genera CSS + registra sus presets). En standalone se crea con el `dom` disponible; en attach lee `app.motion`. - `uix.prefs` — preferencias efectivas transversales: `language`, `locale`, `direction`, `motion`, `sound`, `haptic`, etc. - `uix.portal` — target generico de portales; capas como Soma lo adaptan a su API (`portalTo`) sin que `ActiveUix` conozca esas capas. Eidos queda fuera de la superficie de `ActiveUix`: `ActiveEidos.create(...)` crea el contexto visual y, si se necesita CSS runtime, usa `uix.dom`, `uix.langs`, `uix.format` y fuentes explicitas de `mode`/`density` cuando el integrador no quiera los defaults. `prefs.direction` es la unica fuente de direccion efectiva. Si el usuario no ha fijado intent, deriva desde `prefs.language`; si llama a `prefs.direction.set('rtl')`, ese override manda; si llama a `prefs.direction.clear()`, vuelve a derivar. El atributo `html[dir]` es solo la proyeccion DOM de ese valor efectivo. La proyeccion DOM queda separada por ownership: ```text ActivePrefsDomProjection -> dir, data-motion, data-sound, data-haptic ActiveEidos -> data-theme, data-mode, data-density ``` En modo standalone, `createActiveUix()` instancia `ActivePrefs` con el preset estandar de UIX y crea los servicios configurados. En modo attach, `attachActiveUix(app)` reutiliza `app.prefs` porque `prefs` pertenece al core de `ActiveApp`. `langs` y `dom` son los servicios requeridos para attach: si faltan, `attachActiveUix(app)` falla temprano. `clipboard`, `events` y `format` son opcionales; si una capa los necesita y no fueron declarados en la app, el getter de `ActiveUix` falla explicitamente. `ActiveUix` no auto-proyecta preferencias al DOM. La proyeccion cross-modal existe en `arts/prefs` como `createActivePrefsDomProjection(...)`; la cablea el composition root que quiera esos atributos globales. Esto permite usar `ActiveApp` sin UIX, UIX sin Eidos, o Eidos con CSS precompilado sin crear proyectores duplicados. Cuando una shell UIX quiere modo visual runtime, el flujo canonico es: ```ts const prefsProjection = createActivePrefsDomProjection({ prefs: uix.prefs, dom: uix.dom }); const eidos = ActiveEidos.create({ theme: 'base', modeSource, applyDom: true }); ``` `prefsProjection` y `eidos` se disponen con la shell. El modo claro/oscuro no se escribe en `prefs.theme`; se pasa a `ActiveEidos` como fuente visual. --- ## 4. Cómo se articulan — la cadena de transcripción Las cuatro capas forman una cadena de transcripción declarativa donde cada una traduce el contrato de la anterior a su lenguaje: ``` Morfo declara (TypeScript constant + sium schema) ↓ SomaRuntime transcribe (Soma — leyendo morfo + sources) ↓ Provider aporta sources/handlers (Soma — TypeScript class) ↓ Effects sincronizan attrs (Soma — $effect + dom.apply) ↓ EngineSemantic despacha señales (Sema — registry + prepare/dispatch) ↓ VisualChannel prepara data-event* (Sema — via SignalProjector/uix.dom) ↓ Eidos lee el DOM y aplica CSS (Eidos — selectores + tokens) ``` Más, en paralelo (no en cadena): - **ADom** materializa los `dom.apply`/`dom.remove` que Soma le pide para los attrs estructurales derivados (data-state, aria-\*, etc.). - **Canales no visuales de Sema** (sound, haptic, futuros) reciben la misma señal y la materializan en su modalidad — fire-and-forget. Las piezas con responsabilidades disjuntas: | Pieza | Responsabilidad | No hace | | ------------------- | ---------------------------------------------------- | ------------------------------------- | | **Morfo** | Declarar el contrato | Ejecutar nada | | **SomaRuntime** | Transcribir morfo a comportamiento | Decidir lógica de negocio | | **Provider** | Aportar fuentes reactivas + handlers | Escribir attrs mutables al DOM | | **Effects** | Aplicar attrs derivados via `dom.apply` | Decidir qué attrs (eso lo dice morfo) | | **EngineSemantic** | Registry de canales + prepare/dispatch | Conocer DOM, audio, vibración | | **VisualChannel** | Proyectar `data-event*` via projector + hold awaited | Escribir attrs estructurales | | **SignalProjector** | Proyectar `data-event*` via `dom.apply` | Decidir cuándo emitir | | **ADom** | Mutaciones DOM imperativas para attrs estructurales | Conocer las capas superiores | `Eidos` queda fuera de esa cadena: lee del DOM, no participa en la transcripción. --- ## 5. La cadena causal de una interacción Ejemplo concreto: el usuario clickea el botón **×** de un Toast. ``` 1. Browser dispara click → Svelte llama Close.onclick 2. Close.onclick ejecuta: void this.toastItem.runtime.trigger('dismiss') 3. SomaRuntime.trigger('dismiss'): 3.1. Busca event 'dismiss' en morfo.events ✓ 3.2. Resuelve target = Item DOM element via partRef('item') 3.3. AWAIT events.emit({ target, name: 'dismiss', family: 'emerge' }) EngineSemantic despacha la señal a TODOS los canales registrados: - VisualChannel.prepare(): SignalProjector aplica data-event* via dom.apply(target, data-event-family=emerge) - VisualChannel.handle(): mantiene el hold (240ms para emerge) - cleanup: dom.apply(target, data-event*=undefined) - SoundChannel, HapticChannel: fire-and-forget (no awaited) La Promise resuelve cuando el VisualChannel ha terminado el cleanup (semántica secuencial estricta) 4. SomaRuntime invoca el handler del provider: sources.events.dismiss() → this.provider.toaster.dismiss(opts.toast.current.id) → toast.dismissing = true (state mutation) 5. EFFECTS reactivos del runtime ven que isOpen cambió: resolvePartAttrs recomputa los attrs del item part dom.apply(target, { 'data-state': 'closed' }) en el siguiente tick 6. Eidos (CSS) ha reaccionado durante toda la secuencia: - durante t=0..240ms: [data-event^="dismiss"] dispara animation @keyframes fade-out (CSS animation, no transition: corre full-duration aunque el attr desaparezca después) - en t≈245ms: [data-state="closed"] toma el relevo - Presence layer aplica data-ending-style; CSS termina la animación ``` State es la única fuente de verdad. El DOM es derivación. La señal perceptiva PRECEDE al cambio estructural por el hold completo (~240ms para emerge) — el caller espera el cleanup antes de mutar estado, dándole a CSS una ventana perceptible para coreografiar la salida. --- ## 6. Las primitivas que pasan entre capas ### Atributos DOM — el canal universal Todo lo que pasa entre capas pasa por atributos DOM: | Atributo | Quién escribe | Quién lee | | ------------------------------------------- | -------------------------------------- | ---------------------------- | | `data-{component}` | partProps (estático) | Eidos (selector raíz) | | `data-{component}-{part}` | partProps (estático) | Eidos (selector parte) | | `data-archetype="trigger"` | partProps (estático) | Eidos (selector transversal) | | `id` | partProps | ARIA refs, tests | | `role` | dom.apply (effect) | Lectores de pantalla, Eidos | | `aria-*` | dom.apply (effect) | Lectores de pantalla, Eidos | | `data-state="open"` | dom.apply (effect) | Eidos (selector variant) | | `data-disabled` | dom.apply (effect) | Eidos (selector estado) | | `data-event="dismiss"` | sema.emit (transient) | Eidos (selector evento) | | `data-event-phase="active"` | sema.emit (transient) | Eidos | | `data-event-id="sig-N"` | sema.emit (transient) | Sound/Haptic futuros | | `data-event-family="commit"` | sema.emit (transient) | Eidos (selector familia) | | `data-event-intent="risk"` | sema.emit (transient) | Eidos (tinta de la señal) | | `data-color="primary"` | dom.apply (effect) | Eidos (recipe per token) | | `data-intent="risk"` | dom.apply (effect, opcional per morfo) | Eidos (estado persistente) | | `data-last-action="cancelled"` | trigger prewrite | Eidos (tinta exit) | | `data-starting-style` / `data-ending-style` | Presence layer | Eidos (animation hooks) | **Regla operativa**: lo que `dom.apply` escribe, Svelte no lo renderiza. La identidad estática (id + marker + archetype + ref attachment) ship via `partProps`. Lo derivado de state ship via `dom.apply` desde effects. No hay double-write. ### Vocabularies cross-layer > **Canonical:** the semantic vocabulary (families, intents, verbs) lives in > [`docs/CANON.md`](../../docs/CANON.md). The summary below is for the cross-layer > view; the canon + code are authoritative. Dos vocabulary estables anclan la articulación: **Archetypes** (`src/uix/morfo/types.ts:ARCHETYPE_VOCABULARY`): ``` provider · trigger · content · overlay · viewport item · option · indicator · thumb · track label · title · description · close · action header · image · fallback · arrow · separator group · input · segment · preview ``` 24 categorías de parte que aparecen en múltiples componentes. Una `Trigger` de Dialog, Popover, DropdownMenu y Tooltip son la misma categoría — Eidos las puede estilar transversalmente con `[data-archetype=trigger]`. **Verbs** (`src/uix/sema/verbs.ts:SEMA_VERBS`), agrupados por familia: ``` contact: press · tap · activate · focus · trigger · release commit: select · unselect · toggle · save · submit · confirm · complete · fail · cancel · reset · discard · delete · restore · expire · acknowledge · apply · partial · block · move · set · remove · reorder · upload signal: announce · notify · warn · alert · inform · emphasize · remind handle: pick · carry · drop · drag · resize · reorder · rotate · scroll · zoom emerge: present · dismiss · open · close · expand · collapse · reveal · hide shift: enter-mode · exit-mode · navigate · route · step · return · context sustain: start · progress · loading · waiting · syncing · processing · streaming · pending · retrying · upload · end delegate: offer · plan · authorize · act · review · escalate · return ``` Verbs que parecen de una familia pero son de otra per la canon: **select / toggle / acknowledge** son `commit` (fijan estado, no son solo contacto); **edit** es `shift.enter-mode` (cambia régimen). `morfo.events[].name` debería alinear con este vocabulario en una de dos formas: `{verb}-{variant}` (`dismiss-outside`, `close-cancel`) o `{family}-{verb}` (`commit-toggle`, `commit-save`). Permite a Sema/ Sound/Haptic/Eidos suscribir o estilar por verb o por familia sin enumerar componentes. `validateEventName` reconoce ambas formas. **Intents** (`src/uix/sema/sema-map.ts:SEMA_MAP.intents`), 6 valores: ``` neutral — sin carga afectiva (default) affirm — positivo bajo ("todo va bien") fulfill — positivo resolutivo ("objetivo cumplido") risk — negativo moderado ("revisa esto") threat — negativo activo ("alarma, atención inmediata") loss — consecuencia consumada (negativo + baja activación, posterior) ``` Intent es ortogonal a familia: una `commit` puede ser `affirm` (subscribe), `risk` (publish), `threat` (delete), o `neutral` (toggle plano). El provider lo declara en `morfo.events[].semantic.intent` (literal) o lo expone como prop (`fromProp + supported subset`). --- ## 7. Reglas duras Las invariantes operativas que mantienen el sistema coherente: 1. **Morfo no conoce código de runtime.** Es declaración pura. 2. **SomaRuntime depende de Dom y Semantic.** Por construcción, no por import. Provider las inyecta. 3. **Provider no escribe attrs mutables al DOM directamente.** Los aporta como sources al runtime. 4. **`Semantic` puede usar `Dom` (hacia abajo).** `Dom` no conoce `Semantic`. 5. **`ADom` no conoce capas superiores.** Solo aplica mutaciones, listeners y acciones DOM transversales que recibe. La frontera DOM no exige envolver lecturas locales: un componente puede llamar `el.contains(...)`, `el.closest(...)`, `el.getBoundingClientRect()` o leer `scrollTop` de su propio elemento. En cambio, listeners de `document/window`, consultas globales, foco imperativo y scroll de ventana pasan por `ActiveDom`. 6. **`Eidos` consume DOM y `data-*`, no internals de Soma ni Sema.** Si lo necesita, debe estar declarado en morfo o emitido en una señal de sema. 7. **Lo que `dom.apply` escribe, Svelte no lo renderiza desde `partProps`.** Una sola autoridad por atributo. 8. **State es la única fuente de verdad. El DOM es derivación.** Los handlers mutan state; los effects derivan attrs. 9. **Event handlers en `runtime.trigger` son síncronos.** Async va antes de llamar `trigger`. 10. **Guards van en el call-site, no dentro del handler.** Si el guard entra al handler, ya emitió la señal perceptiva. 11. **`morfo.events[].commits` es descriptivo, no ejecutable.** Documenta lo observable; el smoke valida. 12. **La regla 2-de-3 para extender Morfo.** Una extensión a morfo solo se justifica si **al menos dos de las tres capas** (soma, sema, eidos) la consumen. Soma-only conveniences viven en provider via virtual prop. --- ## 8. La distinción autoría / transcripción Una lente útil para decidir dónde vive cada cosa: - **Autoría** — escrito una vez por un humano, con intención. Los `Props` de un componente, el morfo, los handlers de eventos. Vive en TypeScript del autor. - **Transcripción** — derivado mecánicamente de la autoría. Los `Opts` del provider, el wrapping `readableActive(() => x)` por prop, los attrs estructurales. Lo deriva una helper / runtime / generator. UIX intenta que solo la autoría sea humana. La transcripción es código que escribe código: | Autoría | Transcripción | Cómo | | -------------------------------------- | ---------------------- | ---------------------------------- | | `Props` | `Opts` | `OptsFromProps` | | Cada prop de wrapper | Active/State boxes | `bindProps({ ... })` | | `morfo.events[].commits` | DOM final tras handler | Effects derivan | | `morfo.parts[].data` | Atributos en cada tick | Resolver + dom.apply | | `morfo.events[].name` + verb canonical | `data-event="..."` | sema.emit | Esta distinción explica por qué la regla 2-de-3 se mantiene: el morfo es **autoral cross-layer**. Si solo soma necesita algo, es transcripción soma-internal — no autoral, no merece estar en morfo. --- ## 9. Lo que NO es esta arquitectura Para evitar mission creep, conviene fijar lo que UIX **no quiere ser**: - **No es una colección visual.** Eidos será visual; UIX como sistema no. - **No es un wrapper opinionated sobre primitives existentes.** Las cuatro capas son originales, no envuelven Radix/Headless UI. - **No es un design system clásico.** Tokens, themes y recipes pertenecen a Eidos, no al núcleo. - **No es un servicio monolitico de eventos que ejecuta todas las modalidades.** Sound, Haptic, Motion y futuras modalidades se registran como **canales** del `EngineSemantic`; cada uno gestiona su propia modalidad. El engine es solo registry + dispatch. - **No es un EventEmitter global disfrazado de arquitectura.** Cada evento tiene un target específico en el DOM y un dueño semántico declarado en morfo. - **No es un mini-DSL en JSON.** Morfo es declarativo descriptivo, no programa. La lógica vive en TypeScript del provider; morfo solo dice qué attrs y qué semántica. --- ## 10. Estado del proyecto > Los snapshots de estado fechados ("qué hay implementado a fecha X") viven en > [`docs/process/`](../../docs/process/) — p. ej. > `active-architecture-snapshot-2026-05.md`. Este documento describe la > arquitectura, no el avance. ## 11. Riesgos reconocidos Ningún diseño está libre de riesgos. UIX tiene cuatro de manera explícita: ### 11.1 Exceso de capas Si las fronteras no se mantienen nítidas, el sistema se siente más complejo de lo que resuelve. La regla 2-de-3 y la doctrina de "virtual prop" mitigan, pero requieren disciplina sostenida. ### 11.2 Nombres sin disciplina `Morfo`, `Sema`, `Soma`, `Eidos` son nombres que solo funcionan si los contratos son nítidos. Si Sema empieza a saber de DOM, o Soma decide visualidad, los nombres se vuelven decoración. ### 11.3 Invasión de responsabilidades El peligro constante es que una capa intente hacer el trabajo de otra: - `Sema` convirtiéndose en runtime multimodal (regresión). - `Soma` decidiendo CSS o motion. - `SomaRuntime` interpretando lógica de negocio. - `Eidos` accediendo a internals de soma. UIX solo funciona si cada capa acepta sus límites. ### 11.4 Falta de precedentes No hay sistemas de UI con esta composición exacta. Eso significa más libertad arquitectónica pero también menos patrones externos que copiar cuando aparece un caso límite. --- ## 12. Por qué puede valer la pena Si las fronteras se mantienen, UIX ofrece algo poco común: - **Explicabilidad arquitectónica**. Cada decisión cae en una capa reconocible; el "dónde vive esto" tiene una respuesta predecible. - **Menos drift entre capas**. El morfo es autoritativo; las demás capas derivan. Renombrar una part toca un sitio, no seis. - **Validación automática del contrato**. Sium schema + smoke + morfo-check detectan drift estructural antes de que llegue a producción. - **Más libertad para introducir engines nuevos**. Sound, Haptic, Motion, cualquier modalidad futura se registra como un `Channel` adicional en `EngineSemantic` sin tocar morfo ni soma. - **Honestidad sobre fronteras de framework vs integrador**. UIX provee vocabularios, contratos, transporte y puntos de extensión; no finge decidir por todas las modalidades de todas las apps. La idea importante: > **La coherencia cross-modal puede tratarse como responsabilidad del > integrador, no como una falsa promesa de un runtime centralizado que > pretende saberlo todo.** --- ## 13. La frase resumen > **Morfo declara · SomaRuntime transcribe · Provider aporta · Effects > sincronizan · Semantic emite · Dom aplica · Eidos lee.** Siete palabras que describen la cadena entera. Si una decisión arquitectónica contradice una de esas siete, la decisión está mal — o la arquitectura tiene que evolucionar conscientemente. --- ## 14. Para profundizar - [src/uix/README.md](./README.md) — posicionamiento general (más narrativo) - [src/uix/morfo/README.md](./morfo/README.md) — declaración, archetypes, regla 2-de-3 - [src/uix/sema/README.md](./sema/README.md) — `emit` contract, verbs canónicos - [src/uix/soma/SOMA_ARCHITECTURE.md](./soma/SOMA_ARCHITECTURE.md) — runtime + componentes - [src/uix/soma/COMPONENT_GUIDE.md](./soma/COMPONENT_GUIDE.md) — guía operativa para crear / migrar componentes - [src/uix/eidos/README.md](./eidos/README.md) — capa visual: tokens, themes, recipes, wrappers - [`src/arts/adom/README.md`](../arts/adom/README.md) — `dom.apply` + servicios DOM reactivos - [`src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md`](../docs/GUIA_IMPLEMENTACION_SEMAUIX.md) — convenciones doctrinales del API Decisiones de diseño detalladas y trade-offs históricos en el git log de la rama `active-uix`.