# 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-13 (rama `active-uix`). Cambios > mayores desde la revisión anterior: > > - **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/sema-implementation-guide.md`](../docs/sema-implementation-guide.md) > Parte IV (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. > - **DOM audit P1 cerrada** — live regions, descripciones ocultas y bloqueo > de seleccion de texto pasan por `ActiveDom`; ver > [`dom_audit.md`](./dom_audit.md). --- ## 0. Handoff 2026-05-13 Estado acordado al cierre de la sesion: - **No tocar componentes Eidos** hasta cerrar de nuevo la arquitectura. La carpeta `src/uix/eidos/components/*` queda congelada salvo orden explicita. - **Reauditar todo UIX antes de seguir**. La tabla autoritativa de contratos minimos entre modulos vive en `src/uix/contracts.ts` y se valida con `src/uix/contracts.test.ts`. - **Regla de composition roots cerrada**. Solo `ActiveApp` y `ActiveUix` standalone crean servicios compartidos. Las capas inferiores consumen servicios desde `ActiveUix` o fallan; no crean `dom`, `langs`, `prefs`, `format` ni equivalentes. Preguntas que quedan abiertas / cerradas tras la reauditoria: 1. `ActiveUix`: cerrado. Expone `uix.prefs` bruto porque es composition root o adapter de `ActiveApp`; las capas inferiores usan vistas read-only cuando no deben mutar preferencias. 2. `Dom`: standalone puede usar `disabledDom` cuando el integrador pide `dom:false`; attach mode requiere `app.dom` y falla si falta. 3. `Sema`: fuera de `ActiveUix`, `EngineSemantic` con visual activo debe recibir `dom` o `projector`; si no, falla temprano. `visual:false` es la degradacion explicita para entornos sin DOM. 4. `Eidos`: cerrado. `ActiveEidos` queda como API publica de authoring, validacion, generacion, persistencia, contexto visual y runtime CSS opcional. 5. `Soma`: confirmar que no necesita `ActiveSoma`/`EngineSoma`; su runtime debe recibir los servicios minimos desde el scope `Soma.runtime(...)`. 6. `Langs` y `Format`: `langs` sigue `prefs.language`, `format` sigue `prefs.locale`; no mezclar traducciones, idioma activo y locale. 7. `Prefs`: cerrado. El nombre canonico es `prefs`; `soma.prefs` sustituye al antiguo `soma.presentation`. No se introduce `settings`. 8. `attachActiveUix`: P1 queda decidido: requiere `langs` y `dom` en la app. Si falta un servicio requerido, falla temprano y no crea sustitutos. 9. Docs site: resuelto. La ruta UIX ya sincroniza idioma, direccion, locale, currency, unit system, theme, sound y motion via `uix.prefs`; no muta `document.documentElement.dir` ni `uix.langs` directamente. Hallazgos P1 resueltos en esta pasada: - `createActiveUix({ dom:false })` ya crea una superficie `disabledDom` y se la pasa tambien a `EngineSemantic`. Sema no cae a un escritor DOM directo cuando se construye desde `ActiveUix`. - `defineUixServices({ dom:false })` ya no declara `dom` ni `events`. Esa configuracion no es suficiente para `attachActiveUix(app)`, que exige `app.dom` porque en attach mode `ActiveUix` no crea servicios faltantes. - La regla queda: `ActiveUix.dom` siempre existe como superficie para las capas inferiores. En standalone puede ser real o `disabledDom`; en attach debe venir del `ActiveApp`. - `ActiveUix` proyecta `prefs.theme` como `data-mode`; `ActiveEidos` proyecta el id de theme real como `data-theme`. - La auditoria DOM P1 queda cerrada: los nodos gestionados por UIX (live regions, descripciones ocultas) usan `ActiveDom.writeNode/writeText` y el bloqueo de seleccion de texto usa `ActiveDom.apply`. - `SoundChannel` ya no registra listeners globales en el constructor; el priming ocurre en `prepare()` para señales sonoras. - Sema ya no propaga `motion`, `color` ni `presence` en `EffectiveSignature`; esas dimensiones pertenecen a Eidos/CSS. Hallazgos P2/P3 resueltos en esta pasada: - Standalone `createActiveUix()` usa `createSvelteEngineBus({ logger, clock: timers.clock })`, igual que `ActiveApp`, para que los listeners del bus no creen dependencias reactivas accidentales. - `attachActiveUix(app)` ya no llama a `connectLangsToPrefs(...)`. En attach, la conexion `prefs.language -> langs` pertenece a `defineActiveLangs` y al composition root de `ActiveApp`. Contexto de `frontend` / `active-app` cerrado para esta fase: - `arts/frontend` ya no forma parte de la base de `ActiveApp`: es un servicio legacy opt-in bajo `services.frontend`. - `ActiveUix` no exige `frontend`; proyecta sus preferencias UIX directamente mediante `uix.dom`. - `Frontend` no debe ser la fuente de `locale`. `prefs.locale` alimenta `format`; `prefs.language` alimenta `langs`; `prefs.direction` resuelve la direccion efectiva y `ActiveUix` la proyecta a `html[dir]`. - Si `ActiveUix` se crea sin `ActiveApp`, debe poder crear los servicios minimos equivalentes o fallar con errores explicitos, segun la tabla de contrato ejecutable en `src/uix/contracts.ts`. - No introducir nuevo codigo contra `frontend` salvo mantenimiento legacy. Contrato minimo actual: > 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* format,events,portal standalone disabledDom falta langs/dom en attach morfo ninguno translations no registra traducciones no soma dom events,langs,format disabledDom desde uix morfo/event/part invalido sema projector/dom sound,haptic disabledDom desde uix por definir fuera de uix eidos prefs,dom* langs,format applyDom:false falta dom con applyDom activo adom surface ActiveDom breakpoints disabledDom en UIX por definir * `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`. ``` 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`; `componentLangs` nuevo | | 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.translations` es el campo declarativo para catalogos owned por el componente. `langs` queda para el servicio runtime y para ficheros legacy que se retiraran durante la migracion. - `prefs` es el unico nombre para preferencias. `ActiveUix` expone el `ActivePrefs` bruto; Soma/Eidos consumen vistas acotadas. No se introduce `settings`. 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, vibración, motion) - 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`): - **7 familias** — `contact`, `commit`, `signal`, `handle`, `emerge`, `shift`, `sustain`. 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) └── vibra.ts VibraChannel (placeholder V1) ``` 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 })`. El **VibraChannel** sigue como placeholder V1; 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) ├── contracts/ CSS legacy/estatico durante migracion ├── tokens/ valores CSS legacy hasta generarlos desde ActiveEidos ├── themes/base/ CSS legacy de base hasta sustituirlo por salida generada ├── lib/ soporte de config, contrato CSS, tokens 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 `ActiveUix.prefs` (`theme` como mode efectivo light/dark, mas `density`) 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. `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 sema y eidos. Sema escribe; eidos reacciona. --- ## 3.bis ActiveUix sin `frontend` (bajo auditoria) La direccion deliberada es que `frontend` deje de ser requisito de `ActiveUix`. Esto no queda cerrado como contrato final hasta completar la auditoria de servicios minimos. La fuente de verdad candidata pasa a ser `ActivePrefs`, siguiendo el mismo patron que ya usa `ActiveApp`. La particion actual: - `uix.langs` — idioma y traducciones; se sincroniza desde `prefs.language`. - `uix.format` — formatos regionales; consume `prefs.locale` como `LocaleSource`. - `uix.dom` — unico escritor de attrs globales mediante `dom.apply`. - `uix.prefs` — preferencias efectivas: `direction`, `theme`, `density`, `motion`, `sound`, 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.prefs`, `uix.dom`, `uix.langs` y `uix.format`. `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 global de UIX escribe: ```text prefs.direction -> dir prefs.theme -> data-mode eidos.getThemeId() -> data-theme prefs.density -> data-density prefs.motion -> data-reduced-motion prefs.sound -> data-reduced-sound ``` En modo standalone, `createActiveUix()` deberia instanciar `ActivePrefs` con el preset estandar de UIX o fallar explicitamente si falta una dimension obligatoria. En modo attach, `attachActiveUix(app)` deberia reutilizar `app.prefs` porque `prefs` pertenece al core de `ActiveApp`. Queda pendiente decidir si `langs`, `dom`, `events` y `format` son obligatorios, opcionales, no-op o errores tempranos en cada boot path. --- ## 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, vibra, 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, VibraChannel: 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/Vibra 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 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 (per `src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md` §1.3): ``` contact: press · tap · activate · focus · trigger · release commit: select · toggle · save · submit · confirm · cancel · complete · fail · delete · restore · reset · discard · expire · acknowledge · set · remove · reorder signal: announce · notify · warn · alert · emphasize · remind handle: pick · carry · drop · drag · resize · reorder · rotate · scroll 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 · end ``` 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/Vibra/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 que recibe. 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, Vibra, 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 actual (2026-05-10) **Implementado y verificado**: - `morfo` — 66 componentes con declaración completa, sium validator, archetype catalog (~265 part declarations clasificadas). Event shape doctrinal: `target` dentro de `semantic`, opcional `verb` y `sequence: 'pre' | 'coincident' | 'post'`. Incluye `langs` opcional por componente y refs relativas/absolutas (`translationRef`, `commonRef`, `langRef`) registradas via `registerMorfo`. 75/75 unit tests verdes. - `soma/morfo/runtime` — `part(...)`, `partProps`, `keydown`, `trigger`. Provider inheritance dropped en todo Soma: las clases provider son concretas y registran sus partes solo con `SomaRuntime.part(...)`; no existe una segunda API publica de registro de partes. Avatar queda como primitiva eidos-native. - `sema` — `EngineSemantic` (registry + prepare/dispatch) + `chans/` modulares. Vocabulario doctrinal cerrado: 7 familias × 6 intents (`SEMA_MAP`), verbs grouped per family (`SEMA_VERBS`). VisualChannel built-in proyecta `data-event-*` con hold per-family; **SoundChannel implementado** con prepare-time priming (synchronous AudioContext resume when a sound signal is prepared); VibraChannel placeholder. 75/75 unit tests verdes. - `eidos` — capa visual con `ActiveEidos` para wrappers. `ActiveEidos` valida primitivas, roles canonicos y themes; se conecta a `ActiveUix` solo cuando hace falta contexto visual/runtime CSS, y expone a los componentes la superficie minima de Eidos para que no conozcan la raiz activa completa. El color usa jerarquia + intents (`primary`, `secondary`, `tertiary`, `neutral`, `affirm`, `fulfill`, `risk`, `threat`, `loss`) sobre escalas de 12 pasos. La convencion de componentes eidos es root visual + partes attached, sin `Provider` publico ni flat snippets. `Size` es discreto (`xxs..xxl/full`): ActiveEidos renderiza `xxs..xxl` como tokens coordinados y deja `full` como layout. El contrato generado tambien incluye alpha color scales, borde (`width/style` - aliases), layout (`containerWidth`, `contentWidth`, `aspectRatio`), opacidad, z-index y sombras `1..6` con aliases semanticos por theme. El authoring se puede hacer con `EidosConfig` completo o con `EidosConfigPatch` sobre `themeBase`, y los valores de theme pueden delegarse a CSS externo via `themeSource`. La persistencia queda definida como `EidosConfigDocument` versionado; `ActiveEidos` puede bootear desde ese documento y exponerlo de nuevo via `toDocument()` / `serialize()`. - `adom` — `dom.apply(change)`, `dom.remove(target, names)`. Toda la superficie reactiva (viewport, breakpoints, BodyScrollLock, DOMContext, RovingFocusGroup) ya estable. - `active-uix` + `active-app` — composition root con dos modos de boot (`createActiveUix(options)` / `attachActiveUix(activeApp)`). Props bridge V2 (`OptsFromProps` + `bindProps`) operativo. **Pendiente**: - Sustituir progresivamente `contracts/`, `tokens/` y `themes/base/` por salida generada desde `ActiveEidos`, dejando los recipes por componente como CSS de estructura visual. - Reparar demos/rutas y componentes eidos que sigan en una mezcla historica de `Provider`/flat snippets para que todo compile contra la opcion C disciplinada. - `persistence` field para señales (`'transient' | 'untilAction' | 'untilFix' | 'stateBound'`) — diferido hasta que aparezca el primer consumer real de `signal.warn` / `signal.alert`. Hoy todas las señales son `transient` con hold numérico. - Holds-by-intent (commit + threat = 240ms vs commit + neutral = 200ms, etc., per guide §6.2) — diferido junto con persistence. - Wiring del topbar mute al `SoundChannel.masterGain` en la docs site. - Polimorfismo de eventos (`allowedFamilies` + `defaultSemantic` per guide §5.3) — diferido; ningún componente actual lo necesita. - `a11ySemantic` per evento (guide §9.1) — diferido; sin runtime que lo lea aún. **Layers ya muertos** (ya borrados del repo, ver branch `active-uix` cleanup): `src/lib/`, `src/uix/terra/`, `src/uix/air/`, `src/routes/test`. --- ## 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, Vibra, 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/sema-implementation-guide.md`](../docs/sema-implementation-guide.md) — convenciones doctrinales del API (Parte IV) Decisiones de diseño detalladas y trade-offs históricos en el git log de la rama `active-uix`.