9.4 KiB
Eidos
Eidos es la capa visual de UIX. Cubre lo que en la rama muerta air/
era el "runtime visual" más el sistema de tokens — sin heredar código.
Lee del DOM lo que las capas anteriores han escrito (morfo runtime + sema
visual channel) y aplica estilos, animaciones y wrappers ergonómicos.
Morfo declara la genética
↓
Soma transcribe el comportamiento → DOM (data-state, data-color, aria-*)
↓
Sema emite señales perceptivas → DOM (data-event-*) durante el hold
↓
Eidos aplica el visual: tokens, themes, recipes, archetypes, wrappers
Eidos nunca importa internals de soma ni de sema. Su única fuente de verdad es lo que está escrito en el DOM (parts, data-attrs, ARIA, archetypes, event signals) — y los tipos públicos del soma para componer wrappers.
No es solo CSS
El primer mental model fue "eidos = CSS reactivo". Insuficiente: hay concerns visuales puros (variant, size, layout flags, icon slots) que no son parte del comportamiento headless de soma pero sí son ortogonales al CSS. Eidos los aloja como wrappers Svelte sobre los soma providers.
src/uix/eidos/
index.css → entrypoint que importa todo el CSS
archetypes.css → reglas comunes a [data-archetype=*]
events.css → reacciones a [data-event-*] (sema visual)
contracts/ → APIs de variables CSS (declaraciones vacías)
tokens/ → valores per-componente que referencian contracts
themes/base/ → light + dark + _static
lib/ → tipos/helpers transversales (Size, …)
components/{x}/ → recipe + wrapper + tipos por componente
{x}.css recipe CSS (selectores [data-{x}], etc.)
{x}.svelte wrapper Svelte sobre soma
types.ts props del wrapper (extiende soma)
index.ts re-exports públicos (default + Provider)
components/{x}/ es la forma actual del componente — toggle es el
piloto. Los componentes legacy quedan en components/{x}.css (CSS
solamente) hasta su migración a la forma de subdirectorio.
Qué consume
De morfo (declaración)
| Pieza | Eidos la usa para |
|---|---|
parts[].kebab |
selectores [data-{component}-{kebab}] |
parts[].archetype |
reglas transversales [data-archetype=trigger] |
parts[].states + data[].values |
variantes [data-state=open] |
parts[].data con data-starting-style / data-ending-style |
hooks de animación enter/exit |
events[].name |
selectores [data-event=dismiss], [data-event^=commit] |
events[].semantic.family + .intent |
tinta semántica de transiciones |
events[].prewrite[] (e.g. data-last-action) |
tintar exit anim por causa |
focus.trap |
hint de layout para overlays |
De soma (tipos públicos)
Sólo importa tipos del soma, vía '$soma/components/{x}/types',
nunca clases ni state managers internos. Ejemplo:
// eidos/components/toggle/types.ts
import type { ToggleProps as SomaToggleProps } from '$soma/components/toggle/types';
export type ToggleProps = SomaToggleProps & { variant?: ToggleVariant; size?: ToggleSize; … };
El wrapper .svelte reexporta el provider headless y le añade los
data-attrs de tokens visuales (data-variant, data-size, data-block,
data-icon-only).
De sema (DOM)
Sólo el DOM. El visual channel escribe data-event-* durante el hold y
eidos reacciona vía events.css:
[data-event-family='commit'][data-event-phase='active'] {
animation: eidos-commit-settle 260ms var(--ease-out);
}
[data-event-family='commit'][data-event-intent='threat'][data-event-phase='active'] {
animation: eidos-announce-pulse-threat 400ms var(--ease-spring);
}
events.css documenta los hold defaults por familia y por qué se usa
animation: @keyframes (no transition) para reacciones a señales.
Qué NO consume
- Computed state lógico del provider (e.g. la composición de
isDisabledpropio del componente con eldisabledheredado de un Field). Eidos sólo ve el resultado:[data-disabled]está o no está. - Internals de runtime. No sabe si un attr lo escribió
dom.apply, Svelte render o el provider manualmente. Sólo le importa que esté. - Layers de soma (Presence, Dismissal, ScrollLock, FocusScope). Reacciona a sus efectos visibles, no a su existencia.
La regla de los --* tokens
Eidos posee el namespace --* en el visual layer. Razones:
- Authorship clarity en debug — inspeccionar un elemento y ver
--toggle-bginforma que viene del visual layer de UIX. - Override discipline — un consumer que sobreescribe
--color-primary-elementsabe que está tocando contrato visual, no nombrando-colisionando con una variable local.
Las capas superiores (sema, soma, morfo) NO consumen estos tokens y no usan el prefijo. Cada una carga sus propias concerns (perceptual durations, behavior, contract DNA) ortogonales al rendering visual.
La regla "2-de-3" (heredada de morfo)
Una extensión a morfo se justifica si al menos dos de las tres capas (soma, sema, eidos) la consumen. Las que entraron por el voto de eidos:
archetype— eidos + sema (+ soma como emisor)events[].semantic.family/intent— sema + eidosevents[].prewrite[]— soma (ejecuta) + eidos (anima)data-starting-style/data-ending-style— soma (Presence) + eidos (anima)
Convenciones del API
Las convenciones doctrinales (operación instantánea = un evento;
sistema unificado de 8 tokens; intent ↔ color resolution; subset por
componente; eidos no es solo CSS; estructura de directorios; wrapper
composition; soma compound vs eidos flat; iconOnly sr-only; sound
eager-init) viven en
src/docs/sema-implementation-guide.md
sección Parte IV — Convenciones del API. Esa guía es autoritativa.
Los puntos esenciales para autores que migran un componente a eidos:
- Wrapper, no fork. El
.sveltede eidos consume el provider de soma y le añade los data-attrs de tokens visuales. No reimplementa estado. Sizedesdelib/types.ts. Componentes que aceptan tamaños reusan el tipo compartido y narrowingan al subset que su recipe soporta (Extract<Size, 'sm' | 'md' | 'lg'>).- Sin prefijo
Eidosen los tipos. El path$uix/eidos/components/{x}ya identifica la capa. index.tsexportadefault+Provider. Para componentes single-part el consumer puede usarimport Toggley<Toggle>plano; elProviderqueda paraimport * as Toggleconsumers que prefieren la forma compound.- CSS recipe sin prefijo
--eidos-. Los custom properties usan--{component}-…para los públicos y--_{component}-…para los internos.
Defensa contra drift de selectores
La cadena morfo → soma → eidos depende de que los selectores que eidos
escribe ([data-{component}], [data-{component}-{part}],
[data-state=...], [data-event-*=...]) se mantengan en sintonía con
los attrs que el morfo declara y el runtime emite. Hay dos defensas
distintas según el tipo de consumidor:
Compile-time (TS / Svelte) — typed builder
Cualquier consumidor TypeScript que construya selectores
MUST usar semaSelector(morfo, partKebab, matchers?) desde
$uix/morfo. Esto cubre las cascade rules de sema/components/*.ts y
cualquier lógica TypeScript en eidos/components/{x}/ que apunte a
attrs morfo-backed.
import { semaSelector } from '$uix/morfo';
import { toggleMorfo } from '$uix/morfo/components/toggle';
semaSelector(toggleMorfo, 'provider', { eventName: 'commit-toggle' });
// → '[data-toggle][data-event="commit-toggle"]'
Renombrar una part o un evento en el morfo rompe el typecheck. Es
imposible que un selector TypeScript drifte silenciosamente. Ver
morfo/README.md#typed-selector-builder--semaselector.
Run-time (CSS recipes) — eidos-lint como red de seguridad opt-in
Los recipes son CSS plano (components/{x}/{x}.css); no hay typed
builder en el lado CSS. Para esa superficie:
node scripts/eidos-lint.ts toggle # un componente
node scripts/eidos-lint-all.ts # todos
Clasifica cada selector [data-*] como:
- morfo-backed — declarado en el morfo; soma runtime lo emite;
el valor (si hay enum) cae dentro de
data[].values. - eidos-only — el marker está, pero al menos un
data-*no está declarado en el morfo. Válido por convención (tokens visuales comodata-variant,data-sizevienen del wrapper). - invalid — referencia un attr declarado pero con un valor fuera del enum. Bug.
El lint es una red de seguridad, no el contrato. El contrato vive en el morfo y se defiende a nivel de tipos donde se puede. El lint existe sólo para la porción CSS-pura que aún no consume el morfo a través de TypeScript. Cuando los recipes migren a un builder, el lint podrá retirarse.
Estado actual (2026-05-08)
- Capa: existente, en migración progresiva.
- Pilot:
components/toggle/— wrapper + recipe + tipos. - Migrados a wrapper/native: toggle, switch, collapsible, avatar.
- Pendientes de migrar a wrapper: dialog, drawer, popover, toast
(sus archivos
.cssya existen encomponents/; falta crear el subdirectorio con.svelte+index.tstypes.ts).
- Tokens y themes: completos en
tokens/,themes/base/. - Linter: funcional sobre CSS legacy plano y recipes en subdirectorio.