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/README.md

15 KiB

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 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 = 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.

<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.

<!-- 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.

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

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

<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.

// 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>

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-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)

La tanda 2026-05-17 reanuda la migración Soma -> Eidos por orden explícita. Los wrappers nuevos siguen el mismo criterio: envolver partes públicas de Soma, añadir sólo props visuales (size en esta tanda) y dejar comportamiento, estado, ARIA, traducciones y escritura headless en Soma/Morfo.

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

Orden recomendado para continuar

  1. Componentes simples de composición/formulario: field, toolbar, breadcrumb, tag-group.
  2. Componentes medianos con más partes o layout interno: file-upload, tags-input, editable, stepper.
  3. Componentes grandes sólo con tabla previa de migración: select, combobox, calendar, date-field, date-picker, date-range-picker, time-field, time-picker.

Powered by TurnKey Linux.