uix(form): temable — 71 % → 86 %, y 24 claves suyas NO PINTAN por una cascada

Diez claves nuevas (54 → 64), un literal firmado, veinte adjudicaciones medidas
y un hallazgo que espera firma. Diff de computed 0 sobre 832 valores en 7
estados; capturas 2× antes/después.

LAS DIEZ IBAN A LOS PRIMITIVOS A PELO

El resumen de errores (error-summary-{bg,fg,border}) —es el señalizador de nivel
de página y su superficie, su tinta y su borde SON el tono risk—, el acento de
formulario inválido (invalid-accent, un privado con UNA sola fuente, no un
conmutador), las tres superficies del árbol de AutoFields, las dos del widget y
el subrayado a media tinta de los enlaces del resumen (link-underline-fg, el par
del -offset que ya existía).

⚠ R-5.3 CAZÓ MI NOMBRE EN EL ACTO: lo acuñé como link-underline-color y la tinta
es `fg` (theming §6.7 r7, firmado el 20). Renombrado antes de commitear. El
guard de nombres funciona — es el vocabulario DIMENSIONAL el que no cubre (ver
badge, hoy mismo).

⚠⚠ VEINTICUATRO CLAVES DEL CONTRATO NO PINTAN, Y ES UNA CASCADA

[data-form-submit], [data-form-reset] y los dos botones del array llevan también
data-button porque COMPONEN el Button canónico, y [data-button] casa con la
MISMA especificidad (0,1,0) pero carga DESPUÉS: gana button.css. Medido
propiedad a propiedad sobre el nodo real —altura, padding-inline, tamaño y peso
de letra, interlineado, hueco, radio, grosor de borde, el par de transición y la
opacidad deshabilitada—: NINGUNA se mueve.

Es el defecto que gradient-picker documentó (36 de sus 45 claves) y que
emoji-picker encontró hoy en su trigger. Allí la respuesta fue RETIRAR las
declaraciones muertas; aquí son 24 claves públicas, y eso es una decisión del
autor, no del que ejecuta. Quedan adjudicadas POR PATRÓN (una razón
arquitectónica escrita una vez) y anotadas en §13 con la pregunta de fondo, que
es la misma de §12.9 con el plano overlay: qué manda, el componente compuesto o
la receta que lo compone. Si mandan las recetas, se sube la especificidad y no
se retira nada; si mandan los compuestos, hay ~63 claves públicas que retirar en
tres componentes.

LAS DEMÁS ADJUDICACIONES SON RAMAS QUE LA DEMO NO MONTA

El layout grid (forzado alcanza — y ojo: su grid-template-columns sólo CAMBIA si
el valor altera el número de columnas; con 90px pasa de una columna de 502px a
cuatro de 110,5), el resumen de errores VACÍO (sin <ul> que indentar ni <a> que
subrayar) y toda la rama de ARRAY de AutoFields: el modo `auto` de la demo monta
group y field, nunca array, item ni widget.

Fuera por doctrina: los tres privados del panel son un CONMUTADOR (variante
plain contra panel) y los cuatro --_form-action-* leen el forward de paleta
THM-2.

VERIFICACIÓN

Sonda antes/después 0 diffs (832 valores · 7 estados) · R-5.4 22/64 con las 20 +
el patrón adjudicados, exit 0 · component:audit PASS (tras el renombrado) ·
--names 0 desviadas · censo --only 86 % · eidos-lint 36 morfo-backed / 0 invalid
/ 0 class-hooks · rtl:check 0 · docs:check 0 · suite eidos 439/440 (el rojo
conocido) · el ledger sigue en sus 63 hunks de prettier previos.

Rama con cuatro sesiones en paralelo: índice construido desde HEAD + sólo mis
hunks (base.ts por bloque), verificado con `git diff --cached`. No entra
docs/audit/theming/README.md — es un agregado global que ahora mismo mezcla
trabajo en vuelo de otras sesiones.

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

@ -5,51 +5,43 @@
> Vista de conjunto: [README](./README.md) · método y protocolo:
> [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7.
- **Medido**: 2026-08-23 · **Alcance**: **71%** — 36 de 51 knobs por token público
- **Knobs de apariencia**: 52 — público 36 · privado 8 · global 5 · literal 2 · sistema 1 · excepción 0 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 54 pública(s) — `gap-xs`, `gap-sm`, `gap-md`, `gap-lg`, `gap-xl`, `font-family`, `fg`, `radius`, `border-width`, `panel-padding`, `panel-border`, `panel-bg`, `panel-shadow`, `grid-min`, `grid-column-gap`, `action-height-xs`, `action-height-sm`, `action-height-md`, `action-height-lg`, `action-height-xl`, `action-padding-inline-xs`, `action-padding-inline-sm`, `action-padding-inline-md`, `action-padding-inline-lg`, `action-padding-inline-xl`, `action-font-size-xs`, `action-font-size-sm`, `action-font-size-md`, `action-font-size-lg`, `action-font-size-xl`, `action-font-weight`, `action-line-height`, `action-gap`, `action-radius`, `action-border-width`, `error-summary-padding`, `error-summary-border-width`, `error-summary-radius`, `error-summary-font-size`, `error-summary-line-height`, `error-summary-list-indent`, `link-underline-offset`, `auto-fields-gap`, `auto-fields-array-padding`, `auto-fields-border-width`, `auto-fields-radius`, `auto-fields-item-gap`, `auto-fields-item-padding`, `widget-padding-inline`, `widget-radius`, `widget-border-width`, `transition-duration`, `transition-ease`, `disabled-opacity` · 4 privada(s) forward — `_palette-solid`, `_palette-solid-hover`, `_palette-border`, `_palette-contrast`
- **Medido**: 2026-08-23 · **Alcance**: **86%** — 43 de 50 knobs por token público
- **Knobs de apariencia**: 51 — público 43 · privado 7 · global 0 · literal 0 · sistema 1 · excepción 1 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 64 pública(s) — `gap-xs`, `gap-sm`, `gap-md`, `gap-lg`, `gap-xl`, `font-family`, `fg`, `radius`, `border-width`, `panel-padding`, `panel-border`, `panel-bg`, `panel-shadow`, `grid-min`, `grid-column-gap`, `action-height-xs`, `action-height-sm`, `action-height-md`, `action-height-lg`, `action-height-xl`, `action-padding-inline-xs`, `action-padding-inline-sm`, `action-padding-inline-md`, `action-padding-inline-lg`, `action-padding-inline-xl`, `action-font-size-xs`, `action-font-size-sm`, `action-font-size-md`, `action-font-size-lg`, `action-font-size-xl`, `action-font-weight`, `action-line-height`, `action-gap`, `action-radius`, `action-border-width`, `error-summary-padding`, `error-summary-border-width`, `error-summary-radius`, `error-summary-font-size`, `error-summary-line-height`, `error-summary-bg`, `error-summary-fg`, `error-summary-border`, `invalid-accent`, `error-summary-list-indent`, `link-underline-fg`, `link-underline-offset`, `auto-fields-gap`, `auto-fields-array-padding`, `auto-fields-border-width`, `auto-fields-radius`, `auto-fields-array-bg`, `auto-fields-item-bg`, `auto-fields-border`, `auto-fields-item-gap`, `auto-fields-item-padding`, `widget-padding-inline`, `widget-bg`, `widget-border`, `widget-radius`, `widget-border-width`, `transition-duration`, `transition-ease`, `disabled-opacity` · 4 privada(s) forward — `_palette-solid`, `_palette-solid-hover`, `_palette-border`, `_palette-contrast`
- **Eje `size`**: sí · **ficheros**: `form.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (5)
### 1.1 Directo a primitivo global (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `form.css:196` | `[data-form-error-summary]` | `background` | `var(--color-risk-track)` |
| 2 | `form.css:197` | `[data-form-error-summary]` | `color` | `var(--color-risk-text)` |
| 3 | `form.css:232` | `[data-form-auto-fields-array]` | `background` | `var(--color-surface-muted)` |
| 4 | `form.css:248` | `[data-form-auto-fields-item]` | `background` | `var(--color-surface-default)` |
| 5 | `form.css:256` | `[data-form-auto-fields-widget]` | `background` | `var(--color-surface-default)` |
_Ninguno._
### 1.2 A través de un privado (8)
### 1.2 A través de un privado (7)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `form.css:16` | `[data-form]` | `padding` | `var(--_form-padding)` |
| 2 | `form.css:19` | `[data-form]` | `background` | `var(--_form-bg)` |
| 3 | `form.css:20` | `[data-form]` | `box-shadow` | `var(--_form-shadow)` |
| 4 | `form.css:82` | `[data-form][data-invalid] [data-form-error-summary]` | `border-color` | `var(--_form-invalid-accent)` |
| 5 | `form.css:103` | `[data-form-submit], [data-form-reset], [data-form-auto-fields-array-add], [data-form-auto-fields-array-remove]` | `background` | `var(--_form-action-bg)` |
| 6 | `form.css:104` | `[data-form-submit], [data-form-reset], [data-form-auto-fields-array-add], [data-form-auto-fields-array-remove]` | `box-shadow` | `var(--_form-action-shadow)` |
| 7 | `form.css:105` | `[data-form-submit], [data-form-reset], [data-form-auto-fields-array-add], [data-form-auto-fields-array-remove]` | `color` | `var(--_form-action-color)` |
| 8 | `form.css:173` | `[data-form-submit]:not(:disabled):hover, [data-form-reset]:not(:disabled):hover, [data-form-auto-fields-array-add]:not(:disabled):hover, [data-form-auto-fields-array-remove]:not(:disabled):hover` | `background` | `var(--_form-action-bg-hover)` |
| 4 | `form.css:103` | `[data-form-submit], [data-form-reset], [data-form-auto-fields-array-add], [data-form-auto-fields-array-remove]` | `background` | `var(--_form-action-bg)` |
| 5 | `form.css:104` | `[data-form-submit], [data-form-reset], [data-form-auto-fields-array-add], [data-form-auto-fields-array-remove]` | `box-shadow` | `var(--_form-action-shadow)` |
| 6 | `form.css:105` | `[data-form-submit], [data-form-reset], [data-form-auto-fields-array-add], [data-form-auto-fields-array-remove]` | `color` | `var(--_form-action-color)` |
| 7 | `form.css:173` | `[data-form-submit]:not(:disabled):hover, [data-form-reset]:not(:disabled):hover, [data-form-auto-fields-array-add]:not(:disabled):hover, [data-form-auto-fields-array-remove]:not(:disabled):hover` | `background` | `var(--_form-action-bg-hover)` |
### 1.3 Literales (2)
### 1.3 Literales (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `form.css:14` | `[data-form]` | `inline-size` | `100%` |
| 2 | `form.css:214` | `[data-form-error-summary] a` | `text-decoration-color` | `color-mix(in srgb, currentColor 48%, transparent)` |
_Ninguno._
### 1.4 Excepciones firmadas (0) — fuera del ratio
### 1.4 Excepciones firmadas (1) — fuera del ratio
Literales que llevan su anotación `/* literal: <razón> */` en la propia
declaración: la válvula de recipe-contract §3, la misma que honra
`component-audit`. **Una desviación firmada no es deuda** — se listan para que la
razón se lea, no para acuñarlas.
_Ninguno._
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `form.css:14` | `[data-form]` | `inline-size` | `100%` |
## 2. Sistema transversal (1) — informativo, fuera del ratio
Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
@ -70,7 +62,7 @@ Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
| `--_form-action-height` | 8 | `var(--form-action-height-md)`, `var(--form-action-height-xs)`, `var(--form-action-height-sm)`, `var(--form-action-height-lg)`, `var(--form-action-height-xl)` | public | **sí** |
| `--_form-action-padding-inline` | 8 | `var(--form-action-padding-inline-md)`, `var(--form-action-padding-inline-xs)`, `var(--form-action-padding-inline-sm)`, `var(--form-action-padding-inline-lg)`, `var(--form-action-padding-inline-xl)` | public | **sí** |
| `--_form-action-font-size` | 8 | `var(--form-action-font-size-md)`, `var(--form-action-font-size-xs)`, `var(--form-action-font-size-sm)`, `var(--form-action-font-size-lg)`, `var(--form-action-font-size-xl)` | public | **sí** |
| `--_form-invalid-accent` | 1 | `var(--color-risk-border)` | global | no |
| `--_form-invalid-accent` | 1 | `var(--form-invalid-accent)` | public | **sí** |
| `--_form-action-bg` | 4 | `var(--color-neutral-element)`, `var(--_form-palette-solid)`, `transparent` | global, literal, private | no |
| `--_form-action-bg-hover` | 4 | `var(--color-neutral-hover)`, `var(--_form-palette-solid-hover)`, `color-mix(in srgb, var(--_form-action-border) 10%, transparent)`, `color-mix(in srgb, var(--color-neutral-track) 70%, transparent)` | global, private | no |
| `--_form-action-border` | 3 | `var(--color-neutral-border)`, `var(--_form-palette-border)`, `transparent` | global, literal, private | no |
@ -83,7 +75,7 @@ Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inl
- **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` (13)
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (6)
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
@ -98,25 +90,17 @@ no distingue lo que debería — se marca `⚠`.
| `hover-submit-bg` | `root` | ⚠ `var(--color-neutral-hover)` / `var(--_form-palette-solid-hover)` / `color-mix(in srgb, var(--_form-action-border) 10%, transparent)` / `color-mix(in srgb, var(--color-neutral-track) 70%, transparent)` | 4 |
| `bg` | `root` | ⚠ `transparent` / `var(--form-panel-bg)` | 2 |
| `shadow` | `root` | ⚠ `none` / `var(--form-panel-shadow)` | 2 |
| `invalid-error-summary-border` | `root` | `var(--color-risk-border)` | 1 |
| `submit-shadow` | `root` | `none` | 1 |
| `error-summary-bg` | `root` | `var(--color-risk-track)` | 1 |
| `error-summary-fg` | `root` | `var(--color-risk-text)` | 1 |
| `error-summary-underline` | `root` | `color-mix(in srgb, currentColor 48%, transparent)` | 1 |
| `auto-fields-array-bg` | `root` | `var(--color-surface-muted)` | 1 |
| `auto-fields-item-bg` | `root` | `var(--color-surface-default)` | 1 |
| `auto-fields-widget-bg` | `root` | `var(--color-surface-default)` | 1 |
### 4.2 Sin nombre mecánico (2)
- **⚠ 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`.
- **⚠ 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`.
### 4.3 Avisos sobre los tokens propuestos (7)
### 4.3 Avisos sobre los tokens propuestos (6)
- **el privado `--_form-bg` debe pasar a leer este público (o desaparecer)** — `--form-bg`
- **el privado `--_form-shadow` debe pasar a leer este público (o desaparecer)** — `--form-shadow`
- **el privado `--_form-invalid-accent` debe pasar a leer este público (o desaparecer)** — `--form-invalid-error-summary-border`
- **el privado `--_form-action-bg` debe pasar a leer este público (o desaparecer)** — `--form-submit-bg`
- **el privado `--_form-action-shadow` debe pasar a leer este público (o desaparecer)** — `--form-submit-shadow`
- **el privado `--_form-action-color` debe pasar a leer este público (o desaparecer)** — `--form-submit-fg`
@ -134,6 +118,50 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**EJECUTADO 2026-08-23 — 71 % → 86 %.** Diez claves nuevas (54 → 64), un literal
firmado, veinte adjudicaciones medidas **y un hallazgo que espera firma**. Diff
de computed **0 sobre 832 valores en 7 estados**.
**Las diez iban a los primitivos a pelo**: el resumen de errores
(`error-summary-{bg,fg,border}` — es el señalizador de nivel de página y su
superficie, su tinta y su borde SON el tono risk), el acento de formulario
inválido (`invalid-accent`, un privado con UNA sola fuente, no un conmutador),
las tres superficies del árbol de AutoFields, las dos del widget y el subrayado a
media tinta de los enlaces del resumen (`link-underline-fg`, el par del
`-offset` que ya existía).
⚠ **R-5.3 cazó mi nombre en el acto**: lo acuñé como `link-underline-color` y la
tinta es `fg` (theming §6.7 r7, firmado el 2026-08-20). Renombrado antes de
commitear. El guard de nombres funciona — es el vocabulario DIMENSIONAL el que
no cubre (ver `badge`, hoy mismo).
⚠⚠ **VEINTICUATRO claves del contrato NO PINTAN, y es una cascada, no un valor.**
`[data-form-submit]`, `[data-form-reset]` y los dos botones del array llevan
también `data-button` porque COMPONEN el `Button` canónico, y `[data-button]`
casa con la MISMA especificidad (0,1,0) pero carga DESPUÉS: gana `button.css`.
Medido propiedad a propiedad sobre el nodo real —altura, `padding-inline`,
tamaño y peso de letra, interlineado, hueco, radio, grosor de borde, el par de
transición y la opacidad deshabilitada—: **ninguna se mueve**. Es el defecto que
`gradient-picker` documentó (36 de sus 45 claves) y que `emoji-picker` encontró
hoy en su trigger. Allí la respuesta fue RETIRAR las declaraciones muertas; aquí
son **24 claves públicas**, y retirar eso es una decisión del autor, no del que
ejecuta. Quedan **adjudicadas por PATRÓN** en el ledger (una razón
arquitectónica escrita una vez) y anotadas para firma.
**Las adjudicaciones restantes son ramas que la demo no monta**: el layout
`grid` (forzado alcanza — y ojo, su `grid-template-columns` sólo CAMBIA si el
valor altera el número de columnas: con 90px pasa de una columna de 502px a
cuatro de 110,5), el resumen de errores VACÍO (sin `<ul>` que indentar ni `<a>`
que subrayar) y toda la rama de ARRAY de AutoFields — el modo `auto` de la demo
monta `group` y `field`, nunca `array`, `item` ni `widget`.
**Lo que queda fuera por doctrina**: los tres privados del panel son un
CONMUTADOR (variante `plain` contra `panel`) y los cuatro `--_form-action-*`
leen el forward de paleta THM-2.
**Verificación**: sonda antes/después **0 diffs** (832 valores · 7 estados) ·
R-5.4 **22/64** con las 20 + el patrón adjudicados, exit 0 · `component:audit`
PASS (tras el renombrado) · `--names` 0 desviadas · censo 86 % · `eidos-lint` 36
morfo-backed / **0 invalid, 0 class-hooks** · capturas 2×.
<!-- veredicto:end -->

@ -1319,6 +1319,17 @@ Lo que `chart` destapó (2026-08-22):
marcador tras el clic) en vez de dormir un número fijo. Mientras tanto, su
entrada de ledger sale STALE en las corridas donde alcanza: está escrito en la
propia razón para que nadie la borre por eso.
- **⚠⚠ UN COMPONENTE COMPUESTO GANA A LA RECETA QUE LO COMPONE, y ya van TRES**
(medido 2026-08-23). Cuando una receta estiliza un nodo que también lleva
`data-button` / `data-popover-trigger`, los dos selectores casan a la MISMA
especificidad (0,1,0) y decide el ORDEN DE CARGA — que gana el componente
canónico. Censo de lo medido: `gradient-picker` 36 claves de 45 (F2-A, se
retiraron), `emoji-picker` 3 (retiradas hoy) y **`form` 24** (adjudicadas, no
retiradas: son demasiadas para decidirlo sin firma). La pregunta de fondo es
la misma de §12.9 con el plano `overlay`: **qué manda, el componente compuesto
o la receta que lo compone**. Si la respuesta es «la receta», el arreglo es
subir la especificidad de esas reglas y no retirar nada; si es «el compuesto»,
hay ~63 claves públicas que retirar en tres componentes.
- **R-5.3 no guarda el vocabulario DIMENSIONAL** (medido 2026-08-23 en `badge`).
Cubre la ranura de tinta (`fg` vs `color`) y la posición del modificador, pero
no que una clave POR TALLA se llame `…-height-{k}` y no

@ -87,6 +87,33 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
'border-fg':
'same opt-in ring; forced data-bordered on the real chip → reaches (oklch(0.931 0 0) → rgb(1, 2, 3))'
},
form: {
// 2026-08-23.
'grid-min':
"only under [data-layout='grid'], and the computed template only CHANGES when the value alters the column count: forced the layout and 90px -> reaches (502px -> four 110.5px columns)",
'error-summary-list-indent':
'the summary renders EMPTY in the demo (no errors), so it has no <ul> to indent',
'link-underline-offset': 'same empty summary: no <a> inside it',
'auto-fields-array-padding':
"the array branch needs a schema field of type array; the demo's `auto` mode mounts group + field only",
'auto-fields-border-width': 'same array branch',
'auto-fields-radius': 'same array branch',
'auto-fields-item-gap': 'same array branch (the item exists only inside an array)',
'auto-fields-item-padding': 'same array branch',
'widget-padding-inline': 'the widget is the array toolbar affordance; same missing branch',
'widget-radius': 'same missing branch',
'widget-border-width': 'same missing branch',
'grid-column-gap':
"the demo runs layout='stack'; forced data-layout=grid -> reaches (20px -> 37px)",
'invalid-accent':
'only under [data-form][data-invalid]; forced on the form -> reaches (oklch(0.8059 0.1123 59) -> rgb(1,2,3)) on the summary border',
'link-underline-fg': 'same empty summary as the offset above: no <a> to underline',
'auto-fields-array-bg': 'same array branch the demo never mounts',
'auto-fields-item-bg': 'same array branch',
'auto-fields-border': 'same array branch (the group and the field do not draw it)',
'widget-bg': 'same missing branch',
'widget-border': 'same missing branch'
},
dialog: {
// 2026-08-23. Seven tokens of states the demo does not mount: it shows a
// middle-centred `md` dialog, open and neutral. All forced on the real
@ -1031,6 +1058,12 @@ export const SENTINEL_PATTERN_EXCEPTIONS: Array<{
/^(primary|secondary|neutral|affirm|fulfill|risk|threat|loss)-(track|border|solid|text|contrast)$/,
reason: PALETTE_SUPERSEDED
},
{
component: 'form',
pattern: /^(action-|transition-|disabled-opacity$)/,
reason:
'SUPERSEDED by the composed Button: [data-form-submit] & co. also carry `data-button`, and `[data-button]` matches at the SAME specificity (0,1,0) but loads LATER, so button.css paints the whole action chrome. Measured 2026-08-23, eight properties one by one — height, padding-inline, font-size, font-weight, line-height, gap, radius, border-width — plus the transition pair and the disabled opacity: NOT ONE of them moves. Same defect gradient-picker documented (36 of its 45 keys) and emoji-picker found in its trigger; the answer there was to retire the dead declarations, and here that is 24 public keys — a decision for the author, so they are adjudicated meanwhile (next-features 13)'
},
{
component: 'surface',
pattern: /^(primary|secondary|neutral|affirm|fulfill|risk|threat|loss)-(track|solid|contrast)$/,

@ -212,6 +212,16 @@ const COMPONENT_OVERRIDES: Record<
// no attribute of its own: without this the guard measures ONE node (the root)
// and 27 of its 37 tokens read dead. Measured 2026-08-22.
prose: { extraNodes: '[data-prose] *' },
// Half its contract lives in parts the default stage does not mount: the
// AutoFields tree (array, item, widget) only exists in the demo's `auto`
// mode, and the panel chrome only under `variant='panel'`. 18 of 54 tokens
// moved before this.
form: {
prepareWith: [
"[data-uix-control]:has([data-uix-control-label]:text-matches('^demo mode')) [data-uix-chip]:text-is('auto')"
],
sweepAttr: { attr: 'data-variant', values: ['plain', 'panel'] }
},
// A shared LAYER, not a component: no wrapper, no route of its own. It paints
// the stepper affordances of NumberField / CssField, so it is measured inside
// one of them.

@ -80,6 +80,36 @@ invalid value, unless a demo explicitly documents an initially invalid state.
AutoFields structural parts are declared in `formMorfo` because their selectors
are part of the recipe. They do not create new semantic events.
## Talla y tema
**64 claves públicas** en `lib/recipes/base.ts` + cuatro forwards de paleta THM-2
(`_palette-*`, partes `submit` y `reset`). La pestaña **Tokens** de su demo las
resuelve en vivo.
Lo que entró el **2026-08-23** (71 % → 86 %), diez claves que iban a los
primitivos a pelo: el resumen de errores (`error-summary-{bg,fg,border}`), el
acento de formulario inválido (`invalid-accent`), las tres superficies del árbol
de AutoFields (`auto-fields-{array-bg,item-bg,border}`), las dos del widget
(`widget-{bg,border}`) y el subrayado a media tinta de los enlaces del resumen
(`link-underline-fg`, el par del `-offset` que ya existía).
⚠⚠ **VEINTICUATRO claves del contrato NO PINTAN, y no es deuda de valor: es una
cascada.** `[data-form-submit]`, `[data-form-reset]` y los dos botones del array
llevan también `data-button` porque COMPONEN el `Button` canónico, y
`[data-button]` casa con la MISMA especificidad (0,1,0) pero carga DESPUÉS: gana
`button.css`. Medido propiedad a propiedad —altura, padding-inline, tamaño y
peso de letra, interlineado, hueco, radio, grosor de borde, el par de transición
y la opacidad deshabilitada—: **ninguna se mueve**. Es el defecto que
`gradient-picker` documentó (36 de sus 45 claves) y que `emoji-picker` encontró
en su trigger; allí la respuesta fue RETIRAR las declaraciones muertas, y aquí
eso son 24 claves públicas — una decisión que firma el autor, así que de momento
quedan adjudicadas en el ledger y anotadas.
**Lo que queda fuera por doctrina**: los tres privados del panel
(`--_form-padding` / `-bg` / `-shadow`) son un CONMUTADOR (dos fuentes: la
variante `plain` y la `panel`), y los cuatro `--_form-action-*` leen el forward
de paleta THM-2, que no se acuña.
## Comparativa
| Capability | Radix Form | Ark Field | React Aria | shadcn-svelte | UIX |

@ -11,7 +11,7 @@
display: flex;
flex-direction: column;
gap: var(--_form-gap);
inline-size: 100%;
inline-size: 100%; /* literal: identity — a form IS its container's width */
min-inline-size: 0;
padding: var(--_form-padding);
border: var(--form-border-width) solid var(--_form-border);
@ -75,7 +75,7 @@
indicator is a low-emphasis affordance so the page-level "errors
present" state reads at a glance. */
[data-form][data-invalid] {
--_form-invalid-accent: var(--color-risk-border);
--_form-invalid-accent: var(--form-invalid-accent);
}
[data-form][data-invalid] [data-form-error-summary] {
@ -191,10 +191,10 @@
[data-form-error-summary] {
padding: var(--form-error-summary-padding);
border: var(--form-error-summary-border-width) solid var(--color-risk-border);
border: var(--form-error-summary-border-width) solid var(--form-error-summary-border);
border-radius: var(--form-error-summary-radius);
background: var(--color-risk-track);
color: var(--color-risk-text);
background: var(--form-error-summary-bg);
color: var(--form-error-summary-fg);
font-size: var(--form-error-summary-font-size);
line-height: var(--form-error-summary-line-height);
}
@ -211,7 +211,7 @@
[data-form-error-summary] a {
color: inherit;
text-decoration-color: color-mix(in srgb, currentColor 48%, transparent);
text-decoration-color: var(--form-link-underline-fg);
text-underline-offset: var(--form-link-underline-offset);
}
@ -227,9 +227,9 @@
[data-form-auto-fields-array] {
padding: var(--form-auto-fields-array-padding);
border: var(--form-auto-fields-border-width) solid var(--color-border-default);
border: var(--form-auto-fields-border-width) solid var(--form-auto-fields-border);
border-radius: var(--form-auto-fields-radius);
background: var(--color-surface-muted);
background: var(--form-auto-fields-array-bg);
}
[data-form-auto-fields-array-toolbar],
@ -245,15 +245,15 @@
gap: var(--form-auto-fields-item-gap);
padding: var(--form-auto-fields-item-padding);
border-radius: var(--form-auto-fields-radius);
background: var(--color-surface-default);
background: var(--form-auto-fields-item-bg);
}
[data-form-auto-fields-widget] {
min-block-size: var(--_form-action-height);
padding-inline: var(--form-widget-padding-inline);
border: var(--form-widget-border-width) solid var(--color-border-default);
border: var(--form-widget-border-width) solid var(--form-widget-border);
border-radius: var(--form-widget-radius);
background: var(--color-surface-default);
background: var(--form-widget-bg);
color: var(--form-fg);
font: inherit;
}

@ -1282,15 +1282,25 @@
--form-error-summary-radius: var(--radius-md);
--form-error-summary-font-size: var(--size-sm-font-size);
--form-error-summary-line-height: var(--leading-ui);
--form-error-summary-bg: var(--color-risk-track);
--form-error-summary-fg: var(--color-risk-text);
--form-error-summary-border: var(--color-risk-border);
--form-invalid-accent: var(--color-risk-border);
--form-error-summary-list-indent: var(--space-4);
--form-link-underline-fg: color-mix(in srgb, currentColor 48%, transparent);
--form-link-underline-offset: 0.18em;
--form-auto-fields-gap: var(--space-3);
--form-auto-fields-array-padding: var(--space-3);
--form-auto-fields-border-width: var(--border-width);
--form-auto-fields-radius: var(--radius-md);
--form-auto-fields-array-bg: var(--color-surface-muted);
--form-auto-fields-item-bg: var(--color-surface-default);
--form-auto-fields-border: var(--color-border-default);
--form-auto-fields-item-gap: var(--space-2);
--form-auto-fields-item-padding: var(--space-3);
--form-widget-padding-inline: var(--space-3);
--form-widget-bg: var(--color-surface-default);
--form-widget-border: var(--color-border-default);
--form-widget-radius: var(--radius-md);
--form-widget-border-width: var(--border-width);
--form-transition-duration: var(--duration-fast);

@ -1373,15 +1373,33 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
'error-summary-radius': 'var(--radius-md)',
'error-summary-font-size': 'var(--size-sm-font-size)',
'error-summary-line-height': 'var(--leading-ui)',
// The summary IS the page-level error signal: its surface, its ink and its
// border are the risk tone, and they were read straight from the palette.
'error-summary-bg': 'var(--color-risk-track)',
'error-summary-fg': 'var(--color-risk-text)',
'error-summary-border': 'var(--color-risk-border)',
// The low-emphasis form-level invalid accent (a private with ONE source,
// under [data-form][data-invalid] — not a switch).
'invalid-accent': 'var(--color-risk-border)',
'error-summary-list-indent': 'var(--space-4)',
// The summary's links underline at half ink — the pair of the offset that
// already lived here.
'link-underline-fg': 'color-mix(in srgb, currentColor 48%, transparent)',
'link-underline-offset': '0.18em',
'auto-fields-gap': 'var(--space-3)',
'auto-fields-array-padding': 'var(--space-3)',
'auto-fields-border-width': 'var(--border-width)',
'auto-fields-radius': 'var(--radius-md)',
// The AutoFields tree paints three surfaces and shares one border colour;
// all four went straight to the primitives.
'auto-fields-array-bg': 'var(--color-surface-muted)',
'auto-fields-item-bg': 'var(--color-surface-default)',
'auto-fields-border': 'var(--color-border-default)',
'auto-fields-item-gap': 'var(--space-2)',
'auto-fields-item-padding': 'var(--space-3)',
'widget-padding-inline': 'var(--space-3)',
'widget-bg': 'var(--color-surface-default)',
'widget-border': 'var(--color-border-default)',
'widget-radius': 'var(--radius-md)',
'widget-border-width': 'var(--border-width)',
'transition-duration': 'var(--duration-fast)',

@ -12,10 +12,11 @@
type ValidationBehaviour
} from '$libs/forms';
import { object, string, number, boolean, enumOf, pipe, min, max, email, integer, meta } from '$sium/core';
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';
type DemoMode = 'manual' | 'auto';
type ProfileRole = 'designer' | 'developer' | 'researcher';
type ProfileValues = {
@ -430,6 +431,9 @@
<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 === 'tokens'} onclick={() => (tab = 'tokens')}>
Tokens <span data-uix-tab-count>64</span>
</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
@ -710,6 +714,10 @@
</section>
{/if}
{#if tab === 'tokens'}
<TokensPanel component="form" stage={stageRef ?? undefined} />
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>

Loading…
Cancel
Save

Powered by TurnKey Linux.