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 —
ActiveEidosgestiona 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.mdParte 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
AudioContextse crea + resume enprepare()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,colorypresencesalen del mapa perceptivo; Sema resuelvehold,soundyhaptic, 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; verdom_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.tsy se valida consrc/uix/contracts.test.ts. - Regla de composition roots cerrada. Solo
ActiveAppyActiveUixstandalone crean servicios compartidos. Las capas inferiores consumen servicios desdeActiveUixo fallan; no creandom,langs,prefs,formatni equivalentes.
Preguntas que quedan abiertas / cerradas tras la reauditoria:
ActiveUix: cerrado. Exponeuix.prefsbruto porque es composition root o adapter deActiveApp; las capas inferiores usan vistas read-only cuando no deben mutar preferencias.Dom: standalone puede usardisabledDomcuando el integrador pidedom:false; attach mode requiereapp.domy falla si falta.Sema: fuera deActiveUix,EngineSemanticcon visual activo debe recibirdomoprojector; si no, falla temprano.visual:falsees la degradacion explicita para entornos sin DOM.Eidos: cerrado.ActiveEidosqueda como API publica de authoring, validacion, generacion, persistencia, contexto visual y runtime CSS opcional.Soma: confirmar que no necesitaActiveSoma/EngineSoma; su runtime debe recibir los servicios minimos desde el scopeSoma.runtime(...).LangsyFormat:langssigueprefs.language,formatsigueprefs.locale; no mezclar traducciones, idioma activo y locale.Prefs: cerrado. El nombre canonico esprefs;soma.prefssustituye al antiguosoma.presentation. No se introducesettings.attachActiveUix: P1 queda decidido: requierelangsydomen la app. Si falta un servicio requerido, falla temprano y no crea sustitutos.- Docs site: resuelto. La ruta UIX ya sincroniza idioma, direccion, locale,
currency, unit system, theme, sound y motion via
uix.prefs; no mutadocument.documentElement.dirniuix.langsdirectamente.
Hallazgos P1 resueltos en esta pasada:
createActiveUix({ dom:false })ya crea una superficiedisabledDomy se la pasa tambien aEngineSemantic. Sema no cae a un escritor DOM directo cuando se construye desdeActiveUix.defineUixServices({ dom:false })ya no declaradomnievents. Esa configuracion no es suficiente paraattachActiveUix(app), que exigeapp.domporque en attach modeActiveUixno crea servicios faltantes.- La regla queda:
ActiveUix.domsiempre existe como superficie para las capas inferiores. En standalone puede ser real odisabledDom; en attach debe venir delActiveApp. ActiveUixya no proyecta preferencias al DOM.ActivePrefsDomProjectionproyecta preferencias transversales (dir,data-motion,data-sound,data-haptic) cuando el composition root lo cablea.ActiveEidosproyectadata-theme,data-modeydata-density.- La auditoria DOM P1 queda cerrada: los nodos gestionados por UIX
(live regions, descripciones ocultas) usan
ActiveDom.writeNode/writeTexty el bloqueo de seleccion de texto usaActiveDom.apply. SoundChannelya no registra listeners globales en el constructor; el priming ocurre enprepare()para señales sonoras.- Sema ya no propaga
motion,colornipresenceenEffectiveSignature; esas dimensiones pertenecen a Eidos/CSS.
Hallazgos P2/P3 resueltos en esta pasada:
- Standalone
createActiveUix()usacreateSvelteEngineBus({ logger, clock: timers.clock }), igual queActiveApp, para que los listeners del bus no creen dependencias reactivas accidentales. attachActiveUix(app)ya no llama aconnectLangsToPrefs(...). En attach, la conexionprefs.language -> langspertenece adefineActiveLangsy al composition root deActiveApp.
Contexto de frontend / active-app cerrado para esta fase:
arts/frontendya no forma parte de la base deActiveApp: es un servicio legacy opt-in bajoservices.frontend.ActiveUixno exigefrontendy no proyecta preferencias al DOM.Frontendno debe ser la fuente delocale.prefs.localealimentaformat;prefs.languagealimentalangs;prefs.directionresuelve la direccion efectiva y puede proyectarse ahtml[dir]medianteActivePrefsDomProjection.- Si
ActiveUixse crea sinActiveApp, debe poder crear los servicios minimos equivalentes o fallar con errores explicitos, segun la tabla de contrato ejecutable ensrc/uix/contracts.ts. - No introducir nuevo codigo contra
frontendsalvo 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.eventses el nombre canonico del motor perceptivo. En attach mode leeapp.events;defineUixServices(...)declara ese servicio con el mismo nombre.- No existe
ActiveUix.semanticcomo servicio publico.semanticsobrevive solo como nombre del payload enmorfo.events[].semantic. morfo.translationses el campo declarativo para catalogos owned por el componente.langsqueda para el servicio runtime y para ficheros legacy que se retiraran durante la migracion.prefses el unico nombre para preferencias.ActiveUixexpone elActivePrefsbruto; Soma/Eidos consumen vistas acotadas. No se introducesettings.
Orden de retirada:
- Mantener
assertContractcomo validador de data-contract y no como registry paralelo.registerContractqueda para tooling/tests directos; Soma registra contratos viaregisterMorfo(). - 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 declarahold, base para canales reales (sound,haptic) y el set de canales activos. - 6 intents —
neutral,affirm,fulfill,risk,threat,loss. Cada intent declaradeltasper canal que se aplican sobre el base familiar cuando la familia es valenced. - Action verbs (
SEMA_VERBSensrc/uix/sema/verbs.ts) —present,dismiss,commit,cancel,announce,warn, … — son los nombres canónicos de losmorfo.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
idde cada ocurrencia - resuelve la
EffectiveSignatureper 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 tocadata-state,data-intent,data-disabledu otros state attrs — esos pertenecen al runtime/morfo. Eidos leedata-event-intentpara reacciones a la señal transitoria ydata-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 desdeprefs.language.uix.format— formatos regionales; consumeprefs.localecomoLocaleSource.uix.dom— unico escritor de attrs globales mediantedom.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 queActiveUixconozca 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.removeque 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:
-
Morfo no conoce código de runtime. Es declaración pura.
-
SomaRuntime depende de Dom y Semantic. Por construcción, no por import. Provider las inyecta.
-
Provider no escribe attrs mutables al DOM directamente. Los aporta como sources al runtime.
-
Semanticpuede usarDom(hacia abajo).Domno conoceSemantic. -
ADomno conoce capas superiores. Solo aplica mutaciones que recibe. -
Eidosconsume DOM ydata-*, no internals de Soma ni Sema. Si lo necesita, debe estar declarado en morfo o emitido en una señal de sema. -
Lo que
dom.applyescribe, Svelte no lo renderiza desdepartProps. Una sola autoridad por atributo. -
State es la única fuente de verdad. El DOM es derivación. Los handlers mutan state; los effects derivan attrs.
-
Event handlers en
runtime.triggerson síncronos. Async va antes de llamartrigger. -
Guards van en el call-site, no dentro del handler. Si el guard entra al handler, ya emitió la señal perceptiva.
-
morfo.events[].commitses descriptivo, no ejecutable. Documenta lo observable; el smoke valida. -
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
Propsde un componente, el morfo, los handlers de eventos. Vive en TypeScript del autor. - Transcripción — derivado mecánicamente de la autoría. Los
Optsdel provider, el wrappingreadableActive(() => 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:targetdentro desemantic, opcionalverbysequence: 'pre' | 'coincident' | 'post'. Incluyelangsopcional por componente y refs relativas/absolutas (translationRef,commonRef,langRef) registradas viaregisterMorfo. 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 conSomaRuntime.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 proyectadata-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 conActiveEidospara wrappers.ActiveEidosvalida primitivas, roles canonicos y themes; se conecta aActiveUixsolo 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, sinProviderpublico ni flat snippets.Sizees discreto (xxs..xxl/full): ActiveEidos renderizaxxs..xxlcomo tokens coordinados y dejafullcomo layout. El contrato generado tambien incluye alpha color scales, borde (width/style- aliases), layout (
containerWidth,contentWidth,aspectRatio), density scalars conectados adata-density, opacidad, z-index y sombras1..6con aliases semanticos por theme. El authoring se puede hacer conEidosConfigcompleto o conEidosConfigPatchsobrethemeBase, y los valores de theme pueden delegarse a CSS externo viathemeSource. La persistencia queda definida comoEidosConfigDocumentversionado;ActiveEidospuede bootear desde ese documento y exponerlo de nuevo viatoDocument()/serialize().
- aliases), layout (
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 ensrc/uix/eidos/generated/base.css;contracts/ythemes/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. persistencefield para señales ('transient' | 'untilAction' | 'untilFix' | 'stateBound') — diferido hasta que aparezca el primer consumer real designal.warn/signal.alert. Hoy todas las señales sontransientcon 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.masterGainen la docs site. - Polimorfismo de eventos (
allowedFamilies+defaultSemanticper guide §5.3) — diferido; ningún componente actual lo necesita. a11ySemanticper 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:
Semaconvirtiéndose en runtime multimodal (regresión).Somadecidiendo CSS o motion.SomaRuntimeinterpretando lógica de negocio.Eidosaccediendo 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
Channeladicional enEngineSemanticsin 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 — posicionamiento general (más narrativo)
- src/uix/morfo/README.md — declaración, archetypes, regla 2-de-3
- src/uix/sema/README.md —
emitcontract, verbs canónicos - src/uix/soma/SOMA_ARCHITECTURE.md — runtime + componentes
- src/uix/soma/COMPONENT_GUIDE.md — guía operativa para crear / migrar componentes
- src/uix/eidos/README.md — capa visual: tokens, themes, recipes, wrappers
src/arts/adom/README.md—dom.apply+ servicios DOM reactivossrc/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.