# `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. --- ## 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](../../air-old/) y la ergonomía moderna de bits-ui / shadcn-svelte: una sola entidad mental — `` — con hijos accesibles como propiedades — ``, ``, 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 (``, ``, ``, …) — **no** ``, **no** ``. 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 `` 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): ```ts const Drawer = DrawerRoot 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 SomaXxxProviderProps 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. ```svelte {@render children?.()} ``` > **Patrón de import**: `import { Provider as SomaXxxProvider }` > desde `$soma/components/{x}`. Eidos no crea una fachada headless propia > por componente; envuelve las partes públicas de Soma directamente. ### `{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. ```svelte {@render children?.()} ``` ### `index.ts` — compone el namespace Asignación explícita per-property sobre el root component. **NO** usar `Object.assign(DrawerRoot, { 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. ```ts import DrawerRoot 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 DrawerRoot & { 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 = DrawerRoot 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 { DrawerProviderProps as DrawerProps, DrawerTriggerProps as TriggerProps, DrawerOverlayProps as OverlayProps, DrawerContentProps as ContentProps // … etc } from './types'; ``` ### `types.ts` — passthrough + adiciones eidos ```ts import type { ProviderProps as SomaDrawerProviderProps, TriggerProps as SomaDrawerTriggerProps, ContentProps as SomaDrawerContentProps // … } from '$soma/components/drawer'; export type DrawerProviderProps = SomaDrawerProviderProps; export type DrawerTriggerProps = SomaDrawerTriggerProps; // Eidos añade props visuales que el recipe consume vía data-attrs export type DrawerContentProps = SomaDrawerContentProps & { size?: ResponsiveProp; }; ``` > **Sin `XxxFlatProps`, sin `XxxProviderProps as ProviderProps` aliases.** > Hay un solo `XxxProps` (el del root) y los `XxxPartProps` per hijo. ### Uso del consumidor ```svelte Open Title Subtitle

body

Close
``` ### Componentes single-part (toggle, switch, icon, avatar) Sin hijos compound. El default export ES el componente entero. Sin namespace, sin asignación de propiedades, sin `Provider` alias. ```ts // toggle/index.ts export { default } from './toggle.svelte'; export type { ToggleProps, ToggleVariant, ToggleSize } from './types'; ``` ```svelte import Toggle from '$uix/eidos/components/toggle'; Bold ``` --- ## Toast — caso especial (dos roots independientes) `Toast` tiene dos roots públicos que NO se anidan: - `` — manual compound (Provider+Viewport+Item iteración del consumer). Children attached: Viewport, Item, Status, Main, Title, Description, Action, Close. - `` — 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. ```ts import { Toast, Toaster, createToaster } from '$uix/eidos/components/toast'; const t = createToaster(); // Imperative // Manual {#each t.toasts as toast} {toast.title} {/each} ``` --- ## 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-radius-md` | | `--_{component}-…` | tokens internos del recipe (no parte del API público) | `--_dialog-padding`, `--_toggle-palette-track` | 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. --- ## Forma legacy (CSS-only, retiraled) 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. --- ## Estado actual de la migración (2026-05-14) | Componente | Forma canónica | Notas | | ----------- | -------------- | ---------------------------------------------------------------------------- | | toggle | ✅ single-part | piloto | | switch | ✅ single-part | | | collapsible | ✅ multi-part | Trigger, Content | | dialog | ✅ multi-part | Trigger, Portal, Overlay, Content, Title, Description, Close, Header, Footer | | drawer | ✅ multi-part | + Handle | | popover | ✅ multi-part | + Arrow, Anchor | | toast | ✅ multi-part | + Toaster separate | | accordion | ✅ multi-part | Item, Header, Trigger, Content | | avatar | ✅ multi-part | Image, Fallback (eidos-native) | | icon | ✅ single-part | + 1697 lucide glyphs | | tooltip | ✅ multi-part | + Group | | tabs | ✅ multi-part | List, Trigger, Content, Indicator | | checkbox | ✅ multi-part | Indicator, HiddenInput, Group, GroupLabel | | radio-group | ✅ multi-part | Item, Indicator, HiddenInput, Label |