feat(table): la rejilla se vuelve temable — 0 % → 86 %

Segundo del bloque F2-A. 46 knobs sin una sola entrada de contrato; ahora 45
claves públicas.

  censo    0 % → 86 % · global 39 % → 40 %
  computed 13.050 valores en 7 estados: **0 diffs**

**Una trampa esquivada de milagro**: `table` YA tenía bloque en `base.ts` — los
forwards de paleta THM-2. Insertar uno nuevo habría creado un segundo bloque
`table:` y el catálogo lo habría descartado EN SILENCIO (gana el último). Las
claves se fusionan en el existente; el guard de duplicadas lo confirma.

**Dos nombres corregidos, como decía el veredicto**: lo que el privado llamaba
`root-height-{k}` es la altura de la FILA (`row-height-{k}`), y el prefijo
`root-` desaparece del resto — el envoltorio ES el componente y
recipe-contract §1 no repite la parte en ese caso.

**Qué casa con el bundle y qué no**, medido eje a eje: la altura de fila sí
(1:1); el padding NO (la rejilla empaqueta más denso en xs y más suelto de md
en adelante — forzar el bundle cambiaría el default); y la tipografía casa
DESPLAZADA: de md en adelante la tabla tipa un paso por debajo
(`font-size-md` = `var(--size-sm-font-size)`), que es deliberado y ahora se lee
en el propio nombre de la coordenada en vez de esconderse en un primitivo.

**Dos hallazgos del arquetipo `item`, y ninguno es mío** — la fila lo lleva, y
ese velo de `archetypes.css` está a especificidad PLENA (0,5,0), a propósito:

1. **La prop `hoverable` no suprime nada.** Una fila se ilumina al pasar el
   ratón aunque el atributo no esté — medido con hover real, y ocurría ANTES de
   tocar esta receta. El arquetipo y la prop no se ponen de acuerdo sobre quién
   decide: es decisión de morfo, se presenta, no se toca de oficio.
2. **`selected-row-fg` era un token que mentía.** La misma regla fija el
   `color` de la fila seleccionada y gana a la receta (0,5,0 contra 0,3,0), así
   que el token no podía mover nada nunca. Lo cazó el centinela; retirado con su
   declaración muerta. La tinta de la fila seleccionada es del arquetipo.

Y una corrección de rumbo a mitad de camino: al pasar todo a `background-color`
el velo apareció en 3 sort-triggers (su velo sí va en `:where()`, 0,0,0). Eso
es el barrido sistemático del velo, que NO está firmado — así que en los nodos
velados se mantiene el atajo a propósito, con el porqué escrito en la línea.
Primero lo diagnostiqué como regresión mía; la medición lo corrigió.

Los 6 knobs que quedan: 3 privados de paleta (patrón THM-2, capa compartida) y
3 literales de layout/identidad (`inline-size: 100%` ×2, `opacity: 1`).

El test `horizontal-escape` afirmaba el nombre del privado; ahora afirma el
público, que es justo lo que este eje persigue.

  audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados ·
  suite: sólo el rojo conocido `skin-media-player`

Demo con tab Tokens: 46 filas, 0 sin computar. README con «Talla y tema» y los
dos hallazgos del arquetipo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent b43e7e2ec0
commit 564133b485

@ -10,9 +10,9 @@
- **Medido**: 2026-08-20 · **162 recetas** con CSS + **8 componentes sin receta** = 170 fichas, el árbol entero de `eidos/components/`
- **La pregunta**: ¿cuánto de la apariencia de cada componente puede cambiar un tema **sin tocar el sistema ni la receta**?
- **Alcance global**: **39%** — 1885 de 4861 knobs pasan por un token público del componente
- **Reparto**: público 1885 · privado 780 · global 1598 · literal 598 · sistema transversal 338 _(fuera del ratio)_
- **Sin token público propio**: 55 · **alcance < 20 %**: 50 · **alcance 100 %**: 7 · **con eje `size`**: 78
- **Alcance global**: **40%** — 1924 de 4863 knobs pasan por un token público del componente
- **Reparto**: público 1924 · privado 759 · global 1584 · literal 596 · sistema transversal 337 _(fuera del ratio)_
- **Sin token público propio**: 54 · **alcance < 20 %**: 49 · **alcance 100 %**: 7 · **con eje `size`**: 77
## Cómo se lee
@ -82,21 +82,21 @@ theming §6.7 lo pone delante (`hover-bg`). Las fichas proponen la forma
| 10 | [time-picker](./time-picker.md) | 47 | 18% | 59 | 16 |
| 11 | [media-player](./media-player.md) | 45 | 0% | 45 | 2 |
| 12 | [color-picker](./color-picker.md) | 43 | 46% | 82 | 54 |
| 13 | [table](./table.md) | 42 | 0% | 46 | 0 |
| 14 | [field-langs](./field-langs.md) | 39 | 0% | 41 | 0 |
| 15 | [tree-grid](./tree-grid.md) | 39 | 0% | 42 | 0 |
| 16 | [gradient-picker](./gradient-picker.md) | 35 | 0% | 38 | 0 |
| 17 | [listbox](./listbox.md) | 35 | 0% | 38 | 0 |
| 18 | [field](./field.md) | 33 | 51% | 73 | 60 |
| 19 | [feed](./feed.md) | 32 | 0% | 33 | 0 |
| 20 | [carousel](./carousel.md) | 31 | 0% | 36 | 0 |
| 13 | [field-langs](./field-langs.md) | 39 | 0% | 41 | 0 |
| 14 | [tree-grid](./tree-grid.md) | 39 | 0% | 42 | 0 |
| 15 | [gradient-picker](./gradient-picker.md) | 35 | 0% | 38 | 0 |
| 16 | [listbox](./listbox.md) | 35 | 0% | 38 | 0 |
| 17 | [field](./field.md) | 33 | 51% | 73 | 60 |
| 18 | [feed](./feed.md) | 32 | 0% | 33 | 0 |
| 19 | [carousel](./carousel.md) | 31 | 0% | 36 | 0 |
| 20 | [date-range-picker](./date-range-picker.md) | 30 | 40% | 53 | 22 |
## Sin ningún token público propio (55)
## Sin ningún token público propio (54)
Ni una clave pública en `lib/recipes/base.ts` — alguno tiene privados forward
(`_palette-*`), que no son contrato: un tema no puede nombrarlos.
[`alert-dialog`](./alert-dialog.md) · [`announce`](./announce.md) · [`aspect-ratio`](./aspect-ratio.md) · [`auto-grid`](./auto-grid.md) · [`avatar`](./avatar.md) · [`card-group`](./card-group.md) · [`carousel`](./carousel.md) · [`cascade`](./cascade.md) · [`clipboard`](./clipboard.md) · [`code`](./code.md) · [`code-block`](./code-block.md) · [`css-field`](./css-field.md) · [`date-picker`](./date-picker.md) · [`display`](./display.md) · [`feed`](./feed.md) · [`field-langs`](./field-langs.md) · [`gradient-picker`](./gradient-picker.md) · [`grid-list`](./grid-list.md) · [`heading`](./heading.md) · [`highlight`](./highlight.md) · [`kbd`](./kbd.md) · [`knob`](./knob.md) · [`label`](./label.md) · [`link`](./link.md) · [`link-preview`](./link-preview.md) · [`listbox`](./listbox.md) · [`mark`](./mark.md) · [`mask-field`](./mask-field.md) · [`menu-dial`](./menu-dial.md) · [`mockup`](./mockup.md) · [`month-grid`](./month-grid.md) · [`motion`](./motion.md) · [`number-field`](./number-field.md) · [`palabras`](./palabras.md) · [`picker-shell`](./picker-shell.md) · [`range-calendar`](./range-calendar.md) · [`scroll-frames`](./scroll-frames.md) · [`skeleton`](./skeleton.md) · [`skin-media-player`](./skin-media-player.md) · [`spinner`](./spinner.md) · [`stack`](./stack.md) · [`table`](./table.md) · [`text`](./text.md) · [`text-blur`](./text-blur.md) · [`text-circular`](./text-circular.md) · [`text-focus`](./text-focus.md) · [`text-gradient`](./text-gradient.md) · [`text-scramble`](./text-scramble.md) · [`textarea`](./textarea.md) · [`tree-grid`](./tree-grid.md) · [`tree-view`](./tree-view.md) · [`virtual-grid`](./virtual-grid.md) · [`virtual-list`](./virtual-list.md) · [`wrap`](./wrap.md) · [`year-grid`](./year-grid.md)
[`alert-dialog`](./alert-dialog.md) · [`announce`](./announce.md) · [`aspect-ratio`](./aspect-ratio.md) · [`auto-grid`](./auto-grid.md) · [`avatar`](./avatar.md) · [`card-group`](./card-group.md) · [`carousel`](./carousel.md) · [`cascade`](./cascade.md) · [`clipboard`](./clipboard.md) · [`code`](./code.md) · [`code-block`](./code-block.md) · [`css-field`](./css-field.md) · [`date-picker`](./date-picker.md) · [`display`](./display.md) · [`feed`](./feed.md) · [`field-langs`](./field-langs.md) · [`gradient-picker`](./gradient-picker.md) · [`grid-list`](./grid-list.md) · [`heading`](./heading.md) · [`highlight`](./highlight.md) · [`kbd`](./kbd.md) · [`knob`](./knob.md) · [`label`](./label.md) · [`link`](./link.md) · [`link-preview`](./link-preview.md) · [`listbox`](./listbox.md) · [`mark`](./mark.md) · [`mask-field`](./mask-field.md) · [`menu-dial`](./menu-dial.md) · [`mockup`](./mockup.md) · [`month-grid`](./month-grid.md) · [`motion`](./motion.md) · [`number-field`](./number-field.md) · [`palabras`](./palabras.md) · [`picker-shell`](./picker-shell.md) · [`range-calendar`](./range-calendar.md) · [`scroll-frames`](./scroll-frames.md) · [`skeleton`](./skeleton.md) · [`skin-media-player`](./skin-media-player.md) · [`spinner`](./spinner.md) · [`stack`](./stack.md) · [`text`](./text.md) · [`text-blur`](./text-blur.md) · [`text-circular`](./text-circular.md) · [`text-focus`](./text-focus.md) · [`text-gradient`](./text-gradient.md) · [`text-scramble`](./text-scramble.md) · [`textarea`](./textarea.md) · [`tree-grid`](./tree-grid.md) · [`tree-view`](./tree-view.md) · [`virtual-grid`](./virtual-grid.md) · [`virtual-list`](./virtual-list.md) · [`wrap`](./wrap.md) · [`year-grid`](./year-grid.md)
## Tabla completa (162 recetas, por alcance ascendente)
@ -107,7 +107,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [range-calendar](./range-calendar.md) | 0% | 98 | 0 | 7 | 84 | 4 | 3 | 0 | y |
| [month-grid](./month-grid.md) | 0% | 61 | 0 | 13 | 40 | 5 | 3 | 0 | y |
| [year-grid](./year-grid.md) | 0% | 61 | 0 | 13 | 40 | 5 | 3 | 0 | y |
| [table](./table.md) | 0% | 46 | 0 | 23 | 14 | 5 | 4 | 0 | y |
| [media-player](./media-player.md) | 0% | 45 | 0 | 0 | 37 | 8 | 0 | 2 | y |
| [tree-grid](./tree-grid.md) | 0% | 42 | 0 | 29 | 8 | 2 | 3 | 0 | y |
| [field-langs](./field-langs.md) | 0% | 41 | 0 | 0 | 31 | 8 | 2 | 0 | – |
@ -238,12 +237,13 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [fab](./fab.md) | 82% | 17 | 14 | 0 | 2 | 1 | 0 | 10 | – |
| [toast](./toast.md) | 83% | 42 | 35 | 6 | 0 | 1 | 0 | 85 | – |
| [text-circular](./text-circular.md) | 83% | 6 | 5 | 0 | 1 | 0 | 0 | 0 | – |
| [table](./table.md) | 86% | 47 | 38 | 3 | 0 | 3 | 3 | 46 | – |
| [tabs](./tabs.md) | 88% | 76 | 66 | 6 | 2 | 1 | 1 | 79 | y |
| [chat-typing](./chat-typing.md) | 89% | 9 | 8 | 0 | 1 | 0 | 0 | 7 | – |
| [breadcrumb](./breadcrumb.md) | 90% | 23 | 18 | 2 | 0 | 0 | 3 | 28 | y |
| [collapsible](./collapsible.md) | 90% | 12 | 9 | 0 | 1 | 0 | 2 | 11 | – |
| [scroll-area](./scroll-area.md) | 94% | 17 | 15 | 0 | 0 | 1 | 1 | 13 | – |
| [command](./command.md) | 96% | 52 | 48 | 1 | 0 | 1 | 2 | 57 | – |
| [command](./command.md) | 98% | 52 | 49 | 0 | 0 | 1 | 2 | 57 | – |
| [heading](./heading.md) | — | 36 | 0 | 0 | 0 | 0 | 36 | 0 | – |
| [s-text](./s-text.md) | 100% | 30 | 2 | 0 | 0 | 0 | 28 | 3 | – |
| [text](./text.md) | — | 24 | 0 | 0 | 0 | 0 | 24 | 0 | – |

@ -5,8 +5,8 @@
> Vista de conjunto: [README](./README.md) · método y protocolo:
> [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7.
- **Medido**: 2026-08-20 · **Alcance**: **96%** — 48 de 50 knobs por token público
- **Knobs de apariencia**: 52 — público 48 · privado 1 · global 0 · literal 1 · sistema 2 _(fuera del ratio)_
- **Medido**: 2026-08-20 · **Alcance**: **98%** — 49 de 50 knobs por token público
- **Knobs de apariencia**: 52 — público 49 · privado 0 · global 0 · literal 1 · sistema 2 _(fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 57 pública(s) — `input-padding-inline-sm`, `input-padding-inline-md`, `input-padding-inline-lg`, `input-font-size-sm`, `input-font-size-md`, `input-font-size-lg`, `item-padding-inline-sm`, `item-padding-inline-md`, `item-padding-inline-lg`, `item-font-size-sm`, `item-font-size-md`, `item-font-size-lg`, `input-padding-inline`, `input-font-size`, `item-padding-inline`, `item-font-size`, `radius`, `font-family`, `line-height`, `fg`, `input-padding-block`, `input-margin`, `input-border-width`, `input-border`, `input-radius`, `input-bg`, `input-fg`, `input-placeholder-fg`, `focus-input-border`, `loading-height`, `loading-fg`, `viewport-max-block`, `viewport-gap`, `viewport-padding-inline`, `viewport-padding-block-end`, `scrollbar-inset`, `viewport-padding`, `group-gap`, `group-heading-padding-block`, `group-heading-fg`, `group-heading-font-size`, `group-heading-font-weight`, `group-heading-tracking`, `group-items-gap`, `item-gap`, `item-fg`, `item-icon-size`, `item-icon-fg`, `item-shortcut-gap`, `item-shortcut-fg`, `item-shortcut-font-size`, `selected-item-sub-fg`, `empty-padding-block`, `empty-fg`, `separator-height`, `separator-margin-block`, `separator-bg`
- **Eje `size`**: no · **ficheros**: `command.css`
@ -16,17 +16,15 @@
_Ninguno._
### 1.2 A través de un privado (1)
### 1.2 A través de un privado (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `command.css:57` | `[data-command]` | `border-radius` | `var(--_command-radius)` |
_Ninguno._
### 1.3 Literales (1)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `command.css:42` | `[data-command]` | `inline-size` | `100%` |
| 1 | `command.css:41` | `[data-command]` | `inline-size` | `100%` |
## 2. Sistema transversal (2) — informativo, fuera del ratio
@ -34,14 +32,12 @@ Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `command.css:129` | `[data-command-input]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
| 2 | `command.css:240` | `[data-command-item][data-disabled], [data-command-link-item][data-disabled]` | `opacity` | `var(--opacity-disabled)` |
| 1 | `command.css:128` | `[data-command-input]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
| 2 | `command.css:239` | `[data-command-item][data-disabled], [data-command-link-item][data-disabled]` | `opacity` | `var(--opacity-disabled)` |
## 3. Privados de la receta — ¿de dónde sale su valor?
| privado | declaraciones | valor(es) | origen | ¿deriva de un público? |
| --- | ---: | --- | --- | :-: |
| `--_command-radius` | 2 | `var(--command-radius)`, `var(--dialog-content-radius)` | global, public | no |
_La receta no declara privados propios en su CSS._
Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inline del wrapper): `--_command-loading-progress`.
@ -49,7 +45,7 @@ Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inl
- **Consume la capa compartida `list-surface`.** Un eje que la capa posee se consume como `var(--_x, var(--x))`; el consumidor **no acuña** `--command-{eje}` para él — sería un vocabulario paralelo (README de `eidos/components`, «Capas compartidas» regla 2).
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (1)
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (0)
Valor **verbatim** del CSS de hoy: el default no se mueve, sólo cambia quién
puede moverlo. Nombres derivados de recipe-contract §1 (ejes lógicos, talla
@ -59,16 +55,11 @@ no distingue lo que debería — se marca `⚠`.
| token (`--command-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `radius` _(ya existe)_ | `root` | ⚠ `var(--command-radius)` / `var(--dialog-content-radius)` | 2 |
### 4.2 Sin nombre mecánico (1)
- **⚠ decisión: `100%` es un valor identidad o geometría de layout, no un knob de tema — el perímetro de «knob» es D-TH.2, sin firmar** — 1: `inline-size`.
### 4.3 Avisos sobre los tokens propuestos (1)
- **el privado `--_command-radius` debe pasar a leer este público (o desaparecer)** — `--command-radius`
### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4)
- [ ] **Privado que no deriva de un público** — §3 lo marca; el privado debe leer el público o desaparecer.

@ -5,102 +5,52 @@
> Vista de conjunto: [README](./README.md) · método y protocolo:
> [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7.
- **Medido**: 2026-08-20 · **Alcance**: **0%** — 0 de 42 knobs por token público
- **Knobs de apariencia**: 46 — público 0 · privado 23 · global 14 · literal 5 · sistema 4 _(fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 0 pública(s) · 2 privada(s) forward — `_palette-solid`, `_palette-element`
- **Eje `size`**: sí · **ficheros**: `table.css`
- **Medido**: 2026-08-20 · **Alcance**: **86%** — 38 de 44 knobs por token público
- **Knobs de apariencia**: 47 — público 38 · privado 3 · global 0 · literal 3 · sistema 3 _(fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 46 pública(s) — `row-height-xs`, `row-height-sm`, `row-height-md`, `row-height-lg`, `row-height-xl`, `cell-padding-inline-xs`, `cell-padding-inline-sm`, `cell-padding-inline-md`, `cell-padding-inline-lg`, `cell-padding-inline-xl`, `cell-padding-block-xs`, `cell-padding-block-sm`, `cell-padding-block-md`, `cell-padding-block-lg`, `cell-padding-block-xl`, `font-size-xs`, `font-size-sm`, `font-size-md`, `font-size-lg`, `font-size-xl`, `row-height`, `cell-padding-inline`, `cell-padding-block`, `font-size`, `radius`, `font-family`, `fg`, `line-height`, `border-width`, `divider`, `surface-bg`, `row-bg`, `header-bg`, `header-fg`, `header-font-weight`, `header-z`, `striped-row-bg`, `hover-row-bg`, `selected-row-fg`, `row-detail-bg`, `row-detail-fg`, `row-detail-padding-block`, `sort-trigger-gap`, `sort-trigger-radius`, `sort-indicator-size`, `sort-indicator-opacity` · 2 privada(s) forward — `_palette-solid`, `_palette-element`
- **Eje `size`**: no · **ficheros**: `table.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (14)
### 1.1 Directo a primitivo global (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `table.css:43` | `[data-table-root]` | `font-family` | `var(--style-label-font-family)` |
| 2 | `table.css:44` | `[data-table-root]` | `color` | `var(--color-content-primary)` |
| 3 | `table.css:71` | `[data-table-root][data-variant='surface']` | `background` | `var(--color-surface-default)` |
| 4 | `table.css:80` | `[data-table-root][data-variant='ghost']` | `border` | `var(--border-width) solid transparent` |
| 5 | `table.css:154` | `[data-table]` | `line-height` | `var(--leading-ui)` |
| 6 | `table.css:183` | `[data-table-column-header]` | `font-weight` | `var(--font-weight-semibold, 600)` |
| 7 | `table.css:184` | `[data-table-column-header]` | `color` | `var(--color-content-primary)` |
| 8 | `table.css:205` | `[data-table-body] [data-table-row]` | `background` | `var(--color-surface-default)` |
| 9 | `table.css:230` | `[data-table-body] [data-table-row][data-selected]` | `color` | `var(--color-content-primary)` |
| 10 | `table.css:273` | `[data-table-footer] [data-table-cell]` | `font-weight` | `var(--font-weight-semibold, 600)` |
| 11 | `table.css:280` | `[data-table-row-detail]` | `background` | `var(--color-surface-muted)` |
| 12 | `table.css:290` | `[data-table-row-detail] > td` | `color` | `var(--color-content-secondary)` |
| 13 | `table.css:297` | `[data-table-sort-trigger]` | `gap` | `var(--space-1)` |
| 14 | `table.css:306` | `[data-table-sort-trigger]` | `border-radius` | `var(--radius-sm)` |
### 1.2 A través de un privado (23)
_Ninguno._
### 1.2 A través de un privado (3)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `table.css:45` | `[data-table-root]` | `border-radius` | `var(--_table-radius)` |
| 2 | `table.css:70` | `[data-table-root][data-variant='surface']` | `border` | `var(--border-width) solid var(--_table-divider)` |
| 3 | `table.css:75` | `[data-table-root][data-variant='outline']` | `border` | `var(--border-width) solid var(--_table-divider)` |
| 4 | `table.css:137` | `[data-table-root][data-stable-rows]` | `min-block-size` | `calc( (var(--_table-row-block-size) + var(--_table-cell-padding-block) * 2 + 1px) * (var(--_table-stable-rows, 0) + 1) )` |
| 5 | `table.css:153` | `[data-table]` | `font-size` | `var(--_table-font-size)` |
| 6 | `table.css:161` | `[data-table-header]` | `background` | `var(--_table-header-bg)` |
| 7 | `table.css:174` | `[data-table-root][data-sticky-header] [data-table-column-header]` | `background` | `var(--_table-header-bg)` |
| 8 | `table.css:180` | `[data-table-column-header]` | `padding-block` | `var(--_table-cell-padding-block)` |
| 9 | `table.css:181` | `[data-table-column-header]` | `padding-inline` | `var(--_table-cell-padding-inline)` |
| 10 | `table.css:185` | `[data-table-column-header]` | `border-block-end` | `var(--border-width) solid var(--_table-divider)` |
| 11 | `table.css:196` | `[data-table-column-header][data-sorted]` | `color` | `var(--_table-palette-solid)` |
| 12 | `table.css:200` | `[data-table-column-header][data-pinned]` | `background` | `var(--_table-header-bg)` |
| 13 | `table.css:217` | `[data-table-root][data-striped]:not([data-variant='ghost']) [data-table-body] [data-table-row]:nth-of-type(even)` | `background` | `var(--_table-row-striped-bg)` |
| 14 | `table.css:224` | `[data-table-root][data-hoverable] [data-table-body] [data-table-row]:not([data-selected]):hover` | `background` | `var(--_table-row-hover-bg)` |
| 15 | `table.css:229` | `[data-table-body] [data-table-row][data-selected]` | `background` | `var(--_table-palette-element)` |
| 16 | `table.css:251` | `[data-table-cell]` | `padding-block` | `var(--_table-cell-padding-block)` |
| 17 | `table.css:252` | `[data-table-cell]` | `padding-inline` | `var(--_table-cell-padding-inline)` |
| 18 | `table.css:253` | `[data-table-cell]` | `border-block-end` | `var(--border-width) solid var(--_table-divider)` |
| 19 | `table.css:269` | `[data-table-footer]` | `background` | `var(--_table-header-bg)` |
| 20 | `table.css:275` | `[data-table-footer] [data-table-cell]` | `border-block-start` | `var(--border-width) solid var(--_table-divider)` |
| 21 | `table.css:288` | `[data-table-row-detail] > td` | `padding` | `var(--space-3) var(--_table-cell-padding-inline)` |
| 22 | `table.css:289` | `[data-table-row-detail] > td` | `border-block-end` | `var(--border-width) solid var(--_table-divider)` |
| 23 | `table.css:315` | `[data-table-sort-trigger][data-sorted]` | `color` | `var(--_table-palette-solid)` |
### 1.3 Literales (5)
| 1 | `table.css:167` | `[data-table-column-header][data-sorted]` | `color` | `var(--_table-palette-solid)` |
| 2 | `table.css:208` | `[data-table-body] [data-table-row][data-selected]` | `background` | `var(--_table-palette-element)` |
| 3 | `table.css:300` | `[data-table-sort-trigger][data-sorted]` | `color` | `var(--_table-palette-solid)` |
### 1.3 Literales (3)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `table.css:65` | `[data-table-root][data-block]` | `inline-size` | `100%` |
| 2 | `table.css:150` | `[data-table]` | `inline-size` | `100%` |
| 3 | `table.css:322` | `[data-table-sort-indicator]` | `inline-size` | `1em` |
| 4 | `table.css:323` | `[data-table-sort-indicator]` | `block-size` | `1em` |
| 5 | `table.css:329` | `[data-table-sort-trigger][data-sorted] [data-table-sort-indicator]` | `opacity` | `1` |
| 1 | `table.css:55` | `[data-table-root][data-block]` | `inline-size` | `100%` |
| 2 | `table.css:121` | `[data-table]` | `inline-size` | `100%` |
| 3 | `table.css:314` | `[data-table-sort-trigger][data-sorted] [data-table-sort-indicator]` | `opacity` | `1` |
## 2. Sistema transversal (4) — informativo, fuera del ratio
## 2. Sistema transversal (3) — informativo, fuera del ratio
Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `table.css:144` | `[data-table-root][data-disabled]` | `opacity` | `var(--opacity-disabled)` |
| 2 | `table.css:244` | `[data-table-row]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
| 3 | `table.css:310` | `[data-table-sort-trigger]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
| 4 | `table.css:324` | `[data-table-sort-indicator]` | `opacity` | `var(--opacity-disabled)` |
| 1 | `table.css:115` | `[data-table-root][data-disabled]` | `opacity` | `var(--opacity-disabled)` |
| 2 | `table.css:223` | `[data-table-row]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
| 3 | `table.css:295` | `[data-table-sort-trigger]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
## 3. Privados de la receta — ¿de dónde sale su valor?
| privado | declaraciones | valor(es) | origen | ¿deriva de un público? |
| --- | ---: | --- | --- | :-: |
| `--_table-radius` | 1 | `var(--radius-md)` | global | no |
| `--_table-row-block-size` | 5 | `var(--control-height-md)`, `var(--control-height-xs)`, `var(--control-height-sm)`, `var(--control-height-lg)`, `var(--control-height-xl)` | global | no |
| `--_table-cell-padding-inline` | 5 | `var(--space-3)`, `var(--space-2)`, `var(--space-4)`, `var(--space-5)` | global | no |
| `--_table-cell-padding-block` | 5 | `var(--space-2)`, `var(--space-1)`, `var(--space-3)` | global | no |
| `--_table-font-size` | 5 | `var(--font-size-sm)`, `var(--font-size-xs)`, `var(--font-size-md)`, `var(--font-size-lg)` | global | no |
| `--_table-header-bg` | 1 | `var(--color-surface-muted)` | global | no |
| `--_table-row-hover-bg` | 1 | `var(--color-surface-overlay)` | global | no |
| `--_table-row-striped-bg` | 1 | `var(--color-surface-muted)` | global | no |
| `--_table-divider` | 1 | `var(--color-border-default)` | global | no |
_La receta no declara privados propios en su CSS._
Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inline del wrapper): `--_table-palette-element`, `--_table-palette-solid`, `--_table-stable-rows`.
## 4. Propuesta de corrección
- **Tiene eje `size`**: los tokens dimensionales van por talla (`{part}-{eje}-{k}`) apuntando al bundle `--size-{k}-*`, nunca al primitivo crudo (theming §5; el guard `recipe-css-contract` prohíbe el primitivo).
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (51)
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (0)
Valor **verbatim** del CSS de hoy: el default no se mueve, sólo cambia quién
puede moverlo. Nombres derivados de recipe-contract §1 (ejes lógicos, talla
@ -110,79 +60,13 @@ no distingue lo que debería — se marca `⚠`.
| token (`--table-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `root-bg` | `root` | `var(--color-surface-muted)` | 2 |
| `root-font-family` | `root` | `var(--style-label-font-family)` | 1 |
| `root-fg` | `root` | `var(--color-content-primary)` | 1 |
| `root-radius` | `root` | `var(--radius-md)` | 1 |
| `surface-root-bg` | `root` | `var(--color-surface-default)` | 1 |
| `root-height-md` | `size:md` | `var(--control-height-md)` | 1 |
| `root-height-xs` | `size:xs` | `var(--control-height-xs)` | 1 |
| `root-height-sm` | `size:sm` | `var(--control-height-sm)` | 1 |
| `root-height-lg` | `size:lg` | `var(--control-height-lg)` | 1 |
| `root-height-xl` | `size:xl` | `var(--control-height-xl)` | 1 |
| `font-size-md` | `size:md` | `var(--font-size-sm)` | 1 |
| `font-size-xs` | `size:xs` | `var(--font-size-xs)` | 1 |
| `font-size-sm` | `size:sm` | `var(--font-size-sm)` | 1 |
| `font-size-lg` | `size:lg` | `var(--font-size-md)` | 1 |
| `font-size-xl` | `size:xl` | `var(--font-size-lg)` | 1 |
| `line-height` | `root` | `var(--leading-ui)` | 1 |
| `header-bg` | `root` | `var(--color-surface-muted)` | 1 |
| `column-header-padding-block-md` | `size:md` | `var(--space-2)` | 1 |
| `column-header-padding-block-xs` | `size:xs` | `var(--space-1)` | 1 |
| `column-header-padding-block-sm` | `size:sm` | `var(--space-1)` | 1 |
| `column-header-padding-block-lg` | `size:lg` | `var(--space-3)` | 1 |
| `column-header-padding-block-xl` | `size:xl` | `var(--space-3)` | 1 |
| `column-header-padding-inline-md` | `size:md` | `var(--space-3)` | 1 |
| `column-header-padding-inline-xs` | `size:xs` | `var(--space-2)` | 1 |
| `column-header-padding-inline-sm` | `size:sm` | `var(--space-2)` | 1 |
| `column-header-padding-inline-lg` | `size:lg` | `var(--space-4)` | 1 |
| `column-header-padding-inline-xl` | `size:xl` | `var(--space-5)` | 1 |
| `column-header-font-weight` | `root` | `var(--font-weight-semibold, 600)` | 1 |
| `column-header-fg` | `root` | `var(--color-content-primary)` | 1 |
| `column-header-bg` | `root` | `var(--color-surface-muted)` | 1 |
| `body-bg` | `root` | `var(--color-surface-default)` | 1 |
| `hover-root-bg` | `root` | `var(--color-surface-overlay)` | 1 |
| `selected-body-fg` | `root` | `var(--color-content-primary)` | 1 |
| `cell-padding-block-md` | `size:md` | `var(--space-2)` | 1 |
| `cell-padding-block-xs` | `size:xs` | `var(--space-1)` | 1 |
| `cell-padding-block-sm` | `size:sm` | `var(--space-1)` | 1 |
| `cell-padding-block-lg` | `size:lg` | `var(--space-3)` | 1 |
| `cell-padding-block-xl` | `size:xl` | `var(--space-3)` | 1 |
| `cell-padding-inline-md` | `size:md` | `var(--space-3)` | 1 |
| `cell-padding-inline-xs` | `size:xs` | `var(--space-2)` | 1 |
| `cell-padding-inline-sm` | `size:sm` | `var(--space-2)` | 1 |
| `cell-padding-inline-lg` | `size:lg` | `var(--space-4)` | 1 |
| `cell-padding-inline-xl` | `size:xl` | `var(--space-5)` | 1 |
| `footer-bg` | `root` | `var(--color-surface-muted)` | 1 |
| `footer-font-weight` | `root` | `var(--font-weight-semibold, 600)` | 1 |
| `row-detail-bg` | `root` | `var(--color-surface-muted)` | 1 |
| `row-detail-fg` | `root` | `var(--color-content-secondary)` | 1 |
| `sort-trigger-gap` | `root` | `var(--space-1)` | 1 |
| `sort-trigger-radius` | `root` | `var(--radius-sm)` | 1 |
| `sort-indicator-width` | `root` | `1em` | 1 |
| `sort-indicator-height` | `root` | `1em` | 1 |
### 4.2 Sin nombre mecánico (14)
### 4.2 Sin nombre mecánico (6)
- **⚠ decisión: `100%` es un valor identidad o geometría de layout, no un knob de tema — el perímetro de «knob» es D-TH.2, sin firmar** — 2: `inline-size`.
- **⚠ decisión: `border` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 3: `border`.
- **⚠ decisión: `border-block-end` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 3: `border-block-end`.
- **⚠ decisión: el privado que alimenta este knob no se declara en el CSS (viene de `base.ts` o de un estilo inline) — hay que resolverlo antes de nombrarlo** — 3: `color`, `background`.
- **⚠ decisión: `border-block-start` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 1: `border-block-start`.
- **⚠ decisión: `padding` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 1: `padding`.
- **⚠ decisión: `1` es un valor identidad o geometría de layout, no un knob de tema — el perímetro de «knob» es D-TH.2, sin firmar** — 1: `opacity`.
### 4.3 Avisos sobre los tokens propuestos (8)
- **el privado `--_table-radius` debe pasar a leer este público (o desaparecer)** — `--table-root-radius`
- **el privado `--_table-row-block-size` debe pasar a leer este público (o desaparecer)** — `--table-root-height-md`, `--table-root-height-xs`, `--table-root-height-sm`, `--table-root-height-lg`, `--table-root-height-xl`
- **el privado `--_table-font-size` debe pasar a leer este público (o desaparecer)** — `--table-font-size-md`, `--table-font-size-xs`, `--table-font-size-sm`, `--table-font-size-lg`, `--table-font-size-xl`
- **el privado `--_table-header-bg` debe pasar a leer este público (o desaparecer)** — `--table-header-bg`, `--table-root-bg`, `--table-column-header-bg`, `--table-footer-bg`
- **el privado `--_table-cell-padding-block` debe pasar a leer este público (o desaparecer)** — `--table-column-header-padding-block-md`, `--table-column-header-padding-block-xs`, `--table-column-header-padding-block-sm`, `--table-column-header-padding-block-lg`, `--table-column-header-padding-block-xl`, `--table-cell-padding-block-md`, `--table-cell-padding-block-xs`, `--table-cell-padding-block-sm`, `--table-cell-padding-block-lg`, `--table-cell-padding-block-xl`
- **el privado `--_table-cell-padding-inline` debe pasar a leer este público (o desaparecer)** — `--table-column-header-padding-inline-md`, `--table-column-header-padding-inline-xs`, `--table-column-header-padding-inline-sm`, `--table-column-header-padding-inline-lg`, `--table-column-header-padding-inline-xl`, `--table-cell-padding-inline-md`, `--table-cell-padding-inline-xs`, `--table-cell-padding-inline-sm`, `--table-cell-padding-inline-lg`, `--table-cell-padding-inline-xl`
- **el privado `--_table-row-striped-bg` debe pasar a leer este público (o desaparecer)** — `--table-root-bg`
- **el privado `--_table-row-hover-bg` debe pasar a leer este público (o desaparecer)** — `--table-hover-root-bg`
### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4)
- [ ] **Privado que no deriva de un público** — §3 lo marca; el privado debe leer el público o desaparecer.

@ -97,6 +97,59 @@ Referencias: [APG Table](https://www.w3.org/WAI/ARIA/apg/patterns/table/) ·
de scroll del CONSUMIDOR; con `maxHeight` la tabla envuelve su propia
región. Dos modos explícitos, sin heurística.
## Talla y tema
Contrato en `lib/recipes/base.ts > table` (45 claves). Alcance del censo:
**86 %** (era 0 % antes del 2026-08-20).
**El eje `size`** (xs..xl) se estampa en el ENVOLTORIO `[data-table-root]`, no
en el `<table>`, así que los nombres resueltos declaran `parts: ['root']`
(precedente: `proof-of-human`). Coordenadas por talla en `root` + resuelto con
default `host`; el CSS no lleva bloques `[data-size]` propios — los emite el
TSC.
Qué casa con el bundle y qué no, medido:
| eje | bundle | por qué |
| --------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `row-height-{k}` | **sí**, 1:1 | es la altura de control, la del bundle |
| `cell-padding-{inline,block}-{k}` | **no**, verbatim | la tabla empaqueta más que una pila de controles: más denso en xs, más suelto de md en adelante. Forzar el bundle cambiaría el default |
| `font-size-{k}` | **sí, pero desplazado** | de md en adelante la tabla tipa UN PASO POR DEBAJO (`font-size-md` = `var(--size-sm-font-size)`): una rejilla se lee en bloque, no de un vistazo. El nombre de la coordenada deja esa desviación a la vista en vez de esconderla en un primitivo crudo |
**El nombre que estaba mal**: lo que el privado llamaba `root-height-{k}` es la
altura de la FILA, no del root — ahora `row-height-{k}`. Y el prefijo `root-`
desaparece del resto (`bg`, `radius`, `fg`): el envoltorio ES el componente, y
recipe-contract §1 no repite la parte cuando la parte es el componente.
**Los tres privados que quedan** son los forwards de paleta THM-2
(`--_table-palette-solid` / `-element`, capa compartida), que siguen privados
por diseño. `R-5.1 exception: inline-size: 100%` ×2 y `opacity: 1` — mecánica
de layout e identidad, no apariencia.
### El arquetipo y la prop `hoverable` no se ponen de acuerdo
Medido el 2026-08-20, y es **anterior a la tokenización**: la fila lleva
`archetype: 'item'`, y el velo de hover de `archetypes.css` para ese arquetipo
está a especificidad PLENA (0,5,0) — a propósito, según su propio comentario —
así que gana a la regla base de la fila, se escriba con atajo o con longhand. El
resultado es que **una fila se ilumina al pasar el ratón aunque `hoverable` esté
apagada**: la prop no suprime nada.
O el arquetipo decide (y `hoverable` sobra, o pasa a significar sólo el tinte
propio de la receta), o la fila no es un `item` para el sistema. Es una
decisión de morfo y se presenta; no se toca de oficio (PLAN-theming §7.3-9).
Lo mismo se llevó por delante un token: la regla del arquetipo fija también el
`color` de la fila seleccionada, a (0,5,0) contra los (0,3,0) de la receta, así
que un `selected-row-fg` no podría mover nada nunca. Lo cazó el centinela y se
retiró con su declaración — un token que no mueve nada es un token que miente.
La tinta de la fila seleccionada es del arquetipo.
Nota de cascada relacionada: el velo del `sort-trigger` sí va envuelto en
`:where()` (0,0,0), así que el `background: transparent` de la receta lo cancela
y el botón no tiene realimentación de hover. Dárselo es el barrido sistemático
del velo, no la tokenización de este componente.
## Gaps
| Gap | Disposición | Detalle |

@ -37,7 +37,9 @@ describe('Table horizontal escape', () => {
});
it('still clips for the corner radius', () => {
expect(ruleBody('[data-table-root]')).toContain('border-radius: var(--_table-radius)');
// `--table-radius`, public since the 2026-08-20 tokenization — it was the
// private `--_table-radius`, which no theme could name.
expect(ruleBody('[data-table-root]')).toContain('border-radius: var(--table-radius)');
});
it('takes BOTH axes when the consumer asked for a scroll region', () => {

@ -28,21 +28,11 @@
*/
[data-table-root] {
--_table-radius: var(--radius-md);
--_table-row-block-size: var(--control-height-md);
--_table-cell-padding-inline: var(--space-3);
--_table-cell-padding-block: var(--space-2);
--_table-font-size: var(--font-size-sm);
--_table-header-bg: var(--color-surface-muted);
--_table-row-hover-bg: var(--color-surface-overlay);
--_table-row-striped-bg: var(--color-surface-muted);
--_table-divider: var(--color-border-default);
position: relative;
display: block;
font-family: var(--style-label-font-family);
color: var(--color-content-primary);
border-radius: var(--_table-radius);
font-family: var(--table-font-family);
color: var(--table-fg);
border-radius: var(--table-radius);
/*
* `overflow-x: auto` — NOT `overflow: hidden`.
*
@ -67,45 +57,26 @@
/* ── Variant — surface (default) ─ */
[data-table-root][data-variant='surface'] {
border: var(--border-width) solid var(--_table-divider);
background: var(--color-surface-default);
border: var(--table-border-width) solid var(--table-divider);
background-color: var(--table-surface-bg);
}
[data-table-root][data-variant='outline'] {
border: var(--border-width) solid var(--_table-divider);
background: transparent;
border: var(--table-border-width) solid var(--table-divider);
background-color: transparent;
}
[data-table-root][data-variant='ghost'] {
border: var(--border-width) solid transparent;
background: transparent;
border: var(--table-border-width) solid transparent;
background-color: transparent;
}
/* ── Sizes ─ */
[data-table-root][data-size='xs'] {
--_table-row-block-size: var(--control-height-xs);
--_table-cell-padding-inline: var(--space-2);
--_table-cell-padding-block: var(--space-1);
--_table-font-size: var(--font-size-xs);
}
[data-table-root][data-size='sm'] {
--_table-row-block-size: var(--control-height-sm);
--_table-cell-padding-inline: var(--space-2);
--_table-cell-padding-block: var(--space-1);
--_table-font-size: var(--font-size-sm);
}
[data-table-root][data-size='lg'] {
--_table-row-block-size: var(--control-height-lg);
--_table-cell-padding-inline: var(--space-4);
--_table-cell-padding-block: var(--space-3);
--_table-font-size: var(--font-size-md);
}
[data-table-root][data-size='xl'] {
--_table-row-block-size: var(--control-height-xl);
--_table-cell-padding-inline: var(--space-5);
--_table-cell-padding-block: var(--space-3);
--_table-font-size: var(--font-size-lg);
}
/* ── Sizes ─
The per-size cascade is emitted by the TSC from `recipes/base.ts`
(`declarations` with `scope: 'size:{k}'`, targeting `parts: ['root']` because
`data-size` is stamped on the wrapper). Writing the blocks here too would
keep painting the OLD values over the new tokens, and the computed diff would
read 0 because the old path still won. */
/*
* Scrolling region — only when consumer sets `maxHeight`. Pure CSS
@ -125,18 +96,18 @@
* wrapper is already capped by `maxHeight` (scroll region).
*
* The consumer sets `--_table-stable-rows` via inline style; the recipe
* multiplies by `--_table-row-block-size` (already scaled by `data-size`)
* multiplies by `--table-row-height` (already scaled by `data-size`)
* and adds one row for the header.
*/
[data-table-root][data-stable-rows] {
/*
* Real row block-size at render = `--_table-row-block-size` (control
* height, the centred content) + 2 × `--_table-cell-padding-block` (cell vertical
* Real row block-size at render = `--table-row-height` (control
* height, the centred content) + 2 × `--table-cell-padding-block` (cell vertical
* padding) + 1px (cell `border-block-end`). Sum × (rows + 1 header).
*/
min-block-size: calc(
(var(--_table-row-block-size) + var(--_table-cell-padding-block) * 2 + 1px)
* (var(--_table-stable-rows, 0) + 1)
(var(--table-row-height) + var(--table-cell-padding-block) * 2 + 1px) *
(var(--_table-stable-rows, 0) + 1)
);
}
@ -150,15 +121,15 @@
inline-size: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: var(--_table-font-size);
line-height: var(--leading-ui);
font-size: var(--table-font-size);
line-height: var(--table-line-height);
color: inherit;
caption-side: top;
}
/* ── Header (thead) ─ */
[data-table-header] {
background: var(--_table-header-bg);
background-color: var(--table-header-bg);
}
/*
@ -170,19 +141,19 @@
[data-table-root][data-sticky-header] [data-table-column-header] {
position: sticky;
inset-block-start: 0;
z-index: 2;
background: var(--_table-header-bg);
z-index: var(--table-header-z);
background-color: var(--table-header-bg);
}
/* ── ColumnHeader (th) ─ */
[data-table-column-header] {
box-sizing: border-box;
padding-block: var(--_table-cell-padding-block);
padding-inline: var(--_table-cell-padding-inline);
padding-block: var(--table-cell-padding-block);
padding-inline: var(--table-cell-padding-inline);
text-align: start;
font-weight: var(--font-weight-semibold, 600);
color: var(--color-content-primary);
border-block-end: var(--border-width) solid var(--_table-divider);
font-weight: var(--table-header-font-weight);
color: var(--table-header-fg);
border-block-end: var(--table-border-width) solid var(--table-divider);
white-space: nowrap;
vertical-align: middle;
user-select: none;
@ -197,12 +168,20 @@
}
[data-table-column-header][data-pinned] {
background: var(--_table-header-bg);
background-color: var(--table-header-bg);
}
/* ── Body (tbody) ─ */
/* The row carries `archetype: 'item'`, and that veil in `archetypes.css` sits
at FULL specificity (0,5,0) — deliberately, as its own comment says — so it
beats this rule whether it is written shorthand or longhand. Measured
2026-08-20: a row highlights on hover even with `data-hoverable` absent, and
it did so before this recipe was touched. The archetype and the `hoverable`
prop therefore disagree about who decides, which is a morfo decision and is
written up in the README. Shorthand is kept here only because it changes
nothing either way. */
[data-table-body] [data-table-row] {
background: var(--color-surface-default);
background: var(--table-row-bg);
transition: background var(--duration-fast) var(--ease-default);
}
@ -214,20 +193,23 @@
[data-table-root][data-striped]:not([data-variant='ghost'])
[data-table-body]
[data-table-row]:nth-of-type(even) {
background: var(--_table-row-striped-bg);
background-color: var(--table-striped-row-bg);
}
/* Hoverable — row hover highlight. Drop when row is selected. */
[data-table-root][data-hoverable]
[data-table-body]
[data-table-row]:not([data-selected]):hover {
background: var(--_table-row-hover-bg);
background-color: var(--table-hover-row-bg);
}
/* Selected row — accent tint. Overrides hover + striped. */
/* No `color` here: the row's `archetype: 'item'` rule in `archetypes.css` sets
it at (0,5,0) and beats this (0,3,0), so a declaration would be dead and a
token for it would be a token that lies (measured 2026-08-20 — the sentinel
caught it). The selected row's ink belongs to the archetype. */
[data-table-body] [data-table-row][data-selected] {
background: var(--_table-palette-element);
color: var(--color-content-primary);
}
/* Last row has no bottom divider — the wrapper border handles the edge. */
@ -248,9 +230,9 @@
/* ── Cell (td) ─ */
[data-table-cell] {
box-sizing: border-box;
padding-block: var(--_table-cell-padding-block);
padding-inline: var(--_table-cell-padding-inline);
border-block-end: var(--border-width) solid var(--_table-divider);
padding-block: var(--table-cell-padding-block);
padding-inline: var(--table-cell-padding-inline);
border-block-end: var(--table-border-width) solid var(--table-divider);
vertical-align: middle;
color: inherit;
}
@ -266,18 +248,18 @@
/* ── Footer (tfoot) ─ */
[data-table-footer] {
background: var(--_table-header-bg);
background-color: var(--table-header-bg);
}
[data-table-footer] [data-table-cell] {
font-weight: var(--font-weight-semibold, 600);
font-weight: var(--table-header-font-weight);
border-block-end: 0;
border-block-start: var(--border-width) solid var(--_table-divider);
border-block-start: var(--table-border-width) solid var(--table-divider);
}
/* ── RowDetail (disclosure) ─ */
[data-table-row-detail] {
background: var(--color-surface-muted);
background-color: var(--table-row-detail-bg);
}
[data-table-row-detail][data-state='closed'] {
@ -285,25 +267,31 @@
}
[data-table-row-detail] > td {
padding: var(--space-3) var(--_table-cell-padding-inline);
border-block-end: var(--border-width) solid var(--_table-divider);
color: var(--color-content-secondary);
padding-block: var(--table-row-detail-padding-block);
padding-inline: var(--table-cell-padding-inline);
border-block-end: var(--table-border-width) solid var(--table-divider);
color: var(--table-row-detail-fg);
}
/* ── SortButton (eidos helper) ─ */
[data-table-sort-trigger] {
display: inline-flex;
align-items: center;
gap: var(--space-1);
gap: var(--table-sort-trigger-gap);
padding: 0;
margin: 0;
border: 0;
/* SHORTHAND on purpose, and here it DOES matter: the trigger's veil is
wrapped in `:where()` (0,0,0), so this rule cancels it. The button has no
hover feedback today; giving it one is the systemic veil sweep, not this
component's tokenization (measured: switching to longhand painted the veil
on 3 sort triggers). */
background: transparent;
color: inherit;
cursor: pointer;
font: inherit;
user-select: none;
border-radius: var(--radius-sm);
border-radius: var(--table-sort-trigger-radius);
}
[data-table-sort-trigger]:focus-visible {
@ -319,9 +307,9 @@
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: 1em;
block-size: 1em;
opacity: var(--opacity-disabled);
inline-size: var(--table-sort-indicator-size);
block-size: var(--table-sort-indicator-size);
opacity: var(--table-sort-indicator-opacity);
transition: opacity var(--duration-fast) var(--ease-default);
}

@ -1656,6 +1656,47 @@
--button-loss-solid-hover: var(--color-loss-solid-hover);
--button-loss-text: var(--color-loss-text);
--button-loss-contrast: var(--color-loss-contrast);
--table-row-height-xs: var(--size-xs-control-height);
--table-row-height-sm: var(--size-sm-control-height);
--table-row-height-md: var(--size-md-control-height);
--table-row-height-lg: var(--size-lg-control-height);
--table-row-height-xl: var(--size-xl-control-height);
--table-cell-padding-inline-xs: var(--space-2);
--table-cell-padding-inline-sm: var(--space-2);
--table-cell-padding-inline-md: var(--space-3);
--table-cell-padding-inline-lg: var(--space-4);
--table-cell-padding-inline-xl: var(--space-5);
--table-cell-padding-block-xs: var(--space-1);
--table-cell-padding-block-sm: var(--space-1);
--table-cell-padding-block-md: var(--space-2);
--table-cell-padding-block-lg: var(--space-3);
--table-cell-padding-block-xl: var(--space-3);
--table-font-size-xs: var(--size-xs-font-size);
--table-font-size-sm: var(--size-sm-font-size);
--table-font-size-md: var(--size-sm-font-size);
--table-font-size-lg: var(--size-md-font-size);
--table-font-size-xl: var(--size-lg-font-size);
--table-radius: var(--radius-md);
--table-font-family: var(--style-label-font-family);
--table-fg: var(--color-content-primary);
--table-line-height: var(--leading-ui);
--table-border-width: var(--border-width);
--table-divider: var(--color-border-default);
--table-surface-bg: var(--color-surface-default);
--table-row-bg: var(--color-surface-default);
--table-header-bg: var(--color-surface-muted);
--table-header-fg: var(--color-content-primary);
--table-header-font-weight: var(--font-weight-semibold);
--table-header-z: 2;
--table-striped-row-bg: var(--color-surface-muted);
--table-hover-row-bg: var(--color-surface-overlay);
--table-row-detail-bg: var(--color-surface-muted);
--table-row-detail-fg: var(--color-content-secondary);
--table-row-detail-padding-block: var(--space-3);
--table-sort-trigger-gap: var(--space-1);
--table-sort-trigger-radius: var(--radius-sm);
--table-sort-indicator-size: 1em;
--table-sort-indicator-opacity: var(--opacity-disabled);
--date-range-picker-accent-border: var(--color-primary-border);
--date-range-picker-accent-track: var(--color-primary-track);
--date-range-picker-accent-text: var(--color-primary-text);
@ -5173,10 +5214,42 @@
}
[data-table-root] {
--table-row-height: var(--table-row-height-md);
--table-cell-padding-inline: var(--table-cell-padding-inline-md);
--table-cell-padding-block: var(--table-cell-padding-block-md);
--table-font-size: var(--table-font-size-md);
--_table-palette-solid: var(--color-primary-solid);
--_table-palette-element: var(--color-primary-element);
}
[data-table-root][data-size='xs'] {
--table-row-height: var(--table-row-height-xs);
--table-cell-padding-inline: var(--table-cell-padding-inline-xs);
--table-cell-padding-block: var(--table-cell-padding-block-xs);
--table-font-size: var(--table-font-size-xs);
}
[data-table-root][data-size='sm'] {
--table-row-height: var(--table-row-height-sm);
--table-cell-padding-inline: var(--table-cell-padding-inline-sm);
--table-cell-padding-block: var(--table-cell-padding-block-sm);
--table-font-size: var(--table-font-size-sm);
}
[data-table-root][data-size='lg'] {
--table-row-height: var(--table-row-height-lg);
--table-cell-padding-inline: var(--table-cell-padding-inline-lg);
--table-cell-padding-block: var(--table-cell-padding-block-lg);
--table-font-size: var(--table-font-size-lg);
}
[data-table-root][data-size='xl'] {
--table-row-height: var(--table-row-height-xl);
--table-cell-padding-inline: var(--table-cell-padding-inline-xl);
--table-cell-padding-block: var(--table-cell-padding-block-xl);
--table-font-size: var(--table-font-size-xl);
}
[data-table-root][data-color], [data-table-root][data-color-custom] {
--_table-palette-element: var(--palette-element, var(--color-primary-element));
--_table-palette-solid: var(--palette-solid, var(--color-primary-solid));

@ -1945,6 +1945,121 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
// so their forward targets that part via `parts: ['root']`. drag-drop / combobox
// / timeline already own a recipe → their palette slots live in those entries.
table: {
// Row height per size - 1:1 with the bundle, measured. NOTE the name: the
// value is the ROW's height (the centred content of a cell), not the
// root's; the old private was called `root-*` and named the wrong thing.
'row-height-xs': 'var(--size-xs-control-height)',
'row-height-sm': 'var(--size-sm-control-height)',
'row-height-md': 'var(--size-md-control-height)',
'row-height-lg': 'var(--size-lg-control-height)',
'row-height-xl': 'var(--size-xl-control-height)',
// Cell padding - the table keeps its OWN rhythm, denser than the control
// bundle at xs and looser from md up. Verbatim on purpose: a data grid
// packs rows tighter than a control stack, and forcing the bundle here
// would change the default. (Spacing is outside the bundle guard, which
// only governs control-height / font-size / icon-size.)
'cell-padding-inline-xs': 'var(--space-2)',
'cell-padding-inline-sm': 'var(--space-2)',
'cell-padding-inline-md': 'var(--space-3)',
'cell-padding-inline-lg': 'var(--space-4)',
'cell-padding-inline-xl': 'var(--space-5)',
'cell-padding-block-xs': 'var(--space-1)',
'cell-padding-block-sm': 'var(--space-1)',
'cell-padding-block-md': 'var(--space-2)',
'cell-padding-block-lg': 'var(--space-3)',
'cell-padding-block-xl': 'var(--space-3)',
// Type. The table reads ONE STEP BELOW the 1:1 from md up - a grid is read
// in bulk, not glanced at. The coordinate names that step explicitly, so
// the deviation is legible instead of hidden in a raw primitive.
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-sm-font-size)',
'font-size-lg': 'var(--size-md-font-size)',
'font-size-xl': 'var(--size-lg-font-size)',
// The RESOLVED names. `data-size` is stamped on `[data-table-root]`, so the
// per-size cascade targets that part, not the host.
'row-height': {
parts: ['root'],
declarations: [
{ value: 'var(--table-row-height-md)', scope: 'host' },
{ value: 'var(--table-row-height-xs)', scope: 'size:xs' },
{ value: 'var(--table-row-height-sm)', scope: 'size:sm' },
{ value: 'var(--table-row-height-lg)', scope: 'size:lg' },
{ value: 'var(--table-row-height-xl)', scope: 'size:xl' }
]
},
'cell-padding-inline': {
parts: ['root'],
declarations: [
{ value: 'var(--table-cell-padding-inline-md)', scope: 'host' },
{ value: 'var(--table-cell-padding-inline-xs)', scope: 'size:xs' },
{ value: 'var(--table-cell-padding-inline-sm)', scope: 'size:sm' },
{ value: 'var(--table-cell-padding-inline-lg)', scope: 'size:lg' },
{ value: 'var(--table-cell-padding-inline-xl)', scope: 'size:xl' }
]
},
'cell-padding-block': {
parts: ['root'],
declarations: [
{ value: 'var(--table-cell-padding-block-md)', scope: 'host' },
{ value: 'var(--table-cell-padding-block-xs)', scope: 'size:xs' },
{ value: 'var(--table-cell-padding-block-sm)', scope: 'size:sm' },
{ value: 'var(--table-cell-padding-block-lg)', scope: 'size:lg' },
{ value: 'var(--table-cell-padding-block-xl)', scope: 'size:xl' }
]
},
'font-size': {
parts: ['root'],
declarations: [
{ value: 'var(--table-font-size-md)', scope: 'host' },
{ value: 'var(--table-font-size-xs)', scope: 'size:xs' },
{ value: 'var(--table-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--table-font-size-lg)', scope: 'size:lg' },
{ value: 'var(--table-font-size-xl)', scope: 'size:xl' }
]
},
// Chassis. No `root-` prefix: the wrapper IS the component, so the token is
// named by its axis - recipe-contract §1 does not repeat the part when the
// part is the component itself.
radius: 'var(--radius-md)',
'font-family': 'var(--style-label-font-family)',
fg: 'var(--color-content-primary)',
'line-height': 'var(--leading-ui)',
'border-width': 'var(--border-width)',
divider: 'var(--color-border-default)',
// Variants. `surface` carries the card chrome; `outline` keeps the border
// over nothing; `ghost` has no chrome at all.
'surface-bg': 'var(--color-surface-default)',
'row-bg': 'var(--color-surface-default)',
// Header / footer share one surface.
'header-bg': 'var(--color-surface-muted)',
'header-fg': 'var(--color-content-primary)',
'header-font-weight': 'var(--font-weight-semibold)',
'header-z': '2',
// Row states. The striped and hover tints are the table's own: the row's
// `archetype: 'item'` veil is currently cancelled by the row background
// (see README - the `hoverable` prop depends on it).
'striped-row-bg': 'var(--color-surface-muted)',
'hover-row-bg': 'var(--color-surface-overlay)',
// Row detail (disclosure).
'row-detail-bg': 'var(--color-surface-muted)',
'row-detail-fg': 'var(--color-content-secondary)',
'row-detail-padding-block': 'var(--space-3)',
// Sort trigger + its indicator.
'sort-trigger-gap': 'var(--space-1)',
'sort-trigger-radius': 'var(--radius-sm)',
'sort-indicator-size': '1em',
'sort-indicator-opacity': 'var(--opacity-disabled)',
'_palette-solid': {
parts: ['root'],
declarations: [{ value: 'var(--color-primary-solid)', scope: 'host' }]
@ -4455,6 +4570,7 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
'indicator-line-thickness': '2px',
'scrollable-fade': '16px'
},
's-text': {
'hint-fg': 'var(--color-content-secondary)',
'hint-opacity': '0.8',

@ -18,10 +18,11 @@
import { compileMorfo } from '$uix/morfo';
import { tableMorfo } from '@/uix/morfo/components/table';
import { getActiveUix } from '$active-uix';
import TokensPanel from '../../lib/TokensPanel.svelte';
const uix = getActiveUix();
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'tokens' | 'a11y';
let tab = $state<Tab>('live');
// ── Sample data ──────────────────────────────────────────────────────
@ -37,27 +38,149 @@
};
const PEOPLE: Person[] = [
{ id: '1', name: 'Ada Lovelace', role: 'Founder', team: 'engineering', score: 96, joined: '2018-03-12', status: 'active', bio: 'Pioneer of analytical engines. Believes in legible code over clever tricks.' },
{ id: '2', name: 'Ben Eich', role: 'Architect', team: 'engineering', score: 89, joined: '2019-07-08', status: 'active', bio: 'Standards veteran. Writes interpreters at night.' },
{ id: '3', name: 'Cora Ng', role: 'Designer', team: 'design', score: 91, joined: '2020-02-19', status: 'active', bio: 'Type-driven systems thinker. Aspiring origami master.' },
{ id: '4', name: 'Dio Park', role: 'Engineer', team: 'engineering', score: 78, joined: '2021-09-04', status: 'paused', bio: 'Reactive runtime obsessive. Maintains 3 mechanical keyboards.' },
{ id: '5', name: 'Elin Sato', role: 'Account exec', team: 'sales', score: 84, joined: '2020-11-23', status: 'active', bio: 'Closes deals with empathy. Hikes on Sundays.' },
{ id: '6', name: 'Fae Olu', role: 'Researcher', team: 'design', score: 88, joined: '2019-04-14', status: 'invited', bio: 'Diary interviewer. Believes in slow research.' },
{ id: '7', name: 'Gus Lin', role: 'Operations', team: 'ops', score: 72, joined: '2022-01-30', status: 'active', bio: 'Logistics minded. Plans every commute.' },
{ id: '8', name: 'Hari Sen', role: 'Engineer', team: 'engineering', score: 81, joined: '2021-06-11', status: 'active', bio: 'CRDTs by day, contra-dance by night.' },
{ id: '9', name: 'Ines Vega', role: 'Sales lead', team: 'sales', score: 93, joined: '2018-09-27', status: 'active', bio: 'Pipeline poet. Drinks espresso, distrusts dashboards.' },
{ id: '10', name: 'Joon Han', role: 'Engineer', team: 'engineering', score: 87, joined: '2020-08-05', status: 'paused', bio: 'WASM tinkerer. Hosts board-game nights.' },
{ id: '11', name: 'Kai Reyes', role: 'Designer', team: 'design', score: 79, joined: '2022-03-17', status: 'invited', bio: 'Motion-system curator. Sketches every meeting.' },
{ id: '12', name: 'Lin Park', role: 'Operations', team: 'ops', score: 85, joined: '2019-12-01', status: 'active', bio: 'Process auditor. Knows where every key is kept.' }
{
id: '1',
name: 'Ada Lovelace',
role: 'Founder',
team: 'engineering',
score: 96,
joined: '2018-03-12',
status: 'active',
bio: 'Pioneer of analytical engines. Believes in legible code over clever tricks.'
},
{
id: '2',
name: 'Ben Eich',
role: 'Architect',
team: 'engineering',
score: 89,
joined: '2019-07-08',
status: 'active',
bio: 'Standards veteran. Writes interpreters at night.'
},
{
id: '3',
name: 'Cora Ng',
role: 'Designer',
team: 'design',
score: 91,
joined: '2020-02-19',
status: 'active',
bio: 'Type-driven systems thinker. Aspiring origami master.'
},
{
id: '4',
name: 'Dio Park',
role: 'Engineer',
team: 'engineering',
score: 78,
joined: '2021-09-04',
status: 'paused',
bio: 'Reactive runtime obsessive. Maintains 3 mechanical keyboards.'
},
{
id: '5',
name: 'Elin Sato',
role: 'Account exec',
team: 'sales',
score: 84,
joined: '2020-11-23',
status: 'active',
bio: 'Closes deals with empathy. Hikes on Sundays.'
},
{
id: '6',
name: 'Fae Olu',
role: 'Researcher',
team: 'design',
score: 88,
joined: '2019-04-14',
status: 'invited',
bio: 'Diary interviewer. Believes in slow research.'
},
{
id: '7',
name: 'Gus Lin',
role: 'Operations',
team: 'ops',
score: 72,
joined: '2022-01-30',
status: 'active',
bio: 'Logistics minded. Plans every commute.'
},
{
id: '8',
name: 'Hari Sen',
role: 'Engineer',
team: 'engineering',
score: 81,
joined: '2021-06-11',
status: 'active',
bio: 'CRDTs by day, contra-dance by night.'
},
{
id: '9',
name: 'Ines Vega',
role: 'Sales lead',
team: 'sales',
score: 93,
joined: '2018-09-27',
status: 'active',
bio: 'Pipeline poet. Drinks espresso, distrusts dashboards.'
},
{
id: '10',
name: 'Joon Han',
role: 'Engineer',
team: 'engineering',
score: 87,
joined: '2020-08-05',
status: 'paused',
bio: 'WASM tinkerer. Hosts board-game nights.'
},
{
id: '11',
name: 'Kai Reyes',
role: 'Designer',
team: 'design',
score: 79,
joined: '2022-03-17',
status: 'invited',
bio: 'Motion-system curator. Sketches every meeting.'
},
{
id: '12',
name: 'Lin Park',
role: 'Operations',
team: 'ops',
score: 85,
joined: '2019-12-01',
status: 'active',
bio: 'Process auditor. Knows where every key is kept.'
}
];
const COLUMNS: ColumnDef<Person>[] = [
{ accessorKey: 'name', header: 'Name', enableSorting: true, enableFiltering: true, enablePinning: true, size: 180 },
{
accessorKey: 'name',
header: 'Name',
enableSorting: true,
enableFiltering: true,
enablePinning: true,
size: 180
},
{ accessorKey: 'role', header: 'Role', enableSorting: true, enableFiltering: true, size: 140 },
{ accessorKey: 'team', header: 'Team', enableSorting: true, enableFiltering: true, size: 130 },
{ accessorKey: 'score', header: 'Score', enableSorting: true, sortDescFirst: true, size: 90 },
{ accessorKey: 'joined', header: 'Joined', enableSorting: true, size: 130 },
{ accessorKey: 'status', header: 'Status', enableSorting: true, enableFiltering: true, enablePinning: true, size: 120 }
{
accessorKey: 'status',
header: 'Status',
enableSorting: true,
enableFiltering: true,
enablePinning: true,
size: 120
}
];
// ── Visual props ─────────────────────────────────────────────────────
@ -189,9 +312,7 @@
return `${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
}
const selectedCount = $derived(
Object.values(table.selection).filter(Boolean).length
);
const selectedCount = $derived(Object.values(table.selection).filter(Boolean).length);
const pageIndex = $derived(table.pagination.pageIndex);
const pageCount = $derived(table.pageCount);
@ -206,7 +327,7 @@
'',
'<Table',
' {table}',
" label=\"Team members\"",
' label="Team members"',
locale !== 'en-US' && ` locale="${locale}"`,
dirMode !== 'auto' && ` dir="${dir}"`,
size !== 'md' && ` size="${size}"`,
@ -249,7 +370,11 @@
function fmtDate(s: string) {
try {
return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'short', day: '2-digit' }).format(new Date(s));
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'short',
day: '2-digit'
}).format(new Date(s));
} catch {
return s;
}
@ -261,13 +386,11 @@
<div data-uix-eyebrow>Data · Table</div>
<h1 data-uix-page-title>Table</h1>
<p data-uix-page-lede>
Headless tabular data with sorting, filtering, selection,
pagination, column pinning, sizing and row-detail disclosure
panels — wrapped by an eidos visual recipe (variant / size /
color / striped / sticky header). Use <code>Table</code> for
flat data; reach for <code>TreeGrid</code> when rows are
hierarchical, or <code>GridList</code> when rows carry inline
interactive children.
Headless tabular data with sorting, filtering, selection, pagination, column pinning, sizing
and row-detail disclosure panels — wrapped by an eidos visual recipe (variant / size / color /
striped / sticky header). Use <code>Table</code> for flat data; reach for
<code>TreeGrid</code>
when rows are hierarchical, or <code>GridList</code> when rows carry inline interactive children.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill>
@ -281,19 +404,25 @@
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>apg</span>
<a href="https://www.w3.org/WAI/ARIA/apg/patterns/table/" target="_blank" rel="noreferrer">table ↗</a>
<a href="https://www.w3.org/WAI/ARIA/apg/patterns/table/" target="_blank" rel="noreferrer"
>table ↗</a
>
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>rows</span> {table.filteredRowCount} / {PEOPLE.length}
<span data-uix-meta-key>rows</span>
{table.filteredRowCount} / {PEOPLE.length}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>selected</span> {selectedCount}
<span data-uix-meta-key>selected</span>
{selectedCount}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>locale</span> {locale}
<span data-uix-meta-key>locale</span>
{locale}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>dir</span> {dir}
<span data-uix-meta-key>dir</span>
{dir}
</span>
</div>
</header>
@ -312,7 +441,10 @@
<Button
size="sm"
variant="ghost"
onclick={() => { setGlobalFilter(''); table.clearFilters(); }}
onclick={() => {
setGlobalFilter('');
table.clearFilters();
}}
disabled={!globalFilter && table.columnFilters.length === 0}
>
Clear filters
@ -333,11 +465,7 @@
>
{table.getIsAllSelected() ? 'Deselect all' : 'Select all'}
</Button>
<Button
size="sm"
variant="ghost"
onclick={() => table.closeAllRowDetails()}
>
<Button size="sm" variant="ghost" onclick={() => table.closeAllRowDetails()}>
Close all details
</Button>
</div>
@ -358,7 +486,11 @@
>
<Table.Header>
<tr>
<th data-table-column-header="" style="inline-size: 2.5rem;" aria-label="Row detail toggle"></th>
<th
data-table-column-header=""
style="inline-size: 2.5rem;"
aria-label="Row detail toggle"
></th>
{#each table.headers as header (header.id)}
{@const h = header as unknown as TableHeader}
<Table.ColumnHeader header={h}>
@ -377,7 +509,9 @@
{#each row.cells as cell, i (cell.id)}
<Table.Cell cell={cell as unknown as TableCell} colIndex={i}>
{#if cell.column.id === 'status'}
<span data-uix-meta-pill data-status={cell.value}>{fmtStatus(String(cell.value))}</span>
<span data-uix-meta-pill data-status={cell.value}
>{fmtStatus(String(cell.value))}</span
>
{:else if cell.column.id === 'score'}
<strong>{cell.value}</strong>
{:else if cell.column.id === 'joined'}
@ -389,15 +523,27 @@
{/each}
</Table.Row>
<Table.RowDetail row={r}>
<div style="display: flex; gap: var(--uix-space-3); align-items: flex-start; flex-wrap: wrap;">
<div
style="display: flex; gap: var(--uix-space-3); align-items: flex-start; flex-wrap: wrap;"
>
<div style="flex: 1 1 18rem; min-inline-size: 0;">
<div style="font-weight: var(--font-weight-semibold, 600); margin-block-end: 0.25rem;">{row.original.name}</div>
<div
style="font-weight: var(--font-weight-semibold, 600); margin-block-end: 0.25rem;"
>
{row.original.name}
</div>
<div style="color: var(--color-content-secondary);">{row.original.bio}</div>
</div>
<div style="display: flex; gap: var(--uix-space-2); flex-wrap: wrap;">
<span data-uix-meta-pill><span data-uix-meta-key>id</span>{row.original.id}</span>
<span data-uix-meta-pill><span data-uix-meta-key>team</span>{row.original.team}</span>
<span data-uix-meta-pill><span data-uix-meta-key>joined</span>{fmtDate(row.original.joined)}</span>
<span data-uix-meta-pill
><span data-uix-meta-key>id</span>{row.original.id}</span
>
<span data-uix-meta-pill
><span data-uix-meta-key>team</span>{row.original.team}</span
>
<span data-uix-meta-pill
><span data-uix-meta-key>joined</span>{fmtDate(row.original.joined)}</span
>
</div>
</div>
</Table.RowDetail>
@ -405,10 +551,11 @@
</Table.Body>
</Table>
<div style="display: flex; justify-content: space-between; align-items: center; gap: var(--uix-space-3); flex-wrap: wrap;">
<div
style="display: flex; justify-content: space-between; align-items: center; gap: var(--uix-space-3); flex-wrap: wrap;"
>
<span style="color: var(--color-content-secondary); font-size: var(--font-size-xs);">
Page {pageIndex + 1} of {pageCount} ·
showing {table.rows.length} of {table.filteredRowCount}
Page {pageIndex + 1} of {pageCount} · showing {table.rows.length} of {table.filteredRowCount}
</span>
<Pagination
page={pageIndex + 1}
@ -440,13 +587,19 @@
<span>click sort, select a row, expand a detail panel…</span>
{:else}
{#each trace.slice(0, 3) as entry}
<span><span data-uix-stage-trace-event>{entry.event}</span> · {entry.family}{entry.intent ? ' · ' + entry.intent : ''}</span>
<span
><span data-uix-stage-trace-event>{entry.event}</span> · {entry.family}{entry.intent
? ' · ' + entry.intent
: ''}</span
>
<span style="color: var(--uix-text-faint)">{fmtTime(entry.at)}</span>
{/each}
{/if}
<span style="margin-inline-start: auto;">
<span data-uix-stage-trace-key>sort</span>
{table.sorting.length === 0 ? '—' : table.sorting.map((s) => `${s.id}:${s.desc ? '↓' : '↑'}`).join(' · ')}
{table.sorting.length === 0
? '—'
: table.sorting.map((s) => `${s.id}:${s.desc ? '↓' : '↑'}`).join(' · ')}
</span>
</div>
</div>
@ -464,7 +617,12 @@
<span data-uix-layer-badge="sema">sema</span>
<span data-uix-tab-count>{events.length}</span>
</button>
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button>
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}
>Recipe</button
>
<button data-uix-tab data-active={tab === 'tokens'} onclick={() => (tab = 'tokens')}>
Tokens <span data-uix-tab-count>45</span>
</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
@ -472,9 +630,10 @@
<section data-uix-section>
<h2 data-uix-section-title>Controls</h2>
<p data-uix-section-desc>
Soma owns sort / filter / select / paginate / pin / row-detail state and ARIA wiring.
Eidos adds visual chrome (variant / size / color), striping, hover feedback and the optional sticky header + scroll region.
`dir` and `locale` are demo-driven for verification — Table forwards `dir` to the soma Provider and `locale` is used by the demo for date formatting.
Soma owns sort / filter / select / paginate / pin / row-detail state and ARIA wiring. Eidos
adds visual chrome (variant / size / color), striping, hover feedback and the optional
sticky header + scroll region. `dir` and `locale` are demo-driven for verification — Table
forwards `dir` to the soma Provider and `locale` is used by the demo for date formatting.
</p>
<div data-uix-subsection-head>
@ -493,7 +652,11 @@
<span data-uix-control-label>direction</span>
<span data-uix-chips role="radiogroup">
{#each ['auto', 'ltr', 'rtl'] as value (value)}
<button data-uix-chip data-active={dirMode === value} onclick={() => (dirMode = value as typeof dirMode)}>{value}</button>
<button
data-uix-chip
data-active={dirMode === value}
onclick={() => (dirMode = value as typeof dirMode)}>{value}</button
>
{/each}
</span>
</label>
@ -510,15 +673,19 @@
<button
data-uix-chip
data-active={selectionMode === m}
onclick={() => setSelectionMode(m as typeof selectionMode)}
>{m}</button>
onclick={() => setSelectionMode(m as typeof selectionMode)}>{m}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>multiSort</span>
<span data-uix-switch>
<input type="checkbox" checked={multiSort} onchange={(e) => setMultiSort((e.currentTarget as HTMLInputElement).checked)} />
<input
type="checkbox"
checked={multiSort}
onchange={(e) => setMultiSort((e.currentTarget as HTMLInputElement).checked)}
/>
<span data-uix-switch-label>{multiSort ? 'shift-click adds' : 'replace'}</span>
</span>
</label>
@ -526,17 +693,23 @@
<span data-uix-control-label>pageSize</span>
<span data-uix-chips role="radiogroup">
{#each [3, 5, 8, 12] as n (n)}
<button data-uix-chip data-active={pageSize === n} onclick={() => setPageSize(n)}>{n}</button>
<button data-uix-chip data-active={pageSize === n} onclick={() => setPageSize(n)}
>{n}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>pin name (left)</span>
<button data-uix-chip data-active={pinnedName} onclick={togglePinName}>{pinnedName ? 'sticky' : 'off'}</button>
<button data-uix-chip data-active={pinnedName} onclick={togglePinName}
>{pinnedName ? 'sticky' : 'off'}</button
>
</label>
<label data-uix-control>
<span data-uix-control-label>pin status (right)</span>
<button data-uix-chip data-active={pinnedStatus} onclick={togglePinStatus}>{pinnedStatus ? 'sticky' : 'off'}</button>
<button data-uix-chip data-active={pinnedStatus} onclick={togglePinStatus}
>{pinnedStatus ? 'sticky' : 'off'}</button
>
</label>
</div>
@ -548,7 +721,11 @@
<span data-uix-control-label>variant</span>
<span data-uix-chips role="radiogroup">
{#each ['surface', 'outline', 'ghost'] as v (v)}
<button data-uix-chip data-active={variant === v} onclick={() => (variant = v as TableVariant)}>{v}</button>
<button
data-uix-chip
data-active={variant === v}
onclick={() => (variant = v as TableVariant)}>{v}</button
>
{/each}
</span>
</label>
@ -556,7 +733,9 @@
<span data-uix-control-label>size</span>
<span data-uix-chips role="radiogroup">
{#each ['xs', 'sm', 'md', 'lg', 'xl'] as s (s)}
<button data-uix-chip data-active={size === s} onclick={() => (size = s as TableSize)}>{s}</button>
<button data-uix-chip data-active={size === s} onclick={() => (size = s as TableSize)}
>{s}</button
>
{/each}
</span>
</label>
@ -564,7 +743,11 @@
<span data-uix-control-label>color</span>
<span data-uix-chips role="radiogroup">
{#each ['primary', 'secondary', 'neutral', 'affirm', 'fulfill', 'risk', 'threat', 'loss'] as c (c)}
<button data-uix-chip data-active={color === c} onclick={() => (color = c as TableColor)}>{c}</button>
<button
data-uix-chip
data-active={color === c}
onclick={() => (color = c as TableColor)}>{c}</button
>
{/each}
</span>
</label>
@ -607,7 +790,9 @@
<span data-uix-control-label>stableRows</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={stableRowsOn} disabled={useScrollRegion} />
<span data-uix-switch-label>{stableRowsOn && !useScrollRegion ? `reserve ${pageSize} rows` : 'off'}</span>
<span data-uix-switch-label
>{stableRowsOn && !useScrollRegion ? `reserve ${pageSize} rows` : 'off'}</span
>
</span>
</label>
</div>
@ -627,7 +812,13 @@
<section data-uix-section>
<h2 data-uix-section-title>API reference</h2>
<p data-uix-section-desc>
9 soma parts (<code>Provider · Header · Body · Footer · ColumnHeader · Row · Cell · RowDetail · RowDetailTrigger</code>) plus the eidos-only <code>SortButton</code> ergonomic helper. The <code>table</code> instance carries every imperative method (<code>toggleSort</code>, <code>setGlobalFilter</code>, <code>toggleRowSelection</code>, <code>pinColumn</code>, <code>setPageIndex</code>, <code>toggleRowDetail</code>…).
9 soma parts (<code
>Provider · Header · Body · Footer · ColumnHeader · Row · Cell · RowDetail ·
RowDetailTrigger</code
>) plus the eidos-only <code>SortButton</code> ergonomic helper. The <code>table</code>
instance carries every imperative method (<code>toggleSort</code>,
<code>setGlobalFilter</code>, <code>toggleRowSelection</code>, <code>pinColumn</code>,
<code>setPageIndex</code>, <code>toggleRowDetail</code>…).
</p>
<div data-uix-subsection-head>Provider (Table)</div>
@ -635,18 +826,77 @@
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="name">table</td><td class="type">TableInstance</td><td class="default empty">required</td><td>Returned by <code>createTable</code>.</td></tr>
<tr><td class="name">label</td><td class="type">string</td><td class="default">'Data table'</td><td>Accessible name. Translated.</td></tr>
<tr><td class="name">dir <span data-uix-tag data-kind="soma">soma</span></td><td class="type">'ltr' | 'rtl'</td><td class="default">from Soma prefs</td><td>Text direction.</td></tr>
<tr><td class="name">variant <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'surface' | 'outline' | 'ghost'</td><td class="default">'surface'</td><td>Container chrome.</td></tr>
<tr><td class="name">size <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'xs' | 'sm' | 'md' | 'lg' | 'xl'</td><td class="default">'md'</td><td>Row height + cell padding + font.</td></tr>
<tr><td class="name">color <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">ColorRole</td><td class="default">'primary'</td><td>Accent for selected rows + sort indicator + focus.</td></tr>
<tr><td class="name">block <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">boolean</td><td class="default">true</td><td>Stretch root to container inline-size.</td></tr>
<tr><td class="name">striped <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">boolean</td><td class="default">false</td><td>Alternate-row tint (skipped in ghost variant).</td></tr>
<tr><td class="name">hoverable <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">boolean</td><td class="default">true</td><td>Highlight rows on hover.</td></tr>
<tr><td class="name">stickyHeader <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">boolean</td><td class="default">false</td><td>Pins <code>&lt;thead&gt;</code> to the top of the scroll region.</td></tr>
<tr><td class="name">maxHeight <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">string</td><td class="default empty">—</td><td>Wraps the table in a scrolling region with the given CSS block-size.</td></tr>
<tr><td class="name">stableRows <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">number</td><td class="default empty">—</td><td>Reserve vertical space for N rows + header. Keeps the wrapper height steady so pagination/footer don't jump when filters change row count. Ignored when <code>maxHeight</code> is set.</td></tr>
<tr
><td class="name">table</td><td class="type">TableInstance</td><td
class="default empty">required</td
><td>Returned by <code>createTable</code>.</td></tr
>
<tr
><td class="name">label</td><td class="type">string</td><td class="default"
>'Data table'</td
><td>Accessible name. Translated.</td></tr
>
<tr
><td class="name">dir <span data-uix-tag data-kind="soma">soma</span></td><td
class="type">'ltr' | 'rtl'</td
><td class="default">from Soma prefs</td><td>Text direction.</td></tr
>
<tr
><td class="name">variant <span data-uix-tag data-kind="eidos">eidos</span></td><td
class="type">'surface' | 'outline' | 'ghost'</td
><td class="default">'surface'</td><td>Container chrome.</td></tr
>
<tr
><td class="name">size <span data-uix-tag data-kind="eidos">eidos</span></td><td
class="type">'xs' | 'sm' | 'md' | 'lg' | 'xl'</td
><td class="default">'md'</td><td>Row height + cell padding + font.</td></tr
>
<tr
><td class="name">color <span data-uix-tag data-kind="eidos">eidos</span></td><td
class="type">ColorRole</td
><td class="default">'primary'</td><td
>Accent for selected rows + sort indicator + focus.</td
></tr
>
<tr
><td class="name">block <span data-uix-tag data-kind="eidos">eidos</span></td><td
class="type">boolean</td
><td class="default">true</td><td>Stretch root to container inline-size.</td></tr
>
<tr
><td class="name">striped <span data-uix-tag data-kind="eidos">eidos</span></td><td
class="type">boolean</td
><td class="default">false</td><td>Alternate-row tint (skipped in ghost variant).</td
></tr
>
<tr
><td class="name">hoverable <span data-uix-tag data-kind="eidos">eidos</span></td><td
class="type">boolean</td
><td class="default">true</td><td>Highlight rows on hover.</td></tr
>
<tr
><td class="name">stickyHeader <span data-uix-tag data-kind="eidos">eidos</span></td
><td class="type">boolean</td><td class="default">false</td><td
>Pins <code>&lt;thead&gt;</code> to the top of the scroll region.</td
></tr
>
<tr
><td class="name">maxHeight <span data-uix-tag data-kind="eidos">eidos</span></td><td
class="type">string</td
><td class="default empty">—</td><td
>Wraps the table in a scrolling region with the given CSS block-size.</td
></tr
>
<tr
><td class="name">stableRows <span data-uix-tag data-kind="eidos">eidos</span></td><td
class="type">number</td
><td class="default empty">—</td><td
>Reserve vertical space for N rows + header. Keeps the wrapper height steady so
pagination/footer don't jump when filters change row count. Ignored when <code
>maxHeight</code
> is set.</td
></tr
>
</tbody>
</table>
</div>
@ -656,9 +906,21 @@
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="name">header</td><td class="type">TableHeader</td><td class="default empty">required</td><td>From <code>table.headers</code>.</td></tr>
<tr><td class="name">children</td><td class="type">Snippet</td><td class="default">header.label</td><td>Visible label.</td></tr>
<tr><td class="name">hideIndicator</td><td class="type">boolean</td><td class="default">false</td><td>Hide the tri-state chevron.</td></tr>
<tr
><td class="name">header</td><td class="type">TableHeader</td><td
class="default empty">required</td
><td>From <code>table.headers</code>.</td></tr
>
<tr
><td class="name">children</td><td class="type">Snippet</td><td class="default"
>header.label</td
><td>Visible label.</td></tr
>
<tr
><td class="name">hideIndicator</td><td class="type">boolean</td><td class="default"
>false</td
><td>Hide the tri-state chevron.</td></tr
>
</tbody>
</table>
</div>
@ -668,9 +930,23 @@
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="name">row</td><td class="type">TableRow</td><td class="default empty">required</td><td>From <code>table.rows</code>.</td></tr>
<tr><td class="name">aria-label</td><td class="type">string</td><td class="default">'Show details' / 'Hide details'</td><td>Translated swap.</td></tr>
<tr><td class="name">variant · size · rounded · intent · color · iconOnly · icon</td><td class="type">Button props</td><td class="default">ghost · md · md</td><td>Forwarded to underlying <code>&lt;Button&gt;</code>.</td></tr>
<tr
><td class="name">row</td><td class="type">TableRow</td><td class="default empty"
>required</td
><td>From <code>table.rows</code>.</td></tr
>
<tr
><td class="name">aria-label</td><td class="type">string</td><td class="default"
>'Show details' / 'Hide details'</td
><td>Translated swap.</td></tr
>
<tr
><td class="name">variant · size · rounded · intent · color · iconOnly · icon</td><td
class="type">Button props</td
><td class="default">ghost · md · md</td><td
>Forwarded to underlying <code>&lt;Button&gt;</code>.</td
></tr
>
</tbody>
</table>
</div>
@ -680,13 +956,41 @@
<table data-uix-table>
<thead><tr><th>Flag</th><th>Default</th><th>Effect</th></tr></thead>
<tbody>
<tr><td class="name">enableSorting</td><td class="default">false</td><td>Column shows up in <code>getCanSort</code> + <code>aria-sort</code>.</td></tr>
<tr><td class="name">enableFiltering</td><td class="default">false</td><td>Column participates in global filter + per-column filters.</td></tr>
<tr><td class="name">enableHiding</td><td class="default">true</td><td>Consumer can call <code>toggleColumnVisibility</code>.</td></tr>
<tr><td class="name">enableResizing</td><td class="default">false</td><td>Consumer can call <code>setColumnSize</code> with constraints.</td></tr>
<tr><td class="name">enablePinning</td><td class="default">false</td><td>Consumer can call <code>pinColumn(id, 'left' | 'right')</code>.</td></tr>
<tr><td class="name">size · minSize · maxSize</td><td class="default empty">—</td><td>Initial width in px + bounds.</td></tr>
<tr><td class="name">sortDescFirst</td><td class="default">false</td><td>First click sorts descending (numeric columns).</td></tr>
<tr
><td class="name">enableSorting</td><td class="default">false</td><td
>Column shows up in <code>getCanSort</code> + <code>aria-sort</code>.</td
></tr
>
<tr
><td class="name">enableFiltering</td><td class="default">false</td><td
>Column participates in global filter + per-column filters.</td
></tr
>
<tr
><td class="name">enableHiding</td><td class="default">true</td><td
>Consumer can call <code>toggleColumnVisibility</code>.</td
></tr
>
<tr
><td class="name">enableResizing</td><td class="default">false</td><td
>Consumer can call <code>setColumnSize</code> with constraints.</td
></tr
>
<tr
><td class="name">enablePinning</td><td class="default">false</td><td
>Consumer can call <code>pinColumn(id, 'left' | 'right')</code>.</td
></tr
>
<tr
><td class="name">size · minSize · maxSize</td><td class="default empty">—</td><td
>Initial width in px + bounds.</td
></tr
>
<tr
><td class="name">sortDescFirst</td><td class="default">false</td><td
>First click sorts descending (numeric columns).</td
></tr
>
</tbody>
</table>
</div>
@ -707,7 +1011,10 @@
<tbody>
<tr><td class="name">name</td><td class="type">"{tableMorfo.name}"</td></tr>
<tr><td class="name">kebab</td><td class="type">"{tableMorfo.kebab}"</td></tr>
<tr><td class="name">parts.length</td><td class="default">{tableMorfo.parts.length}</td></tr>
<tr
><td class="name">parts.length</td><td class="default">{tableMorfo.parts.length}</td
></tr
>
<tr><td class="name">events.length</td><td class="default">{events.length}</td></tr>
</tbody>
</table>
@ -716,7 +1023,13 @@
<div data-uix-subsection-head>Parts</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Part</th><th>Marker</th><th>Element</th><th>Role</th><th>Archetype</th><th>States</th><th>Optional</th></tr></thead>
<thead
><tr
><th>Part</th><th>Marker</th><th>Element</th><th>Role</th><th>Archetype</th><th
>States</th
><th>Optional</th></tr
></thead
>
<tbody>
{#each partsList as part (part.kebab)}
<tr>
@ -736,12 +1049,23 @@
<div data-uix-subsection-head>Events declaration</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>name</th><th>family</th><th>verb</th><th>sequence</th><th>intent</th><th>target</th></tr></thead>
<thead
><tr
><th>name</th><th>family</th><th>verb</th><th>sequence</th><th>intent</th><th
>target</th
></tr
></thead
>
<tbody>
{#each events as action (action.name)}
{@const sem = action.semantic}
{@const intentDecl = 'intent' in sem ? sem.intent : undefined}
{@const intentStr = typeof intentDecl === 'string' ? intentDecl : intentDecl ? `fromProp:${(intentDecl as { fromProp?: string }).fromProp ?? '?'}` : '—'}
{@const intentStr =
typeof intentDecl === 'string'
? intentDecl
: intentDecl
? `fromProp:${(intentDecl as { fromProp?: string }).fromProp ?? '?'}`
: '—'}
<tr>
<td class="name">{action.name}</td>
<td class="type">{sem.family}</td>
@ -766,12 +1090,17 @@
<p data-uix-section-desc>
Soma raises <code>shift-sort</code> on column-header click,
<code>commit-select</code> on row toggle and
<code>shift-expand-row</code> when the detail panel toggles.
Use Play to replay each event against the first match in the stage.
<code>shift-expand-row</code> when the detail panel toggles. Use Play to replay each event against
the first match in the stage.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Name</th><th>Family</th><th>Verb</th><th>Sequence</th><th>Intent</th><th>Play</th></tr></thead>
<thead
><tr
><th>Name</th><th>Family</th><th>Verb</th><th>Sequence</th><th>Intent</th><th>Play</th
></tr
></thead
>
<tbody>
{#each events as action (action.name)}
{@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined}
@ -792,7 +1121,8 @@
: action.target === 'column-header'
? '[data-table-column-header]'
: '[data-table]';
const target = (stageRef?.querySelector(selector) ?? stageRef) as HTMLElement | null;
const target = (stageRef?.querySelector(selector) ??
stageRef) as HTMLElement | null;
if (!target) return;
void uix.events?.emit({
name: action.name,
@ -800,8 +1130,8 @@
target,
...(effectiveIntent ? { intent: effectiveIntent } : {})
});
}}
>▶ play</button>
}}>▶ play</button
>
</td>
</tr>
{/each}
@ -814,35 +1144,103 @@
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>
<p data-uix-section-desc>Selectors at <code>src/uix/eidos/components/table/table.css</code>. The visual data-attrs (<code>data-size</code>, <code>data-variant</code>, <code>data-color</code>, <code>data-block</code>, <code>data-striped</code>, <code>data-hoverable</code>, <code>data-sticky-header</code>, <code>data-scrollable</code>) live on the <code>&lt;div data-table-root&gt;</code> wrapper so the semantic <code>&lt;table&gt;</code> stays clean.</p>
<p data-uix-section-desc>
Selectors at <code>src/uix/eidos/components/table/table.css</code>. The visual data-attrs (<code
>data-size</code
>, <code>data-variant</code>, <code>data-color</code>, <code>data-block</code>,
<code>data-striped</code>, <code>data-hoverable</code>, <code>data-sticky-header</code>,
<code>data-scrollable</code>) live on the <code>&lt;div data-table-root&gt;</code> wrapper
so the semantic <code>&lt;table&gt;</code> stays clean.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Selector</th><th>Source</th><th>What it paints</th></tr></thead>
<tbody>
<tr><td class="name">[data-table-root]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Wrapper — radius, chrome, scroll cap.</td></tr>
<tr><td class="name">[data-table-root][data-variant]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Border + background per variant.</td></tr>
<tr><td class="name">[data-table-root][data-size]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Row block-size, cell padding, font-size scale.</td></tr>
<tr><td class="name">[data-table-root][data-color]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Accent token cascade.</td></tr>
<tr><td class="name">[data-table-root][data-striped]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Alternate-row tint.</td></tr>
<tr><td class="name">[data-table-root][data-hoverable]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Row hover highlight.</td></tr>
<tr><td class="name">[data-table-root][data-sticky-header] [data-table-column-header]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Sticky header inside scroll region.</td></tr>
<tr><td class="name">[data-table-column-header][data-sortable]</td><td><span data-uix-tag data-kind="soma">soma</span></td><td>Cursor + sort indicator hook.</td></tr>
<tr><td class="name">[data-table-column-header][data-sorted]</td><td><span data-uix-tag data-kind="soma">soma</span></td><td>Active sort accent colour.</td></tr>
<tr><td class="name">[data-table-row][data-selected]</td><td><span data-uix-tag data-kind="soma">soma</span></td><td>Selected-row accent-soft fill.</td></tr>
<tr><td class="name">[data-table-cell][data-pinned]</td><td><span data-uix-tag data-kind="soma">soma</span></td><td>Pinned-cell sticky offset + background.</td></tr>
<tr><td class="name">[data-table-row-detail][data-state]</td><td><span data-uix-tag data-kind="soma">soma</span></td><td>Disclosure panel show/hide.</td></tr>
<tr><td class="name">[data-table-sort-button]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Header sort affordance — bare button + chevron.</td></tr>
<tr
><td class="name">[data-table-root]</td><td
><span data-uix-tag data-kind="eidos">eidos</span></td
><td>Wrapper — radius, chrome, scroll cap.</td></tr
>
<tr
><td class="name">[data-table-root][data-variant]</td><td
><span data-uix-tag data-kind="eidos">eidos</span></td
><td>Border + background per variant.</td></tr
>
<tr
><td class="name">[data-table-root][data-size]</td><td
><span data-uix-tag data-kind="eidos">eidos</span></td
><td>Row block-size, cell padding, font-size scale.</td></tr
>
<tr
><td class="name">[data-table-root][data-color]</td><td
><span data-uix-tag data-kind="eidos">eidos</span></td
><td>Accent token cascade.</td></tr
>
<tr
><td class="name">[data-table-root][data-striped]</td><td
><span data-uix-tag data-kind="eidos">eidos</span></td
><td>Alternate-row tint.</td></tr
>
<tr
><td class="name">[data-table-root][data-hoverable]</td><td
><span data-uix-tag data-kind="eidos">eidos</span></td
><td>Row hover highlight.</td></tr
>
<tr
><td class="name">[data-table-root][data-sticky-header] [data-table-column-header]</td
><td><span data-uix-tag data-kind="eidos">eidos</span></td><td
>Sticky header inside scroll region.</td
></tr
>
<tr
><td class="name">[data-table-column-header][data-sortable]</td><td
><span data-uix-tag data-kind="soma">soma</span></td
><td>Cursor + sort indicator hook.</td></tr
>
<tr
><td class="name">[data-table-column-header][data-sorted]</td><td
><span data-uix-tag data-kind="soma">soma</span></td
><td>Active sort accent colour.</td></tr
>
<tr
><td class="name">[data-table-row][data-selected]</td><td
><span data-uix-tag data-kind="soma">soma</span></td
><td>Selected-row accent-soft fill.</td></tr
>
<tr
><td class="name">[data-table-cell][data-pinned]</td><td
><span data-uix-tag data-kind="soma">soma</span></td
><td>Pinned-cell sticky offset + background.</td></tr
>
<tr
><td class="name">[data-table-row-detail][data-state]</td><td
><span data-uix-tag data-kind="soma">soma</span></td
><td>Disclosure panel show/hide.</td></tr
>
<tr
><td class="name">[data-table-sort-button]</td><td
><span data-uix-tag data-kind="eidos">eidos</span></td
><td>Header sort affordance — bare button + chevron.</td></tr
>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'tokens'}
<TokensPanel component="table" stage={stageRef} />
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>
<p data-uix-section-desc>
Emits a native <code>&lt;table&gt;</code> with <code>aria-label</code> and per-column <code>aria-sort</code>. Rows expose <code>aria-rowindex</code> + <code>aria-selected</code> (when selection is enabled). Detail panels are wired with <code>aria-expanded</code> + <code>aria-controls</code>. Use <code>TreeGrid</code> for true hierarchical rows (arrow-key navigation) — Table is for flat tabular data only.
Emits a native <code>&lt;table&gt;</code> with <code>aria-label</code> and per-column
<code>aria-sort</code>. Rows expose <code>aria-rowindex</code> + <code>aria-selected</code>
(when selection is enabled). Detail panels are wired with <code>aria-expanded</code> +
<code>aria-controls</code>. Use <code>TreeGrid</code> for true hierarchical rows (arrow-key navigation)
— Table is for flat tabular data only.
</p>
<div data-uix-subsection-head>Keyboard</div>
@ -850,11 +1248,31 @@
<table data-uix-table>
<thead><tr><th>Key</th><th>Action</th></tr></thead>
<tbody>
<tr><td><span data-uix-kbd>Tab</span></td><td>Move focus through sort buttons, selectable rows, detail triggers, pagination.</td></tr>
<tr><td><span data-uix-kbd>Enter</span> / <span data-uix-kbd>Space</span> on row</td><td>Toggle selection (when selectionMode ≠ 'none').</td></tr>
<tr><td><span data-uix-kbd>Enter</span> / <span data-uix-kbd>Space</span> on RowDetailTrigger</td><td>Open / close the row's detail panel.</td></tr>
<tr><td><span data-uix-kbd>Click</span> on sort button</td><td>Cycle asc → desc → none.</td></tr>
<tr><td><span data-uix-kbd>Shift+Click</span> on sort button</td><td>Add column to multi-sort (when <code>sorting.multi: true</code>).</td></tr>
<tr
><td><span data-uix-kbd>Tab</span></td><td
>Move focus through sort buttons, selectable rows, detail triggers, pagination.</td
></tr
>
<tr
><td><span data-uix-kbd>Enter</span> / <span data-uix-kbd>Space</span> on row</td><td
>Toggle selection (when selectionMode ≠ 'none').</td
></tr
>
<tr
><td
><span data-uix-kbd>Enter</span> / <span data-uix-kbd>Space</span> on RowDetailTrigger</td
><td>Open / close the row's detail panel.</td></tr
>
<tr
><td><span data-uix-kbd>Click</span> on sort button</td><td
>Cycle asc → desc → none.</td
></tr
>
<tr
><td><span data-uix-kbd>Shift+Click</span> on sort button</td><td
>Add column to multi-sort (when <code>sorting.multi: true</code>).</td
></tr
>
</tbody>
</table>
</div>
@ -864,13 +1282,40 @@
<table data-uix-table>
<thead><tr><th>Part</th><th>Attribute</th><th>Value</th></tr></thead>
<tbody>
<tr><td class="name">Provider</td><td>aria-label</td><td class="type">translated 'Data table'</td></tr>
<tr><td class="name">ColumnHeader</td><td>role</td><td class="type">"columnheader"</td></tr>
<tr><td class="name">ColumnHeader</td><td>aria-sort</td><td class="type">"ascending" | "descending" | "none"</td></tr>
<tr><td class="name">Row</td><td>role / aria-rowindex</td><td class="type">"row" / 1-based</td></tr>
<tr><td class="name">Row</td><td>aria-selected</td><td class="type">"true" | "false" (selection enabled)</td></tr>
<tr><td class="name">Cell</td><td>role / aria-colindex</td><td class="type">"gridcell" / 1-based</td></tr>
<tr><td class="name">RowDetailTrigger</td><td>aria-expanded / aria-controls</td><td class="type">boolean / id of sibling RowDetail</td></tr>
<tr
><td class="name">Provider</td><td>aria-label</td><td class="type"
>translated 'Data table'</td
></tr
>
<tr
><td class="name">ColumnHeader</td><td>role</td><td class="type">"columnheader"</td
></tr
>
<tr
><td class="name">ColumnHeader</td><td>aria-sort</td><td class="type"
>"ascending" | "descending" | "none"</td
></tr
>
<tr
><td class="name">Row</td><td>role / aria-rowindex</td><td class="type"
>"row" / 1-based</td
></tr
>
<tr
><td class="name">Row</td><td>aria-selected</td><td class="type"
>"true" | "false" (selection enabled)</td
></tr
>
<tr
><td class="name">Cell</td><td>role / aria-colindex</td><td class="type"
>"gridcell" / 1-based</td
></tr
>
<tr
><td class="name">RowDetailTrigger</td><td>aria-expanded / aria-controls</td><td
class="type">boolean / id of sibling RowDetail</td
></tr
>
</tbody>
</table>
</div>

Loading…
Cancel
Save

Powered by TurnKey Linux.