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
parent
270cc3d764
commit
20e2e7a1f0
@ -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,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,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,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,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…
Reference in new issue