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/eidos/README.md

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 isDisabled propio del componente con el disabled heredado 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:

  1. Authorship clarity en debug — inspeccionar un elemento y ver --toggle-bg informa que viene del visual layer de UIX.
  2. Override discipline — un consumer que sobreescribe --color-primary-element sabe 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 + eidos
  • events[].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:

  1. Wrapper, no fork. El .svelte de eidos consume el provider de soma y le añade los data-attrs de tokens visuales. No reimplementa estado.
  2. Size desde lib/types.ts. Componentes que aceptan tamaños reusan el tipo compartido y narrowingan al subset que su recipe soporta (Extract<Size, 'sm' | 'md' | 'lg'>).
  3. Sin prefijo Eidos en los tipos. El path $uix/eidos/components/{x} ya identifica la capa.
  4. index.ts exporta default + Provider. Para componentes single-part el consumer puede usar import Toggle y <Toggle> plano; el Provider queda para import * as Toggle consumers que prefieren la forma compound.
  5. 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 como data-variant, data-size vienen 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 .css ya existen en components/; falta crear el subdirectorio con .svelte + index.ts
    • types.ts).
  • Tokens y themes: completos en tokens/, themes/base/.
  • Linter: funcional sobre CSS legacy plano y recipes en subdirectorio.

Powered by TurnKey Linux.