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

999 lines
49 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# UIX — Active Architecture
> Documento vivo de la arquitectura activa de UIX: motivaciones, las cuatro
> capas, cómo se articulan, qué problema resuelven, qué dejan deliberadamente
> fuera. Este doc es la visión de conjunto; los READMEs por capa son la
> referencia operativa.
>
> Última revisión arquitectónica: 2026-05-15 (rama `active-uix`). Cambios
> mayores desde la revisión anterior:
>
> - **Auditoría P0/P1 aplicada** — `SoundChannel` usa la superficie DOM
> inyectada para listeners globales, los ciclos type-only de
> `sema-map/types` y `floating/types` quedan rotos, y hay tests iniciales
> para los providers/motores de mayor riesgo: Dialog, Drawer, Command,
> Calendar, DateField, Select, Popover, Toast y RangeCalendar. Los motores
> de Table/Form/Command scoring salen de Soma hacia `libs`.
> - **Pausa de coherencia arquitectónica** — contratos mínimos por módulo
> auditados y fijados en `src/uix/contracts.ts`: qué necesita cada capa,
> qué instancia si no se le pasa, cuándo degrada y cuándo debe fallar.
> - **Provider inheritance dropped** — los providers ya no heredan de un
> base compartido; son clases concretas y centralizan la mecanica
> DOM con `SomaRuntime.part(...)`.
> - **Eidos entro en produccion como runtime visual aplanado** —
> `ActiveEidos` gestiona primitivas/themes, validacion, contrato CSS,
> persistencia e inyeccion runtime cuando la app lo pide.
> - **Doctrina del API cerrada** — convenciones 1-13 en
> [`src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md`](../docs/GUIA_IMPLEMENTACION_SEMAUIX.md)
> (single-event para operaciones instantáneas, 8 color tokens,
> intent ↔ color resolution, subset por componente, root visual con
> partes attached en eidos, sound prepare-time priming, etc.).
> - **SoundChannel prepare-time priming** — el `AudioContext` se crea +
> resume en `prepare()` cuando una señal sonora nace dentro del gesto de
> usuario, evitando side-effects globales en el constructor sin reabrir
> la race con la autoplay policy.
> - **Sema sin slices visuales** — `motion`, `color` y `presence` salen del
> mapa perceptivo; Sema resuelve `hold`, `sound` y `haptic`, y Eidos
> materializa la respuesta visual desde CSS/token contract.
> - **Ownership DOM P1 cerrado** — live regions, descripciones ocultas,
> bloqueo de seleccion de texto, observers y acciones imperativas de foco /
> scroll pasan por `ActiveDom`.
---
## 0. Handoff 2026-05-14
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.
- **Contratos mínimos fijados 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`, `clipboard` ni equivalentes.
Preguntas que quedan abiertas / cerradas tras la revision arquitectonica:
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, sound, motion y haptic via `uix.prefs`; el tema,
modo y densidad visuales pertenecen a `ActiveEidos`. La ruta 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 shell `/uix` sigue ese contrato: cablea `createActivePrefsDomProjection`
con `uix.prefs/uix.dom`, crea `ActiveEidos.create({ theme: 'base',
modeSource, applyDom: true })` y no escribe `uix.prefs.theme`.
- Ownership DOM P1 queda cerrado: los nodos gestionados por UIX
(live regions, descripciones ocultas) usan `ActiveDom.writeNode/writeText`
y el bloqueo de seleccion de texto usa `ActiveDom.apply`. Los observers de
Soma (`ResizeObserver`, `MutationObserver`, `IntersectionObserver`) se crean
mediante `ActiveDom` para respetar `targetWindow`/iframes/disabledDom. Las
acciones imperativas de foco y scroll se enrutan por `ActiveDom.focus`,
`ActiveDom.scrollTo`, `ActiveDom.scrollIntoView` o `ActiveDom.scrollWindowTo`.
- `SoundChannel` ya no registra listeners globales en el constructor; el
priming ocurre en `prepare()` para señales sonoras y el listener de unlock
se instala mediante la superficie DOM inyectada, no con
`document.addEventListener` directo.
- 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` fue retirado del arbol activo. No hay servicio
`services.frontend` ni alias `$frontend`.
- `ActiveUix` no conoce `frontend` y no proyecta preferencias al DOM.
- `frontend` no debe reaparecer como 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`; no hay superficie legacy que mantener.
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* clipboard,format,events,portal standalone disabledDom falta langs/dom en attach
morfo ninguno translations no registra traducciones no
soma dom events,langs,format,clipboard disabledDom desde uix morfo/event/part invalido; servicio opcional ausente
sema projector/dom* sound,haptic,visual:false ninguno SemaConfigError sin dom/projector
eidos dom* langs,format,prefs,mode/density sources applyDom:false falta dom con applyDom activo
adom surface ActiveDom target/window/breakpoints disabledDom solo explicito errores ADom sin DOM real
* `dom` significa superficie `ActiveDom`, no necesariamente DOM real. Puede
ser `disabledDom` solo en standalone cuando el integrador pide `dom:false`.
En attach debe venir de `ActiveApp`.
* Fuera de `ActiveUix`, `EngineSemantic` con canal visual activo debe recibir
`dom` o `projector`; `visual:false` es la degradacion explicita.
```
Los siguientes cambios deben derivar de esta tabla, no de constructores
inventados desde capas inferiores.
Correccion aplicada: `ActiveUix` no importa ni instancia `Soma`/`Eidos`.
`portal` queda como setting generico de UIX; `Soma` lo consume como default
para `portalTo`, y `ActiveEidos.create(...)` crea el scope visual cuando la
app necesita Eidos.
---
## 0.1 Naming canonico
La arquitectura puede mantener los nombres historicos de carpetas
(`morfo`, `soma`, `sema`, `eidos`), pero la superficie publica debe usar una
gramatica consistente. Regla general: un nombre representa un unico concepto;
si un termino es alias historico, debe quedar marcado y con ruta de retirada.
| Concepto | Nombre canonico | Evitar / retirar |
| ------------------------------------ | ----------------------------------------- | --------------------------------------------------------- |
| Servicio runtime de traducciones | `langs` | `lang` como servicio |
| Idioma activo | `prefs.language` | `locale` para idioma de traducciones |
| Locale / formatos regionales | `prefs.locale` | `language` para formatos |
| Catalogos declarativos de texto | `translations` | `langs` dentro de `morfo`; tablas globales por componente |
| Preferencias UIX | `prefs` | `settings`, `presentation` como nombre nuevo |
| Eventos perceptivos UIX | `events` | `semantic` como servicio publico |
| Ocurrencia en vuelo | `signal` | usarlo para la capa completa |
| Payload semantico de un evento morfo | `semantic` | mezclarlo con el servicio runtime |
| Contrato declarativo TS | `morfo` | `contract` como API TS duplicada |
| Contrato CSS/data exportado | `contract` | `morfo` para CSS externo |
| Bridge CSS runtime | `ActiveEidos` | runtime visual obligatorio de componentes |
| Engine puro independiente | `EngineX` solo si vive fuera de `ActiveX` | engines decorativos |
| Root visual de Eidos | `DrawerProps`, `DialogProps` | `DrawerProviderProps` en API visual |
Decisiones ya aplicadas:
- `ActiveUix.events` es el nombre canonico del motor perceptivo. En attach
mode lee `app.events`; `defineUixServices(...)` declara ese servicio con
el mismo nombre.
- No existe `ActiveUix.semantic` como servicio publico. `semantic`
sobrevive solo como nombre del payload en `morfo.events[].semantic`.
- `morfo.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, haptic y reacciones CSS vía eventos DOM)
- la **integración con servicios de app** (i18n, dates, theme, etc.)
Todo eso vive mezclado. Renombrar una `part` toca seis sitios sin verificación
automática. La semántica de un evento se entierra en strings hardcodeados que
solo el componente conoce. El CSS se acopla a estructura DOM incidental. Los
motores de sonido reescriben mapeos por componente. Cuando quieres cambiar
una decisión transversal — "todos los triggers deben tener un hover dim
común" — tienes que enumerar los 30 componentes que tienen un trigger.
UIX rompe ese bloque en cuatro capas con responsabilidades disjuntas y un
canal de comunicación común: **el DOM con atributos declarados por el
contrato cross-layer**.
---
## 3. Las cuatro capas
### Morfo — el contrato cross-layer
`Morfo` declara la genética del componente: sus partes, los `data-*` que
emite, los ARIA que aporta, los roles, los estados, los eventos semánticos
que puede disparar, y las teclas que dispatcha. Una declaración por
componente, en TypeScript, validada por sium.
Morfo **no ejecuta nada**. Es DNA, no proteína.
```ts
// src/uix/morfo/components/dialog.ts (extracto)
export const dialogMorfo = {
name: 'Dialog',
kebab: 'dialog',
scope: ['soma', 'sema'],
events: [{
name: 'close-cancel',
semantic: {
family: 'emerge',
verb: 'close',
target: v.partRef('content'),
sequence: 'pre'
},
prewrite: [{ part: v.partRef('content'),
attr: 'data-last-action', value: 'cancelled' }],
commits: { part: v.partRef('content'),
attr: 'data-state', value: 'closed' }
}],
parts: [
{ name: 'Trigger', kebab: 'trigger', archetype: 'trigger', role: 'button', ... },
{ name: 'Content', kebab: 'content', archetype: 'content', role: 'dialog', ... },
// ...
]
} as const satisfies Morfo
```
Morfo es **el único punto de articulación cross-layer**. Cualquier dato que
las otras capas necesitan compartir entre sí pasa por aquí. Es la regla
estructural más importante: si dos capas necesitan saber lo mismo, ese
"lo mismo" vive en morfo.
### Soma — el comportamiento headless
`Soma` consume morfo y lo transcribe a comportamiento ejecutable. Lee
`morfo.events`, `morfo.keyboard`, `morfo.parts[].data` y `aria`, y los
materializa: dispatcha teclas, aplica atributos al DOM, gestiona estado,
integra con context (Field, Form, Soma).
Soma **no decide visualidad**. No sabe colores. No sabe transiciones. No
sabe sonidos. Solo sabe estados, eventos, foco, teclado y cómo materializar
todo eso en el DOM.
La pieza central de soma es `SomaRuntime`: un intérprete del morfo que
recibe del provider las fuentes reactivas (states, props, parts, events,
actions) y se encarga de:
- emitir los attrs estáticos (`partProps`)
- aplicar los attrs derivados de state via `dom.apply` (effects)
- dispatchear teclas via `keydown(part, event)`
- ejecutar eventos via `trigger(eventName)` con la cadena perceptiva
completa
El provider aporta **lo que morfo no puede inferir**: getters reactivos
sobre el estado interno, handlers concretos, glue de layers ortogonales
(Presence, Dismissal, ScrollLock).
### Sema — vocabulario + canales perceptivos
`Sema` define el vocabulario canónico del framework y orquesta el
**dispatch de señales perceptivas** a un conjunto de canales modulares.
Vocabulario canónico (`SEMA_MAP` en `src/uix/sema/sema-map.ts`):
- **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)
└── haptic.ts HapticChannel
```
El **canal visual** (built-in) es el único que comparte plano DOM con el
commit estructural posterior, y por tanto el único que bloquea al caller.
`EngineSemantic` ejecuta hooks genéricos de canales; el
`VisualChannel.prepare()` proyecta `data-event` + `data-event-id` +
`data-event-phase` (y opcionalmente `data-event-family` y
`data-event-intent`) al target mediante un `SignalProjector`. En
`ActiveUix` ese proyector recibe `uix.dom`, por lo que la escritura de attrs
entra por el mismo dueño DOM que usa soma. `VisualChannel` mantiene el
`hold` configurable y el cleanup retira la proyección antes de resolver la
Promise (semántica secuencial estricta).
> **Namespace discipline**: la proyección semántica escribe **solo**
> atributos bajo el prefijo `data-event-*`. Nunca toca `data-state`,
> `data-intent`,
> `data-disabled` u otros state attrs — esos pertenecen al runtime/morfo.
> Eidos lee `data-event-intent` para reacciones a la señal transitoria
> y `data-intent` (cuando lo emita el morfo) para el estado persistente.
Los hold defaults internos al canal visual vienen de
`SEMA_MAP.families[*].hold` y se resuelven sobre la escala perceptiva
`SEMA_DURATIONS`: `glimpse`, `brief`, `noticed`, etc. El integrador puede
sobreescribir per signal (`signal.hold`) o globalmente vía
`new EngineSemantic({ dom, visual: { defaultHold } })`.
El **SoundChannel** está implementado: sintetiza earcons cortos vía
Web Audio (dos osciladores → biquad lowpass → envelope ADSR-lite,
parametrizado por `effective.sound.{pitch, centroid, gain, contour,
roughness, duration}`). Hace **prepare-time priming**: crea + resume el
`AudioContext` en el `prepare()` de una señal sonora, síncronamente dentro
del gesto de usuario. Solo despues registra el listener capture-phase en
`document` para re-resume posteriores. Es opt-in:
`new EngineSemantic({ sound: true })`.
El **HapticChannel** sigue como canal opt-in; el registry ya lo
acepta para que cuando se implemente encaje sin cambios estructurales.
Cualquier canal no-visual es fire-and-forget: gestiona su propio
timing en su plano sin afectar al caller.
### Eidos — la capa visual
`Eidos` es la capa visual. Su acceso al sistema es **el DOM**: lee
parts, data-attrs, ARIA, archetypes y event signals que las otras
capas escriben. No importa internals de soma; no pregunta a sema.
Eidos **no es solo CSS**. Cubre lo que en la rama muerta `air/` era el
"runtime visual" más el sistema de tokens — sin heredar código. Su
estructura actual:
```
src/uix/eidos/
├── active-eidos.svelte.ts ActiveEidos: runtime/contexto visual creado por ActiveEidos.create
├── archetypes.css reglas comunes a [data-archetype=*]
├── events.css reacciones a [data-event-*] (sema visual)
├── generated/base.css foundation CSS generado desde EidosConfig base
├── themes/fonts.css font faces usados por el base generado
├── lib/ soporte de config, recipes, contrato CSS y tipos compartidos
└── components/{x}/ recipe + wrapper Svelte + tipos por componente
├── {x}.css recipe (selectores [data-{x}], variants)
├── {x}.svelte wrapper sobre el provider headless de soma
├── types.ts Props visuales + props publicas de soma
└── index.ts default root + partes attached
```
`ActiveEidos` es la fuente de verdad nueva del theming: primitivas
(color + alpha scales, size map, espacios, control height, radius, borde,
opacidad, z-index, focus ring, layout, tipografia, shadow, motion, icon), roles
semanticos y themes. Tambien resuelve el theme activo desde sus fuentes
visuales (`theme`, `modeSource`, `densitySource` o defaults) e inyecta CSS runtime
cuando la app no lo precompila. Los themes externos pueden venir solo por CSS
si respetan el contrato de custom properties (`themeSource: 'auto' | 'config' | 'css'`);
`getCssContract()` publica ese contrato como datos typed y
`renderContractCss()` lo materializa como CSS vacío desde la config.
Los aliases de recipe por componente (`--toast-*`, `--dialog-*`, etc.) viven en
`EidosConfig.recipes` y se generan dentro de `generated/base.css`; las recipes
CSS permanecen como selectores/estados, no como fuente paralela de tokens.
`ActiveEidos.listRecipes()` y `getRecipeTokens(component)` son la superficie
de consulta para editores de theme; devuelven nombres y copias defensivas,
no handles mutables al config interno.
`ActiveEidos` tambien puede escribir variables runtime en un style block
propio, validandolas contra el contrato para que un editor de theme no tenga
que mutar CSS a mano variable por variable. La persistencia de configuracion
completa usa `EidosConfigDocument` (`kind + version + options`), de modo que
`EidosConfig` queda como objeto puro de authoring y el versionado vive
en el borde de almacenamiento/intercambio.
`ActiveEidos` es tambien el contexto que consumen los wrappers Svelte:
`ActiveEidos.require()` expone solo la superficie visual (`dom`,
`langs`, `format`, `prefs` y helpers como `resolve(...)`, `breakpoint(...)` e
`isBelow(...)`). Los wrappers no importan `getActiveUix()` directamente.
El wrapper Svelte publico sigue la opcion C disciplinada: un root visual
`<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):
```css
/* Estilo común a todos los triggers, independiente del componente */
[data-archetype='trigger'] {
cursor: pointer;
}
/* Tinta exit anim según la causa (saved/cancelled/dismissed) */
[data-state='closed'][data-last-action='cancelled'] {
animation: ...;
}
/* Reacción a una señal perceptiva durante el hold (200–260ms según familia) */
[data-event-family='commit'][data-event-phase='active'] {
animation: eidos-commit-settle 260ms var(--ease-out);
}
/* Variante por intent transitorio (de la señal, no del estado) */
[data-event-family='commit'][data-event-intent='threat'][data-event-phase='active'] {
animation: eidos-announce-pulse-threat 400ms var(--ease-spring);
}
```
El DOM es el canal entre events/sema y eidos. El `VisualChannel` proyecta la
ocurrencia mediante `SignalProjector` + `ActiveDom`; Eidos reacciona.
---
## 3.bis ActiveUix sin `frontend` (cerrado)
`frontend` ya no existe como artefacto activo. La fuente transversal de
preferencias es `ActivePrefs`, siguiendo el mismo patron que usa `ActiveApp`;
la proyeccion DOM es explicita y vive fuera de `ActiveUix`.
La particion actual:
- `uix.langs` — idioma y traducciones; se sincroniza desde `prefs.language`.
- `uix.format` — formatos regionales; consume `prefs.locale` como
`LocaleSource`.
- `uix.clipboard` — capacidad de escritura al portapapeles; en standalone se
crea salvo `clipboard:false`, en attach se consume de `app.clipboard` cuando
un componente lo pide.
- `uix.dom` — unico escritor de attrs globales mediante `dom.apply`.
- `uix.prefs` — preferencias efectivas transversales: `language`, `locale`,
`direction`, `motion`, `sound`, `haptic`, etc.
- `uix.portal` — target generico de portales; capas como Soma lo adaptan a
su API (`portalTo`) sin que `ActiveUix` conozca esas capas.
Eidos queda fuera de la superficie de `ActiveUix`: `ActiveEidos.create(...)`
crea el contexto visual y, si se necesita CSS runtime, usa `uix.dom`,
`uix.langs`, `uix.format` y fuentes explicitas de `mode`/`density` cuando el
integrador no quiera los defaults.
`prefs.direction` es la unica fuente de direccion efectiva. Si el usuario no
ha fijado intent, deriva desde `prefs.language`; si llama a
`prefs.direction.set('rtl')`, ese override manda; si llama a
`prefs.direction.clear()`, vuelve a derivar. El atributo `html[dir]` es solo
la proyeccion DOM de ese valor efectivo.
La proyeccion DOM queda separada por ownership:
```text
ActivePrefsDomProjection -> dir, data-motion, data-sound, data-haptic
ActiveEidos -> data-theme, data-mode, data-density
```
En modo standalone, `createActiveUix()` instancia `ActivePrefs` con el preset
estandar de UIX y crea los servicios configurados. En modo attach,
`attachActiveUix(app)` reutiliza `app.prefs` porque `prefs` pertenece al core
de `ActiveApp`. `langs` y `dom` son los servicios requeridos para attach:
si faltan, `attachActiveUix(app)` falla temprano. `clipboard`, `events` y
`format` son opcionales; si una capa los necesita y no fueron declarados en la
app, el getter de `ActiveUix` falla explicitamente.
`ActiveUix` no auto-proyecta preferencias al DOM. La proyeccion cross-modal
existe en `arts/prefs` como `createActivePrefsDomProjection(...)`; la cablea
el composition root que quiera esos atributos globales. Esto permite usar
`ActiveApp` sin UIX, UIX sin Eidos, o Eidos con CSS precompilado sin crear
proyectores duplicados.
Cuando una shell UIX quiere modo visual runtime, el flujo canonico es:
```ts
const prefsProjection = createActivePrefsDomProjection({ prefs: uix.prefs, dom: uix.dom });
const eidos = ActiveEidos.create({
theme: 'base',
modeSource,
applyDom: true
});
```
`prefsProjection` y `eidos` se disponen con la shell. El modo claro/oscuro no
se escribe en `prefs.theme`; se pasa a `ActiveEidos` como fuente visual.
---
## 4. Cómo se articulan — la cadena de transcripción
Las cuatro capas forman una cadena de transcripción declarativa donde cada
una traduce el contrato de la anterior a su lenguaje:
```
Morfo declara (TypeScript constant + sium schema)
↓
SomaRuntime transcribe (Soma — leyendo morfo + sources)
↓
Provider aporta sources/handlers (Soma — TypeScript class)
↓
Effects sincronizan attrs (Soma — $effect + dom.apply)
↓
EngineSemantic despacha señales (Sema — registry + prepare/dispatch)
↓
VisualChannel prepara data-event* (Sema — via SignalProjector/uix.dom)
↓
Eidos lee el DOM y aplica CSS (Eidos — selectores + tokens)
```
Más, en paralelo (no en cadena):
- **ADom** materializa los `dom.apply`/`dom.remove` que Soma le pide para
los attrs estructurales derivados (data-state, aria-\*, etc.).
- **Canales no visuales de Sema** (sound, haptic, futuros) reciben la misma
señal y la materializan en su modalidad — fire-and-forget.
Las piezas con responsabilidades disjuntas:
| Pieza | Responsabilidad | No hace |
| ------------------- | ---------------------------------------------------- | ------------------------------------- |
| **Morfo** | Declarar el contrato | Ejecutar nada |
| **SomaRuntime** | Transcribir morfo a comportamiento | Decidir lógica de negocio |
| **Provider** | Aportar fuentes reactivas + handlers | Escribir attrs mutables al DOM |
| **Effects** | Aplicar attrs derivados via `dom.apply` | Decidir qué attrs (eso lo dice morfo) |
| **EngineSemantic** | Registry de canales + prepare/dispatch | Conocer DOM, audio, vibración |
| **VisualChannel** | Proyectar `data-event*` via projector + hold awaited | Escribir attrs estructurales |
| **SignalProjector** | Proyectar `data-event*` via `dom.apply` | Decidir cuándo emitir |
| **ADom** | Mutaciones DOM imperativas para attrs estructurales | Conocer las capas superiores |
`Eidos` queda fuera de esa cadena: lee del DOM, no participa en la
transcripción.
---
## 5. La cadena causal de una interacción
Ejemplo concreto: el usuario clickea el botón **×** de un Toast.
```
1. Browser dispara click → Svelte llama Close.onclick
2. Close.onclick ejecuta:
void this.toastItem.runtime.trigger('dismiss')
3. SomaRuntime.trigger('dismiss'):
3.1. Busca event 'dismiss' en morfo.events ✓
3.2. Resuelve target = Item DOM element via partRef('item')
3.3. AWAIT events.emit({ target, name: 'dismiss', family: 'emerge' })
EngineSemantic despacha la señal a TODOS los canales registrados:
- VisualChannel.prepare(): SignalProjector aplica data-event*
via dom.apply(target, data-event-family=emerge)
- VisualChannel.handle(): mantiene el hold (240ms para emerge)
- cleanup: dom.apply(target, data-event*=undefined)
- SoundChannel, HapticChannel: fire-and-forget (no awaited)
La Promise resuelve cuando el VisualChannel ha terminado el cleanup
(semántica secuencial estricta)
4. SomaRuntime invoca el handler del provider:
sources.events.dismiss() →
this.provider.toaster.dismiss(opts.toast.current.id) →
toast.dismissing = true (state mutation)
5. EFFECTS reactivos del runtime ven que isOpen cambió:
resolvePartAttrs recomputa los attrs del item part
dom.apply(target, { 'data-state': 'closed' }) en el siguiente tick
6. Eidos (CSS) ha reaccionado durante toda la secuencia:
- durante t=0..240ms: [data-event^="dismiss"] dispara animation @keyframes
fade-out (CSS animation, no transition: corre full-duration aunque el
attr desaparezca después)
- en t≈245ms: [data-state="closed"] toma el relevo
- Presence layer aplica data-ending-style; CSS termina la animación
```
State es la única fuente de verdad. El DOM es derivación. La señal
perceptiva PRECEDE al cambio estructural por el hold completo (~240ms para
emerge) — el caller espera el cleanup antes de mutar estado, dándole a CSS
una ventana perceptible para coreografiar la salida.
---
## 6. Las primitivas que pasan entre capas
### Atributos DOM — el canal universal
Todo lo que pasa entre capas pasa por atributos DOM:
| Atributo | Quién escribe | Quién lee |
| ------------------------------------------- | -------------------------------------- | ---------------------------- |
| `data-{component}` | partProps (estático) | Eidos (selector raíz) |
| `data-{component}-{part}` | partProps (estático) | Eidos (selector parte) |
| `data-archetype="trigger"` | partProps (estático) | Eidos (selector transversal) |
| `id` | partProps | ARIA refs, tests |
| `role` | dom.apply (effect) | Lectores de pantalla, Eidos |
| `aria-*` | dom.apply (effect) | Lectores de pantalla, Eidos |
| `data-state="open"` | dom.apply (effect) | Eidos (selector variant) |
| `data-disabled` | dom.apply (effect) | Eidos (selector estado) |
| `data-event="dismiss"` | sema.emit (transient) | Eidos (selector evento) |
| `data-event-phase="active"` | sema.emit (transient) | Eidos |
| `data-event-id="sig-N"` | sema.emit (transient) | Sound/Haptic futuros |
| `data-event-family="commit"` | sema.emit (transient) | Eidos (selector familia) |
| `data-event-intent="risk"` | sema.emit (transient) | Eidos (tinta de la señal) |
| `data-color="primary"` | dom.apply (effect) | Eidos (recipe per token) |
| `data-intent="risk"` | dom.apply (effect, opcional per morfo) | Eidos (estado persistente) |
| `data-last-action="cancelled"` | trigger prewrite | Eidos (tinta exit) |
| `data-starting-style` / `data-ending-style` | Presence layer | Eidos (animation hooks) |
**Regla operativa**: lo que `dom.apply` escribe, Svelte no lo renderiza. La
identidad estática (id + marker + archetype + ref attachment) ship via
`partProps`. Lo derivado de state ship via `dom.apply` desde effects. No
hay double-write.
### Vocabularies cross-layer
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/Haptic/Eidos suscribir o estilar por verb o por familia sin
enumerar componentes. `validateEventName` reconoce ambas formas.
**Intents** (`src/uix/sema/sema-map.ts:SEMA_MAP.intents`), 6 valores:
```
neutral — sin carga afectiva (default)
affirm — positivo bajo ("todo va bien")
fulfill — positivo resolutivo ("objetivo cumplido")
risk — negativo moderado ("revisa esto")
threat — negativo activo ("alarma, atención inmediata")
loss — consecuencia consumada (negativo + baja activación, posterior)
```
Intent es ortogonal a familia: una `commit` puede ser `affirm` (subscribe),
`risk` (publish), `threat` (delete), o `neutral` (toggle plano). El
provider lo declara en `morfo.events[].semantic.intent` (literal) o lo
expone como prop (`fromProp + supported subset`).
---
## 7. Reglas duras
Las invariantes operativas que mantienen el sistema coherente:
1. **Morfo no conoce código de runtime.** Es declaración pura.
2. **SomaRuntime depende de Dom y Semantic.** Por construcción, no por
import. Provider las inyecta.
3. **Provider no escribe attrs mutables al DOM directamente.** Los aporta
como sources al runtime.
4. **`Semantic` puede usar `Dom` (hacia abajo).** `Dom` no conoce
`Semantic`.
5. **`ADom` no conoce capas superiores.** Solo aplica mutaciones, listeners
y acciones DOM transversales que recibe.
La frontera DOM no exige envolver lecturas locales: un componente puede
llamar `el.contains(...)`, `el.closest(...)`, `el.getBoundingClientRect()`
o leer `scrollTop` de su propio elemento. En cambio, listeners de
`document/window`, consultas globales, foco imperativo y scroll de ventana
pasan por `ActiveDom`.
6. **`Eidos` consume DOM y `data-*`, no internals de Soma ni Sema.** Si lo
necesita, debe estar declarado en morfo o emitido en una señal de sema.
7. **Lo que `dom.apply` escribe, Svelte no lo renderiza desde `partProps`.**
Una sola autoridad por atributo.
8. **State es la única fuente de verdad. El DOM es derivación.** Los
handlers mutan state; los effects derivan attrs.
9. **Event handlers en `runtime.trigger` son síncronos.** Async va antes
de llamar `trigger`.
10. **Guards van en el call-site, no dentro del handler.** Si el guard
entra al handler, ya emitió la señal perceptiva.
11. **`morfo.events[].commits` es descriptivo, no ejecutable.** Documenta
lo observable; el smoke valida.
12. **La regla 2-de-3 para extender Morfo.** Una extensión a morfo solo se
justifica si **al menos dos de las tres capas** (soma, sema, eidos)
la consumen. Soma-only conveniences viven en provider via virtual
prop.
---
## 8. La distinción autoría / transcripción
Una lente útil para decidir dónde vive cada cosa:
- **Autoría** — escrito una vez por un humano, con intención. Los `Props`
de un componente, el morfo, los handlers de eventos. Vive en
TypeScript del autor.
- **Transcripción** — derivado mecánicamente de la autoría. Los `Opts`
del provider, el wrapping `readableActive(() => x)` por prop, los
attrs estructurales. Lo deriva una helper / runtime / generator.
UIX intenta que solo la autoría sea humana. La transcripción es código
que escribe código:
| Autoría | Transcripción | Cómo |
| -------------------------------------- | ---------------------- | ---------------------------------- |
| `Props` | `Opts` | `OptsFromProps<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, Haptic, Motion y futuras modalidades se registran
como **canales** del `EngineSemantic`; cada uno gestiona su propia
modalidad. El engine es solo registry + dispatch.
- **No es un EventEmitter global disfrazado de arquitectura.** Cada
evento tiene un target específico en el DOM y un dueño semántico
declarado en morfo.
- **No es un mini-DSL en JSON.** Morfo es declarativo descriptivo, no
programa. La lógica vive en TypeScript del provider; morfo solo dice
qué attrs y qué semántica.
---
## 10. Estado actual (2026-05-14)
**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 `translations`
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. La logica de dominio que no es
comportamiento headless vive en `src/libs`: `datagrid`, `forms` y
`strings/fuzzy-score`.
- `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); HapticChannel opt-in. 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, listeners, observers,
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**:
- Afilar progresivamente `EidosConfig.recipes`. Los CSS historicos de
`contracts/`, `themes/base/` y `tokens/` se retiraron del arbol activo; los
aliases de recipe ya se generan desde `src/uix/eidos/generated/base.css`.
Si una recipe necesita estructura real, subirla a tipo propio en vez de
añadir convenciones implícitas en CSS.
- 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, Haptic, Motion,
cualquier modalidad futura se registra como un `Channel` adicional en
`EngineSemantic` sin tocar morfo ni soma.
- **Honestidad sobre fronteras de framework vs integrador**. UIX provee
vocabularios, contratos, transporte y puntos de extensión; no finge
decidir por todas las modalidades de todas las apps.
La idea importante:
> **La coherencia cross-modal puede tratarse como responsabilidad del
> integrador, no como una falsa promesa de un runtime centralizado que
> pretende saberlo todo.**
---
## 13. La frase resumen
> **Morfo declara · SomaRuntime transcribe · Provider aporta · Effects
> sincronizan · Semantic emite · Dom aplica · Eidos lee.**
Siete palabras que describen la cadena entera. Si una decisión arquitectónica
contradice una de esas siete, la decisión está mal — o la arquitectura tiene
que evolucionar conscientemente.
---
## 14. Para profundizar
- [src/uix/README.md](./README.md) — posicionamiento general (más narrativo)
- [src/uix/morfo/README.md](./morfo/README.md) — declaración, archetypes, regla 2-de-3
- [src/uix/sema/README.md](./sema/README.md) — `emit` contract, verbs canónicos
- [src/uix/soma/SOMA_ARCHITECTURE.md](./soma/SOMA_ARCHITECTURE.md) — runtime + componentes
- [src/uix/soma/COMPONENT_GUIDE.md](./soma/COMPONENT_GUIDE.md) — guía operativa para crear / migrar componentes
- [src/uix/eidos/README.md](./eidos/README.md) — capa visual: tokens, themes, recipes, wrappers
- [`src/arts/adom/README.md`](../arts/adom/README.md) — `dom.apply` + servicios DOM reactivos
- [`src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md`](../docs/GUIA_IMPLEMENTACION_SEMAUIX.md) — convenciones doctrinales del API
Decisiones de diseño detalladas y trade-offs históricos en el git log de la
rama `active-uix`.

Powered by TurnKey Linux.