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

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

⚠️ Canon de arquetipos (2026-06-19) — leer antes de escribir un recipe/wrapper. La fuente de verdad de qué tokens/arquetipos consume un componente visual es el Contrato de construcción (§13) de ../ARCHETYPE_COHERENCE_AUDIT_2026-06-19.md: elevación vía data-depth (bundle completo, NO surface-raised a dedo) · estado vía --state-* (NO color-mix ad-hoc) · radio vía factor global + [data-shape-nest] (NO calc(--radius-md − space)) · label de campo canónico · bundle --size-* (NO re-derivar size→fuente). Cada fila del §13 marca su estado (LIVE / pendiente Etapa A): no referencies un token aún pendiente. Lo LIVE ya es obligatorio: color por token (cero hex), densidad --space-*/--control-height-*, RTL lógico, i18n langs.ts, portal-safe.


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

<script lang="ts">
	/**
	 * Eidos `<Drawer>` — root component. Wraps the Soma
	 * to set up the component context.
	 */
	import * as Drawer from '$soma/components/drawer';
	import type { DrawerProps } from './types';

	let {
		open = $bindable(false),
		activeSnapPoint = $bindable(null),
		isDragging = $bindable(false),
		children,
		...rest
	}: DrawerProps = $props();
</script>

<Drawer.Provider {...rest} bind:open bind:activeSnapPoint bind:isDragging>
	{@render children?.()}
</Drawer.Provider>

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 <Drawer.Provider>, <Drawer.Trigger>, etc.

Prohibiciones de naming interno

El namespace interno que apunta a Soma se llama igual que el componente. No se prefija ni se renombra:

<!-- Correcto -->
import * as Collapsible from '$soma/components/collapsible';

<Collapsible.Provider>
	<Collapsible.Trigger />
</Collapsible.Provider>

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 <Provider>, <Trigger>, <Content> 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 <Collapsible.Provider>; si necesita una parte, escribe <Collapsible.Trigger>, <Collapsible.Content>, 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.

<!-- drawer-trigger.svelte -->
<script lang="ts">
	import * as Drawer from '$soma/components/drawer';
	import type { DrawerTriggerProps } from './types';

	let { children, ...rest }: DrawerTriggerProps = $props();
</script>

<Drawer.Trigger {...rest}>
	{@render children?.()}
</Drawer.Trigger>

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.

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

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<DrawerSize>;
};

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<T> 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:

import type { ColorRole, ControlVariant, Size } from '$uix/eidos/lib/types';

export type DateFieldSize = Extract<Size, 'sm' | 'md' | 'lg'>;
export type DateFieldVariant = ControlVariant;
export type DateFieldColor = ColorRole;

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

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>

Partes visuales por defecto

Algunos componentes multi-part necesitan una parte visual minima para no renderizar una superficie rota. Switch es el caso canonico: el root sigue exponiendo Switch.Thumb, pero si el consumidor no aporta children, <Switch /> monta un thumb visual por defecto. Esto no crea una API flat ni esconde el contrato de Soma; solo evita que el uso minimo renderice un track sin indicador.


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.


Comparativa obligatoria por componente

Ningun componente Eidos se declara cerrado solo por envolver Soma. Antes de implementar o revisar un componente:

  1. Leer Air en la rama anterior (morfo-runtime:src/uix/air/components/{name}) cuando exista. Air es la primera baseline visual.
  2. Leer Soma/Morfo actuales para separar comportamiento, ARIA, estado, traducciones y data-attrs de la superficie visual Eidos.
  3. Auditar Morfo/Sema. El morfo no se considera correcto solo porque compile:
    • Clasificar el componente como pasivo, interactivo o mixto.
    • Justificar cualquier 0 events de forma explicita.
    • Para cada accion real de usuario revisar family, verb, sequence, intent, target, prewrite y commit.
    • Verificar que Soma dispara esas ocurrencias con runtime.trigger(...).
    • Evitar eventos de alta frecuencia para cambios continuos; normalmente se modelan inicio/drag confirmado/commit, no cada frame.
  4. Comparar contra referentes externos relevantes: Radix/Radix Themes, Ark UI, Bits UI, shadcn-svelte y React Aria cuando aplique.
  5. Crear/actualizar components/{name}/README.md con tabla de funcionalidades, tabla Morfo/Sema, gaps y decisiones. Cada ⚠️ / ❌ debe acabar en una decision explicita: implementar ahora, diferir a Morfo/Soma, diferir a v2 o descartar por no pertenecer a Eidos.
  6. Si hay demo en web/routes/uix/components/{name}, sus snippets forman parte de la arquitectura del componente: deben reproducir el mismo contrato visible que el preview. En componentes schema-driven (form, auto-fields, date/time con formatos, etc.) no se permite un schema reducido que omita campos visibles, validators o defaults reales.
  7. Solo despues tocar wrapper, recipe o tokens.

El objetivo no es copiar APIs, sino que Eidos no quede por debajo de Air ni de los referentes en funcionalidades reales. Si una capacidad pertenece a Soma, la tabla debe decirlo; si es visual, Eidos debe cubrirla o justificar el gap.

Reglas especificas para demos de formularios

  • La validacion en tiempo real se modela como validationBehaviour: 'onChange'.
  • Si una demo arranca en onChange, los defaults iniciales deben ser validos salvo que el README y la UI digan explicitamente que se esta mostrando un formulario inicialmente invalido.
  • El snippet de Soma y el snippet de Eidos deben declarar los mismos campos visibles que el preview: imports SIUM, schema, defaults, Form + Field y Form.AutoFields. No se admiten snippets que omiten age, role, campos de fecha, arrays u otros datos que el preview valida.
  • Form.AutoFields es renderer reflectivo de SIUM. Si necesita comportamiento nuevo, se cambia Soma / $libs/forms / Morfo antes que Eidos.

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

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 ✅ multi-part Thumb
collapsible ✅ multi-part Trigger, Content
dialog ✅ multi-part Trigger, Portal, Overlay, Content, Title, Description, Close, Header, Footer
drawer ✅ multi-part + Handle
field ✅ multi-part Label, RequiredIndicator, Control, Input, HelperText, ErrorText, Prefix, Suffix
form ✅ multi-part Submit, Reset, ErrorSummary, AutoFields
popover ✅ multi-part Arrow, Anchor, Title, Description
toast ✅ multi-part + Toaster separate
accordion ✅ multi-part Item, Header, Trigger, Content
avatar ✅ multi-part Image, Fallback (eidos-native)
breadcrumb ✅ multi-part List, Item, Link, Separator, Ellipsis
calendar ✅ multi-part Header, Heading, Prev/Next, Month/YearSelect, Grid, Cell, Day
date-picker ✅ multi-part DateField + Popover + Calendar composition
icon ✅ single-part + 1697 lucide glyphs
meter ✅ multi-part Indicator
number-field ✅ multi-part Input, IncrementTrigger, DecrementTrigger, Scrubber
pagination ✅ multi-part FirstTrigger, PrevTrigger, NextTrigger, LastTrigger, Item, Ellipsis
progress ✅ multi-part Label, ValueText, Indicator
rating-group ✅ multi-part Item
search-field ✅ multi-part Input, ClearTrigger
select ✅ multi-part Trigger, Value, Indicator, Portal, Content, Viewport, Item, ItemIndicator, Group
combobox ✅ multi-part Control, Input, Trigger, Indicator, Portal, Content, Viewport, Item, ItemIndicator
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
toolbar ✅ multi-part Button, Link, Group, GroupItem, Separator
tag-group ✅ multi-part Label, Item, Link, RemoveButton
tags-input ✅ multi-part Control, Input, Item, ItemText, ItemDeleteTrigger, ClearTrigger
file-upload ✅ multi-part Label, Dropzone, Trigger, HiddenInput, FileList, Item, preview/progress/actions
editable ✅ multi-part Area, Control, Preview, Input, EditTrigger, SubmitTrigger, CancelTrigger
stepper ✅ multi-part List, Item, Trigger, Indicator, Separator, Content, CompletedContent, Prev/Next

Orden recomendado para continuar

  1. Componentes grandes sólo con tabla previa de migración: date-range-picker, time-field, time-picker.

Powered by TurnKey Linux.