# `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`](../../../../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](../../../../docs/canon/direction-contract.md)) · 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 — `` — 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 = 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. ```svelte {@render children?.()} ``` > **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 ``, > ``, etc. #### Prohibiciones de naming interno El namespace interno que apunta a Soma se llama igual que el componente. No se prefija ni se renombra: ```svelte import * as Collapsible from '$soma/components/collapsible'; ``` 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 ``, ``, `` 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 ``; si necesita una parte, escribe ``, ``, 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. ```svelte {@render children?.()} ``` ### `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. ```ts 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 ```ts 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; }; ``` > **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` 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: ```ts import type { ColorRole, ControlVariant, Size } from '$uix/eidos/lib/types'; export type DateFieldSize = Extract; 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 `