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.
712 lines
29 KiB
712 lines
29 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 fuente de verdad es
|
|
**lo que está escrito en el DOM** (parts, data-attrs, ARIA, archetypes,
|
|
event signals) y los tipos públicos de Soma que necesita para componer
|
|
wrappers.
|
|
|
|
## Handoff 2026-05-14
|
|
|
|
Eidos queda congelado a nivel de componentes hasta reauditar la arquitectura
|
|
de UIX. No tocar `src/uix/eidos/components/*` salvo orden explicita.
|
|
|
|
Antes de seguir con wrappers o recipes por componente hay que respetar estas
|
|
decisiones:
|
|
|
|
- que contrato minimo consume `Eidos` desde `ActiveUix`;
|
|
- `ActiveEidos` asume authoring, validacion, generacion CSS, persistencia y
|
|
contexto visual;
|
|
- `events` es el servicio perceptivo runtime y `morfo.translations` es el
|
|
catalogo declarativo de texto owned por el componente;
|
|
- que parte se genera desde codigo y que parte puede venir solo por CSS;
|
|
- como se mantiene la regla de escritura DOM unica en standalone `dom:false`.
|
|
|
|
La referencia de arranque esta en
|
|
[`../active_architecture.md`](../active_architecture.md), seccion
|
|
`Handoff 2026-05-14`.
|
|
|
|
## 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 pero sí son ortogonales al CSS.
|
|
Eidos los aloja como **wrappers Svelte sobre el Soma**.
|
|
|
|
```
|
|
src/uix/eidos/
|
|
active-eidos.svelte.ts → runtime/contexto visual y generacion CSS
|
|
index.css → entrypoint que importa todo el CSS
|
|
archetypes.css → reglas comunes a [data-archetype=*]
|
|
events.css → reacciones a [data-event-*] (sema visual)
|
|
generated/base.css → salida estatica generada desde EidosConfig base
|
|
themes/fonts.css → font faces usados por el theme base generado
|
|
lib/ → soporte de config, recipes, contrato CSS y tipos compartidos
|
|
components/{x}/ → recipe + wrapper + tipos por componente
|
|
{x}.css recipe CSS (selectores [data-{x}], etc.)
|
|
{x}.svelte wrapper Svelte sobre el componente Soma
|
|
types.ts props del wrapper (extiende el contrato público Soma)
|
|
index.ts namespace publico (default + partes attached)
|
|
```
|
|
|
|
`lib/` contiene el soporte puro de configuracion visual: primitivas, semantica
|
|
visual, themes, contrato CSS y render. `generated/base.css` es el primer
|
|
artefacto estatico generado desde esa configuracion (`npm run generate:eidos-css`).
|
|
Los antiguos `contracts/` y `themes/base/` CSS se retiraron del arbol activo:
|
|
el contrato publico se obtiene con `ActiveEidos.getCssContract()` /
|
|
`renderContractCss()` y el base visual sale de `generated/base.css`.
|
|
Los antiguos `tokens/components/*` tambien se retiraron: los nombres de
|
|
custom properties de recipe (`--toast-*`, `--dialog-*`, etc.) siguen siendo
|
|
el contrato estable, pero sus valores base viven en `EidosConfig.recipes` y
|
|
se generan en `generated/base.css`.
|
|
|
|
## Runtime activo
|
|
|
|
Eidos tiene una clase activa:
|
|
|
|
- `ActiveEidos` es el runtime activo y contexto visual de los wrappers Svelte.
|
|
Gestiona configuracion visual pura, primitivas, roles semanticos, themes,
|
|
validacion, render de CSS y persistencia. Conecta esa configuracion con los
|
|
servicios de `ActiveUix` cuando hay contexto:
|
|
`prefs`, `dom`, `langs`, `format` y helpers visuales como `resolve(...)`,
|
|
`breakpoint(...)` o `isBelow(...)`. Si `applyDom` esta activo, inyecta/quita
|
|
`<style data-uix-eidos>` usando `uix.dom`.
|
|
|
|
Regla de dependencia: un componente en `src/uix/eidos/components/*` no importa
|
|
`getActiveUix()` directamente. Consume `ActiveEidos.require()` y solo conoce la
|
|
superficie visual de Eidos.
|
|
|
|
Regla de ownership: `ActiveEidos` no crea servicios compartidos. En modo
|
|
contexto, `ActiveEidos.create(...)` obtiene esos servicios de `ActiveUix`; si
|
|
un wrapper requiere uno que no existe, lanza error. En modo explicito,
|
|
`createActiveEidos(...)` recibe servicios ya construidos. Con `applyDom:false`
|
|
queda como runtime sin escritura DOM: puede renderizar, serializar y validar
|
|
CSS sin insertar estilos.
|
|
|
|
Matiz importante: `prefs`, `langs` y `format` son opcionales durante la
|
|
construccion porque `ActiveEidos` tambien sirve para generar/serializar CSS.
|
|
Los getters `activeEidos.prefs` y `activeEidos.langs` si fallan temprano si un
|
|
wrapper visual los lee y no fueron inyectados. `dom` solo es obligatorio cuando
|
|
`applyDom` esta activo.
|
|
|
|
La configuracion vive en `EidosConfig`:
|
|
|
|
```ts
|
|
interface EidosConfig {
|
|
primitives: PrimitiveSet;
|
|
semantics: SemanticSet;
|
|
themes?: ThemeMap;
|
|
recipes?: RecipeTokenSet;
|
|
}
|
|
```
|
|
|
|
Para authoring incremental sobre el theme base existe
|
|
`EidosConfigPatch`. No sustituye al objeto completo: lo extiende de
|
|
forma profunda, preservando lo no declarado y reemplazando arrays completos
|
|
(por ejemplo `fallbacks` de una familia tipografica).
|
|
|
|
```ts
|
|
const config = createThemeBaseEidosConfig({
|
|
primitives: {
|
|
typography: {
|
|
families: {
|
|
primary: { family: 'Inter' }
|
|
}
|
|
}
|
|
},
|
|
semantics: {
|
|
color: {
|
|
roles: {
|
|
primary: 'blue'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
Si la app quiere partir de cero, usa `defineEidosConfig()` con un
|
|
`EidosConfig` completo y lo pasa a `ActiveEidos.create({ config })`. Si quiere
|
|
partir del theme base, usa `createThemeBaseEidosConfig(patch)` o directamente
|
|
`ActiveEidos.create({ themeBase: patch })`.
|
|
|
|
`primitives` contiene las bases no semanticas: escalas de color de 12 pasos,
|
|
size map canonico, espacios, alturas de control, radios, borde, opacidad,
|
|
z-index, focus ring, layout, tipografia, sombras, motion e iconos.
|
|
`semantics.color.roles` mapea esas escalas a los roles canonicos:
|
|
|
|
```text
|
|
primary · secondary · tertiary · neutral · affirm · fulfill · risk · threat · loss
|
|
```
|
|
|
|
No hay `danger`, `success`, `warning` ni `info`: esos nombres pertenecen a
|
|
otros modelos. En Eidos los componentes hablan por intents y jerarquia visual.
|
|
|
|
`recipes` contiene los aliases publicos de cada recipe activa. No define
|
|
selectores ni estados de componente: solo valores para custom properties que
|
|
las recipes CSS consumen. Por ejemplo, `recipes.tooltip['content-z']` genera
|
|
`--tooltip-content-z`; `recipes.dialog['overlay-bg']` genera
|
|
`--dialog-overlay-bg`. Asi los componentes mantienen su CSS estable y el tema
|
|
puede persistir/editar esos valores desde el mismo objeto de authoring.
|
|
Para editores de theme, `ActiveEidos.listRecipes()` enumera los componentes
|
|
con recipe tokens y `ActiveEidos.getRecipeTokens(component)` devuelve una
|
|
copia defensiva del mapa de aliases. No muta el config interno.
|
|
|
|
La decision vigente es mantener `recipes` como mapa plano de aliases
|
|
(`RecipeTokenSet`). Subir una recipe a tipo estructurado propio solo se
|
|
justifica cuando exista un builder o consumer real que necesite semantica
|
|
interna; mientras las recipes sigan siendo CSS plano, el contrato publico es
|
|
el custom property generado.
|
|
|
|
ActiveEidos genera tambien la escala alpha de cada paleta fisica:
|
|
|
|
```text
|
|
--scale-blue-a1 … --scale-blue-a12
|
|
--primitive-primary-a1 … --primitive-primary-a12
|
|
```
|
|
|
|
Si el theme declara `color.alphaScales`, esos valores se respetan. Si no, se
|
|
derivan desde el paso sólido de la escala (`--scale-{name}-9`) con una
|
|
progresion canonica de opacidad. Esto permite usar tintas translucidas para
|
|
overlays, rings, hovers suaves o scrims sin que cada recipe invente su propia
|
|
formula.
|
|
|
|
### Size canonico
|
|
|
|
`Size` es discreto y estable:
|
|
|
|
```text
|
|
xxs · xs · sm · md · lg · xl · xxl · full
|
|
```
|
|
|
|
`full` es semantica de layout y no genera primitiva fisica. La config mapea
|
|
solo `xxs..xxl` a tokens coordinados:
|
|
|
|
```text
|
|
--size-md-control-height
|
|
--size-md-font-size
|
|
--size-md-font-line-height
|
|
--size-md-font-letter-spacing
|
|
--size-md-icon-size
|
|
--size-md-padding-inline
|
|
--size-md-padding-block
|
|
--size-md-gap
|
|
--size-md-radius
|
|
```
|
|
|
|
La regla es: `md` no cambia de significado por viewport. Lo responsive decide
|
|
que size activo se usa (`ResponsiveProp<Size>`), no redefine los tokens de
|
|
`md`. Porcentajes, `vw`, `dvh`, `clamp()` o `min()` pertenecen a layouts
|
|
como `full`, panels y containers; no al size canonico de controles, iconos o
|
|
tipografia.
|
|
|
|
### Primitivas transversales
|
|
|
|
El bloque static ya genera las primitivas compartidas que suelen aparecer en
|
|
Radix Themes, Ark/Panda, Chakra, Tailwind o shadcn como foundation tokens:
|
|
|
|
- `layout`: anchuras de container, padding inline de container, anchuras de
|
|
contenido y ratios canonicos. Genera:
|
|
`--container-width-*`, `--container-padding-inline`,
|
|
`--content-width-*` y `--aspect-ratio-*`.
|
|
- `density`: escalas `compact · comfortable · spacious` para que las recipes
|
|
puedan ajustar espacio, altura de control o contenido sin redefinir los
|
|
tokens canonicos. Genera `--density-{key}-*` y aliases activos como
|
|
`--density-space-scale`.
|
|
- `border`: anchuras `none · hairline · thin · medium · thick`, estilos
|
|
`solid · dashed · dotted` y aliases `--border-width`, `--border-style`,
|
|
`--border`.
|
|
- `opacity`: `0 · muted · disabled · scrim · overlay · hover · press · full`.
|
|
- `zIndex`: `base · raised · sticky · dropdown · popover · tooltip · modal · toast`.
|
|
- `shadow`: escala fisica `1..6` más aliases semanticos
|
|
`none · subtle · raised · overlay` por theme.
|
|
|
|
La regla es la misma que en size: estos tokens no cambian de significado por
|
|
breakpoint. Un componente o wrapper puede elegir otro token en un viewport
|
|
concreto, pero `--shadow-3`, `--opacity-disabled` o `--z-index-modal` siguen
|
|
siendo la misma coordenada del sistema.
|
|
|
|
El bridge responsive vive en `ActiveDom`/`ActiveEidos`: los wrappers usan
|
|
`ActiveEidos.resolve(...)`, `breakpoint(...)`, `isAtLeast(...)` e
|
|
`isBelow(...)` para decidir que token canonico aplica en cada viewport.
|
|
`--container-width-md` o `--content-width-lg` no se recalculan por responsive;
|
|
lo que cambia es la eleccion del token. La densidad sigue el mismo principio:
|
|
`ActiveEidos` proyecta `data-density` y publica scalars activos:
|
|
|
|
```css
|
|
:root {
|
|
--density-compact-space-scale: 0.84;
|
|
--density-comfortable-space-scale: 1;
|
|
--density-spacious-space-scale: 1.16;
|
|
--density-space-scale: var(--density-comfortable-space-scale);
|
|
}
|
|
|
|
[data-density='compact'] {
|
|
--density-space-scale: var(--density-compact-space-scale);
|
|
}
|
|
```
|
|
|
|
Una recipe puede usar `calc(var(--space-4) * var(--density-space-scale))`.
|
|
`--space-4` no cambia de significado; cambia la policy visual activa.
|
|
|
|
`themes` permite sobrescribir escalas, roles, superficies, contenido, bordes,
|
|
focus y sombras por theme. El theme base actual expone `base-light` y
|
|
`base-dark`; `defaultActiveEidosThemeResolver` usa el theme activo si existe
|
|
como id exacto, preserva ids externos ya cualificados (`*-light`, `*-dark`) y,
|
|
si no, prueba `${theme}-${mode}`.
|
|
|
|
`ActiveEidos` no persiste CSS en disco. Si `applyDom` esta activo, escribe en
|
|
el DOM:
|
|
|
|
- `${styleId}-static` con primitivas estables (`renderStaticCss()`).
|
|
- `${styleId}-theme` con el theme resuelto cuando procede de la config.
|
|
|
|
`themeSource` decide de donde salen los valores de theme:
|
|
|
|
- `'auto'` (default): genera CSS si la config conoce el theme; si no lo
|
|
conoce, asume que viene de CSS externo y deja solo los tokens static.
|
|
- `'config'`: modo estricto; si el theme resuelto no existe en la config,
|
|
`renderThemeCss()` lanza error.
|
|
- `'css'`: nunca genera CSS de theme; el integrador aporta los valores con
|
|
CSS externo que respeta el contrato de custom properties.
|
|
|
|
Para inspeccionar o publicar ese contrato, `ActiveEidos` expone dos superficies:
|
|
|
|
- `getCssContract()` devuelve el contrato estructurado, typed, con cada token
|
|
(`name`, `cssVar`, `scope`, `category`, `path`). Es la superficie pensada
|
|
para editores de theme, validadores, tooling o persistencia de usuario.
|
|
- `renderContractCss()` renderiza ese mismo contrato como CSS vacío para
|
|
documentar o bootstrappear themes externos.
|
|
- `renderCssVariables()` acepta un mapa de custom properties y lo convierte
|
|
en CSS runtime validado contra el contrato de Eidos.
|
|
|
|
```ts
|
|
const tokens = eidos.getCssContract();
|
|
const recipes = eidos.listRecipes();
|
|
const tooltipTokens = eidos.getRecipeTokens('tooltip');
|
|
const contract = eidos.renderContractCss({
|
|
staticSelector: ':root',
|
|
themeSelector: "[data-theme='acme-light']"
|
|
});
|
|
```
|
|
|
|
El resultado no asigna valores reales; declara las custom properties que un
|
|
theme CSS-only puede aportar o sobrescribir:
|
|
|
|
```css
|
|
[data-theme='acme-light'] {
|
|
--scale-blue-9: ;
|
|
--scale-blue-a9: ;
|
|
--primitive-primary-9: ;
|
|
--primitive-primary-a9: ;
|
|
--color-primary-solid: ;
|
|
--size-md-control-height: ;
|
|
--border-width-thin: ;
|
|
--opacity-disabled: ;
|
|
--z-index-modal: ;
|
|
--shadow-3: ;
|
|
}
|
|
```
|
|
|
|
La persistencia usa un envelope versionado:
|
|
|
|
```ts
|
|
const document = eidos.toDocument();
|
|
const json = eidos.serialize();
|
|
|
|
const hydrated = createActiveEidos({
|
|
config: JSON.parse(json),
|
|
prefs,
|
|
dom
|
|
});
|
|
```
|
|
|
|
El documento tiene forma `{ kind: 'uix.eidos-config', version: 1, options }`.
|
|
`EidosConfig` no lleva `version` dentro: sigue siendo configuracion
|
|
visual pura. Si el shape cambia en el futuro, se sube
|
|
`EIDOS_CONFIG_DOCUMENT_VERSION`; Eidos no intenta compatibilidad silenciosa
|
|
con versiones no soportadas. La app decide dónde guardar ese documento
|
|
(prefs, backend, archivo, etc.) y qué metadata de producto lo envuelve
|
|
(`name`, owner, timestamps).
|
|
|
|
Integracion normal dentro de un arbol con `ActiveUix`:
|
|
|
|
```ts
|
|
const uix = createActiveUix({
|
|
langs: { schema, defaultLocale: 'es' }
|
|
});
|
|
|
|
setActiveUix(uix);
|
|
|
|
ActiveEidos.create({
|
|
themeBase: {
|
|
semantics: {
|
|
color: {
|
|
roles: { primary: 'blue' }
|
|
}
|
|
}
|
|
},
|
|
themeSource: 'auto',
|
|
themeResolver,
|
|
styleId: 'uix-eidos'
|
|
});
|
|
```
|
|
|
|
`config` acepta un `EidosConfig` completo o un `EidosConfigDocument`.
|
|
`themeBase` acepta solo un patch sobre el theme base. Son
|
|
mutuamente excluyentes para que no haya ambiguedad entre "config completa" y
|
|
"override del base".
|
|
|
|
`theme` en Eidos nombra la familia/theme visual (`base`, `acme`,
|
|
`acme-light`, etc.). `mode` nombra el esquema efectivo `light | dark` y
|
|
`density` nombra la ergonomia visual activa. Cuando `applyDom` esta activo,
|
|
`ActiveEidos` proyecta `data-theme`, `data-mode` y `data-density` en el
|
|
documento a traves de `ActiveDom`.
|
|
|
|
`mode` y `density` se inyectan como fuentes visuales, no como dimensiones de
|
|
`prefs`:
|
|
|
|
```ts
|
|
const eidos = ActiveEidos.create({
|
|
theme: 'base',
|
|
modeSource: {
|
|
get: () => colorMode,
|
|
onChange: (handler) => subscribeColorMode(handler)
|
|
},
|
|
densitySource,
|
|
applyDom: true
|
|
});
|
|
```
|
|
|
|
Si no se pasa `modeSource`, `ActiveEidos` usa `prefers-color-scheme` con
|
|
fallback `light`. Si no se pasa `densitySource`, usa `comfortable`. No leer ni
|
|
escribir `uix.prefs.theme` para UIX: el modo visual pertenece a Eidos.
|
|
|
|
Un theme CSS-only puede vivir fuera de TypeScript:
|
|
|
|
```css
|
|
[data-theme='acme-light'] {
|
|
--scale-blue-9: #006adc;
|
|
--scale-blue-a9: color-mix(in srgb, var(--scale-blue-9) 56%, transparent);
|
|
--primitive-primary-9: var(--scale-blue-9);
|
|
--primitive-primary-a9: var(--scale-blue-a9);
|
|
--color-primary-solid: var(--primitive-primary-9);
|
|
--font-family-primary: Inter, system-ui, sans-serif;
|
|
--size-md-control-height: 38px;
|
|
--shadow-3: 0 8px 24px rgb(15 23 42 / 0.12);
|
|
}
|
|
```
|
|
|
|
Para pasar valores desde runtime sin recompilar:
|
|
|
|
```ts
|
|
activeEidos.setCssVariables({
|
|
'--color-primary-solid': 'rebeccapurple',
|
|
'size-md-control-height': '40px',
|
|
'shadow-3': '0 10px 28px rgb(20 20 20 / 0.16)'
|
|
});
|
|
```
|
|
|
|
`ActiveEidos` lo escribe en `${styleId}-variables` como un `<style>`
|
|
gestionado. Por defecto valida los nombres contra `getCssContract()`; si una
|
|
app necesita variables locales fuera del contrato puede usar
|
|
`{ strict: false }`.
|
|
|
|
No crear `ActiveEidos` desactiva la capa visual runtime de Eidos. Los
|
|
componentes Soma siguen funcionando headless porque el comportamiento pertenece
|
|
a Soma.
|
|
|
|
## 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
|
|
|
|
Los wrappers de `components/*` importan las partes públicas de Soma
|
|
directamente (`$soma/components/{x}`) y sólo añaden superficie visual de
|
|
Eidos: tokens, recipes, layout shells y data-attrs de presentación. No hay
|
|
facade intermedio por componente. Ejemplo:
|
|
|
|
```ts
|
|
// eidos/components/toggle/types.ts
|
|
import type { ProviderProps as SomaToggleProviderProps } from '$soma/components/toggle';
|
|
export type ToggleProps = SomaToggleProviderProps & { 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 proyecta `data-event-*` durante el hold
|
|
mediante `SignalProjector` y eidos reacciona vía `events.css`:
|
|
|
|
```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 — disciplined option C (vigente 2026-05-10)
|
|
|
|
Las convenciones doctrinales viven en
|
|
[`src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md`](../../docs/GUIA_IMPLEMENTACION_SEMAUIX.md)
|
|
sección 13 (operación instantánea = un evento; sistema
|
|
unificado de 8 tokens; intent ↔ color resolution; subset por
|
|
componente; iconOnly sr-only; sound prepare-time priming).
|
|
|
|
La convención del **shape público** del componente eidos vive en
|
|
[`components/README.md`](./components/README.md). Resumen — 7 reglas
|
|
duras:
|
|
|
|
1. **Un solo punto de entrada por componente** — la default export es
|
|
el componente root visual, llamado igual que el componente
|
|
(`<Drawer>`, `<Tabs>`, `<Checkbox>`). NO `<Drawer.Provider>`,
|
|
NO `<Drawer.Root>`.
|
|
2. **El root vive en `{name}.svelte`** — NO en `{name}-provider.svelte`.
|
|
Un solo fichero por root.
|
|
3. **NO exportar `Provider` públicamente.** La separación
|
|
"Provider compound vs flat" es invención retirada; hay UNA forma
|
|
compound (root + hijos atados como propiedades).
|
|
4. **Hijos siguen el naming de air / headless**: `Trigger`, `Content`,
|
|
`Overlay`, `Title`, `Description`, `Close`, `Portal`, `Header`,
|
|
`Footer`, `Item`, `Indicator`, `HiddenInput`, `Group`, `Label`.
|
|
No inventar nombres.
|
|
5. **`Portal` se incluye donde air lo tenía** (Dialog, Drawer, Popover,
|
|
Tooltip — overlays portaled). Importado de
|
|
`$soma/components/internal`.
|
|
6. **NO flat con snippet slots como API principal** —
|
|
`<Drawer trigger={...} title={...}>` está retirado. La compound
|
|
explícita expone las decisiones de composición que el flat escondía.
|
|
7. **Hijos atados con asignación explícita**, no `Object.assign`
|
|
(Svelte 5 puede manejar mal la mutación bulk del component
|
|
constructor durante hidratación):
|
|
```ts
|
|
const Drawer = DrawerRoot as DrawerNamespace;
|
|
Drawer.Trigger = Trigger;
|
|
Drawer.Content = Content;
|
|
```
|
|
|
|
Plus reglas de implementación:
|
|
|
|
8. **Wrapper, no fork.** El `.svelte` de eidos consume el provider del
|
|
Soma y le añade los data-attrs de tokens visuales. No
|
|
reimplementa estado.
|
|
9. **`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'>`).
|
|
10. **Sin prefijo `Eidos`** en los tipos. El path
|
|
`$uix/eidos/components/{x}` ya identifica la capa.
|
|
11. **CSS recipe sin prefijo `--eidos-`.** Los custom properties usan
|
|
`--{component}-…` para los públicos y `--_{component}-…` para los
|
|
internos.
|
|
|
|
### Por qué disciplined option C
|
|
|
|
El patrón anterior (default flat con snippet slots + Provider compound
|
|
duplicado) tenía dos problemas:
|
|
|
|
- **Doble verdad**: dos APIs (flat con `trigger`/`title`/`actions`
|
|
snippets, compound con children explícitos) llevaban a la misma
|
|
funcionalidad por caminos divergentes. Cualquier tweak visual
|
|
exigía actualizar ambas.
|
|
- **Inventaba sobre air**: air era pure compound (`<Drawer.Provider>`
|
|
con children). El flat con snippets fue una invención sin
|
|
baseline ni firma del usuario.
|
|
|
|
Option C disciplinada:
|
|
|
|
- Toma de **air** la convención de hijos (Trigger, Content, Portal, …)
|
|
- Toma de **bits-ui / shadcn-svelte** la ergonomía del root con propiedades
|
|
attached (`<Drawer><Drawer.Trigger>`)
|
|
- Elimina la invención del flat con snippets (no era ni air ni soma)
|
|
|
|
### Caso especial — Toast
|
|
|
|
Tiene dos roots independientes (no anidados):
|
|
|
|
- `<Toast>` — manual compound (consumer itera `toaster.toasts`)
|
|
- `<Toaster />` — imperative auto-mount (template default)
|
|
|
|
Export separados; `Toaster` NO va attached como `Toast.Toaster` porque
|
|
es un root competidor, no un hijo. Documentado en
|
|
[`components/toast/index.ts`](./components/toast/index.ts).
|
|
|
|
## 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.
|
|
|
|
```ts
|
|
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`](../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:
|
|
|
|
```bash
|
|
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-14)
|
|
|
|
- **ActiveEidos**: implementado como runtime/contexto visual y superficie de
|
|
configuracion. Gestiona primitivas, roles canonicos, themes, validacion,
|
|
contrato CSS, persistencia y render CSS (`renderStaticCss`,
|
|
`renderThemeCss`).
|
|
- **Authoring API**: `defineEidosConfig`, `extendEidosConfig` y
|
|
`createThemeBaseEidosConfig`
|
|
permiten crear configuraciones completas o extender el theme base sin
|
|
mutar las constantes del sistema. `getCssContract()` expone el contrato
|
|
estructurado, `renderContractCss()` lo materializa como CSS para themes
|
|
externos, `renderCssVariables()` permite escribir overrides runtime
|
|
contract-aware y `toDocument()` / `serialize()` exponen el envelope
|
|
versionado para persistencia.
|
|
- **Runtime CSS**: `ActiveEidos` reacciona a su `preferences` compuesto o a
|
|
fuentes explicitas `modeSource` / `densitySource`, soporta themes de config
|
|
y CSS-only via `themeSource`, acepta variables runtime en un style block
|
|
propio, y con `applyDom:false` no escribe en el DOM.
|
|
- **Color**: basado en roles canonicos de jerarquia e intents
|
|
(`primary`, `secondary`, `tertiary`, `neutral`, `affirm`, `fulfill`,
|
|
`risk`, `threat`, `loss`) y escalas de 12 pasos. Por cada escala genera
|
|
alpha tokens `a1..a12`, derivadas automaticamente o sobrescribibles con
|
|
`color.alphaScales` por theme. Los roles semanticos validos son solo los
|
|
declarados por el contrato de Eidos.
|
|
- **Size**: `xxs..xxl` se renderiza como map global coordinado
|
|
(`control-height`, font, icon, padding, gap, radius). `full` queda como
|
|
valor de layout, no como primitiva fisica.
|
|
- **Border / opacity / z-index / shadow**: ya forman parte del contrato
|
|
generado. Border define escala de width/style y aliases globales; opacity
|
|
cubre estados de UI y overlays; z-index cubre capas comunes; shadow combina
|
|
escala fisica `1..6` con aliases semanticos por theme.
|
|
- **Layout**: ya forma parte del contrato generado. Incluye
|
|
`containerWidth`, `containerPaddingInline`, `contentWidth` y `aspectRatio`
|
|
como tokens estables y authorables desde `EidosConfig`.
|
|
- **Density**: ya forma parte del contrato generado. Incluye `scale`,
|
|
`spaceScale`, `controlScale` y `contentScale` para los tres niveles
|
|
canonicos `compact`, `comfortable` y `spacious`, conectados al
|
|
`data-density` que proyecta `ActiveEidos`.
|
|
- **Tipografia**: usa tamaños canonicos `xxs` a `xxxl`, familias
|
|
libres por key y estilos tipograficos (`h1`, `h2`, `body`, etc.) como
|
|
`Record<string, TypographyStyle>`.
|
|
- **Convencion del API de componentes**: disciplined option C esta
|
|
documentada en [`components/README.md`](./components/README.md), pero
|
|
la migracion de componentes/rutas no debe mezclarse con el trabajo del
|
|
modulo general.
|
|
- **CSS generado**: `generated/base.css` ya se genera desde la config
|
|
base de Eidos con `npm run generate:eidos-css` y se importa como foundation
|
|
estatica. Los CSS historicos de `contracts/` y `themes/base/` ya no existen
|
|
en el arbol activo; el contrato se publica desde `ActiveEidos` y los valores
|
|
base desde `generated/base.css`. Los antiguos `tokens/components/*` tambien
|
|
salen del entrypoint: `EidosConfig.recipes` genera los aliases de recipe
|
|
estables.
|
|
- **Recipes**: quedan deliberadamente como `RecipeTokenSet` plano. No se crea
|
|
jerarquia estructurada hasta que una recipe tenga un builder/consumer real
|
|
que necesite mas semantica que aliases CSS.
|
|
- **Check del repo**: `npm run check` no reporta errores ni warnings en este
|
|
punto.
|