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