docs(components): S4 lotes 2-5 + drag-drop — 24 consumer-door dossiers, E-2.3 = 0 (P7)

Batches: pickers/nav (range-calendar, natural-time-picker, menubar,
navigation-menu, command, listbox) + data (table, tree-view, tree-grid,
grid-list, feed, virtual-list, virtual-grid) + typography/display (s-text,
s-text-virtual-list, badge, card, image, skeleton, spinner — passives carry
## Passive justification) + util (link-preview, announce, clipboard) +
drag-drop (29th missing README, omitted from the 5-batch plan; census said
28, real count was 29).

R-1.x exceptions only with read evidence (announce: no interactive parts;
clipboard: trigger composes Button; trf/drf mirror). Non-README flags stay
recorded in each dossier's Gaps with disposition (ntp S9 package, badge
state-layer census, stvl archetype artifact, demo D-* rules).

Machine: 85 -> 102 PASS, E-2.3 = 0 catalog-wide; type baseline stays 57.
Handoff: S4 COMPLETE — audit-fix P0-P7 closed; S9 as dedicated sessions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
menubar-v4-safe
dev 3 months ago
parent 270cc3d764
commit 20e2e7a1f0

@ -2,11 +2,17 @@
**Handoff. Fecha: 2026-07-08 · actualizado 2026-07-10 (S4 lote 1 ✅).** Rama `alpha-0.1-sec-dom`.
> **Arranca aquí → S4 lote 2 (pickers/nav)**: range-calendar ·
> natural-time-picker · menubar · navigation-menu · command · listbox.
> Método fijado en lote 1 (`7b82aa79`, detalle abajo): plantilla date-field,
> verificación por componente con `component-audit.ts --only {kebab}`,
> exceptions solo con evidencia. Después: lotes 3–5 · S9 post-S4.
> **S4 COMPLETO (2026-07-10) → el audit-fix P0–P7 está CERRADO.**
> E-2.3 = 0 en el catálogo (29 dossiers eidos + README soma field-langs;
> máquina 81→**102** PASS; baseline 57). Los NEEDS-WORK restantes son flags
> no-README documentados en los `## Gaps` de cada dossier (S9, demos D-\*,
> censos R-1.x). **Pendiente de decisión del usuario**: (a) los one-liners
> recopilados abajo; (b) ¿"lote 6" opcional? — 13 READMEs ANTIGUOS con
> F-1.x en warn (color-swatch, float-panel, format-date/number,
> gradient-\*·metrics [son S9], onion-menu, radio-cards, relative-time,
> scroll-frames, timeline, trans) que existen pero no tienen forma dossier;
> NO estaban en el plan S4 (solo cubría los ausentes). Después: S9 en
> sesiones dedicadas + reconciliación touch-rows↔§37.
> **P5 y P6 están CERRADOS**: P5 = S1·S5·depth·touch·S6·S8 ✅ (S6 `53b6f629..d1670caa`;
> S8 `83e13be6`). P6 = C6 `3b02fd11` · C7 `444e05eb` · B.11 `0f0feb4f` ✅.
> Pende tu decisión touch-rows↔§37. Follow-ups no bloqueantes abajo.
@ -30,7 +36,7 @@ componentes. La **fuente de verdad de los veredictos** es
| **P4** naming/API N1–N10 | ✅ **COMPLETO** |
| **P5** sistema/eidos | ✅ **CERRADO** — S1 ✅ · S5 ✅ · depth ✅ · touch-rows ✅ · **S6 ✅** (composición + poda + de-alias) · **S8 ✅** (`--calendar-range-*` API pública `83e13be6`) · (touch↔§37 pende decisión) |
| **P6** C7 textarea-measure + D13 · C6 weekInfo · B.11 splitter | ✅ **CERRADO** — C6 `3b02fd11` · C7 `444e05eb` · B.11 `0f0feb4f` |
| **P7** S9 iniciativas · S4 28 READMEs · S7 papeleo · errores de tipo baseline | ⬜ pendiente — **empezar aquí** |
| **P7** S9 iniciativas · S4 READMEs · S7 papeleo · errores de tipo baseline | ✅ **S4 COMPLETO 2026-07-10** (baseline ✅ · S7 ✅ · S4 29+1 dossiers ✅, E-2.3=0, máquina 102 PASS) — S9 queda como sesiones dedicadas POST-audit-fix |
## ⚠️ Reconciliar PRIMERO — conflicto touch-rows ↔ §37
@ -204,10 +210,56 @@ Doctrina del usuario: **todos los fields deben SER fields** (componen `field.css
(`spinbutton`) y apg none-con-razón de field-langs (su `A-1.4 exception` en
README ya no se honra tras C5); README soma de button drifted
(`pending`/`pressed` vs types reales) + docblocks F-2/F-3 de la ficha.
2. pickers/nav: range-calendar · natural-time-picker · menubar · navigation-menu · command · listbox
3. data: table · tree-view · tree-grid · grid-list · feed · virtual-list · virtual-grid
4. typography/display: s-text · s-text-virtual-list · badge · card · image · skeleton · spinner
5. util: link-preview · announce · clipboard
2. ✅ **pickers/nav (2026-07-10)**: range-calendar · natural-time-picker ·
menubar · navigation-menu · command · listbox. Máquina 85→90 PASS; F-1.x
verde en los 6; baseline 57. Verificado pre-escritura el estado
post-fixes (los 6 con hallazgos de ficha YA resueltos por P2/P3/S3b/S8:
packs de range-calendar y listbox, aria-selected, role application, apg
de ntp). **Hallazgo nuevo con evidencia** (README de range-calendar,
Gaps): las marcas holiday/event siguen MUERTAS en range-calendar — sus 2
tokens (`--calendar-day-holiday-shadow`, `--calendar-event-shadow`) se
emiten bajo `[data-calendar]` en generated/base.css y no resuelven bajo
`[data-range-calendar]` (el resto del préstamo va en `:root` y sí).
ntp sigue NEEDS-WORK por su paquete S9 "ntp integral" (A-3.1 del Picker
genérico sin eventos · R-2.1 los 9 cielos sin anotar · README soma +
tests) — todo trasladado a su `## Gaps` con disposición; one-liner
`expression: 'delegated'` en ntp pendiente de decisión (diría la verdad
y silencia A-3.1). R-1.3 readonly de range-calendar/listbox = gap real
(no pinta en ningún sitio) → censo, SIN exception.
3. ✅ **data (2026-07-10)**: table · tree-view · tree-grid · grid-list ·
feed · virtual-list · virtual-grid. Máquina 90→95 PASS; F-1.x verde en
los 7; baseline 57. Los 7 tenían soma README ✓ y suite ✓; varios
hallazgos de ficha ya resueltos post-audit y documentados en los
dossiers (table pack S3b + sort reclasificado a commit.set; apg de
virtual-list en forma C5 "none — rationale"; D13 de virtual-* vía
dom.measure, C7; error de tipos de grid-list en baseline `c0ae3de1`).
virtual-list/virtual-grid quedan NEEDS-WORK SOLO por `D-1.5` (regla de
DEMO, fuera del alcance — mismo caso que button). **Hallazgo nuevo**
(Gaps de tree-grid): asimetría de naming entre gemelos — tree-grid usa
`value`+`expanded`, tree-view usa `selectedValue`+`expandedValue` (N4
solo ratificó el segundo; ambos multi-eje) → decisión de espejo
pendiente. R-1.3 readonly de tree-grid/grid-list = gap real → censo,
SIN exception.
4. ✅ **typography/display (2026-07-10)**: s-text · s-text-virtual-list ·
badge · card · image · skeleton · spinner. Máquina 95→99 PASS; F-1.x (+
F-1.5 en los 4 pasivos: s-text, image, skeleton, spinner) verde en los
7; baseline 57. Quedan NEEDS-WORK con F-1.x verde y el resto en Gaps:
badge (R-4.3 hover del Remove con color-mix hand-rolled → censo
state-layer; R-4.3 NO honra exceptions de README), s-text-virtual-list
(R-1.5 = ARTEFACTO del `archetype: 'item'` en Row — display de 0
eventos `scope: ['eidos']`; one-liner de morfo pendiente que lo vuelca
a passive, el README ya trae la Passive justification preparada) y
spinner (D-7.4, demo). apg de badge/card/virtual-list ya estaban en
forma C5 "none — rationale" ✓.
5. ✅ **util (2026-07-10)**: link-preview · announce · clipboard +
**drag-drop** (el nº29 del censo E-2.3, omitido del plan de 5 lotes e
incorporado al cierre — el "28" del plan era 29 en el censo real).
Máquina 99→102 PASS. Exceptions con evidencia: announce R-1.5 (cero
partes interactivas — eventos programáticos de live region) y
clipboard R-1.5 (el Trigger compone `<Button>`; focus en button.css).
Estados post-fixes verificados: clipboard y drag-drop con pack S3b ✓,
link-preview con `expression: 'delegated'` ✓, apg C5 en drag-drop ✓
(link-preview aún sin apg → one-liner en su Gaps).
Criterio de éxito por lote: `npx tsx scripts/component-audit.ts` → los F-1.x
del lote pasan (el informe cae en `tmp/component-audit.md`).
- **S9 iniciativas** ⬜ — **decisión del usuario (2026-07-10): sesiones dedicadas
@ -215,7 +267,7 @@ Doctrina del usuario: **todos los fields deben SER fields** (componen `field.css
feature-work; gradient tenía trabajo sin commitear según memoria 2026-06-27 —
verificar su estado real al arrancarlo). **El audit-fix se cierra con S4.**
## Lo que queda — **arrancar aquí: S4 lote 2 (pickers/nav)**
## Lo que queda — **S4 CERRADO; siguiente = decisiones + S9**
### Follow-ups no bloqueantes (registrados)

@ -0,0 +1,81 @@
# Announce (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Superficie de anuncios para AT: el par de live regions (polite + assertive)
del sistema, con los eventos sema `signal-announce` / `signal-alert` — este
componente ES el literal de los verbos `signal.announce/alert` del libro
(nota in-place del morfo). Dos partes display (Provider + Region); sin
superficie interactiva.
Morfo: [`morfo/components/announce.ts`](../../../morfo/components/announce.ts) ·
APG: [alert](https://www.w3.org/WAI/ARIA/apg/patterns/alert/).
## Baseline
El patrón announcer/live-region de React Aria (`LiveAnnouncer`) y Chakra
(`useLiveRegion`) — normalmente un util imperativo sin componente. UIX lo
materializa como superficie con contrato morfo (partes + eventos sema +
a11ySemantic) en vez de un singleton invisible.
## Superficie
```svelte
<Announce>
<Announce.Region />
</Announce>
<!-- Imperativo, desde cualquier parte: -->
<!-- uix.announce.polite('Guardado') · uix.announce.assertive('Sesión expirada') -->
```
- Partes: `Provider` (identidad + estado) y `Region` (las live regions
reales — `aria-live="polite"` + `"assertive"`).
- Eventos sema: `signal-announce` (polite, neutral — cap. 24 §5.1 literal:
"esto existe, sin urgencia"), `signal-alert` (assertive, **threat** con
`persistence: 'untilAction'` — §6.2: la proyección queda hasta que el
caller la limpia — y `a11ySemantic` con traza persistente + live region,
§9.2), `commit-reset` (regiones limpiadas, neutral).
## Comparativa
| Capacidad | UIX | React Aria LiveAnnouncer | Chakra useLiveRegion | Ad-hoc aria-live |
| --- | --- | --- | --- | --- |
| Par polite/assertive gestionado | ✓ | ✓ | Parcial | Manual |
| Contrato declarado (morfo + partes) | ✓ | No (util) | No (hook) | No |
| Semántica perceptiva del anuncio | ✓ (signal.announce/alert) | No | No | No |
| Persistencia doctrinal del alert (untilAction) | ✓ | No | No | No |
| Reclasificación commit→signal documentada | ✓ (in-place) | — | — | — |
Referencias: [APG Alert](https://www.w3.org/WAI/ARIA/apg/patterns/alert/) ·
[React Aria LiveAnnouncer](https://react-spectrum.adobe.com/react-aria/accessibility.html).
## Decisiones
- **Announce ES el literal de `signal.announce/alert`** (nota del morfo):
los verbos del cap. 24 tienen aquí su superficie canónica;
`expression: 'family-default'` con la razón escrita — "Pack exists in
sema/components/announce.ts when needed; for now family base is fine"
(las firmas announce-* retuneadas en A.4/A.5 le pertenecen).
- **Reclasificación documentada**: ambos eventos estaban mal tipados como
`commit.submit` — commit FIJA estado; anunciar solo ORIENTA la atención
(signal). El alert lleva `threat` + `untilAction` (el aviso persiste
hasta que el usuario actúa o el siguiente mensaje lo pisa).
- **Sin partes interactivas a propósito**: announce no tiene close/action —
el descarte del aviso pertenece a quien lo muestra (Toast/Banner); esta
superficie solo EMITE.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Cola/deduplicación de mensajes (rate-limit de anuncios) | **diferir** | El caso "spam de announcements" no ha aparecido; el par de regiones cubre el uso actual. |
| Ascenso a pack (firmas con más carácter) | **diferir** | Explícitamente previsto en el morfo ("when needed"); hoy family base. |
## Audit exceptions
- `R-1.5 exception:` announce has NO interactive parts — Provider + live `Region` only (no trigger, no keyboard, archetypes provider/content); its events are PROGRAMMATIC live-region emissions, not user gestures. The machine classifies it interactive because events > 0, but there is no focusable surface to treat.
## Referencias
- Consumidores del anuncio: [`../toast/`](../toast/) · [`../banner/`](../banner/)
- Ficha de auditoría: [`docs/audit/components/announce.md`](../../../../../docs/audit/components/announce.md)

@ -0,0 +1,82 @@
# Badge (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Etiqueta inline / píldora de estado: chip coloreado para estados, recuentos,
tags y marcadores de versión, con dot de estado, icono, y affordance de
borrado opcional. Primitivo eidos-native (sin capa soma).
Morfo: [`morfo/components/badge.ts`](../../../morfo/components/badge.ts) ·
APG: `none — display chip` (forma C5; el Remove opcional es un botón plano).
## Baseline
Chip clásico del catálogo air (soft translúcido per THEMING §24.2 ✓;
correlación tipográfica 1:1 ✓). Referencias externas del patrón: Radix
Themes Badge, Chakra Badge/Tag (Tag = el removable), Mantine Badge, Ant Tag.
## Superficie
```svelte
<Badge color="affirm" variant="soft" size="sm">Activo</Badge>
<Badge dot color="risk">Degradado</Badge>
<Badge removable onRemove={() => quitar(tag)} color="neutral">{tag}</Badge>
```
- Partes: `Dot` (círculo de estado decorativo, `aria-hidden`), `Icon`
(slot decorativo), `Label` (wrapper del texto — la receta estila texto
sin sangrar en dot/icon/remove), `Remove` (`<button>` con `aria-label`
localizado "Remove", solo bajo `removable`).
- Props: `variant` (`ChipVariant`), `size` (`xs`–`lg`, responsive), `color`
(`ColorRole`), `rounded` + `shape` (radio y familia de esquina),
`dot`, `removable` + `onRemove` + `removeLabel`, `icon` (snippet),
`motion` (preset de entrada).
Tokens públicos del recipe `badge` (110 entradas): tallas
(`height/padding/font/icon` por talla), radios, y las paletas por color del
patrón chip.
## Comparativa
| Capacidad | UIX | Radix Themes | Chakra | Mantine | Ant |
| --- | --- | --- | --- | --- | --- |
| Chip de estado con variantes | ✓ | ✓ | ✓ (Badge) | ✓ | ✓ (Tag) |
| Dot de estado | ✓ (parte) | No | No | ✓ (`leftSection`) | ✓ (`status`) |
| Removable con botón accesible | ✓ | No | ✓ (Tag) | No | ✓ (`closable`) |
| Icono decorativo | ✓ (snippet) | Children | ✓ | ✓ | ✓ |
| Radio/esquina desacoplados de talla | ✓ (`rounded`+`shape`) | `radius` | No | `radius` | No |
| Preset de motion de entrada | ✓ | No | No | No | No |
Referencias: [Radix Themes Badge](https://www.radix-ui.com/themes/docs/components/badge) ·
[Chakra Tag](https://chakra-ui.com/docs/components/tag) ·
[Mantine Badge](https://mantine.dev/core/badge/) ·
[Ant Tag](https://ant.design/components/tag).
## Decisiones
- **0 eventos sema, con la justificación in-place** (header del morfo): como
Spinner/Skeleton, Badge es chrome perceptual puro. El `Remove` SÍ recibe
click — pero la forma canónica de "el usuario pulsó la X" es el `onRemove`
del consumidor, no un evento que el engine deba rutear. Promoción a
`commit-remove` queda abierta si emerge un patrón real de consumo (nota
del morfo).
- **Badge ≠ colección**: el chip removible DENTRO de un input pertenece a
`tags-input` (que sí tiene semántica de colección); Badge es el chip
suelto de display.
- **Dot e Icon son decorativos** (`aria-hidden`): el texto del Label lleva
el significado — un badge nunca comunica solo por color/punto.
- **APG `none — rationale`** (C5): no hay patrón de widget para un chip de
display; el Remove es un botón plano con su nombre accesible.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Hover del `Remove` con state-layer hand-rolled (R-4.3: `color-mix(currentColor 15%…)` en vez de `var(--state-hover)`) | **implementar** | Censo state-layer (la iniciativa MD3 dejó cleanup diferido): sustituir por el token del tier neutro. Cambio de recipe de una línea — pase de censo, no S4. |
| `commit-remove` como evento sema | **diferir** | Explícitamente abierto en el morfo ("promote later if a real consumer pattern emerges"). |
| Badge numérico con overflow (`99+`) | **diferir** | El consumidor formatea sus children; un `max` integrado espera caso real. |
## Referencias
- Chip en colección: [`../tags-input/`](../tags-input/) · Primitivos hermanos de feedback: [`../spinner/`](../spinner/) · [`../skeleton/`](../skeleton/)
- Ficha de auditoría: [`docs/audit/components/badge.md`](../../../../../docs/audit/components/badge.md)

@ -0,0 +1,86 @@
# Card (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Contenedor display con slots estructurales — y, bajo `interactive`, objetivo
de selección clicable (card en grid/dashboard). Primitivo eidos-native
(scope `sema+eidos`, sin soma): un evento doctrinal `commit-select` que solo
dispara con `interactive` activo.
Morfo: [`morfo/components/card.ts`](../../../morfo/components/card.ts) ·
APG: `none — surface primitive` (forma C5; la variante interactiva lleva la
semántica link/button de su trigger compuesto).
## Baseline
La anatomía de 6 partes espeja la composición canónica del ecosistema
(documentado en el header del morfo): Header / Title / Description / Body /
Footer, el patrón compartido por shadcn, Chakra y Radix Themes. Tier B de
squircle ✓ y piloto del patrón `data-motion-state` (R-4.5).
## Superficie
```svelte
<Card variant="surface" size="md">
<Card.Header><img src={cover} alt="" /></Card.Header>
<Card.Title as="h3">Título</Card.Title>
<Card.Description>Resumen secundario.</Card.Description>
<Card.Body>…</Card.Body>
<Card.Footer><Button size="sm">Acción</Button></Card.Footer>
</Card>
<!-- Card seleccionable en un grid -->
<Card interactive bind:selected onSelect={pick} motion="lift">…</Card>
```
- Partes: `Header`, `Title` (`as` h1–h6), `Description`, `Body`, `Footer`.
- Props: `variant` (`ChipVariant`), `size` (`xs`–`xl`, responsive), `color`
+ `colorCustom`, `rounded` + `shape`, **`interactive`** (el provider pasa
a superficie accionable), `selected` (estado pintado por la receta vía
`data-state`), `disabled`, `motion` (preset), `noEmerge` (desactiva el
fade-in de montaje), `onSelect(event)`.
## Comparativa
| Capacidad | UIX | shadcn | Chakra | Radix Themes | Mantine |
| --- | --- | --- | --- | --- | --- |
| Anatomía header/title/description/body/footer | ✓ | ✓ | ✓ | Children | ✓ (Section) |
| Variante interactiva/seleccionable | ✓ (`interactive`+`selected`) | Manual | No | `asChild` | No |
| Evento perceptivo del select | ✓ (`commit-select`) | No | No | No | No |
| Motion de superficie (mount/hover/press/selected) | ✓ (receta + preset) | No | No | No | No |
| Radio/esquina desacoplados (squircle Tier B) | ✓ | No | No | `radius` | `radius` |
Referencias: [shadcn Card](https://ui.shadcn.com/docs/components/card) ·
[Chakra Card](https://chakra-ui.com/docs/components/card) ·
[Radix Themes Card](https://www.radix-ui.com/themes/docs/components/card) ·
[Mantine Card](https://mantine.dev/core/card/).
## Decisiones
- **Dual pasivo/interactivo con UN evento doctrinal**: `commit-select` solo
existe bajo `interactive` — el análogo del click en un list-item.
`sequence: 'pre'` (la señal perceptiva aterriza en el click; el feedback
diferido lo emite el consumidor desde el estado resuelto — precedente
Button). `expression: 'family-default'` declarado.
- **La animación vive ENTERA en la receta** (mount fade-in, hover-lift,
press-scale, selected-ring): el morfo declara el estado `selected` para
que eidos pinte la selección sin que sema conduzca el visual — la
separación de canales canónica.
- **`noEmerge` como opt-out del montaje**: en grids largos el fade-in por
card es ruido; la prop apaga solo ese momento.
- **APG `none`** (C5): superficie sin patrón propio; si la card ES un link
o botón, esa semántica la trae el trigger compuesto (asChild), no el
contenedor.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Cardinalidad de selección (radio/checkbox de cards) | **descartar** | Ya existe: `CardGroup` + `ToggleGroup` poseen la cardinalidad (decisión 2026-06-27) — Card suelto no re-modela grupo. |
| Slots de media dedicados (cover/aspect) | **diferir** | `Header` + `AspectRatio` compuestos lo cubren; parte propia espera caso. |
| Tests del wrapper eidos | **diferir** | Pasada SYS-2. |
## Referencias
- Grupo con cardinalidad: [`../card-group/`](../card-group/) · Composición de medios: [`../aspect-ratio/`](../aspect-ratio/)
- Ficha de auditoría: [`docs/audit/components/card.md`](../../../../../docs/audit/components/card.md)

@ -0,0 +1,84 @@
# Clipboard (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Copiar-con-feedback: un Trigger (que compone el `<Button>` canónico) copia
el `value` al portapapeles vía el servicio `$clipboard`, y el `Indicator`
proyecta el "¡copiado!" — el commit.fulfill de manual, ahora con pack
propio (ascenso S3b).
Morfo: [`morfo/components/clipboard.ts`](../../../morfo/components/clipboard.ts) ·
APG: [button](https://www.w3.org/WAI/ARIA/apg/patterns/button/).
## Baseline
Patrón copy-button del ecosistema (Ark UI Clipboard — la referencia
compositiva con Root/Trigger/Indicator —, shadcn copy button, code-blocks de
docs). UIX añade el eje que a todos les falta: la semántica perceptiva del
desenlace (éxito Y fallo como eventos declarados).
## Superficie
```svelte
<Clipboard value={snippet}>
<Clipboard.Trigger>
{#snippet children({ copied })}
<Icon name={copied ? 'check' : 'copy'} />
{/snippet}
</Clipboard.Trigger>
<Clipboard.Indicator>¡Copiado!</Clipboard.Indicator>
</Clipboard>
```
- Partes: `Trigger` (compone el `<Button>` del sistema — patrón consumidor
documentado en el wrapper), `Indicator` (proyección temporal del
resultado).
- Eventos sema: **`commit-save-copy`** (el copiado aterrizó —
commit.save, el fulfill de manual) y **`commit-fail-copy`** (el
portapapeles falló — permisos/contexto inseguro): el desenlace REAL,
ambos declarados.
- El acceso al portapapeles es el servicio `$clipboard` del sistema (con
fallback y detección de soporte), nunca `navigator.clipboard` crudo.
## Comparativa
| Capacidad | UIX | Ark UI Clipboard | shadcn copy | Chakra useClipboard |
| --- | --- | --- | --- | --- |
| Anatomía Trigger/Indicator | ✓ | ✓ | Manual | Hook |
| Estado copied con timeout | ✓ | ✓ | ✓ | ✓ |
| Evento de FALLO declarado | ✓ (`commit-fail-copy`) | No | No | No |
| Trigger = Button del sistema | ✓ (compose) | Sin visual | ✓ (su Button) | — |
| Carácter perceptivo del copiado | ✓ (pack S3b) | No | No | No |
Referencias: [Ark UI Clipboard](https://ark-ui.com/docs/components/clipboard) ·
[Chakra useClipboard](https://chakra-ui.com/docs/hooks/use-clipboard).
## Decisiones
- **Pack sema propio** (`expression: 'pack'`, ascenso S3b): el "¡copiado!"
es el commit.fulfill de manual — la confirmación con carácter, no el
default genérico. El fallo suena distinto (el par save/fail es el
desenlace honesto de una operación que PUEDE fallar).
- **El Trigger compone `<Button>`** (patrón consumidor): chrome, focus
ring, touch-target y estados vienen del botón canónico — clipboard no
re-implementa un trigger.
- **`$clipboard` como única puerta** al portapapeles (doctrina de
servicios): permisos, fallback de ejecución y soporte viven en el art,
no en el componente.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Copiar contenido rico (HTML/imagen) | **diferir** | `value` string cubre el uso actual; rich clipboard = extensión del servicio `$clipboard` primero. |
| Timeout del Indicator configurable | **diferir** | El periodo actual es doctrinal (persistencia de la señal); abrirlo espera caso real. |
| Tests del wrapper eidos (soma con suite ✓) | **diferir** | Pasada SYS-2. |
## Audit exceptions
- `R-1.5 exception:` the copy trigger COMPOSES the canonical `<Button>` (Button consumer pattern — see `clipboard-trigger.svelte` header); the focus treatment lives in `button.css` on the composed button, not in this recipe.
## Referencias
- Servicio: `$clipboard` (arts) · Botón compuesto: [`../button/README.md`](../button/README.md)
- Ficha de auditoría: [`docs/audit/components/clipboard.md`](../../../../../docs/audit/components/clipboard.md)

@ -0,0 +1,111 @@
# Command (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Paleta de comandos (anatomía combobox): input de búsqueda con filtrado,
lista agrupada con highlight por teclado, ítems de acción y de navegación, y
los estados vacío/cargando como PARTES declaradas. Soma posee filtrado,
navegación, selección y ARIA; eidos añade `size` y la receta sobre la capa
compartida list-surface.
Contrato headless: [soma README](../../../soma/components/command/README.md) ·
Morfo: [`morfo/components/command.ts`](../../../morfo/components/command.ts) ·
APG: [combobox](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/).
## Baseline
La referencia externa canónica es **cmdk** (la librería que define el patrón;
shadcn `Command` la envuelve y Bits UI la porta a Svelte). Sin baseline air.
UIX implementa la anatomía completa del patrón como morfo propio y le añade
lo que cmdk no modela: el evento perceptivo del invoke y el `LinkItem` de
navegación como parte de primera clase.
## Superficie
```svelte
<Command size="md" onSelect={(v) => run(v)}>
<Command.Input placeholder="Type a command…" />
<Command.List>
<Command.Viewport>
<Command.Group>
<Command.GroupHeading>Actions</Command.GroupHeading>
<Command.GroupItems>
<Command.Item value="new-file" keywords={['create']}>New file…</Command.Item>
<Command.LinkItem value="settings" href="/settings">Settings</Command.LinkItem>
</Command.GroupItems>
</Command.Group>
<Command.Separator />
<Command.Empty>No results.</Command.Empty>
<Command.Loading>Loading…</Command.Loading>
</Command.Viewport>
</Command.List>
</Command>
```
- Partes (11): `Input` (`role="combobox"`, `aria-autocomplete="list"`,
siempre expanded), `List` (contenedor ARIA), `Viewport` (caja de scroll),
`Item`, `LinkItem` (`<a href>` — ítem de navegación), `Group` +
`GroupHeading` + `GroupItems`, `Empty`, `Loading`, `Separator`.
- Props headless: `value/onValueChange` (ítem highlighted),
`search/onSearchChange`, `onSelect`, `shouldFilter` + `filter`
(`CommandFilterFn` custom con score), `keywords` por ítem, `loop`,
**`vimBindings`** (Ctrl+j/k), **`columns`** (paletas en grid — la
navegación por flechas respeta las columnas), `disablePointerSelection`,
`dir`, `label`.
- Props eidos: `size` (`sm|md|lg`, responsive).
## Comparativa
| Capacidad | UIX | cmdk | Bits UI | kbar | shadcn |
| --- | --- | --- | --- | --- | --- |
| Filtrado con score custom + keywords | ✓ | ✓ | ✓ | ✓ | ✓ (cmdk) |
| Grupos con heading + separator | ✓ | ✓ | ✓ | ✓ | ✓ |
| `Empty` / `Loading` como partes | ✓ | ✓ | ✓ | No | ✓ |
| Ítem de navegación (`LinkItem` `<a>`) | ✓ | No (genérico) | ✓ | No | No |
| `vimBindings` | ✓ | No | ✓ | No | No |
| Paletas en grid (`columns`) | ✓ | No | ✓ | No | No |
| Evento perceptivo del invoke | ✓ (`commit-submit-invoke`) | No | No | No | No |
| Overlay | Se compone `<Dialog>` | `Command.Dialog` | `Command.Dialog` | Portal propio | Dialog |
Referencias: [cmdk](https://github.com/pacocoursey/cmdk) ·
[Bits UI Command](https://bits-ui.com/docs/components/command) ·
[kbar](https://kbar.vercel.app/) ·
[shadcn Command](https://ui.shadcn.com/docs/components/command).
## Decisiones
- **`commit-submit-invoke` es `commit.submit + affirm`, NO fulfill** — la
cita exacta vive en el morfo: cap. 23 (el usuario SOMETE su elección al
sistema de comandos) + cap. 22 §8 (el desenlace real del comando dispara
aguas abajo; celebrar en el click es "celebrate before time" — mismo
precedente que Button).
- **`expression: 'family-default'` RAZONADO** — tercera delegación-a-default
documentada del catálogo (con button y field-langs): el submit genérico de
la familia ES la firma; el carácter del desenlace lo pone el comando que
se ejecute.
- **Estados de resultado como anatomía**: `Empty` y `Loading` son partes del
morfo, no branching del consumidor — la paleta siempre tiene dónde poner
"sin resultados" y "cargando" sin inventarse markup.
- **`List` ≠ `Viewport`**: el List es el contenedor ARIA (`aria-controls`
del input); el Viewport es la caja de scroll — separar ambos permite
height animado y scroll shadows sin tocar la semántica.
- **Sin `Command.Dialog`**: el overlay se COMPONE con el `<Dialog>` del
sistema (composición sobre conveniencia — el dialog ya existe y trae su
propio contrato de foco/escape).
- **list-surface**: la receta consume la capa compartida `--list-*` +
`data-list-surface` (tallas de ítem/indent coherentes con select/
combobox/listbox); squircle Tier A en la superficie.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Filtrado asíncrono orquestado (debounce + cancelación integrados) | **diferir** | `search` controlado + `Loading` ya permiten async manual; orquestación integrada espera caso real. |
| Historial / ranking por frecuencia de uso | **descartar** | Política de producto, no del primitivo — el consumidor puede ordenar sus ítems. |
| Tests del wrapper eidos (soma con suite ✓) | **diferir** | Pasada SYS-2. |
## Referencias
- Soma Command: [`src/uix/soma/components/command/README.md`](../../../soma/components/command/README.md)
- Capa list-surface: [`../../lib/list-surface.css`](../../lib/list-surface.css)
- Ficha de auditoría: [`docs/audit/components/command.md`](../../../../../docs/audit/components/command.md)

@ -0,0 +1,83 @@
# DragDrop (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07; el
componente nº29 del censo E-2.3 — omitido del plan de 5 lotes, incorporado
al cierre).
Drag & drop declarativo: `Draggable` + `Droppable` + `Preview` sobre el
coordinador de soma, con el ciclo perceptivo de manipulación directa
(pick → drop / cancel), anuncios live localizados para AT y pack propio
(ascenso S3b).
Contrato headless: [soma README](../../../soma/components/drag-drop/README.md) ·
Morfo: [`morfo/components/drag-drop.ts`](../../../morfo/components/drag-drop.ts) ·
APG: `none — no official APG pattern for drag-and-drop` (forma C5; teclado
alternativo + anuncios live siguen la guía general WAI-ARIA).
## Baseline
Sin baseline air. Referencias del patrón: dnd-kit (el estándar React de
sensores + anuncios), Pragmatic drag and drop (Atlassian) y la API HTML5
nativa. UIX aporta el eje perceptivo (el ciclo handle con carácter) y los
anuncios como CONTRATO del morfo (12 textos localizados).
## Superficie
```svelte
<DragDrop color="primary" size="md">
<DragDrop.Draggable value="card-1">Tarjeta</DragDrop.Draggable>
<DragDrop.Droppable value="col-done">Hecho</DragDrop.Droppable>
<DragDrop.Preview>{#snippet children({ item })}<Card>{item}</Card>{/snippet}</DragDrop.Preview>
</DragDrop>
```
- Partes: `Draggable` (agarrable, rol localizado "draggable"), `Droppable`
(zona, estados over/active), `Preview` (fantasma que sigue el puntero).
- Props eidos: `size`, `color` (acento de la zona activa; consume
`--ring-inset-width` ✓).
- Anuncios live del contrato: drag-started / drag-over / dropped /
cancelled / no-targets — interpolados y localizados.
## Comparativa
| Capacidad | UIX | dnd-kit | Pragmatic DnD | HTML5 nativo |
| --- | --- | --- | --- | --- |
| Draggable/Droppable declarativos | ✓ | ✓ | ✓ (adapters) | Atributos |
| Preview/fantasma controlado | ✓ (parte) | ✓ (DragOverlay) | ✓ | Limitado |
| Anuncios AT como contrato | ✓ (12 textos morfo) | ✓ (announcements) | ✓ | No |
| Ciclo perceptivo con carácter | ✓ (pack: earcons+haptics) | No | No | No |
| Cancel como evento propio | ✓ (`commit-cancel`) | ✓ | ✓ | Parcial |
| Dependencia externa | Ninguna | Dependencia | Dependencia | — |
Referencias: [dnd-kit](https://dndkit.com/) ·
[Pragmatic drag and drop](https://atlassian.design/components/pragmatic-drag-and-drop/).
## Decisiones
- **El ciclo es `handle`, y el commit NO es suyo** (cap. 25 §4 literal,
documentado in-place): `handle-pick` ("¿he agarrado esto?") →
`handle-drop` — la CONSECUENCIA del drop (mover/borrar la entidad) la
comete el morfo dueño de esa entidad, no el DnD. `commit-cancel` (Escape/
drop fuera) sí es suyo: el usuario deshace el gesto.
- **Pack propio** (S3b, razón in-place): "pickup/release earcons + haptics;
evaluation concentrates on the drop (cap. 25)" — el peso perceptivo cae
en soltar, no en arrastrar.
- **APG `none — rationale`** (C5): DnD no tiene patrón oficial; la
alternativa de teclado y los anuncios siguen la guía general — y los
anuncios son PARTES del contrato (texts), no cortesía del consumidor.
- **`Preview` como parte**: el fantasma es anatomía themeable (no un clon
screenshot del navegador).
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Sensores de teclado completos (mover con flechas entre zonas) | **diferir** | Los anuncios y el cancel existen; el desplazamiento por teclado es la mitad pendiente de la guía WAI-ARIA — pase dedicado. |
| Auto-scroll en bordes durante el drag | **diferir** | Sin caso real todavía. |
| Sortable de listas (reordenar con huecos) | **descartar** | Patrón distinto (reorder) — si llega, componente propio sobre este coordinador. |
| Tests del wrapper eidos (soma con suite ✓) | **diferir** | Pasada SYS-2. |
## Referencias
- Soma DragDrop: [`src/uix/soma/components/drag-drop/README.md`](../../../soma/components/drag-drop/README.md)
- Ficha de auditoría: [`docs/audit/components/drag-drop.md`](../../../../../docs/audit/components/drag-drop.md)

@ -0,0 +1,95 @@
# Feed (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Flujo de artículos con scroll infinito (patrón APG **feed** — el patrón
oficial existe y está citado): `role="feed"` con artículos posicionados
(`aria-posinset`/`aria-setsize`), teclado de feed (PageUp/PageDown,
Ctrl+Home/End), `Sentinel` para load-more y `Thread` para hilos anidados.
Soma posee posiciones/busy/teclado; eidos añade `size`/`variant` (densidad,
indent del hilo, gap entre artículos).
Contrato headless: [soma README](../../../soma/components/feed/README.md) ·
Morfo: [`morfo/components/feed.ts`](../../../morfo/components/feed.ts) ·
APG: [feed](https://www.w3.org/WAI/ARIA/apg/patterns/feed/).
## Baseline
Sin baseline air y sin implementación headless publicada en las referencias
(Radix/Ark/Bits/React Aria no publican feed) — el patrón APG es la fuente
directa. UIX añade sobre el patrón la semántica perceptiva del load-more y
el hilo anidado como parte.
## Superficie
```svelte
<Feed busy={loading} totalItems={total} onLoadMore={fetchMore} size="md">
{#each items as item, i}
<Feed.Article posinset={i + 1}>
<Feed.ArticleTitle level={3}>{item.title}</Feed.ArticleTitle>
<Feed.ArticleDescription>{item.summary}</Feed.ArticleDescription>
{#if item.replies}
<Feed.Thread level={1}><!-- artículos anidados --></Feed.Thread>
{/if}
</Feed.Article>
{/each}
<Feed.Sentinel />
</Feed>
```
- Partes: `Article` (`aria-posinset`/`aria-setsize`), `ArticleTitle`
(`level` 1–6), `ArticleDescription`, `Thread` (feed anidado,
`data-level`), `Sentinel` (disparador de load-more por intersección).
- Props headless: `totalItems` (→ `aria-setsize`; `undefined` = desconocido),
`busy` (→ `aria-busy` + `data-busy` durante el fetch), `onLoadMore`,
`aria-label(ledby)`.
- Props eidos: `size` (densidad + indent de hilo + gap), `variant`.
- Teclado (APG feed): `PageDown`/`PageUp` mueven entre artículos,
`Ctrl+Home`/`Ctrl+End` a los extremos.
## Comparativa
| Capacidad | UIX | APG feed | Radix/Ark/Bits/RAC | Implementaciones ad-hoc |
| --- | --- | --- | --- | --- |
| `role="feed"` + posinset/setsize | ✓ | ✓ | No publicado | Raro |
| Teclado de feed (Page/Ctrl+Home/End) | ✓ | ✓ | — | Raro |
| `busy` durante el fetch | ✓ | ✓ | — | A veces |
| Sentinel de scroll infinito como parte | ✓ | (sugerido) | — | Custom |
| Hilos anidados (`Thread` role=feed) | ✓ | ✓ (nested feeds) | — | Raro |
| Semántica perceptiva del load-more | ✓ | — | — | No |
Referencias: [APG Feed](https://www.w3.org/WAI/ARIA/apg/patterns/feed/) —
el único patrón normativo; no hay librería headless de referencia que lo
publique.
## Decisiones
- **El load-more es `commit.submit` NEUTRAL** (cap. 23, documentado
in-place): el usuario SOMETE su petición de más contenido; el fetch real
es aguas abajo (`sustain.processing` → `commit.complete + affirm` o
`commit.fail + risk` los emite el consumidor). Soma decide si/cuándo
disparar; el evento marca la petición.
- **Navegar entre artículos es `shift.navigate`** (cap. 27 §5) — aquí SÍ es
shift (el foco cambia de artículo = cambio de contexto de lectura), a
diferencia de los expand de tree/table que son emerge.
- **`expression: 'family-default'` declarado**: petición y navegación
genéricas — sin carácter que añadir.
- **`Thread` es un feed ANIDADO** (`role="feed"` + `data-level` +
`aria-labelledby` del título del artículo padre) — el patrón APG de
nested feeds para hilos de respuestas, como parte de primera clase.
- **`Sentinel` como parte** (no un IntersectionObserver del consumidor): el
disparo del load-more pertenece al contrato del componente; el consumidor
solo escucha `onLoadMore`.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Estados de resultado del fetch (complete/fail) como eventos propios | **diferir** | Hoy son del consumidor (que posee el fetch); si el censo de "sustain" del catálogo se consolida, feed es candidato natural. |
| Skeleton integrado durante `busy` | **descartar** | Componer `<Skeleton>` dentro del feed — no absorber. |
| Tests del wrapper eidos (soma con suite ✓) | **diferir** | Pasada SYS-2. |
## Referencias
- Soma Feed: [`src/uix/soma/components/feed/README.md`](../../../soma/components/feed/README.md)
- Ficha de auditoría: [`docs/audit/components/feed.md`](../../../../../docs/audit/components/feed.md)

@ -0,0 +1,88 @@
# GridList (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Lista-grid seleccionable (patrón APG **grid** aplicado a filas ricas): cada
fila puede contener celdas con contenido interactivo propio y un checkbox de
selección declarado como parte. El punto medio entre `Listbox` (opciones
planas) y `Table` (datos columnados). Soma posee selección/teclado/ARIA;
eidos añade `size`/`variant`/`color`.
Contrato headless: [soma README](../../../soma/components/grid-list/README.md) ·
Morfo: [`morfo/components/grid-list.ts`](../../../morfo/components/grid-list.ts) ·
APG: [grid](https://www.w3.org/WAI/ARIA/apg/patterns/grid/).
## Baseline
Sin baseline air. La referencia externa directa es React Aria **GridList**
(el patrón "list of rows with interactive children" que motiva usar grid
semantics en vez de listbox: un option no puede contener botones; una fila
de grid sí). Ark/Radix/Bits no publican el patrón.
## Superficie
```svelte
<GridList bind:value selectionMode="multiple" size="md">
<GridList.Row value="doc-1">
<GridList.SelectionCheckbox />
<GridList.Cell>Informe Q3.pdf</GridList.Cell>
<GridList.Cell><Button variant="ghost" size="xs">Abrir</Button></GridList.Cell>
</GridList.Row>
</GridList>
```
- Partes: `Row` (selección, `data-selected`), `Cell` (contenido libre,
puede llevar interactivos propios), `SelectionCheckbox`
(`role="checkbox"` + `aria-checked`, label localizado "Select row").
- Props headless: `value: string[]`/`onValueChange`, `selectionMode`,
`loop`, `typeahead` + `typeaheadTimeout`,
`disabled/readonly/required/invalid`, `name` (form bridge),
`aria-label(ledby)`.
- Props eidos: `size` (`xs`–`xl`, responsive), `variant`, `color`.
## Comparativa
| Capacidad | UIX | React Aria GridList | Ark/Radix/Bits | APG grid |
| --- | --- | --- | --- | --- |
| Filas con hijos interactivos | ✓ | ✓ | No publicado | ✓ (motivación) |
| Checkbox de selección como parte | ✓ | ✓ | — | Recomendado |
| `selectionMode` single/multiple | ✓ (N1) | ✓ | — | ✓ |
| Typeahead | ✓ | ✓ | — | Recomendado |
| Form bridge (`name`) | ✓ | Vía Form | — | — |
| Eventos perceptivos select/unselect | ✓ | No | — | — |
Referencias: [React Aria GridList](https://react-spectrum.adobe.com/react-aria/GridList.html) ·
[APG Grid](https://www.w3.org/WAI/ARIA/apg/patterns/grid/).
## Decisiones
- **`commit-unselect` es `affirm`** — censo unselect=affirm (matriz C1):
deseleccionar en una colección es acción afirmativa del usuario sobre su
selección (quinto miembro del censo con listbox/select/combobox/
tag-group).
- **`expression: 'family-default'` declarado**: select/unselect genéricos de
colección — el default de familia es la firma; sin carácter extra que
añadir (mismo criterio ratificado que listbox pre-S3b… y a diferencia de
listbox, grid-list NO fue ascendido en S3b — la selección de filas ricas
no es su gesto dominante).
- **Grid semantics a propósito**: los hijos de una fila son interactivos
(botones, links) — `role="option"` lo prohíbe; el grid con navegación por
celdas lo permite (la motivación del patrón React Aria).
- **El checkbox de selección es PARTE del morfo** (no un `<Checkbox>`
compuesto): su estado ES la selección de la fila (derivado, no propio) y
su `aria-checked` la espeja — componer el Checkbox real duplicaría estado.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| `data-readonly` declarado sin estilo (R-1.3) | **implementar** | Censo de familia data — tratamiento único (con listbox/tree-grid). |
| Reordenación de filas (drag) | **descartar** | Componer con `drag-drop`. |
| Virtualización | **descartar** | Componer con `VirtualList`. |
| Tests del wrapper eidos (soma con suite ✓; el error de tipos preexistente de su suite se arregló en el baseline P7 `c0ae3de1`) | **diferir** | Pasada SYS-2. |
## Referencias
- Soma GridList: [`src/uix/soma/components/grid-list/README.md`](../../../soma/components/grid-list/README.md)
- Hermanos: [`../listbox/`](../listbox/) (opciones planas) · [`../table/`](../table/) (datos columnados)
- Ficha de auditoría: [`docs/audit/components/grid-list.md`](../../../../../docs/audit/components/grid-list.md)

@ -0,0 +1,88 @@
# Image (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Superficie de imagen responsive con fallback dirigido por estado: el
`ImageProvider` compartido (soma/layers) rastrea el ciclo de carga
(idle / loading / loaded / error) y gatea cuándo se ven los slots
`Fallback` / `Error`. Es la base que los demás componentes usan en vez de
soltar `<img>` crudo — Avatar.Image, imágenes de Card, slides de Carousel,
thumbnails.
Morfo: [`morfo/components/image.ts`](../../../morfo/components/image.ts).
## Passive justification
Superficie de 0 eventos con la razón in-place (header del morfo): como
Avatar, el ciclo de carga es OBSERVACIÓN pasiva, no acto evaluativo — el
consumidor que quiera señal perceptiva al aterrizar una imagen la cablea al
contexto circundante (el `commit-select` de la galería, el `emerge-mount`
del diálogo padre), no a la imagen.
## Baseline
Sin baseline air. Referencias del patrón status-driven: Radix Avatar
(el modelo Fallback + delayMs), Chakra Image (fallbackSrc), Next/Image
(atributos nativos de rendimiento). UIX combina ambos ejes: slots por
estado + passthrough nativo completo.
## Superficie
```svelte
<Image src={url} alt="Portada" fit="cover" radius="md" loading="lazy" delayMs={300}>
<Image.Fallback><Skeleton /></Image.Fallback>
<Image.Error><Icon name="image-off" /></Image.Error>
</Image>
```
- Partes: `Img` (el `<img>` real — `src/srcset/sizes/loading/decoding/
fetchpriority/crossorigin/referrerpolicy` reenviados nativos),
`Fallback` (visible durante la carga, con `delayMs` opcional para evitar
flash), `Error` (visible al fallar; cae automáticamente al Fallback si no
se suministra).
- Props visuales: `fit` (`cover|contain|fill|none|scale-down`, responsive),
`position`, `radius` (`none`–`full`), `size`, `placeholder` +
`placeholderColor` (fondo mientras carga), `width`/`height`.
- Textos localizados: `loading` / `error` para AT.
## Comparativa
| Capacidad | UIX | Radix Avatar | Chakra Image | Next/Image |
| --- | --- | --- | --- | --- |
| Slots por estado de carga (Fallback/Error) | ✓ | ✓ (Fallback) | `fallbackSrc` | `placeholder` blur |
| `delayMs` anti-flash | ✓ | ✓ | No | No |
| Error ≠ Loading diferenciados | ✓ | No (un solo Fallback) | Parcial | No |
| Passthrough nativo completo (srcset/priority/decoding) | ✓ | Sí (img) | Parcial | Gestionado |
| `fit`/`position` responsive | ✓ | CSS externo | ✓ | ✓ |
| Aspect ratio integrado | No (se compone) | No | No | ✓ (`width/height`) |
Referencias: [Radix Avatar](https://www.radix-ui.com/primitives/docs/components/avatar) ·
[Chakra Image](https://chakra-ui.com/docs/components/image) ·
[Next/Image](https://nextjs.org/docs/app/api-reference/components/image).
## Decisiones
- **Aspect ratio NO es prop, a propósito** (header del morfo): se compone
con el primitivo `<AspectRatio ratio>` existente — "one axis, one
primitive", el mismo principio que Card/Banner.
- **`Error` cae a `Fallback` automáticamente**: el consumidor que solo
define un slot obtiene comportamiento razonable en ambos estados.
- **Passthrough nativo, no abstracción**: los atributos de rendimiento del
`<img>` (`srcset/sizes/fetchpriority/decoding`) se reenvían tal cual — el
navegador es el dueño de la carga; UIX solo observa el estado.
- **Fundación compartida**: el ciclo idle/loading/loaded/error vive en el
`ImageProvider` de soma/layers y lo consumen también Avatar y las
superficies con thumbnail — un solo contrato de carga.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Placeholder blur-up (LQIP) integrado | **diferir** | `placeholder`+`Fallback` cubren el patrón básico; blur-up con data-URI espera caso real. |
| Zoom/lightbox | **descartar** | Otro componente (composición con Dialog/cropper) — no pertenece al primitivo. |
| Tests del wrapper eidos | **diferir** | Pasada SYS-2. |
## Referencias
- Composición de ejes: [`../aspect-ratio/`](../aspect-ratio/) · Consumidor canónico: [`../avatar/`](../avatar/)
- Ficha de auditoría: [`docs/audit/components/image.md`](../../../../../docs/audit/components/image.md)

@ -0,0 +1,76 @@
# LinkPreview (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Hover-card de enlaces: al posar el puntero sobre un ancla nativa, un
`Content` flotante muestra la vista previa (avatar de perfil, resumen de
URL, tarjeta social). Enriquecimiento solo-puntero — el enlace funciona
íntegro sin él.
Morfo: [`morfo/components/link-preview.ts`](../../../morfo/components/link-preview.ts).
## Baseline
Referencia externa directa: Bits UI **Link Preview** (mismo nombre) y Radix
**HoverCard** (el patrón origen). Ark no lo publica; React Aria no tiene
hover-card (deliberadamente — su postura de a11y es que el contenido
solo-hover es inaccesible, lo que UIX resuelve manteniendo el enlace
autosuficiente y el preview como extra).
## Superficie
```svelte
<LinkPreview size="md" motion="pop">
<LinkPreview.Trigger href="https://github.com/user">@user</LinkPreview.Trigger>
<LinkPreview.Content>
<Avatar src={avatar} /> <Text>Perfil de @user…</Text>
<LinkPreview.Arrow />
</LinkPreview.Content>
</LinkPreview>
```
- Partes: `Trigger` (ancla NATIVA — la semántica de link es suya),
`Content` (overlay flotante, `data-depth="overlay"` ✓ ×2), `Arrow`.
- Props eidos: `size`, `motion` (preset de presencia).
- El posicionamiento flotante es el del sistema (`$ethereal` vía el Popover
compuesto en soma).
## Comparativa
| Capacidad | UIX | Radix HoverCard | Bits Link Preview | React Aria |
| --- | --- | --- | --- | --- |
| Preview flotante on-hover | ✓ | ✓ | ✓ | No (postura a11y) |
| Trigger = ancla nativa intacta | ✓ | ✓ | ✓ | — |
| Delays de apertura/cierre | ✓ (soma) | ✓ | ✓ | — |
| Arrow + plano de profundidad | ✓ (`data-depth`) | ✓ | ✓ | — |
| Semántica sonora en hover | **No, by-design** | — | — | — |
Referencias: [Bits UI Link Preview](https://bits-ui.com/docs/components/link-preview) ·
[Radix HoverCard](https://www.radix-ui.com/primitives/docs/components/hover-card).
## Decisiones
- **Silencio by-design, ESCRITO** (`expression: 'delegated'` + razón
in-place, fix del checkpoint 2026-07-07): la presencia viaja por el canal
MOTION; sonido en hover sería ruido. El trigger es un ancla nativa — su
semántica (y cualquier expresión de `Link`) posee la interacción. Con esa
línea, link-preview dejó de ser el único 0-eventos de overlays sin razón
documentada.
- **Solo-puntero a propósito**: el preview es enhancement; el enlace es
100% funcional por teclado/AT sin abrirlo — la respuesta a la objeción de
React Aria.
- **Sin re-declarar overlay**: open/close/posicionamiento son del Popover
compuesto; este morfo solo aporta identidad + partes.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Sin cita `apg:` (warn A-1.4) | **implementar** | One-liner en forma C5: `apg: 'none — pointer-only enhancement; the anchor carries the link semantics'` — mismo cajón que el apg de trf. |
| Fetch/caché de metadatos de URL integrado | **descartar** | El contenido del preview es del consumidor; UIX no hace red. |
| Touch (long-press para abrir) | **diferir** | En coarse el preview no aplica hoy (hover-only); esperar caso real. |
## Referencias
- Overlay hermano: [`../tooltip/`](../tooltip/) (texto) · este es la tarjeta rica.
- Ficha de auditoría: [`docs/audit/components/link-preview.md`](../../../../../docs/audit/components/link-preview.md)

@ -0,0 +1,94 @@
# Listbox (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Lista de selección plana STANDALONE (patrón APG listbox): la superficie de
selección sin popover ni trigger — el hermano embebido de `Select`/`Combobox`
para cuando la lista vive fija en el layout. Soma posee selección, roving,
typeahead y form bridge; eidos añade `size`/`variant`/`color` sobre la capa
compartida list-surface.
Contrato headless: [soma README](../../../soma/components/listbox/README.md) ·
Morfo: [`morfo/components/listbox.ts`](../../../morfo/components/listbox.ts) ·
APG: [listbox](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/).
## Baseline
Sin baseline air propio — la referencia interna es el par `Select`/`Combobox`
(misma semántica de selección, con overlay) y la capa list-surface que
comparte con ellos. Las referencias externas del patrón standalone: React
Aria ListBox y Ark UI Listbox (Radix/Bits no publican listbox suelto — solo
dentro de Select).
## Superficie
```svelte
<Listbox bind:value selectionMode="multiple" size="md">
<Listbox.Group>
<Listbox.GroupLabel>Fruits</Listbox.GroupLabel>
<Listbox.Item value="apple">
Apple
<Listbox.ItemIndicator>✓</Listbox.ItemIndicator>
</Listbox.Item>
<Listbox.Item value="pear">Pear</Listbox.Item>
</Listbox.Group>
</Listbox>
```
- Partes: `Item` (`role="option"`, `data-selected`/`data-highlighted`),
`ItemIndicator` (marca de selección), `Group` + `GroupLabel`.
- Props headless: `value: string[]`/`onValueChange`, **`selectionMode`**
(norma N1 — unión discriminada single/multiple), `loop`, `typeahead` +
`typeaheadTimeout`, `orientation`, `dir`, `disabled/readonly/required/
invalid`, `name` (form bridge), `aria-label(ledby)`.
- Props eidos: `size` (`xs`–`xl`, responsive), `variant` (`ControlVariant`
del chrome contenedor), `color` (`ColorRole` del acento de selección).
## Comparativa
| Capacidad | UIX | React Aria | Ark UI | Radix/Bits |
| --- | --- | --- | --- | --- |
| Listbox standalone (sin overlay) | ✓ | ✓ (ListBox) | ✓ | No (solo en Select) |
| `selectionMode` single/multiple | ✓ (N1) | ✓ | ✓ | — |
| Typeahead con timeout | ✓ | ✓ | ✓ | — |
| Grupos con label | ✓ | ✓ (Section) | ✓ | — |
| Orientación + RTL | ✓ | ✓ | ✓ | — |
| Form bridge (`name`) | ✓ | Vía Form | ✓ | — |
| Eventos perceptivos select/unselect | ✓ (pack) | No | No | — |
Referencias: [APG Listbox](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/) ·
[React Aria ListBox](https://react-spectrum.adobe.com/react-aria/ListBox.html) ·
[Ark UI Listbox](https://ark-ui.com/docs/components/listbox).
## Decisiones
- **`commit-unselect` es `affirm`, NO neutral** — la matriz de intents del
deshacer (veredicto C1, canonizada): deseleccionar en una COLECCIÓN es una
acción afirmativa del usuario sobre su selección (censo ×5:
listbox/select/combobox/tag-group/toggle-group), a diferencia del uncheck
binario (neutral).
- **Pack sema propio** (`expression: 'pack'`, ascenso S3b): la asimetría con
el hermano `Select` (misma semántica, select con pack / listbox sin) se
resolvió clonando el criterio del compuesto — el standalone hereda el
carácter.
- **Standalone a propósito**: sin trigger/popover/estado open — eso es
`Select`. Listbox es la lista fija (paneles laterales, transfer lists,
settings). La receta comparte la capa list-surface con select/combobox
para que un ítem mida igual en los tres.
- **`value` siempre `string[]`** (también en single): un solo tipo de dato
para las dos cardinalidades; `selectionMode` gobierna el gesto, no el tipo.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| `data-readonly` declarado sin estilo (R-1.3) | **implementar** | Censo de familia de selección: tratamiento muted + cursor default cuando aterrice el pase readonly. |
| Selección por arrastre (drag-select de rango, estilo file manager) | **diferir** | Sin caso real; multiple + Shift/Ctrl cubre el uso actual. |
| Virtualización de listas largas | **descartar** | Pertenece a `VirtualList`/`s-text-virtual-list` — componer, no absorber. |
| Tests del wrapper eidos (soma con suite ✓) | **diferir** | Pasada SYS-2. |
## Referencias
- Soma Listbox: [`src/uix/soma/components/listbox/README.md`](../../../soma/components/listbox/README.md)
- Hermanos con overlay: [`../select/`](../select/) · [`../combobox/`](../combobox/) · capa list-surface: [`../../lib/list-surface.css`](../../lib/list-surface.css)
- Ficha de auditoría: [`docs/audit/components/listbox.md`](../../../../../docs/audit/components/listbox.md)

@ -0,0 +1,98 @@
# Menubar (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Barra de menús de escritorio (File / Edit / View / …): triggers de nivel
superior con roving focus y, por menú, el árbol interno completo de
`DropdownMenu` reutilizado literalmente. Soma posee valor (qué menú está
abierto), teclado y ARIA; eidos añade `size` y la receta de la barra.
Contrato headless: [soma README](../../../soma/components/menubar/README.md) ·
Morfo: [`morfo/components/menubar.ts`](../../../morfo/components/menubar.ts) ·
APG: [menubar](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/).
## Baseline
Portado del baseline air (la escala de tallas `sm|md|lg` espeja la de air —
documentado en types: un menubar es chrome de escritorio; `xs` amontonaría
los triggers y `xl` no tiene caso). Los cambios estructurales sobre air:
las partes internas por-menú ya no son propias — son re-exports de
`DropdownMenu` (ver Decisiones).
## Superficie
```svelte
<Menubar bind:value size="md">
<Menubar.Menu value="file">
<Menubar.Trigger>File</Menubar.Trigger>
<Menubar.Content>
<Menubar.Item onSelect={newFile}>New…</Menubar.Item>
<Menubar.Separator />
<Menubar.Sub>
<Menubar.SubTrigger>Export</Menubar.SubTrigger>
<Menubar.SubContent>…</Menubar.SubContent>
</Menubar.Sub>
</Menubar.Content>
</Menubar.Menu>
<Menubar.Menu value="edit">…</Menubar.Menu>
</Menubar>
```
- Partes propias: root (`role="menubar"`, horizontal), `Menu` (scope por
menú), `Trigger` (`role="menuitem"`, `data-state`/`data-highlighted`/
`data-menubar-value`), `Content` (panel).
- Partes re-exportadas de DropdownMenu: `Item`, `Group`, `GroupHeading`,
`Separator`, `CheckboxItem`, `CheckboxGroup`, `RadioGroup`, `RadioItem`,
`Sub`, `SubTrigger`, `SubContent`, `Arrow`.
- Props headless: `value/onValueChange` (menú abierto), `dir`, `loop`;
per-Menu: `onOpenChange`/`onOpenChangeComplete`.
- Props eidos: `size` (`sm|md|lg`, responsive; cascada a los paneles vía
contexto).
- Teclado del trigger: Enter/Space/ArrowDown abren; ArrowLeft/Right mueven
entre triggers; Home/End extremos; dentro del menú rige el teclado de
DropdownMenu.
## Comparativa
| Capacidad | UIX | Radix | Bits UI | shadcn | React Aria |
| --- | --- | --- | --- | --- | --- |
| Menubar dedicado | ✓ | ✓ | ✓ | ✓ (Radix) | No (Menu suelto) |
| Roving + hover-open tras engage | ✓ | ✓ | ✓ | ✓ | — |
| Partes por-menú = las del dropdown | ✓ (re-export literal) | ✓ (paridad API) | ✓ | ✓ | — |
| Checkbox/Radio items + submenús | ✓ | ✓ | ✓ | ✓ | — |
| `loop` + RTL | ✓ | ✓ | ✓ | ✓ | — |
| Evento perceptivo en el select | ✓ (`commit-select`) | No | No | No | — |
Referencias: [Radix Menubar](https://www.radix-ui.com/primitives/docs/components/menubar) ·
[Bits UI Menubar](https://bits-ui.com/docs/components/menubar) ·
[APG Menubar](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/).
## Decisiones
- **Pack sema propio con la razón in-place** (header del morfo): "subtle
commit + tap haptic for high-frequency top-level menu triggers" — un
menubar se pulsa decenas de veces por sesión; el carácter baja la
intensidad en vez de subirla.
- **UN evento propio** (`commit-select`, affirm, sobre el trigger): los
ítems, checkbox/radio y submenús DELEGAN en el DropdownMenu compuesto
(quinta cita del patrón de delegación del catálogo) — sus eventos sema son
los del dropdown.
- **Re-export literal de las partes internas** en eidos: `Menubar.Item` ES
`DropdownMenu.Item`, etc. — el partial compartido menu-indicator aplica
automáticamente y un fix visual del dropdown alcanza al menubar sin drift.
- **`data-menubar-value` en el trigger** enlaza trigger↔menú para el estado
controlado (`value`/`onValueChange`).
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Tests del wrapper eidos (el soma tiene suite ✓) | **diferir** | Pasada SYS-2 conjunta. |
| API extendida sobre las referencias (p. ej. trigger-on-hover configurable) | **diferir** | Paridad Radix/Bits ya alcanzada; sin caso real. |
| Intent del `commit-select` fijo en `affirm` | **descartar** | Seleccionar una entrada de menú no carga evaluación variable; el intent por-evento es intrínseco (doctrina per-event intent). |
## Referencias
- Soma Menubar: [`src/uix/soma/components/menubar/README.md`](../../../soma/components/menubar/README.md)
- DropdownMenu (partes internas): [`../dropdown-menu/`](../dropdown-menu/)
- Ficha de auditoría: [`docs/audit/components/menubar.md`](../../../../../docs/audit/components/menubar.md)

@ -0,0 +1,109 @@
# NaturalTimePicker (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Picker de hora cuyo control primario es una **banda daylight de 24h**: un
espectro horizontal (noche → alba → día → ocaso → noche) con un knob sol/luna
que se arrastra para fijar la hora, más **chips de momentos** (Mañana /
Mediodía / …) que saltan a una hora canónica, un TimeField segmentado y
steppers.
Morfo: [`morfo/components/natural-time-picker.ts`](../../../morfo/components/natural-time-picker.ts) ·
APG (patrón compuesto): [dialog-modal](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)
vía el `Popover` compuesto — mismo criterio que date-picker.
## Baseline
Componente NUEVO — sin baseline air y sin análogo en ninguna librería de
referencia (nadie publica un time picker de banda solar). El baseline
metodológico es el patrón compuesto de `date-picker` (el Popover posee el
diálogo; el picker no re-declara semántica de overlay) y la doctrina
"compose existing components": **solo el gradiente daylight es bespoke** —
todo lo demás compone primitivos del sistema (documentado en el header del
morfo):
- `Picker` genérico — el host transaccional del valor (Listo/Cancelar/Borrar,
**`deferValue`**: ntp es el primer consumidor real del host genérico).
- `TimeField` — la entrada segmentada.
- `Slider` — el track de la banda daylight.
- `Button` — steppers y chips de momentos.
- `PickerShell` — el footer.
## Superficie
```svelte
<!-- Inline: sin children renderiza el Panel -->
<NaturalTimePicker bind:value granularity="minute" minuteStep={5} />
<!-- Popover: Trigger (muestra el valor comprometido) + Content portaled -->
<NaturalTimePicker bind:value deferValue mode="footer">
<NaturalTimePicker.Trigger placeholder="Seleccionar hora" />
<NaturalTimePicker.Content />
</NaturalTimePicker>
```
- Partes: `Panel` (cuerpo del picker), `Trigger` (botón field-style que
renderiza su propio contenido — valor + reloj; no acepta children),
`Content` (popover portaled que renderiza el Panel).
- Props eidos: `size`, `shape` (propagada a los Buttons compuestos).
- Props headless: `value/onValueChange`, `placeholder`,
`open/onOpenChange/onOpenChangeComplete`, **`deferValue`**, `mode`
(`PickerShellMode`), `hourCycle`, `granularity`, `minuteStep`/`secondStep`,
`showMoments`, `showPeriod`, `band`, `chipsIconOnly`,
`disabled/readonly/required`, `locale`, `dir`.
- Los seis segmentos del día son la fuente única de: chips de momentos,
readout del header y knob de la banda (sol/luna según el segmento).
## Comparativa
| Capacidad | UIX | MUI X | Ant Design | Ark UI | React Aria |
| --- | --- | --- | --- | --- | --- |
| Banda daylight 24h (drag sol/luna) | **✓** | No | No | No | No |
| Chips de momentos canónicos | **✓** | No | No | No | No |
| Entrada segmentada compuesta | ✓ (TimeField) | ✓ | No (text) | ✓ | ✓ (solo field) |
| Valor transaccional (defer + Listo/Cancelar) | ✓ (`deferValue`) | ✓ (actions) | ✓ (footer) | No | No |
| `granularity` + steps | ✓ | ✓ | ✓ (`step`) | ✓ | ✓ |
| `hourCycle` 12/24 | ✓ | ✓ | ✓ | ✓ | ✓ |
| Overlay delegado al Popover del sistema | ✓ | Interno | Interno | ✓ | ✓ |
Referencias: [MUI X TimePicker](https://mui.com/x/react-date-pickers/time-picker/) ·
[Ant TimePicker](https://ant.design/components/time-picker) ·
[Ark UI Time Picker](https://ark-ui.com/docs/components/time-picker) ·
[React Aria TimeField](https://react-spectrum.adobe.com/react-aria/TimeField.html).
## Decisiones
- **Delegación real y ESCRITA** (header del morfo): "Open/close are delegated
to the composed Popover, so this morfo declares no dialog semantics and no
events". `scope: ['soma','eidos']` — sin sema, coherente. La corrección de
la re-auditoría exoneró a ntp del A-3.1 superficial: la cadena semántica
existe — el valor comete por el `commit-set` del TimeField embebido; el
footer fluye por el Picker genérico (ver Gaps).
- **Contrato morfo moderno**: `data-state` CON `values: ['open','closed']`
enumerados (el patrón que el censo de familia pide) +
`data-starting-style`/`data-ending-style` condicionales como hooks de
motion; flags disabled/readonly/required/invalid.
- **El Trigger es dueño de su contenido** (valor formateado + icono reloj):
no acepta `children` — la coherencia del readout no es componible.
- **Cielos como tokens privados** `--_ntp-sky-*`: los colores del gradiente
daylight son physically-fixed (cielo nocturno, alba, oro, día, ocaso) — la
excepción de la doctrina de tokens existe exactamente para esto, pero exige
anotación línea a línea que aún falta (ver Gaps).
- **Teclado del Content**: Escape cierra, Tab/Shift+Tab con trap — lo demás
es el teclado de los compuestos (Slider en la banda, TimeField en los
segmentos).
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| El footer transaccional no tiene semántica propia: delega en el `Picker` genérico cuyo morfo declara 0 eventos (deuda RECONOCIDA in-place en `picker.ts`) — el "Listo" de un picker suena a Button genérico (A-3.1 de la máquina) | **implementar** (en el Picker genérico, iniciativa S9 "ntp integral" + veredicto picker) | `commit-set` fulfill en Accept / `commit-cancel` neutral / `commit-clear` neutral EN el Picker; ntp y los 5 pickers a migrar lo heredan gratis. NO declarar eventos propios aquí — rompería la delegación correcta. Mientras: valorar el one-liner `expression: 'delegated'` (canon S11) que silenciaría A-3.1 diciendo la verdad. |
| 9 hex sin anotación (R-2.1) — los cielos physically-fixed | **implementar** (S9 ntp integral) | Anotar cada línea `/* literal: physically-fixed sky/astro color */`; NO tokenizar (ficha F-2). |
| `data-readonly`/`data-invalid` declarados sin estilo (R-1.3/R-1.4) | **implementar** | Censo de familia pickers. |
| README soma ausente + 0 tests | **implementar** (S9 ntp integral — "iniciativa a medias", SYS-9) | Contrato headless + suite (banda↔valor, chips, defer commit/cancel). |
## Referencias
- Picker genérico (host transaccional): [`src/uix/morfo/components/picker.ts`](../../../morfo/components/picker.ts) — deuda de eventos reconocida in-place; memoria Task #31 (migración deferValue de los date/time pickers, POST audit-fix).
- Patrón compuesto de referencia: [`../date-picker/`](../date-picker/) (apg ×2, popover=diálogo).
- Ficha de auditoría: [`docs/audit/components/natural-time-picker.md`](../../../../../docs/audit/components/natural-time-picker.md)

@ -0,0 +1,106 @@
# NavigationMenu (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Navegación de sitio de nivel superior con mega-menús: triggers en un `<nav>`
horizontal (o vertical), Content por ítem con motion direccional, `Link` con
`aria-current="page"` y un `Indicator` (subrayado) que se desliza entre
triggers. Soma posee estado/hover/teclado/posicionamiento del indicator;
eidos añade `size` y la receta.
Contrato headless: [soma README](../../../soma/components/navigation-menu/README.md) ·
Morfo: [`morfo/components/navigation-menu.ts`](../../../morfo/components/navigation-menu.ts) ·
APG: [disclosure](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/) — un
nav-menu NO es `role="menu"` (línea Radix: semántica de disclosure sobre
links, no menuitems de aplicación).
## Baseline
La referencia externa directa es Radix Navigation Menu (la maquinaria
direccional `data-motion ∈ from-start/from-end/to-start/to-end` es su
vocabulario, materializado aquí con el patrón materials R-4.5). Sin baseline
air. El evento perceptivo y el pack son propios de UIX.
## Superficie
```svelte
<NavigationMenu size="md">
<NavigationMenu.List>
<NavigationMenu.Item value="products">
<NavigationMenu.Trigger>Products</NavigationMenu.Trigger>
<NavigationMenu.Content><!-- mega-menú --></NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Link href="/pricing" active>Pricing</NavigationMenu.Link>
</NavigationMenu.Item>
<NavigationMenu.Indicator />
</NavigationMenu.List>
</NavigationMenu>
```
- Partes: `List` (`<ul>`), `Item` (`<li>`), `Trigger` (Enter/Space/ArrowDown
abren), `Content` (por-ítem, `data-state` + `data-motion`), `Link`
(`<a>`, `data-active` + `aria-current="page"`), `Indicator` (subrayado).
- Props headless: `value/onValueChange` (ítem abierto), `orientation`,
`dir`, `openDelay`/`closeDelay` + `groupSkipDelay` (norma N6),
`hoverEnabled`, `loop`, `aria-label(ledby)`.
- Props eidos: `size` (`sm|md|lg`, responsive — padding del trigger, gap de
la lista, font-size).
- El Content se ancla bajo la fila de triggers (`top: 100%` absoluto) para
que abrir el mega-menú NO empuje a los hermanos; no hay parte `Viewport`
separada (ver Decisiones).
## Comparativa
| Capacidad | UIX | Radix | Bits UI | shadcn | React Aria |
| --- | --- | --- | --- | --- | --- |
| Nav con semántica disclosure (no `role=menu`) | ✓ | ✓ | ✓ | ✓ (Radix) | Disclosure suelto |
| Mega-menú por ítem + motion direccional | ✓ (`data-motion` ×4) | ✓ | ✓ | ✓ | No |
| Indicator deslizante entre triggers | ✓ | ✓ | ✓ | ✓ | No |
| `Link` con `aria-current="page"` | ✓ | ✓ | ✓ | ✓ | — |
| Delays de hover (open/close/group-skip) | ✓ (N6) | ✓ | ✓ | ✓ | — |
| Viewport compartido separado | No (content per-item) | ✓ (opcional) | ✓ | ✓ | — |
| Evento perceptivo en el select | ✓ (`commit-select` + pack) | No | No | No | — |
Referencias: [Radix Navigation Menu](https://www.radix-ui.com/primitives/docs/components/navigation-menu) ·
[Bits UI Navigation Menu](https://bits-ui.com/docs/components/navigation-menu) ·
[APG Disclosure](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/).
## Decisiones
- **Contrato del Indicator — soma POSICIONA, eidos da FORMA** (formalizado
aquí desde las notas de sesión; el porqué vive también como comentario en
`navigation-menu.css`):
- Soma inline-estila `position:absolute` + `left`/`top` al corner TOP-LEFT
del trigger activo y publica el rect en las vars
`--navigation-menu-indicator-{x,y,w,h}`.
- La receta SOLO da forma: `inline-size: var(--…-w)`, subrayado de 2px,
`transform: translateY(var(--…-h))` para sentarse BAJO el trigger.
**NUNCA re-posiciona** (añadir `translateX(var(--…-x))` DUPLICA el
offset — soma ya movió `left`).
- **La transición nunca incluye `transform`**: el offset vertical salta
0→altura cuando soma mide por primera vez; animarlo hace "caer" el
subrayado desde el top del trigger. Se transiciona `left` +
`inline-size` + `opacity` solamente.
- La regla va scoped `[data-navigation-menu] [data-navigation-menu-indicator]`
(0,2,0) para ganar a la transversal `[data-archetype='indicator']`, que
re-introducía `transition: transform`.
- **Sin `Viewport`**: el Content de cada ítem se ancla bajo su fila — el caso
de viewport compartido (un panel morphing) no ha aparecido; se difiere.
- **Pack sema propio** con razón in-place: "subtle commit + tap haptic for
high-frequency nav link selection" — mismo criterio que menubar.
- **APG disclosure, no menubar**: los ítems son LINKS de navegación;
`role="menu"` prometería semántica de aplicación que no existe.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Parte `Viewport` compartida (panel único que se transforma entre contenidos, Radix-style) | **diferir** | Sin caso real; el per-item content cubre los mega-menús actuales. |
| Tests del wrapper eidos (soma con suite ✓) | **diferir** | Pasada SYS-2. |
| Indicator vertical (orientation='vertical' hoy solo mueve la lista) | **diferir** | El subrayado asume fila horizontal (`translateY(h)`); un rail lateral necesita su propia forma — esperar caso. |
## Referencias
- Soma NavigationMenu: [`src/uix/soma/components/navigation-menu/README.md`](../../../soma/components/navigation-menu/README.md)
- Ficha de auditoría: [`docs/audit/components/navigation-menu.md`](../../../../../docs/audit/components/navigation-menu.md)

@ -0,0 +1,114 @@
# RangeCalendar (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Calendario de selección de RANGO (dos endpoints en un grid): primer prestatario
de la capa calendar-surface — reutiliza la estructura y los tokens del donante
[`Calendar`](../calendar/README.md) y añade el vocabulario de rango
(`data-range-start`/`data-range-end`/tramo seleccionado) y su gradación
semántica propia.
Soma: [`src/uix/soma/components/range-calendar/README.md`](../../../soma/components/range-calendar/README.md) ·
Morfo: [`morfo/components/range-calendar.ts`](../../../morfo/components/range-calendar.ts) ·
APG: [grid](https://www.w3.org/WAI/ARIA/apg/patterns/grid/).
## Baseline
El baseline es el donante interno `Calendar` (anatomía
Provider/Header/Prev/Next/Heading/Month·YearSelect/Grid/GridHead/HeadCell/
GridBody/GridRow/Cell/Day + teclado grid APG + weekend derivado del locale,
C6). Sobre él, el rango añade:
- **Préstamo formalizado (S8, `83e13be6`)**: la capa calendar-surface publica
`--calendar-range-{start,end}-{solid,solid-hover,border,text}` como **API
pública**; este recipe los consume por su nombre público (antes tuneaba
privados `--_calendar-range-*` ajenos). Asimetría deliberada documentada:
los `start-*` se definen desde `:root` (tema-overrideable), los `end-*`
quedan atados al accent en el host.
- Los ~79 tokens `--calendar-*` del donante resuelven aquí porque el recipe
los emite en `:root` (verificado en `generated/base.css`).
## Superficie
```svelte
<RangeCalendar bind:value minDays={2} maxDays={14} numberOfMonths={2} size="md">
<RangeCalendar.Header>
<RangeCalendar.PrevButton />
<RangeCalendar.Heading />
<RangeCalendar.NextButton />
</RangeCalendar.Header>
<RangeCalendar.Grid>
<!-- GridHead/HeadCell/GridBody/GridRow/Cell/Day como el donante -->
</RangeCalendar.Grid>
</RangeCalendar>
```
- Partes (13, espejo del donante): `Header`, `PrevButton`, `NextButton`,
`Heading`, `MonthSelect`, `YearSelect`, `Grid`, `GridHead`, `HeadCell`,
`GridBody`, `GridRow`, `Cell`, `Day`.
- Props eidos: `size` (`xs`–`lg`, responsive), `variant`
(`surface`/`outline`/`ghost` — ghost para dentro de popovers de picker),
`color` (`ColorRole` — acento del endpoint END; el START usa `affirm` por
defecto y bascula a `secondary` cuando el accent es `affirm`/`fulfill`,
para que los dos extremos se distingan — documentado en types y recipe).
- Props headless: `value: DateRange`/`onValueChange`, `placeholder`,
`minValue`/`maxValue`, **`minDays`/`maxDays`** (restricción de longitud del
rango), **`allowSingleDay`**, `numberOfMonths`, `pagedNavigation`,
`fixedWeeks`, `weekStartsOn`/`weekdayFormat`, `isDateDisabled`/
`isDateUnavailable`/`isDateHoliday` (matchers), `readonly`, `disabled`,
`initialFocus`, `calendarLabel`.
## Comparativa
| Capacidad | UIX | React Aria | Bits UI | Ark UI | MUI X |
| --- | --- | --- | --- | --- | --- |
| RangeCalendar dedicado | ✓ | ✓ | ✓ | modo de DatePicker | ✓ (Pro) |
| Multi-mes + navegación paginada | ✓ | ✓ (`visibleDuration`) | ✓ | ✓ | ✓ |
| Restricción de longitud (`minDays`/`maxDays`) | ✓ | No (validación externa) | No | No | No |
| `allowSingleDay` explícito | ✓ | — | — | — | — |
| Matcher de festivos (`isDateHoliday`) | ✓ | No | No | No | No |
| Weekend derivado del locale | ✓ (C6 `getWeekInfo`) | Parcial | No | No | No |
| Gradación semántica del rango | ✓ (start→range→reset) | No | No | No | No |
| Tokens de rango como API pública | ✓ (S8) | CSS externo | CSS externo | CSS externo | Theme |
Referencias: [React Aria RangeCalendar](https://react-spectrum.adobe.com/react-aria/RangeCalendar.html) ·
[Bits UI Range Calendar](https://bits-ui.com/docs/components/range-calendar) ·
[Ark UI Date Picker](https://ark-ui.com/docs/components/date-picker) ·
[MUI X DateRangeCalendar](https://mui.com/x/react-date-pickers/date-range-calendar/).
## Decisiones
- **La gradación de rango es el contrato de referencia del catálogo**
(precedente interno citado por los veredictos de los field-ranges):
`commit-select-start` (**affirm** — el rango se abre, confirmación suave) →
`commit-select-range` (**fulfill** — el sellado del PAR es el momento de
cierre) → `commit-reset` (neutral) + `shift-navigate` (paginación de mes).
- **Pack sema propio** (`expression: 'pack'`, ascenso S3b): la riqueza ya
declarada dejó de sonar a default genérico — el fulfill del rango completo
tiene carácter distinto del affirm del start.
- **`aria-selected` vive SOLO en la Cell** (`role='gridcell'`); el Day es
`role='button'` que NO la soporta (fix C4/P2, documentado in-place en el
morfo). Sin `role='application'` (retirado en el pase C4 de familia).
- **Day sin archetype, con razón propia**: selectores bespoke
`[data-range-calendar-day]` — el estilo de rango (endpoints, tramo) no es
el estilo transversal de item.
- **El color del start NO es configurable por separado**: es función del
accent (affirm default, swap a secondary bajo accent affirm/fulfill). Un
par de props separadas invitaría a pares ilegibles; el tema puede override
los `--calendar-range-start-*` desde `:root` (S8).
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Marcas holiday/event MUERTAS: las reglas existen (`range-calendar.css:405,428`) pero `--calendar-day-holiday-shadow` y `--calendar-event-shadow` se emiten bajo `[data-calendar]` (`generated/base.css`) y no resuelven bajo `[data-range-calendar]` (ficha F-4; S8 solo formalizó los tokens de RANGO) | **implementar** | Mover esos 2 tokens al scope `:root` como el resto del préstamo (decisión de la capa calendar-surface), o superponer la identidad del donante. |
| `data-readonly` declarado sin estilo (R-1.3; el donante SÍ lo pinta bajo `[data-calendar][data-readonly]`, que aquí no aplica) | **implementar** | Censo de familia (con date-picker): espejar las 3 reglas readonly del donante bajo `[data-range-calendar]`. |
| `deselectable` (N5) no aplica al rango | **descartar** | Deshacer un rango = `commit-reset`; deseleccionar un endpoint suelto no tiene semántica de rango coherente. |
| Tests del wrapper eidos | **diferir** | El soma tiene suite ✓; el wrapper entra en la pasada SYS-2. |
## Referencias
- Donante: [`../calendar/README.md`](../calendar/README.md) · capa
calendar-surface (tokens S8) en el recipe `calendar`.
- Soma RangeCalendar: [`src/uix/soma/components/range-calendar/README.md`](../../../soma/components/range-calendar/README.md)
- Ficha de auditoría: [`docs/audit/components/range-calendar.md`](../../../../../docs/audit/components/range-calendar.md)

@ -0,0 +1,83 @@
# STextVirtualList (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Lista virtualizada de strings cuyas alturas de fila se computan con el
engine canvas-text de [`SText`](../s-text/README.md) — **sin reflow del
DOM**: `prepare()` cachea anchos de segmento por texto y `layout()` deriva
la altura con aritmética pura. Solo montan las filas del viewport (+
overscan); el resto aporta altura vía un spacer. Diseñada para listas
largas de strings cortos (comentarios, logs, resultados); para filas de
componentes mixtos, el canónico es [`VirtualList`](../virtual-list/README.md).
Morfo: [`morfo/components/s-text-virtual-list.ts`](../../../morfo/components/s-text-virtual-list.ts).
## Passive justification
Superficie de 0 eventos (header del morfo: "0-event surface") y
`scope: ['eidos']` — composición display de dos piezas del sistema (SText ×
windowing). El scroll del usuario no se modela aquí: no hay semántica que
delegar porque las filas son texto plano (`role="list"`/`"listitem"`), no
ítems accionables. *(Nota: la máquina hoy la clasifica "interactive" por el
`archetype: 'item'` de Row — ver Gaps.)*
## Baseline
Composición interna pura: el engine de `SText` (recuento exacto de líneas)
+ el modelo de windowing de `VirtualList`. Sin referencia externa directa —
la combinación "alturas dinámicas SIN medir el DOM" es lo que TanStack
Virtual no puede hacer (su `measureElement` mide nodos reales y fuerza
layout; aquí la altura sale del canvas antes de montar).
## Superficie
```svelte
<STextVirtualList items={logLines} size="md" style="body" overscan={8} />
```
- Partes: `Scroller` (caja de scroll, `role="list"`), `Spacer` (altura
total computada), `Row` (`role="listitem"`, posicionada absoluta).
- Props: los strings (`items`), el trío tipográfico que la receta fija
inline desde tokens resueltos (`--_s-tvl-font-{size,family,weight}`) para
que el canvas (que lee `getComputedStyle` UNA vez sobre sí mismo) y el
texto pintado usen exactamente la misma fuente, más `overscan` y el
vocabulario visual de SText.
## Comparativa
| Capacidad | UIX | TanStack Virtual (dynamic) | react-window (VariableSize) |
| --- | --- | --- | --- |
| Alturas variables por contenido | ✓ | ✓ | ✓ (función del consumidor) |
| Altura computada SIN montar/medir DOM | **✓** (canvas) | No (`measureElement` mide nodos) | No (el consumidor la conoce a priori) |
| Recuento de líneas i18n-correcto | ✓ (UAX #14) | — | — |
| Sincronía fuente-medida ↔ fuente-pintada | ✓ (mismo triple de tokens) | n/a | n/a |
| Dependencia externa | Ninguna | Dependencia | Dependencia |
Referencias: [TanStack Virtual `measureElement`](https://tanstack.com/virtual/latest/docs/api/virtualizer) ·
Engine propio: [`../../lib/canvas-text/`](../../lib/canvas-text/).
## Decisiones
- **La altura sale del canvas ANTES de montar** — la propiedad diferencial:
las libs de virtualización dinámica miden nodos reales (write→read, la
clase D13); aquí el layout es aritmética sobre anchos cacheados.
- **El triple tipográfico viaja por variables inline** desde tokens
resueltos: el canvas y el CSS leen LA MISMA fuente — sin este puente, una
desincronización fuente-medida/fuente-pintada rompería los recuentos.
- **Especializada a propósito**: strings cortos y homogéneos; los casos de
fila rica van a `VirtualList` (documentado en el header del morfo — no
compiten).
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| `Row` declara `archetype: 'item'` siendo display (`role="listitem"`, 0 eventos) — el gotcha conocido: item arrastra estilo interactivo Y vuelca el clasificador de la máquina a "interactive", lo que a su vez exige el focus R-1.5 que un display no tiene | **implementar** | One-liner: quitar el archetype de Row (doctrina de partes display). Con ello la clasificación pasa a passive (este README ya trae la justification) y el R-1.5 deja de aplicar. Decisión de morfo — mismo cajón que los one-liners de trf/ntp. |
| Selección/copia de filas (log viewer) | **diferir** | Convertiría la superficie en interactiva de verdad — sería otro componente o una composición; esperar caso. |
| Sticky headers de sección | **diferir** | Con virtual-list (pass conjunto si llega). |
## Referencias
- Engine: [`../s-text/README.md`](../s-text/README.md) · [`../../lib/canvas-text/`](../../lib/canvas-text/)
- Canónico de filas ricas: [`../virtual-list/README.md`](../virtual-list/README.md)
- Ficha de auditoría: [`docs/audit/components/s-text-virtual-list.md`](../../../../../docs/audit/components/s-text-virtual-list.md)

@ -0,0 +1,91 @@
# SText (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Text aumentado por canvas — la "S" es **Segmented**: `Intl.Segmenter` +
`measureText` de canvas para contar líneas EXACTAS (Unicode-aware, saltos de
línea UAX #14, bidi). Mismo contrato visual que [`Text`](../text/) (style /
size / family / weight / color / align / italic / underline / truncate /
clamp), y cuando el consumidor pasa `text` string + `clamp` numérico, la
receta colapsa el párrafo a N líneas Y pinta el hint hermano
"+N líneas más" con el desbordamiento exacto.
Morfo: [`morfo/components/s-text.ts`](../../../morfo/components/s-text.ts) ·
Engine: [`eidos/lib/canvas-text/`](../../lib/canvas-text/).
## Passive justification
Primitivo visual puro, como `Text`: superficie de 0 eventos (declarado en el
header del morfo), `scope: ['eidos']`, sin partes interactivas — el engine de
canvas computa layout, no gestos. La única "acción" aparente (el hint de
clamp) es display derivado del recuento, no un trigger.
## Baseline
El baseline interno es `Text` (el contrato visual se hereda 1:1 — doctrina de
primitivos tipográficos: exponer size/weight/color). La razón de ser un
componente SEPARADO está documentada en el morfo: el engine canvas-text
(~3900 líneas de análisis Unicode + tablas de line-break) arrastra estado
top-level y efectos — mantenerlo aparte deja a las páginas de tipografía
plana con CERO coste de canvas en el bundle.
## Superficie
```svelte
<SText style="prose" size="md" clamp={3} bind:lineCount
text={articleBody} />
<!-- Sin text+clamp actúa como Text normal -->
<SText style="label" weight="medium">Etiqueta</SText>
```
- Props visuales (contrato Text): `style`
(`body|prose|label|caption`), `size`, `family`, `weight`, `color`,
`align` (todas responsive), `italic`, `underline`, `truncate`, `as` +
`tagStyle`.
- Props del engine: `text` (string a medir), `clamp` (N líneas),
`lineCount` (bindable de salida — el recuento exacto), `hideClampHint`.
- Textos localizados del hint con plural: `+{count} more line(s)`
(`clamp-hint-one`/`clamp-hint-other`).
## Comparativa
| Capacidad | SText | CSS `-webkit-line-clamp` | clamp.js / shave | react-lines-ellipsis |
| --- | --- | --- | --- | --- |
| Clamp visual a N líneas | ✓ | ✓ | ✓ | ✓ |
| Recuento EXACTO de líneas desbordadas | ✓ | No | No | Parcial |
| Hint "+N más" con plural localizado | ✓ | No | Manual | Manual |
| Segmentación Unicode (UAX #14 + bidi) | ✓ (`Intl.Segmenter`) | Motor del navegador | Aproximada | Aproximada |
| Medición sin reflow del DOM | ✓ (canvas) | ✓ | No (mide DOM) | No |
| Dependencia externa | Ninguna (engine propio) | — | Dependencia | Dependencia |
Referencias: [`Intl.Segmenter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter) ·
[UAX #14 Line Breaking](https://unicode.org/reports/tr14/) ·
[CSS line-clamp](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-line-clamp).
## Decisiones
- **Componente separado de `Text` por PESO, no por API**: mismo vocabulario
visual; el que no necesita recuentos exactos usa Text y no paga el engine.
- **La medición es canvas, nunca DOM**: `prepare()` cachea anchos de
segmento por texto, `layout()` los recorre con aritmética pura — cero
lecturas de layout del documento (conforme D13 por construcción).
- **El hint de clamp es parte de la receta** (hermano del párrafo), con
plural localizado — no un tooltip ni markup del consumidor.
- **`lineCount` bindable de salida**: el recuento exacto es dato útil aguas
arriba (p. ej. decidir si mostrar "ver más"); se expone como binding, no
como callback.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Verificar que el morfo pasivo lleva línea de razón in-place (censo SYS-7 "sin línea") | **implementar** | El header ya dice "0-event surface: pure visual primitive, like Text" — validar que el clasificador la lee; si falta el marker formal, una línea. |
| Expand/collapse interactivo del clamp ("ver más" clicable) | **diferir** | Sería un componente interactivo distinto (o composición con Collapsible); SText se mantiene pasivo. |
| Hyphenation (guiones suaves en el recuento) | **diferir** | UAX #14 cubre breaks; hyphenation requiere diccionarios — esperar caso real. |
## Referencias
- Primitivo base: [`../text/`](../text/) · Engine: [`../../lib/canvas-text/`](../../lib/canvas-text/)
- Consumidor del engine a escala: [`../s-text-virtual-list/README.md`](../s-text-virtual-list/README.md)
- Ficha de auditoría: [`docs/audit/components/s-text.md`](../../../../../docs/audit/components/s-text.md)

@ -0,0 +1,77 @@
# Skeleton (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Placeholder de contenido durante la carga: un `<div role="status">` con
`aria-label` localizado ("Loading") cuya geometría (`rect` / `circle` /
`text`) conmuta la receta vía `data-shape`. Primitivo eidos-native — puro
visual + a11y, sin comportamiento ni estado.
Morfo: [`morfo/components/skeleton.ts`](../../../morfo/components/skeleton.ts).
## Passive justification
0 eventos con la justificación in-place (header del morfo): Skeleton no
tiene semántica commit/emerge/shift — desaparece cuando el consumidor lo
sustituye por el contenido real; añadir eventos fabricaría un significado
que el primitivo no lleva. Mismo precedente que AspectRatio / Box / Flex /
Grid.
## Baseline
Patrón universal de loading-placeholder (Chakra Skeleton, Mantine Skeleton,
Ant Skeleton, MUI Skeleton). Decisión de theming ya auditada: el shimmer es
un **gradiente funcional, no tematizable POR DISEÑO** (THEMING §29) — la
animación comunica "cargando", no identidad de marca.
## Superficie
```svelte
<Skeleton shape="text" lines={3} />
<Skeleton shape="circle" size="lg" />
<Skeleton shape="rect" animated={false} />
```
- Props: `shape` (`rect|circle|text`, responsive), `size` (responsive),
`color`, `lines` (multilínea del modo text), `animated` (shimmer on/off —
`data-animated`), `aria-label` (override del localizado).
- `role="status"` + label anunciado: los usuarios de lector de pantalla
reciben "Loading" — el placeholder no es invisible para AT.
## Comparativa
| Capacidad | UIX | Chakra | Mantine | Ant | MUI |
| --- | --- | --- | --- | --- | --- |
| Formas rect/circle/text | ✓ (enum) | ✓ (variants) | ✓ | ✓ | ✓ |
| Multilínea (`lines`) | ✓ | ✓ (SkeletonText) | No | ✓ (`paragraph`) | No |
| `role="status"` + label localizado | ✓ | No | No | No | No |
| Shimmer desactivable | ✓ (`animated`) | ✓ | ✓ | ✓ (`active`) | ✓ |
| Shimmer tematizable | No (§29, por diseño) | ✓ | ✓ | Parcial | ✓ |
Referencias: [Chakra Skeleton](https://chakra-ui.com/docs/components/skeleton) ·
[Mantine Skeleton](https://mantine.dev/core/skeleton/) ·
[Ant Skeleton](https://ant.design/components/skeleton) ·
[MUI Skeleton](https://mui.com/material-ui/react-skeleton/).
## Decisiones
- **El shimmer NO se tematiza** (§29): es gradiente FUNCIONAL — su trabajo
es decir "cargando" de forma uniforme en cualquier tema; abrirlo a
theming invitaría a shimmers de marca que compiten con el contenido.
- **`role="status"` con label**: la ausencia de contenido también se
anuncia — el patrón de los grandes lo omite; aquí es contrato del morfo.
- **`shape` como enum de receta** (`data-shape`): tres geometrías cubren el
espectro (bloque, avatar, párrafo); formas libres = el consumidor estila
el contenedor.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Wrapper "skeletonize children" (envolver contenido real y taparlo) | **diferir** | El patrón slots-explícitos cubre el uso; el modo envolvente (Chakra `isLoaded`) espera caso real. |
| Coordinación de grupo (stagger entre skeletons hermanos) | **descartar** | Ruido perceptual; el shimmer uniforme ya comunica. |
## Referencias
- Hermanos de feedback: [`../spinner/README.md`](../spinner/README.md) · [`../progress/`](../progress/)
- Ficha de auditoría: [`docs/audit/components/skeleton.md`](../../../../../docs/audit/components/skeleton.md)

@ -0,0 +1,81 @@
# Spinner (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Indicador de progreso indeterminado: `<div role="status">` con glifo
rotatorio CSS-only, para donde un `<Progress>` determinado es demasiado
(petición en vuelo, dropdown async, transición de ruta). Primitivo
eidos-native.
Morfo: [`morfo/components/spinner.ts`](../../../morfo/components/spinner.ts).
## Passive justification
0 eventos con la justificación in-place (header del morfo): como Skeleton,
Spinner no emite commit/emerge/shift — es chrome perceptual puro que el
consumidor desmonta al terminar el trabajo.
## Baseline
Indicador clásico del catálogo con decisiones de theming ya auditadas: las
**tres variantes propias** `ring · dots · bars` (§19), el keyframe `spin`
anotado como funcional, y la opacidad cruda legítima documentada (§29 — los
pasos de atenuación de dots/bars son geometría del indicador, no color de
tema).
## Superficie
```svelte
<Spinner size="md" color="primary" />
<Spinner variant="dots" size="lg" />
<Button loading><Spinner variant="ring" size="sm" /></Button>
```
- Props: `variant` (`ring | dots | bars`), `size` (`xs`–`xl`, responsive),
`color`, `presentation` (rebaja a `role="presentation"` cuando otro
elemento ya anuncia la carga — p. ej. dentro de un Button con
`aria-busy`), `aria-label` (override del localizado "Loading").
## Comparativa
| Capacidad | UIX | Chakra | Mantine | Radix Themes | MUI |
| --- | --- | --- | --- | --- | --- |
| Variantes de glifo | ✓ (ring/dots/bars) | 1 (ring) | 3 (oval/bars/dots) | 1 | 1 (+determinate) |
| `role="status"` + label localizado | ✓ | Manual | No | No | No |
| Opt-out de anuncio (`presentation`) | ✓ | No | No | No | No |
| CSS-only (sin JS de animación) | ✓ | ✓ | ✓ | ✓ | SVG+CSS |
| Tallas responsive | ✓ | ✓ | ✓ | ✓ | `size` px |
Referencias: [Chakra Spinner](https://chakra-ui.com/docs/components/spinner) ·
[Mantine Loader](https://mantine.dev/core/loader/) ·
[Radix Themes Spinner](https://www.radix-ui.com/themes/docs/components/spinner) ·
[MUI CircularProgress](https://mui.com/material-ui/react-progress/).
## Decisiones
- **`presentation` para no anunciar dos veces**: dentro de un Button en
`loading` (que ya lleva `aria-busy`), el spinner baja a
`role="presentation"` — un solo anuncio por estado, decidido por el
contexto que lo compone.
- **El keyframe `spin` es FUNCIONAL** (anotado): periodo de rotación
constante — no entra en la doctrina de animación perceptual (no es señal
de evento, es estado sostenido).
- **Opacidades crudas legítimas** (§29): los pasos de atenuación entre
dots/bars son geometría del glifo (qué segmento "va delante"), no roles
de color del tema.
- **Indeterminado a propósito**: el progreso con valor pertenece a
`<Progress>`/`<Meter>` — Spinner nunca acepta `value`.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Chips de la demo desalineados con las unions (`D-7.4` — regla de demo) | **implementar** (pase de demos, no S4) | Realinear a demos computadas-desde-tipos (con combobox — ficha F-2). |
| Velocidad de rotación configurable | **descartar** | El periodo funcional es constante por diseño; variarlo no añade significado. |
| Variante con label visible integrado | **diferir** | Componer `<Spinner /> <Text>` cubre el caso; slot propio espera demanda. |
## Referencias
- Hermanos de feedback: [`../skeleton/README.md`](../skeleton/README.md) · [`../progress/`](../progress/)
- Consumidor canónico: [`../button/README.md`](../button/README.md) (spinner de loading)
- Ficha de auditoría: [`docs/audit/components/spinner.md`](../../../../../docs/audit/components/spinner.md)

@ -0,0 +1,112 @@
# Table (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Tabla de datos: el engine headless (`createTable()` de `$libs/datagrid`,
implementación propia) posee columnas/orden/selección/expansión; soma añade
ARIA (`aria-sort` computado), eventos perceptivos y teclado; eidos añade la
receta (densidades, striped/hoverable, sticky header, scroll región).
Contrato headless: [soma README](../../../soma/components/table/README.md) ·
Morfo: [`morfo/components/table.ts`](../../../morfo/components/table.ts) ·
APG: [table](https://www.w3.org/WAI/ARIA/apg/patterns/table/).
## Baseline
Sin baseline air. La referencia arquitectural es el ecosistema headless-table
(TanStack Table como canon del engine desacoplado), con la doctrina propia
"table belongs in soma": el ENGINE es `$libs/datagrid` (zero-dep, del
framework — nunca una dependencia), el consumidor crea la instancia con
`createTable()` y se la pasa a `<Table table={...}>`; las capas UIX no
re-implementan datos, solo contrato + percepción + receta.
## Superficie
```svelte
<script lang="ts">
import { createTable } from '$libs/datagrid';
const table = createTable({ data, columns });
</script>
<Table {table} size="md" striped stickyHeader maxHeight="24rem" stableRows={10}>
<Table.Header>
<Table.ColumnHeader header={h}>
<Table.SortTrigger>{h.label}</Table.SortTrigger>
</Table.ColumnHeader>
</Table.Header>
<Table.Body>
<Table.Row row={r}>
<Table.Cell cell={c}>…</Table.Cell>
<Table.RowDetailTrigger />
</Table.Row>
<Table.RowDetail row={r}>…detalle expandido…</Table.RowDetail>
</Table.Body>
<Table.Footer>…</Table.Footer>
</Table>
```
- Partes (10): `Header`, `Body`, `Footer`, `ColumnHeader` (`aria-sort` +
`data-sortable`/`data-sorted`), `Row` (selección), `Cell`, `SortTrigger`
(snippet `{ canSort, sorted }`), `RowDetail` + `RowDetailTrigger` (fila de
detalle expandible, textos localizados).
- Props headless: `table` (la instancia de `createTable()`, requerida),
`label`, `dir`.
- Props eidos: `size` (`xs`–`xl` — de densidad data-table compacta a
report-table aireada), `variant`, `color`, `block`, `striped`,
`hoverable`, `stickyHeader`, `maxHeight` (envuelve en scroll región
propia), **`stableRows`** (reserva suelo de altura para N filas — las
tablas paginadas no saltan al filtrar/cambiar de página; sin efecto si hay
`maxHeight`).
## Comparativa
| Capacidad | UIX | TanStack Table | React Aria Table | MUI DataGrid | Radix/Ark |
| --- | --- | --- | --- | --- | --- |
| Engine headless desacoplado | ✓ (`$libs/datagrid` propio) | ✓ (dependencia) | Integrado | Integrado | Sin table |
| Sort con `aria-sort` computado | ✓ | Manual | ✓ | ✓ | — |
| Selección de filas | ✓ | ✓ | ✓ | ✓ | — |
| Fila de detalle expandible | ✓ (partes RowDetail) | ✓ (expanded state) | No | ✓ (detail panel) | — |
| Sticky header + scroll región propia | ✓ | CSS externo | ✓ | ✓ | — |
| Reserva de altura paginada (`stableRows`) | **✓** | No | No | No | — |
| Eventos perceptivos (sort/select/expand) | ✓ (pack) | No | No | No | — |
Referencias: [APG Table](https://www.w3.org/WAI/ARIA/apg/patterns/table/) ·
[TanStack Table](https://tanstack.com/table) ·
[React Aria Table](https://react-spectrum.adobe.com/react-aria/Table.html) ·
[MUI DataGrid](https://mui.com/x/react-data-grid/).
## Decisiones
- **El sort es `commit.set`, NO `shift.navigate`** (reclasificación
documentada in-place, cap. 23): fijar el criterio de orden es "algo queda
aplicado"; el reorden visual es CONSECUENCIA del set — ordenar no cambia el
régimen operativo (misma vista, mismas acciones). Intent neutral.
- **Expandir el detalle es `emerge.expand`** (cap. 26 literal: "una región
crece y revela contenido") — la misma corrección que tree-view/tree-grid:
revelar contenido = emerge, no shift.
- **Pack sema propio** (S3b, comentario in-place): "selection voice + subtle
sort" — el select con voz affirm, el sort con señal sutil neutral.
- **`SortTrigger` promovido de eidos a soma**: soma posee la lectura
sortable/sorted, el `aria-sort` computado (gated a sortability) y los
attrs `data-sortable`/`data-sorted`; el snippet recibe
`{ canSort, sorted }` — eidos no deriva estado.
- **`stableRows`**: la geometría de una tabla paginada es API de layout —
reservar el suelo de N filas evita que paginación/cards vecinas salten
cuando el pageSize no se llena.
- **Sticky header sin scroll propio** (`stickyHeader` solo) pega al ancestro
de scroll del CONSUMIDOR; con `maxHeight` la tabla envuelve su propia
región. Dos modos explícitos, sin heurística.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Column resizing / reordering / pinning | **diferir** | El engine `$libs/datagrid` es el sitio (fase propia de datagrid); la receta ya soporta anchos por columna vía CSS. |
| Virtualización de filas | **descartar** | Componer con `VirtualList` — no absorber (mismo criterio que listbox). |
| Tests del wrapper eidos (engine + soma con suite ✓) | **diferir** | Pasada SYS-2. |
## Referencias
- Engine: `$libs/datagrid` (`createTable`, `TableInstance`) — implementación propia zero-dep.
- Soma Table: [`src/uix/soma/components/table/README.md`](../../../soma/components/table/README.md)
- Ficha de auditoría: [`docs/audit/components/table.md`](../../../../../docs/audit/components/table.md)

@ -0,0 +1,95 @@
# TreeGrid (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Grid jerárquico (patrón APG **treegrid**): filas con columnas Y jerarquía —
la navegación 2D de una tabla (flechas por celdas) más el expand/collapse de
un árbol. El gemelo tabular de [`TreeView`](../tree-view/README.md). Soma
posee estado/teclado/ARIA; eidos añade `size`/`variant`/`color` y la receta.
Contrato headless: [soma README](../../../soma/components/tree-grid/README.md) ·
Morfo: [`morfo/components/tree-grid.ts`](../../../morfo/components/tree-grid.ts) ·
APG: [treegrid](https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/).
## Baseline
Sin baseline air. El patrón APG treegrid casi no tiene implementaciones
headless publicadas (ni Radix, ni Ark, ni Bits lo publican; React Aria lo
aproxima con Table + expansión) — la referencia principal es el propio APG y
el gemelo interno TreeView, con el que comparte firma sema.
## Superficie
```svelte
<TreeGrid bind:value bind:expanded selectionMode="single" size="md">
<TreeGrid.Header>
<TreeGrid.ColumnHeader>Name</TreeGrid.ColumnHeader>
<TreeGrid.ColumnHeader>Size</TreeGrid.ColumnHeader>
</TreeGrid.Header>
<TreeGrid.Row value="src" level={1}>
<TreeGrid.Cell>
<TreeGrid.ExpandTrigger />
src
</TreeGrid.Cell>
<TreeGrid.Cell>—</TreeGrid.Cell>
</TreeGrid.Row>
<TreeGrid.RowChildren>
<TreeGrid.Row value="src/index.ts" level={2}>…</TreeGrid.Row>
</TreeGrid.RowChildren>
</TreeGrid>
```
- Partes: `Header`, `ColumnHeader`, `Row` (selección + `aria-level`/
`aria-expanded`), `RowChildren` (hijos de una fila), `Cell`,
`ExpandTrigger` (textos expand/collapse localizados).
- Props headless: `value: string[]`/`onValueChange` (selección),
`expanded: string[]`/`onExpandedChange`, `selectionMode`, `loop`,
`typeahead` + `typeaheadTimeout`, `disabled`, `readonly`,
`aria-label(ledby)`.
- Props eidos: `size` (`xs`–`xl`, responsive), `variant`, `color`.
## Comparativa
| Capacidad | UIX | APG treegrid | React Aria | Ark/Radix/Bits |
| --- | --- | --- | --- | --- |
| Treegrid dedicado | ✓ | (patrón) | Aproximado (Table+expand) | No publicado |
| Navegación 2D por celdas | ✓ | ✓ | ✓ (Table) | — |
| Expand/collapse por fila con `aria-level` | ✓ | ✓ | Parcial | — |
| Selección single/multiple | ✓ | ✓ | ✓ | — |
| Typeahead | ✓ | Recomendado | ✓ | — |
| Eventos perceptivos (select/expand/collapse) | ✓ (pack) | — | No | — |
Referencias: [APG Treegrid](https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/) ·
[React Aria Table](https://react-spectrum.adobe.com/react-aria/Table.html) ·
Gemelo interno: [`../tree-view/`](../tree-view/).
## Decisiones
- **Mismo trío semántico que el gemelo**: `commit-select` (affirm, fila) +
`emerge-expand`/`emerge-collapse` (cap. 26 — revelar contenido es emerge,
no shift). **Pack propio** con la misma razón sobria in-place: "soft
emerge expand/collapse + subtle commit on row select. Same sober signature
as tree-view" — el par tree es el único de data con carácter, y suena
IGUAL a ambos lados.
- **Treegrid ≠ tree con columnas**: el teclado es el del grid (flechas
mueven por CELDAS, no solo por filas) y la fila lleva
`aria-level`/`aria-expanded` — por eso es morfo propio y no una variante
de TreeView.
- **`RowChildren` como parte** (no anidamiento implícito): el DOM de un
treegrid es plano por filas (tabla), la jerarquía la declara la parte —
ARIA correcto sin sacrificar el layout tabular.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| `data-readonly` declarado sin estilo (R-1.3) | **implementar** | Censo de familia data — tratamiento único con listbox/grid-list cuando aterrice el pase readonly. |
| Asimetría de naming con el gemelo: tree-grid usa `value`+`expanded`, tree-view usa `selectedValue`+`expandedValue` (N4 ratificó el del tree-view; ambos son multi-eje) | **implementar** (decisión N4 pendiente de espejo) | O tree-grid adopta los sufijos de eje del gemelo, o se documenta por qué el grid conserva `value` — decidir en el pase de naming, no aquí. |
| Column sort (el header no ordena) | **diferir** | Ordenar un árbol reordena subárboles — esperar caso real antes de definir la semántica. |
| Tests del wrapper eidos (soma con suite ✓) | **diferir** | Pasada SYS-2. |
## Referencias
- Soma TreeGrid: [`src/uix/soma/components/tree-grid/README.md`](../../../soma/components/tree-grid/README.md)
- Gemelo: [`../tree-view/README.md`](../tree-view/README.md)
- Ficha de auditoría: [`docs/audit/components/tree-grid.md`](../../../../../docs/audit/components/tree-grid.md)

@ -0,0 +1,94 @@
# TreeView (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Árbol jerárquico (patrón APG treeview): ramas expandibles con control /
contenido / indicador / guía de indentación, hojas seleccionables, typeahead
y doble eje de estado (`expandedValue` + `selectedValue`). Soma posee estado,
teclado y ARIA; eidos añade `size`/`variant`/`color` y la receta.
Contrato headless: [soma README](../../../soma/components/tree-view/README.md) ·
Morfo: [`morfo/components/tree-view.ts`](../../../morfo/components/tree-view.ts) ·
APG: [treeview](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/).
## Baseline
Sin baseline air. Referencias externas del patrón compositivo: Ark UI
TreeView (la anatomía Branch/BranchControl/BranchContent/BranchIndicator es
su vocabulario) y React Aria Tree; Radix/Bits no publican tree.
## Superficie
```svelte
<TreeView bind:selectedValue bind:expandedValue selectionMode="single" size="md">
<TreeView.Branch value="src">
<TreeView.BranchControl>
<TreeView.BranchIndicator>▸</TreeView.BranchIndicator>
<TreeView.Label>src</TreeView.Label>
</TreeView.BranchControl>
<TreeView.BranchContent>
<TreeView.BranchIndentGuide />
<TreeView.Item value="src/index.ts"><TreeView.Label>index.ts</TreeView.Label></TreeView.Item>
</TreeView.BranchContent>
</TreeView.Branch>
</TreeView>
```
- Partes: `Branch` (nodo con hijos), `BranchControl` (fila clicable),
`BranchContent` (hijos), `BranchIndicator` (chevron, `data-state`),
`BranchIndentGuide` (guía vertical, decorativa), `Item` (hoja), `Label`.
- Props headless: **`expandedValue: string[]` + `selectedValue: string[]`**
(norma N4: sufijo `{eje}Value` para multi-eje) con
`onExpandedChange`/`onSelectionChange`, `selectionMode`, `expandOnClick`,
`typeahead`, `dir`, `aria-label(ledby)`.
- Props eidos: `size` (`xs`–`xl`, responsive), `variant`, `color`.
## Comparativa
| Capacidad | UIX | Ark UI | React Aria | Radix/Bits |
| --- | --- | --- | --- | --- |
| TreeView compositivo | ✓ | ✓ | ✓ (Tree) | Sin tree |
| Doble eje expanded/selected | ✓ (N4) | ✓ | ✓ | — |
| Guía de indentación como parte | ✓ | ✓ | No | — |
| Typeahead | ✓ | ✓ | ✓ | — |
| `expandOnClick` configurable | ✓ | ✓ | No (afford propio) | — |
| Eventos perceptivos (select/expand/collapse) | ✓ (pack) | No | No | — |
Referencias: [APG TreeView](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/) ·
[Ark UI TreeView](https://ark-ui.com/docs/components/tree-view) ·
[React Aria Tree](https://react-spectrum.adobe.com/react-aria/Tree.html).
## Decisiones
- **Expandir una rama es `emerge.expand`, NO `shift.navigate`**
(reclasificación documentada in-place, cap. 26 literal: "una región crece
y revela contenido"). Dato del censo emerge: la familia cubre disclosure
además de flotantes, con tres pares de verbos (open/close ·
present/dismiss · expand/collapse) — tree-view usa el par
expand/collapse, como collapsible.
- **El target del expand es `branch`, no `item`**: los items son HOJAS (no
expanden); el stamp `data-event-*` aterriza en `data-tree-view-branch`,
que es lo que el selector del pack targetea.
- **Pack sema propio con la razón in-place**: "soft emerge expand/collapse +
subtle commit on item select. Trees are explored repeatedly; signature
stays sober" — exploración de alta frecuencia = firma sobria.
- **`selectedValue`/`expandedValue`** (no `value` a secas): el veredicto N4
reserva el sufijo de eje exactamente para este caso multi-eje.
- **La guía de indentación es una PARTE** (`BranchIndentGuide`,
`aria-hidden`): la línea vertical de nivel es anatomía themeable, no un
border mágico de la receta.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Carga perezosa de ramas (async children) | **diferir** | El estado controlado ya permite montar hijos on-expand desde el consumidor; una API dedicada espera caso real. |
| Drag & drop de nodos | **descartar** | Componer con `drag-drop` — no absorber. |
| Virtualización de árboles grandes | **descartar** | Componer con `VirtualList` (mismo criterio que listbox/table). |
| Tests del wrapper eidos (soma con suite ✓) | **diferir** | Pasada SYS-2. |
## Referencias
- Soma TreeView: [`src/uix/soma/components/tree-view/README.md`](../../../soma/components/tree-view/README.md)
- Gemelo con grid semantics: [`../tree-grid/`](../tree-grid/)
- Ficha de auditoría: [`docs/audit/components/tree-view.md`](../../../../../docs/audit/components/tree-view.md)

@ -0,0 +1,88 @@
# VirtualGrid (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Windowing 2D: virtualiza filas Y columnas a la vez — solo las celdas del
rectángulo visible (+ overscan por eje) existen en el DOM. El gemelo
bidimensional de [`VirtualList`](../virtual-list/README.md); misma
implementación propia (zero-dep), doble eje.
Contrato headless: [soma README](../../../soma/components/virtual-grid/README.md) ·
Morfo: [`morfo/components/virtual-grid.ts`](../../../morfo/components/virtual-grid.ts) ·
APG: [grid](https://www.w3.org/WAI/ARIA/apg/patterns/grid/) (el gemelo 1D
usa la forma `none — rationale`; aquí el rectángulo de celdas SÍ es
material de grid).
## Baseline
Sin baseline air. Referencia de modelo: TanStack Virtual con dos
virtualizadores (fila + columna) — aquí es un solo primitivo simétrico con
opts por eje. Gemelo interno: VirtualList (1D).
## Superficie
```svelte
<VirtualGrid rowCount={1000} columnCount={200} rowSize={36} columnSize={120}
rowOverscan={3} columnOverscan={2} size="md">
<VirtualGrid.Viewport>
{#snippet children({ cells })}
{#each cells as c (c.key)}
<VirtualGrid.Cell cell={c}>R{c.rowIndex}·C{c.columnIndex}</VirtualGrid.Cell>
{/each}
{/snippet}
</VirtualGrid.Viewport>
</VirtualGrid>
```
- Partes: `Viewport` (scroll 2D), `Cell` (posicionada por el windowing).
- Props headless: `rowCount`/`columnCount`, `rowSize`/`columnSize`,
`rowOverscan`/`columnOverscan`, `getRowKey`/`getColumnKey`. El snippet
expone las celdas visibles + `scrollToCell(row, column, { rowAlign,
columnAlign, behavior })`.
- Props eidos: `size` (chrome del viewport).
## Comparativa
| Capacidad | UIX | TanStack Virtual | react-window | AG Grid |
| --- | --- | --- | --- | --- |
| Virtualización 2D simétrica | ✓ (un primitivo) | 2 virtualizadores | ✓ (FixedSizeGrid) | Integrada |
| Overscan por eje | ✓ | ✓ | ✓ | Interno |
| `scrollToCell` con align por eje | ✓ | Manual (×2) | ✓ | ✓ |
| Eventos por eje de scroll | ✓ (row/column) | No | No | No |
| Dependencia externa | **Ninguna** | Dependencia | Dependencia | Dependencia |
Referencias: [TanStack Virtual](https://tanstack.com/virtual) ·
[react-window FixedSizeGrid](https://github.com/bvaughn/react-window).
## Decisiones
- **Los ejes de scroll son eventos SEPARADOS** (`handle-scroll-row` /
`handle-scroll-column`): la cascada sema puede diferenciar carácter por
eje (un spreadsheet que suena distinto al desplazar columnas). Misma
disección de familias que el gemelo, documentada in-place: scroll del
usuario = `handle` (cap. 25, verbo `scroll` extiende el canon per cap. 8
§1) · `scrollToCell` imperativo = `shift.navigate` (el SISTEMA mueve,
cap. 27 §5) · counts recalculados = `commit.set` neutral (cap. 23).
- **`expression: 'family-default'`** con la razón escrita: "family defaults
are appropriate" — señales de infraestructura.
- **`apg: grid` aquí, `none` en el gemelo**: el pase C5 igualó a los
gemelos con la decisión correcta para cada uno — la lista 1D no es
widget; el rectángulo 2D de celdas es exactamente el material del patrón
grid.
- **D13 conforme (C7)**: mediciones del viewport diferidas vía
`dom.measure` del contexto (pass conjunto con virtual-list).
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Tamaños dinámicos por celda (`estimateSize` 2D) | **diferir** | El 1D lo tiene; en 2D el coste de layout es cuadrático — esperar caso real. |
| Sticky primeras filas/columnas (frozen panes) | **diferir** | El caso spreadsheet lo pedirá; hoy no hay consumidor. |
| Window-as-scroller (el gemelo lo tiene) | **diferir** | Un grid 2D scrolleado por la ventana es raro; se añade si aparece. |
| Tests del wrapper eidos (soma con suite ✓) | **diferir** | Pasada SYS-2. |
## Referencias
- Soma VirtualGrid: [`src/uix/soma/components/virtual-grid/README.md`](../../../soma/components/virtual-grid/README.md)
- Gemelo 1D: [`../virtual-list/README.md`](../virtual-list/README.md)
- Ficha de auditoría: [`docs/audit/components/virtual-grid.md`](../../../../../docs/audit/components/virtual-grid.md)

@ -0,0 +1,99 @@
# VirtualList (eidos)
Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).
Primitivo de windowing 1D: dado un `count`, renderiza solo las filas dentro
del `Viewport` más `overscan` de colchón — 10k filas ocupan los mismos ~15
nodos DOM. La matemática de virtualización es implementación propia del
provider (zero-dep, doctrina del framework). Eidos añade `size`/`variant`
del viewport.
Contrato headless: [soma README](../../../soma/components/virtual-list/README.md) ·
Morfo: [`morfo/components/virtual-list.ts`](../../../morfo/components/virtual-list.ts) ·
APG: `none — virtualization is a rendering strategy, not a widget pattern`
(forma C5 "none — rationale"; los roles/teclado los trae el CONTENIDO
compuesto).
## Baseline
Sin baseline air. La referencia externa del modelo es TanStack Virtual
(count + itemSize/estimateSize + overscan + scrollToIndex) — reimplementado
propio, nunca dependencia. El gemelo 2D es
[`VirtualGrid`](../virtual-grid/README.md).
## Superficie
```svelte
<VirtualList count={10000} itemSize={40} overscan={5} size="md">
<VirtualList.Viewport>
{#snippet children({ items })}
{#each items as it (it.key)}
<VirtualList.Item item={it}>Fila {it.index}</VirtualList.Item>
{/each}
{/snippet}
</VirtualList.Viewport>
</VirtualList>
```
- Partes: `Viewport` (caja de scroll propia), **`WindowViewport`** (modo
ventana-como-scroller — la página entera es el scroll container),
`Item` (posicionado absoluto por el windowing).
- Props headless: `count` (requerida), `itemSize` (filas fijas) o
`estimateSize(index)` (dinámicas), `overscan`, `orientation`
(vertical/horizontal), `getItemKey`. El snippet expone `items`
(los `VirtualItem` visibles) + `scrollToIndex(index, { align, behavior })`
+ `totalSize`/`isEmpty`.
- Props eidos: `size`, `variant` (chrome del viewport; consume
`--radius-default`/`--ring-inset-width`).
## Comparativa
| Capacidad | UIX | TanStack Virtual | react-window | svelte-virtual |
| --- | --- | --- | --- | --- |
| Windowing 1D fijo + estimado | ✓ | ✓ | Fijo/variable | ✓ |
| Overscan configurable | ✓ | ✓ | ✓ | ✓ |
| `scrollToIndex` con align/behavior | ✓ | ✓ | ✓ | Parcial |
| Window-as-scroller | ✓ (parte propia) | ✓ (`useWindowVirtualizer`) | No | No |
| Orientación horizontal | ✓ | ✓ | ✓ | No |
| Eventos perceptivos del scroll | ✓ | No | No | No |
| Dependencia externa | **Ninguna** | Dependencia | Dependencia | Dependencia |
Referencias: [TanStack Virtual](https://tanstack.com/virtual) ·
[react-window](https://github.com/bvaughn/react-window).
## Decisiones
- **`apg: 'none — rationale'`** (checkpoint C5): la virtualización es
ESTRATEGIA DE RENDERIZADO, no widget — el patrón ARIA lo aporta el
contenido compuesto (un listbox virtualizado cita listbox). Los gemelos
quedaron igualados en el pase (la asimetría de la ficha, resuelta).
- **Tres eventos con tres familias — la disección documentada in-place**:
- `handle-scroll`: el scroll del USUARIO es manipulación directa del
viewport (cap. 25; `scroll` extiende el canon literal de verbos per
cap. 8 §1). Estaba mal clasificado como shift.
- `shift-navigate-to-index`: `scrollToIndex` imperativo — el SISTEMA
mueve el viewport ("estoy en otro lugar", cap. 27 §5). No es handle:
no hay control directo del usuario.
- `commit-set-resize`: `count` cambió y el totalSize se recalculó — "algo
queda aplicado" (cap. 23). Hook sema útil para cues de "hay elementos
nuevos".
- **`expression: 'family-default'`**: señales de infraestructura — sin
carácter que añadir.
- **D13 conforme (C7)**: las mediciones iniciales del viewport van
diferidas vía `dom.measure` del contexto (ambos viewports); cero
write-then-read síncrono.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Sticky items (cabeceras de sección pegajosas dentro del windowing) | **diferir** | Sin caso real; TanStack lo cubre con `rangeExtractor` — evaluar si llega la demanda. |
| Scroll restoration entre montajes | **diferir** | El consumidor puede guardar/restaurar índice con `scrollToIndex`; API dedicada espera caso. |
| Medición dinámica post-render (re-measure de items ya pintados) | **diferir** | `estimateSize` cubre el caso actual; re-measure automático = fase propia. |
| Tests del wrapper eidos (soma con suite ✓) | **diferir** | Pasada SYS-2. |
## Referencias
- Soma VirtualList: [`src/uix/soma/components/virtual-list/README.md`](../../../soma/components/virtual-list/README.md)
- Gemelo 2D: [`../virtual-grid/README.md`](../virtual-grid/README.md)
- Ficha de auditoría: [`docs/audit/components/virtual-list.md`](../../../../../docs/audit/components/virtual-list.md)
Loading…
Cancel
Save

Powered by TurnKey Linux.