You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/active_architecture.md

46 KiB

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 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.

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 ya no proyecta preferencias al DOM. ActivePrefsDomProjection proyecta preferencias transversales (dir, data-motion, data-sound, data-haptic) cuando el composition root lo cablea. ActiveEidos proyecta data-theme, data-mode y data-density.
  • 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 y no proyecta preferencias al DOM.
  • Frontend no debe ser la fuente de locale. prefs.locale alimenta format; prefs.language alimenta langs; prefs.direction resuelve la direccion efectiva y puede proyectarse a html[dir] mediante ActivePrefsDomProjection.
  • 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.

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.

// 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)
├── generated/base.css        foundation CSS generado desde EidosConfig base
├── contracts/                CSS contract archivado; no import runtime
├── tokens/                   valores CSS legacy hasta generarlos desde ActiveEidos
├── themes/base/              CSS base archivado; no import runtime
├── 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 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. 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 <Drawer> / <Tabs> / <Checkbox> y partes attached <Drawer.Trigger>, <Drawer.Content>, etc. No hay Provider publico y no hay API flat con snippets como forma principal.

Reglas de selección (eidos lee, no escribe):

/* 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 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:

ActivePrefsDomProjection -> dir, data-motion, data-sound, data-haptic
ActiveEidos              -> data-theme, data-mode, data-density

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<P, Managed, State>
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), density scalars conectados a data-density, 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 tokens/. El primer artefacto generado ya existe en src/uix/eidos/generated/base.css; contracts/ y themes/base/ ya no se importan en runtime y quedan como archivos de consulta/tooling.
  • 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

Decisiones de diseño detalladas y trade-offs históricos en el git log de la rama active-uix.

Powered by TurnKey Linux.