6.0 KiB
eidos/components/
Recipes y wrappers visuales por componente. La forma actual de un
componente es un subdirectorio con cuatro archivos; los .css
sueltos del nivel raíz son legacy de la fase "eidos = solo CSS" y se
migran progresivamente.
Forma actual (toggle es el piloto)
toggle/
toggle.css → recipe CSS (selectores [data-toggle], variants, …)
toggle.svelte → wrapper sobre $soma/components/toggle
types.ts → ToggleProps (extiende SomaToggleProps)
index.ts → re-exports públicos
README.md → notas del componente (opcional, recomendado para piloto)
{name}.svelte — el wrapper
Compone sobre el provider headless del soma. Re-bindable de
estado (pressed = $bindable(false)), emite los data-attrs de tokens
visuales y opcionalmente añade slots (icon, checkMark):
<script lang="ts">
import * as Toggle from '$soma/components/toggle';
import type { ToggleProps } from './types';
let {
variant = 'solid',
size = 'md',
block = false,
iconOnly = false,
icon,
checkMark = false,
pressed = $bindable(false),
children: bodyContent,
...somaProps
}: ToggleProps = $props();
</script>
<Toggle.Provider
{...somaProps}
bind:pressed
data-variant={variant}
data-size={size}
data-block={block ? '' : undefined}
data-icon-only={iconOnly ? '' : undefined}
>
{#snippet children(snippetProps)}
{#if icon}{@render icon(snippetProps)}{/if}
<span class="eidos-toggle-body">{@render bodyContent?.(snippetProps)}</span>
{#if checkMark === true && snippetProps.pressed}
<svg class="eidos-toggle-checkmark" …/>
{/if}
{/snippet}
</Toggle.Provider>
Patrones notables:
childrense renombra abodyContentdentro del wrapper porque el{#snippet children}interno shadowearía el prop y haría recursión.pressed = $bindable(false)se declara explícitamente; sin esto un consumer que pasebind:pressedchoca con un error de Svelte ("non-bindable property").<span class="eidos-toggle-body">envuelve el body del usuario para que[data-icon-only] .eidos-toggle-body { sr-only }lo oculte visualmente sin perder accesibilidad.
types.ts — extensión de los tipos del soma
import type { Snippet } from 'svelte';
import type { ToggleProps as SomaToggleProps } from '$soma/components/toggle/types';
import type { Size } from '$uix/eidos/lib/types';
export type ToggleVariant = 'solid' | 'outline' | 'ghost';
export type ToggleSize = Extract<Size, 'sm' | 'md' | 'lg'>;
export type ToggleProps = SomaToggleProps & {
variant?: ToggleVariant;
size?: ToggleSize;
block?: boolean;
iconOnly?: boolean;
icon?: Snippet<[{ pressed: boolean }]>;
checkMark?: boolean | Snippet<[{ pressed: boolean }]>;
};
Reglas:
- Sin prefijo
Eidos. El path$uix/eidos/components/{x}ya identifica la capa. - Subset el tipo compartido.
Sizeglobal tiene 8 valores (xxs..xxl + full); el componente reduce conExtractal subset que su recipe soporta. Compile error antes que fallback silencioso. - El nombre del prop refleja la API doctrinal.
intentycolorpertenecen a soma — no se redeclaran en eidos; se heredan deSomaToggleProps.
index.ts — re-exports
export { default } from './toggle.svelte';
export { default as Provider } from './toggle.svelte';
export type {
ToggleProps,
ToggleProps as ProviderProps,
ToggleVariant,
ToggleSize
} from './types';
Doble export (default + Provider) por la doctrina de API:
| Forma | Cuándo |
|---|---|
import Toggle from '$uix/eidos/components/toggle' → <Toggle> |
single-part components, ergonomía Chakra-style |
import * as Toggle from … → <Toggle.Provider> |
consumers que prefieren la forma compound (Radix-style) |
{name}.css — el recipe
Selectores [data-{component}], variantes por data-color,
data-size, data-variant, etc.
Para el subset por componente, el CSS sólo define los tokens
permitidos por el anexo del libro. Si un componente no admite loss,
no aparece [data-color='loss'] en su recipe.
Convención de naming de tokens CSS
Regla: 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 |
Por qué bare-prefixed (sin --eidos-):
- Los recipes son la skin oficial; el consumer interactúa con tokens por componente, no por capa.
- Si un consumer construye un recipe alternativo a partir de soma+morfo (doctrina §13), reutiliza los mismos tokens — no debe importar de qué capa "salieron".
- Migrar de air → eidos no debe romper override de consumer. Mantener
--{component}-…hace la migración invisible (el renameair-→ `` es interno). - El selector
[data-{component}]ya identifica al componente; el token sólo necesita coincidir con ese mismo nombre.
Histórico: la baseline air/ usaba --air-{component}-…. Al
migrar al wrapper eidos se hace strip del prefijo air- (no se
reemplaza por eidos-).
Forma legacy (CSS-only)
Los archivos sueltos accordion.css, dialog.css, drawer.css, etc.
son la fase anterior cuando eidos sólo emitía CSS. Funcionan, pero se
migran a la forma actual cuando el componente entra en revisión.
Pendientes de migrar a wrapper
| Componente | CSS legacy | Wrapper |
|---|---|---|
| toggle | — | ✅ piloto |
| switch | — | ✅ |
| collapsible | — | ✅ (multi-part: flat + compound) |
| dialog | dialog.css |
⏳ |
| drawer | drawer.css |
⏳ |
| popover | popover.css |
⏳ |
| toast | toast.css |
⏳ |
| avatar | — | ✅ eidos-native |
| accordion | accordion.css |
⏳ |
| tabs | tabs.css |
⏳ |
| checkbox | checkbox.css |
⏳ |
| tooltip | tooltip.css |
⏳ |