|
|
|
|
# `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 — `<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):
|
|
|
|
|
```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
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
/**
|
|
|
|
|
* Eidos `<Drawer>` — root component. Wraps the Soma
|
|
|
|
|
* to set up the component context.
|
|
|
|
|
*/
|
|
|
|
|
import { Provider as SomaDrawerProvider } from '$soma/components/drawer';
|
|
|
|
|
import type { DrawerProps } from './types';
|
|
|
|
|
|
|
|
|
|
let {
|
|
|
|
|
open = $bindable(false),
|
|
|
|
|
activeSnapPoint = $bindable(null),
|
|
|
|
|
isDragging = $bindable(false),
|
|
|
|
|
children,
|
|
|
|
|
...rest
|
|
|
|
|
}: DrawerProps = $props();
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<SomaDrawerProvider {...rest} bind:open bind:activeSnapPoint bind:isDragging>
|
|
|
|
|
{@render children?.()}
|
|
|
|
|
</SomaDrawerProvider>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
> **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
|
|
|
|
|
<!-- drawer-trigger.svelte -->
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
import { Trigger } from '$soma/components/drawer';
|
|
|
|
|
import type { DrawerTriggerProps } from './types';
|
|
|
|
|
|
|
|
|
|
let { children, ...rest }: DrawerTriggerProps = $props();
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<Trigger {...rest}>
|
|
|
|
|
{@render children?.()}
|
|
|
|
|
</Trigger>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `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 {
|
|
|
|
|
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 DrawerProps = SomaDrawerProviderProps;
|
|
|
|
|
export type DrawerTriggerProps = SomaDrawerTriggerProps;
|
|
|
|
|
|
|
|
|
|
// Eidos añade props visuales que el recipe consume vía data-attrs
|
|
|
|
|
export type DrawerContentProps = SomaDrawerContentProps & {
|
|
|
|
|
size?: ResponsiveProp<DrawerSize>;
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
> **Sin `XxxFlatProps`, sin `XxxProviderProps as ProviderProps` aliases.**
|
|
|
|
|
> Hay un solo `XxxProps` (el del root) y los `XxxPartProps` per hijo.
|
|
|
|
|
|
|
|
|
|
### 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
|
|
|
|
|
|
|
|
|
|
```svelte
|
|
|
|
|
<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, 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';
|
|
|
|
|
<Toggle bind:pressed variant="solid">Bold</Toggle>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 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.
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
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-radius-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.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 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.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Estado actual de la migración (2026-05-17)
|
|
|
|
|
|
|
|
|
|
| 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 |
|
|
|
|
|
| meter | ✅ multi-part | Indicator |
|
|
|
|
|
| number-field | ✅ multi-part | Input, IncrementTrigger, DecrementTrigger, Scrubber |
|
|
|
|
|
| pagination | ✅ multi-part | PrevTrigger, NextTrigger, Item, Ellipsis |
|
|
|
|
|
| progress | ✅ multi-part | Indicator |
|
|
|
|
|
| rating-group | ✅ multi-part | Item |
|
|
|
|
|
| search-field | ✅ multi-part | Input, ClearTrigger |
|
|
|
|
|
| slider | ✅ multi-part | Range, Thumb, Tick |
|
|
|
|
|
| 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 |
|