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/components
dev f1f686d097
feat(theming)!: SS16 - el guard vive donde NACE el valor, y el velo del drawer vuelve a pintar
1 month ago
..
accordion feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
affix fix(eidos): la capa sale de components/ — tenerla ahi acoplaba Fab y MenuDial a Affix 2 months ago
alert-dialog feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve 2 months ago
anchor-nav docs(theming): los 21 de la mañana recuperan su README y su pestaña Tokens 2 months ago
announce uix(theming): correcciones de la revisión de F2-B — el empate no se resuelve de oficio 2 months ago
aspect-ratio fix(uix audit): apply Kim audit remediations across morfo/soma/eidos/recipe layers 5 months ago
audio-player docs(theming): la deuda B queda saldada - los tres ultimos README "Talla y tema" 1 month ago
aura feat(color)!: una palabra, un significado - primary/secondary son SIEMPRE jerarquia 2 months ago
auto-grid fix(uix audit): apply Kim audit remediations across morfo/soma/eidos/recipe layers 5 months ago
avatar feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
background feat(theming)!: SS16 - el guard vive donde NACE el valor, y el velo del drawer vuelve a pintar 1 month ago
badge feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
banner docs(theming): los 21 de la mañana recuperan su README y su pestaña Tokens 2 months ago
barcode refactor(eidos): el catálogo habla un idioma — 269 claves al vocabulario firmado 2 months ago
box uix(section): la sección de una página es un componente, no un esqueleto que cada block reescribe 2 months ago
breadcrumb fix(eidos): la capa de estado es un VELO — 38 hovers que no lo eran, al idioma 2 months ago
button feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
button-group docs(theming): los 21 de la mañana recuperan su README y su pestaña Tokens 2 months ago
calendar uix(calendar): temable — 84 % → 89 %, y una entrada de FAMILIA no se juzga en la ruta de UN componente 2 months ago
callout docs(theming): los 21 de la mañana recuperan su README y su pestaña Tokens 2 months ago
card feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
card-group feat(morfo)!: la ARISTA es ley — instrumento composition-aware, residuo a cero y el pase ARIA GATEA 1 month ago
carousel feat(theming)!: SS16 - el guard vive donde NACE el valor, y el velo del drawer vuelve a pintar 1 month ago
cascade revert: deshacer la auditoría entera — se hizo sin leer la doctrina 2 months ago
chart feat(morfo): wave 1 de la cosecha ARIA — 232/258 adjudicados leyendo la fuente; la clase de composición queda ABIERTA 1 month ago
chat-composer fix(theming)!: file-upload pinta lo SUYO - muere el barrido de descendientes 2 months ago
chat-list docs(eidos): los README de componente hablan el idioma nuevo — claves sueltas 2 months ago
chat-log uix(chat-log): temable — 77 % → 100 %, y el instrumento daba DIECISÉIS falsos muertos de 31 2 months ago
chat-message feat(theming)!: el sobre de trigger de popover es un SUELO - tercera ley 55 2 months ago
chat-typing uix(chat-typing): temable — 89 % → 100 %, con UNA clave y siete de sus ocho ya vivas 2 months ago
checkbox docs(theming): los 21 de la mañana recuperan su README y su pestaña Tokens 2 months ago
chronos fix(theming): el velo del sistema no muere por un atajo - y el pin de chronos sobra 2 months ago
clipboard uix(theming): correcciones de la revisión de F2-B — el empate no se resuelve de oficio 2 months ago
code feat(color)!: una palabra, un significado - primary/secondary son SIEMPRE jerarquia 2 months ago
code-block uix(theming): correcciones de la revisión de F2-B — el empate no se resuelve de oficio 2 months ago
collapsible feat(morfo)!: la ARISTA es ley — instrumento composition-aware, residuo a cero y el pase ARIA GATEA 1 month ago
color-field uix(color-field): temable — 56 % → 93 %, y sus TRECE tokens nunca se habían medido 2 months ago
color-picker feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
color-swatch refactor(theming)!: trece canales de valor hablan como lo que son - privados 1 month ago
combobox refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN 1 month ago
command uix(theming): revisión adversarial de F2-A — 8 hallazgos de 34, y nace R-5.4 2 months ago
container feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) 3 months ago
context-menu refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN 1 month ago
count-up fix(eidos): A-69 — `duration` de count-up dice la verdad, y las cifras aterrizan juntas 2 months ago
cropper fix(theming): float-panel tenía DOCE rojos sin medir — el guard R-5.4 vuelve a verde 2 months ago
css-field revert: deshacer la auditoría entera — se hizo sin leer la doctrina 2 months ago
date-field refactor(eidos): el catálogo habla un idioma — 269 claves al vocabulario firmado 2 months ago
date-picker feat(calendar-surface): la capa que ya existía, con nombre, casa y su agujero tapado 2 months ago
date-range-field docs(theming): los 21 de la mañana recuperan su README y su pestaña Tokens 2 months ago
date-range-picker feat(calendar-surface): la capa que ya existía, con nombre, casa y su agujero tapado 2 months ago
dialog uix(dialog): temable — 67 % → 89 %, y el borde dice CÓMO se cerró 2 months ago
display feat(color)!: una palabra, un significado - primary/secondary son SIEMPRE jerarquia 2 months ago
drag-drop uix(theming): revisión adversarial de F2-B — 4 hipótesis, 3 refutadas, 1 token que mentía 2 months ago
drawer feat(theming)!: SS16 - el guard vive donde NACE el valor, y el velo del drawer vuelve a pintar 1 month ago
dropdown-menu refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN 1 month ago
editable uix(editable): temable — 75 % → 82 %, y el guard CERRABA la superficie que medía 2 months ago
emoji-picker refactor(theming)!: trece canales de valor hablan como lo que son - privados 1 month ago
empty-state docs(theming): los 21 de la mañana recuperan su README y su pestaña Tokens 2 months ago
fab uix(fab): temable — 88 % → 100 %, y el modo va DETRÁS, no delante 2 months ago
feed uix(theming): revisión adversarial de F2-A — 8 hallazgos de 34, y nace R-5.4 2 months ago
field docs(theming): los 21 de la mañana recuperan su README y su pestaña Tokens 2 months ago
field-langs feat(color)!: una palabra, un significado - primary/secondary son SIEMPRE jerarquia 2 months ago
file-upload fix(theming)!: file-upload pinta lo SUYO - muere el barrido de descendientes 2 months ago
flex fix(eidos): las variables de `Flex` y `Grid` dejan de heredarse 2 months ago
float perf(eidos): code-split 35 component recipes (Phase 3, batch 1) 4 months ago
float-panel feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
form feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
format-date fix(uix): clear the catalog's clean functional-error flags + reframe C10 tiers 3 months ago
format-number fix(uix): clear the catalog's clean functional-error flags + reframe C10 tiers 3 months ago
gradient-builder docs(theming): quince componentes recuperan su README «Talla y tema» — la deuda B de la revisión 2 months ago
gradient-picker feat(theming)!: SS16 - el guard vive donde NACE el valor, y el velo del drawer vuelve a pintar 1 month ago
grid fix(eidos): las variables de `Flex` y `Grid` dejan de heredarse 2 months ago
grid-list uix(grid-list): temable — 0 % → 85 %, y una selección que nunca pintó 2 months ago
group fix(eidos): dos animadores que no se hablaban y un cluster que no envolvia 2 months ago
heading feat(color)!: una palabra, un significado - primary/secondary son SIEMPRE jerarquia 2 months ago
highlight revert: deshacer la auditoría entera — se hizo sin leer la doctrina 2 months ago
icon test(eidos): triage the 15 long-red suite failures — 289/289 green, check 61→59 3 months ago
icon-button feat(eidos): IconButton primitive + Lote 0 iconOnly->IconButton cascade 4 months ago
image feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
image-adjustments feat(color)!: una palabra, un significado - primary/secondary son SIEMPRE jerarquia 2 months ago
image-picker uix(image-picker): temable — 71 % → 100 %, y el instrumento medía UN nodo 2 months ago
kbd uix(theming): correcciones de la revisión de F2-B — el empate no se resuelve de oficio 2 months ago
knob refactor(theming)!: trece canales de valor hablan como lo que son - privados 1 month ago
label feat(color)!: una palabra, un significado - primary/secondary son SIEMPRE jerarquia 2 months ago
link uix(theming): correcciones de la revisión de F2-B — el empate no se resuelve de oficio 2 months ago
link-preview refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN 1 month ago
listbox feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
mark uix(theming): correcciones de la revisión de F2-B — el empate no se resuelve de oficio 2 months ago
mask-field docs: el corpus entero deja de describir un sistema que ya no existe 2 months ago
media-player refactor(soma)!: fase C2b — renderProps() muere; .props es la única bolsa 1 month ago
menu-dial fix(uix): fase A cierre — los seis rojos del censo morfo:check, adjudicados 1 month ago
menubar refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN 1 month ago
meter uix(meter): ADJUDICADO — 64 % → 75 %, su techo, y el guard medía la MITAD 2 months ago
metrics feat(morfo): wave 1 de la cosecha ARIA — 232/258 adjudicados leyendo la fuente; la clase de composición queda ABIERTA 1 month ago
mockup feat(theming)!: la ley del espacio cerrado - un --{c}-* existe si y solo si base.ts lo declara 1 month ago
month-grid feat(calendar-surface): la capa que ya existía, con nombre, casa y su agujero tapado 2 months ago
motion feat(eidos): el ritmo de revelado sube a config — primitives.motion.staggerViewport, y su regla emigra a la foundation 1 month ago
natural-time-picker fix(natural-time-picker): la anotación del literal volvió a su línea — prettier la había separado del valor 2 months ago
nav-tree docs(theming): quince componentes recuperan su README «Talla y tema» — la deuda B de la revisión 2 months ago
navigation-menu fix(eidos): el guard de sideOffset en navigation-menu-content era un vestigio de popover 1 month ago
number-field revert: deshacer la auditoría entera — se hizo sin leer la doctrina 2 months ago
onion-menu docs(theming): los 21 de la mañana recuperan su README y su pestaña Tokens 2 months ago
pagination fix(eidos): la capa de estado es un VELO — 38 hovers que no lo eran, al idioma 2 months ago
palabras refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN 1 month ago
password-field feat(color)!: una palabra, un significado - primary/secondary son SIEMPRE jerarquia 2 months ago
picker-shell uix(theming): el censo aprende que un privado DERIVADO sí alcanza — 50 % → 56 % 2 months ago
pin-input uix(pin-input): temable — 65 % → 100 %, y un respaldo que mentía por 0,15 2 months ago
popover refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN 1 month ago
progress uix(progress): temable — 69 % → 87 %, y el instrumento medía UNA de sesenta caras 2 months ago
proof-of-human feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
prose uix(prose): temable — 1 % → 83 %, la costura por ELEMENTO y dos cegueras del instrumento 2 months ago
qr-code feat(morfo): wave 1 de la cosecha ARIA — 232/258 adjudicados leyendo la fuente; la clase de composición queda ABIERTA 1 month ago
radio-cards docs(theming): quince componentes recuperan su README «Talla y tema» — la deuda B de la revisión 2 months ago
radio-group docs+fix: correcciones del pase adversarial (3 Opus, refutar) sobre las firmas del dia 1 month ago
range-calendar feat(calendar-surface): la capa que ya existía, con nombre, casa y su agujero tapado 2 months ago
rating-group uix(rating-group): temable — 80 % → 100 %, y su ÚNICO hover mueve lo que el guard no fotografía 2 months ago
relative-time fix(uix): clear the catalog's clean functional-error flags + reframe C10 tiers 3 months ago
result tooling(theming): el censo aprende a leer la válvula del canon — clase `exception` 2 months ago
s-text feat(color)!: una palabra, un significado - primary/secondary son SIEMPRE jerarquia 2 months ago
s-text-virtual-list feat(theming)!: SS16 - el guard vive donde NACE el valor, y el velo del drawer vuelve a pintar 1 month ago
scroll-area feat(color)!: una palabra, un significado - primary/secondary son SIEMPRE jerarquia 2 months ago
scroll-frames uix(theming): correcciones de la revisión de F2-B — el empate no se resuelve de oficio 2 months ago
search-field uix(search-field): temable — 71 % → 100 %, y sus 10 tokens sin medir NO mienten 2 months ago
section docs(theming): los 21 de la mañana recuperan su README y su pestaña Tokens 2 months ago
select refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN 1 month ago
separator uix(separator): temable — 67 % → 100 %, SIN acuñar una sola clave 2 months ago
sidebar refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN 1 month ago
skeleton feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
skin-media-player uix(skin-media-player): temable — 0 % → 22 %, y un techo que es NATURALEZA 2 months ago
skip-link uix(skip-link): temable — 71 % → 100 %, y el guard aprende a abrir por FOCO 2 months ago
slider uix(slider): temable — 97 % → 100 %, y con un solo valor el tick ACTIVO no existe 2 months ago
spin-field uix(spin-field): ADJUDICADO — 67 % es su techo, y lo dice la cabecera de su propio CSS 2 months ago
spinner feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
split-button refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN 1 month ago
splitter fix(splitter): el asa recupera el velo del sistema 2 months ago
stack fix(uix audit): apply Kim audit remediations across morfo/soma/eidos/recipe layers 5 months ago
stepper docs(theming): quince componentes recuperan su README «Talla y tema» — la deuda B de la revisión 2 months ago
sticky docs(theming): los 21 de la mañana recuperan su README y su pestaña Tokens 2 months ago
surface feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
svg feat(eidos): IconButton primitive + Lote 0 iconOnly->IconButton cascade 4 months ago
switch feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
table fix(table): el striped cuenta filas de DATOS — la banda vuelve a existir 2 months ago
tabs refactor(theming)!: MeasuredIndicator escribe en el namespace del HOST - la capa es la pluma, no la dueña 1 month ago
tag-group docs(theming): quince componentes recuperan su README «Talla y tema» — la deuda B de la revisión 2 months ago
tags-input fix(morfo)!: tags-input habla UN patron ARIA - layout grid, cierra F-1 2 months ago
text feat(color)!: una palabra, un significado - primary/secondary son SIEMPRE jerarquia 2 months ago
text-blur feat(packs+text): incorporate the animation collection — Ambient pack + text-effects family + docs 3 months ago
text-circular fix(theming): el slot de tinta es `fg` — R-5.3 me cazó tres claves, y el handoff de mañana 2 months ago
text-focus fix(theming): el slot de tinta es `fg` — R-5.3 me cazó tres claves, y el handoff de mañana 2 months ago
text-gradient refactor(theming)!: trece canales de valor hablan como lo que son - privados 1 month ago
text-scramble feat(theming)!: la ley del espacio cerrado - un --{c}-* existe si y solo si base.ts lo declara 1 month ago
textarea feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
time-field refactor(eidos): el catálogo habla un idioma — 269 claves al vocabulario firmado 2 months ago
time-picker feat(color)!: una palabra, un significado - primary/secondary son SIEMPRE jerarquia 2 months ago
time-range-field docs(theming): los 21 de la mañana recuperan su README y su pestaña Tokens 2 months ago
time-range-picker feat(color)!: una palabra, un significado - primary/secondary son SIEMPRE jerarquia 2 months ago
timeline feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
toast feat(theming)!: SS16 - el guard vive donde NACE el valor, y el velo del drawer vuelve a pintar 1 month ago
toggle uix(toggle): temable — 81 % → 100 %, y el guard sólo sabía barrer UN eje 2 months ago
toggle-group docs(theming): los 21 de la mañana recuperan su README y su pestaña Tokens 2 months ago
toolbar feat(theming): los 150 privados del residuo quedan adjudicados - 84 firmados, 66 contados 1 month ago
tooltip fix(uix)!: decisión 11(a) — la cáscara del Tooltip no pinta ni anuncia la señal de un control ajeno 1 month ago
trans fix(uix): clear the catalog's clean functional-error flags + reframe C10 tiers 3 months ago
tree-grid uix(theming): revisión adversarial de F2-A — 8 hallazgos de 34, y nace R-5.4 2 months ago
tree-view feat(theming)!: SS16 - el guard vive donde NACE el valor, y el velo del drawer vuelve a pintar 1 month ago
virtual-grid uix(theming): correcciones de la revisión de F2-B — el empate no se resuelve de oficio 2 months ago
virtual-list uix(theming): correcciones de la revisión de F2-B — el empate no se resuelve de oficio 2 months ago
waveform fix(theming): float-panel tenía DOCE rojos sin medir — el guard R-5.4 vuelve a verde 2 months ago
wrap fix(uix audit): apply Kim audit remediations across morfo/soma/eidos/recipe layers 5 months ago
year-grid feat(calendar-surface): la capa que ya existía, con nombre, casa y su agujero tapado 2 months ago
README.md feat(calendar-surface): la capa que ya existía, con nombre, casa y su agujero tapado 2 months ago

README.md

eidos/components/

Recipes + wrappers visuales por componente. Cada componente vive en su subdirectorio con la forma canónica documentada abajo. Los .css sueltos al nivel raíz son legacy de la fase "eidos = solo CSS" y se migran progresivamente.

⚠️ Contrato de construcción — leer antes de escribir un recipe/wrapper. La fuente de verdad de qué tokens/arquetipos consume un componente visual es la tabla «Build contract» de docs/guides/component-guide.md (migrada allí el 2026-07-11, DOC-1; el audit de 2026-06-19 que la sembró queda como historia): elevación vía data-depth (bundle completo) · estado vía --state-* · radio vía factor global + [data-shape-nest] · foco outline --focus-ring-* (§32) · label de campo canónico · bundle --size-* · touch-target §37 · color por token (cero hex) · densidad --space-*/--control-height-* · RTL lógico + :dir(rtl) (nunca [dir='rtl']), y el provider estampa el dir crudo si el recipe ramifica con :dir() — sin estampar, :dir() sólo ve la dirección heredada (contrato de dirección) · i18n langs.ts · portal-safe · composición. Todos los ejes están LIVE con su guard (R-*, contracts, elevation-plane).


Forma canónica (disciplined option C — vigente desde 2026-05-10)

Convención unificada para TODOS los componentes multi-part. Sigue la estructura de air (la rama legacy, ya retirada del árbol) y la ergonomía moderna de bits-ui / shadcn-svelte: una sola entidad mental — <Drawer> — con hijos accesibles como propiedades — <Drawer.Trigger>, <Drawer.Content>, etc.

Reglas duras

  1. Un solo punto de entrada por componente: la default export es el componente root visual. Se llama 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. El nombre del fichero coincide con el nombre del componente.
  3. No exportar Provider públicamente. La separación "Provider compound vs flat" es una invención previa que se ha retirado: hay UNA forma compound — el root + sus hijos atados.
  4. Los hijos siguen el naming de air / headless: Trigger, Content, Overlay, Title, Description, Close, Portal, Header, Footer, Item, Indicator, HiddenInput, Group, Label, etc. No inventar nombres nuevos.
  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. La invención <Drawer trigger={...} title={...} actions={...}> está retirada — esconde decisiones de composición que deberían ser explícitas en componentes con portal/overlay/content/close.
  7. Los hijos se atan al root con asignación explícita, no Object.assign (que en Svelte 5 puede causar issues sutiles de hidratación):
    const Drawer = DrawerComponent as DrawerNamespace;
    Drawer.Trigger = Trigger;
    Drawer.Content = Content;
    // ...
    

Estructura de directorio

drawer/
  drawer.svelte                ← root (lo que era air's Provider)
  drawer-trigger.svelte
  drawer-overlay.svelte
  drawer-content.svelte
  drawer-handle.svelte
  drawer-title.svelte
  drawer-description.svelte
  drawer-close.svelte
  drawer-header.svelte         ← eidos-only layout shell (cuando aplique)
  drawer-footer.svelte         ← idem
  drawer.css                   ← recipe
  types.ts                     ← extiende las props públicas de Soma
  index.ts                     ← compone Drawer + hijos

{name}.svelte — el root

Wrapper directo sobre el provider público de Soma. Setea el contexto headless, acepta los bindables del estado (open, value, pressed, etc.), añade los data-attrs visuales propios de eidos (data-size, data-variant, data-color, …), y renderiza {@render children?.()} para que los hijos se compongan dentro.

<script lang="ts">
	/**
	 * Eidos `<Drawer>` — root component. Wraps the Soma
	 * to set up the component context.
	 */
	import * as Drawer from '$soma/components/drawer';
	import type { DrawerProps } from './types';

	let {
		open = $bindable(false),
		activeSnapPoint = $bindable(null),
		isDragging = $bindable(false),
		children,
		...rest
	}: DrawerProps = $props();
</script>

<Drawer.Provider {...rest} bind:open bind:activeSnapPoint bind:isDragging>
	{@render children?.()}
</Drawer.Provider>

Patrón de import interno: import * as Drawer from '$soma/components/drawer'. Eidos no crea una fachada headless propia por componente; envuelve las partes públicas de Soma directamente como <Drawer.Provider>, <Drawer.Trigger>, etc.

Prohibiciones de naming interno

El namespace interno que apunta a Soma se llama igual que el componente. No se prefija ni se renombra:

<!-- Correcto -->
import * as Collapsible from '$soma/components/collapsible';

<Collapsible.Provider>
	<Collapsible.Trigger />
</Collapsible.Provider>

Formas prohibidas:

  • import * as Parts from '$soma/components/collapsible'
  • import * as CollapsibleBase from '$soma/components/collapsible'
  • import { Provider as SomaCollapsibleProvider } from '$soma/components/collapsible'
  • etiquetas sueltas <Provider>, <Trigger>, <Content> dentro de Eidos
  • tipos o aliases internos SomaXxxProvider, XxxBase, XxxRoot

Motivo: Eidos ya declara la capa en el path. El import interno debe expresar la entidad headless concreta de Soma, no una abstraccion inventada. Si un wrapper Eidos necesita el provider de Soma, escribe <Collapsible.Provider>; si necesita una parte, escribe <Collapsible.Trigger>, <Collapsible.Content>, etc.

Guardia de componentes compuestos de fecha

Incidencia 2026-05-20: DateField, DatePicker y DateRangePicker no pueden cerrarse con wrappers Eidos si Morfo/Soma y la demo no estan cerrados primero. Para componentes compuestos de fecha:

  • Comparar antes con morfo-runtime (air / terra) y referencias React Aria, Ark UI, Bits UI y shadcn-svelte.
  • Completar Morfo con partes, data-*, ARIA, estados, keyboard y eventos observables de la superficie compuesta.
  • La demo no puede inventar data-calendar-* o data-range-calendar-*; si la receta los necesita, pertenecen a Morfo/Soma.
  • Cada control visible (granularity, hourCycle, segmentos, min/max, pagedNavigation, modal/no-modal, clear) debe cambiar algo visible en el stage y estar reflejado en los snippets.
  • Verificar visualmente popup, uno/dos meses, seleccion, deseleccion parcial, limites y accion de borrado antes de documentarlo como listo.

{name}-{part}.svelte — hijos passthrough

Cada hijo es un wrapper delgado sobre la part del Soma. Si añade visual-only data-attrs, los stamp aquí (ej. data-size en Content). Si es passthrough puro (Trigger, Close), trivial.

<!-- drawer-trigger.svelte -->
<script lang="ts">
	import * as Drawer from '$soma/components/drawer';
	import type { DrawerTriggerProps } from './types';

	let { children, ...rest }: DrawerTriggerProps = $props();
</script>

<Drawer.Trigger {...rest}>
	{@render children?.()}
</Drawer.Trigger>

index.ts — compone el namespace

Asignación explícita per-property sobre el root component. NO usar Object.assign(DrawerComponent, { Trigger, ... }) — Svelte 5 puede manejar mal la mutación bulk del component constructor durante hidratación, causando que los hijos aparezcan y desaparezcan después de mount.

import DrawerComponent from './drawer.svelte';
import Trigger from './drawer-trigger.svelte';
import Overlay from './drawer-overlay.svelte';
import Content from './drawer-content.svelte';
import Title from './drawer-title.svelte';
import Description from './drawer-description.svelte';
import Close from './drawer-close.svelte';
import Header from './drawer-header.svelte';
import Footer from './drawer-footer.svelte';
import Handle from './drawer-handle.svelte';
import { Portal } from '$soma/components/internal';

type DrawerNamespace = typeof DrawerComponent & {
	Trigger: typeof Trigger;
	Portal: typeof Portal;
	Overlay: typeof Overlay;
	Content: typeof Content;
	Handle: typeof Handle;
	Title: typeof Title;
	Description: typeof Description;
	Close: typeof Close;
	Header: typeof Header;
	Footer: typeof Footer;
};

const Drawer = DrawerComponent as DrawerNamespace;
Drawer.Trigger = Trigger;
Drawer.Portal = Portal;
Drawer.Overlay = Overlay;
Drawer.Content = Content;
Drawer.Handle = Handle;
Drawer.Title = Title;
Drawer.Description = Description;
Drawer.Close = Close;
Drawer.Header = Header;
Drawer.Footer = Footer;

export { Drawer };
export default Drawer;

export type {
	DrawerProps,
	DrawerTriggerProps as TriggerProps,
	DrawerOverlayProps as OverlayProps,
	DrawerContentProps as ContentProps
	// … etc
} from './types';

types.ts — passthrough + adiciones eidos

import type {
	ProviderProps,
	TriggerProps,
	ContentProps
	// …
} from '$soma/components/drawer';

export type DrawerProps = ProviderProps;
export type DrawerTriggerProps = TriggerProps;

// Eidos añade props visuales que el recipe consume vía data-attrs
export type DrawerContentProps = ContentProps & {
	size?: ResponsiveProp<DrawerSize>;
};

Sin XxxFlatProps, sin XxxProviderProps as ProviderProps aliases. Hay un solo XxxProps (el del root) y los XxxPartProps per hijo.

Cuando el tipo root de Soma se importa como ProviderProps, se usa solo como tipo local dentro de types.ts. No se exporta al consumidor con nombre ProviderProps desde Eidos ni se crea un alias SomaXxxProviderProps.

Tipos visuales canonicos

No se redeclaran variantes, colores ni tamanos a mano en cada componente. La unica fuente para las props visuales compartidas es src/uix/eidos/lib/types.ts:

  • Size y ResponsiveProp<T> para escalas responsivas.
  • ControlVariant, SelectionVariant, ChipVariant, MarkerVariant, SurfaceVariant, etc. para variantes canonicas por familia visual.
  • ColorRole para la paleta de intents UIX (primary, secondary, neutral, affirm, fulfill, risk, threat, loss).

Cuando un componente necesita restringir una familia compartida, usa Extract<> sobre el tipo canonico. No se crean unions locales con literales duplicados:

import type { ColorRole, ControlVariant, Size } from '$uix/eidos/lib/types';

export type DateFieldSize = Extract<Size, 'sm' | 'md' | 'lg'>;
export type DateFieldVariant = ControlVariant;
export type DateFieldColor = ColorRole;

Capas compartidas (shared layers)

Cuando VARIOS componentes necesitan la misma geometría, no se copia: se declara una vez y se engancha por un attr de capa que cada consumidor estampa sobre el elemento que ya tiene. Sin nodo envoltorio, sin anidar componentes.

Ejemplares vivos: lib/list-surface.css (ritmo de menú/listbox, consumido por dropdown-menu · context-menu · menubar · select · combobox · command), eidos/lib/viewport-placement.css (colocación contra el viewport, consumida por Affix · Fab · MenuDial) y lib/calendar-surface.css (ritmo de la rejilla de fechas y forma del anillo de evento, consumida por Calendar · RangeCalendar · MonthGrid · YearGrid y los wrappers de DatePicker / DateRangePicker).

calendar-surface es el ejemplar HÍBRIDO y conviene leerlo antes de escribir otra capa: las dos primeras componen primitivos del sistema, así que declaran sus públicos en el propio fichero y no tienen entrada en recipes/base.ts. La familia calendar no puede — su vocabulario son 76 claves SEMÁNTICAS que un tema alcanza una a una por config —, así que la entrada calendar de la receta es la de la FAMILIA y la capa posee sólo lo que una entrada de receta no sabe expresar: la resolución por talla sobre un hook compartido, y la forma del anillo.

Las reglas, y las tres primeras se aprendieron rompiéndose:

  1. La geometría engancha en el attr de CAPA, nunca en la identidad del componente. morfo-check selecciona [data-{kebab}] en toda la página y valida cada coincidencia contra ese morfo — si un consumidor estampara la identidad para heredar la geometría, quedaría soldado a un contrato ajeno.
  2. Un eje = un token público + una ranura privada, consumido como var(--_x, var(--x)). La capa posee el default; el consumidor escribe la ranura si ha evaluado el eje y aterriza en otro sitio. Un consumidor no acuña --{componente}-{eje}: sería un vocabulario paralelo para valores que la capa ya posee. Y si el prop escribiera el mismo nombre que la capa lee, un valor que referencie el token se vuelve una custom property cíclica — guaranteed-invalid, calc() muerto, insets a auto.
  3. El puente reafirma position si el primitivo compuesto declara uno. La regla base de la capa tiene especificidad (0,1,0) y los ficheros van code-split, así que un position del primitivo al mismo peso decide por orden de carga. Fab lo debe (compone <Button>); MenuDial no (no compone nada en el marco).
  4. Se guarda con dos redes, porque ninguna basta sola: el texto (shared-layer-contract.test.ts) ve lo que el navegador no puede — un env() ya sustituido —, y npm run layer:check ve lo que el texto no puede — quién gana la cascada de verdad.

Partes Eidos-only

Algunas partes existen solo para componer la superficie visual: Header, Footer, Status, Main, Handle, filas/labels planas, indicadores decorativos o primitives svg. No necesitan morfo propio mientras cumplan las cuatro reglas:

  1. No crean comportamiento ni estado.
  2. No son target de eventos perceptivos.
  3. No poseen ARIA obligatoria ni relaciones accesibles propias.
  4. Solo emiten estructura, clase/estilo passthrough o data-* visuales que el recipe consume.

Si cualquiera de esas reglas deja de cumplirse, la parte deja de ser Eidos-only y debe subir al contrato correspondiente: primero morfo, luego Soma si necesita runtime.

El agregador scripts/eidos-lint-all.ts mantiene una allowlist explícita de estos attrs/parts visuales. El lint base sigue siendo estricto con valores inválidos de enums morfo; la allowlist solo evita que el reporte de drift se llene de partes visuales intencionales.

src/uix/eidos/component-api-contract.test.ts protege la forma pública del barrel: sin Object.assign, sin Provider público, root en {component}.svelte y miembros del XxxNamespace sincronizados con sus asignaciones explícitas (Drawer.Trigger = Trigger, etc.).

La guardia src/uix/eidos/component-visual-attrs.test.ts fija el cableado mínimo entre wrapper y recipe: si un wrapper visual declara una prop que se consume como data-* (size, variant, color, position, columns, etc.), el fichero debe seguir estampando ese atributo. Esto no añade comportamiento a Eidos; solo evita que la capa visual trague props en silencio.

Uso del consumidor

<script lang="ts">
	import { Drawer } from '$uix/eidos/components/drawer';
	let open = $state(false);
</script>

<Drawer bind:open variant="overlay" direction="right">
	<Drawer.Trigger>Open</Drawer.Trigger>
	<Drawer.Portal>
		<Drawer.Overlay />
		<Drawer.Content>
			<Drawer.Header>
				<Drawer.Title>Title</Drawer.Title>
				<Drawer.Description>Subtitle</Drawer.Description>
			</Drawer.Header>
			<p>body</p>
			<Drawer.Footer>
				<Drawer.Close>Close</Drawer.Close>
			</Drawer.Footer>
		</Drawer.Content>
	</Drawer.Portal>
</Drawer>

Componentes single-part (toggle, icon)

Sin hijos compound. El default export ES el componente entero. Sin namespace, sin asignación de propiedades, sin Provider alias.

// toggle/index.ts
export { default } from './toggle.svelte';
export type { ToggleProps, ToggleVariant, ToggleSize } from './types';
import Toggle from '$uix/eidos/components/toggle';
<Toggle bind:pressed variant="solid">Bold</Toggle>

Partes visuales por defecto

Algunos componentes multi-part necesitan una parte visual minima para no renderizar una superficie rota. Switch es el caso canonico: el root sigue exponiendo Switch.Thumb, pero si el consumidor no aporta children, <Switch /> monta un thumb visual por defecto. Esto no crea una API flat ni esconde el contrato de Soma; solo evita que el uso minimo renderice un track sin indicador.


Toast — caso especial (dos roots independientes)

Toast tiene dos roots públicos que NO se anidan:

  • <Toast> — manual compound (Provider+Viewport+Item iteración del consumer). Children attached: Viewport, Item, Status, Main, Title, Description, Action, Close.
  • <Toaster /> — imperative auto-mount. Renderiza Provider+Viewport+ Item-loop con un template default. Pasas toaster de createToaster(). Export separado, no Toast.Toaster porque es un root competidor, no un hijo.
import { Toast, Toaster, createToaster } from '$uix/eidos/components/toast';

const t = createToaster();

// Imperative
<Toaster toaster={t} />

// Manual
<Toast toaster={t}>
  <Toast.Viewport>
    {#each t.toasts as toast}
      <Toast.Item {toast}>
        <Toast.Title>{toast.title}</Toast.Title>
      </Toast.Item>
    {/each}
  </Toast.Viewport>
</Toast>

Convenciones de naming de tokens CSS

Los tokens CSS llevan el nombre del componente, no de la capa. Sin prefijos de capa — ni --eidos-, ni --air-, ni --terra-, ni --soma-.

Forma Uso Ejemplo
--{component}-… tokens públicos (sobreescribibles por el consumer) --dialog-content-bg, --toggle-height-md
--_{component}-… tokens internos del recipe (no parte del API público) --_tabs-trigger-height, --_toggle-bg

Los [data-{component}] y [data-{component}-{part}] selectors son la única vía pública para que el recipe se acople al runtime — toda información cross-layer pasa por data-attrs declarados en el morfo. Cada --{component}-* consumido por CSS debe salir de EidosConfig.recipes; si un alias publico queda sin consumidor real, el test src/uix/eidos/recipe-css-contract.test.ts falla.


Comparativa obligatoria por componente

Ningun componente Eidos se declara cerrado solo por envolver Soma. Antes de implementar o revisar un componente:

  1. Leer Air en la rama anterior (morfo-runtime:src/uix/air/components/{name}) cuando exista. Air es la primera baseline visual.
  2. Leer Soma/Morfo actuales para separar comportamiento, ARIA, estado, traducciones y data-attrs de la superficie visual Eidos.
  3. Auditar Morfo/Sema. El morfo no se considera correcto solo porque compile:
    • Clasificar el componente como pasivo, interactivo o mixto.
    • Justificar cualquier 0 events de forma explicita.
    • Para cada accion real de usuario revisar family, verb, sequence, intent, target, prewrite y commit.
    • Verificar que Soma dispara esas ocurrencias con runtime.trigger(...).
    • Evitar eventos de alta frecuencia para cambios continuos; normalmente se modelan inicio/drag confirmado/commit, no cada frame.
  4. Comparar contra referentes externos relevantes: Radix/Radix Themes, Ark UI, Bits UI, shadcn-svelte y React Aria cuando aplique.
  5. Crear/actualizar components/{name}/README.md con tabla de funcionalidades, tabla Morfo/Sema, gaps y decisiones. Cada ⚠️ / ❌ debe acabar en una decision explicita: implementar ahora, diferir a Morfo/Soma, diferir a v2 o descartar por no pertenecer a Eidos.
  6. Si hay demo en web/routes/uix/components/{name}, sus snippets forman parte de la arquitectura del componente: deben reproducir el mismo contrato visible que el preview. En componentes schema-driven (form, auto-fields, date/time con formatos, etc.) no se permite un schema reducido que omita campos visibles, validators o defaults reales.
  7. Solo despues tocar wrapper, recipe o tokens.

El objetivo no es copiar APIs, sino que Eidos no quede por debajo de Air ni de los referentes en funcionalidades reales. Si una capacidad pertenece a Soma, la tabla debe decirlo; si es visual, Eidos debe cubrirla o justificar el gap.

Reglas especificas para demos de formularios

  • La validacion en tiempo real se modela como validationBehaviour: 'onChange'.
  • Si una demo arranca en onChange, los defaults iniciales deben ser validos salvo que el README y la UI digan explicitamente que se esta mostrando un formulario inicialmente invalido.
  • El snippet de Soma y el snippet de Eidos deben declarar los mismos campos visibles que el preview: imports SIUM, schema, defaults, Form + Field y Form.AutoFields. No se admiten snippets que omiten age, role, campos de fecha, arrays u otros datos que el preview valida.
  • Form.AutoFields es renderer reflectivo de SIUM. Si necesita comportamiento nuevo, se cambia Soma / $libs/forms / Morfo antes que Eidos.

Forma legacy (CSS-only, retired)

Hubo una fase anterior cuando eidos sólo emitía CSS (accordion.css, dialog.css, etc., al nivel raíz de components/). Todos los componentes han sido migrados a la forma canónica del subdirectorio. Si encuentras un .css suelto, es un descuido — debe vivir dentro de su subdirectorio.


Inventario

El inventario vivo de wrappers es el árbol de subdirectorios de components/ + npm run component:audit. La tabla de estado de la migración 2026-05 (completada) se conserva en docs/process/handoffs-2026-05.md.

Powered by TurnKey Linux.