diff --git a/docs/audit/theming/README.md b/docs/audit/theming/README.md index a0b984137..b6152b954 100644 --- a/docs/audit/theming/README.md +++ b/docs/audit/theming/README.md @@ -10,9 +10,9 @@ - **Medido**: 2026-08-21 · **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**: **46%** — 2130 de 4680 knobs pasan por un token público del componente -- **Reparto**: público 2130 · privado 675 · global 1291 · literal 584 · sistema transversal 509 _(fuera del ratio)_ -- **Sin token público propio**: 48 · **alcance < 20 %**: 42 · **alcance 100 %**: 7 · **con eje `size`**: 67 +- **Alcance global**: **46%** — 2150 de 4680 knobs pasan por un token público del componente +- **Reparto**: público 2150 · privado 661 · global 1286 · literal 583 · sistema transversal 509 _(fuera del ratio)_ +- **Sin token público propio**: 47 · **alcance < 20 %**: 41 · **alcance 100 %**: 7 · **con eje `size`**: 66 ## Cómo se lee @@ -88,15 +88,15 @@ theming §6.7 lo pone delante (`hover-bg`). Las fichas proponen la forma | 16 | [code-block](./code-block.md) | 26 | 0% | 26 | 0 | | 17 | [tree-view](./tree-view.md) | 26 | 0% | 28 | 0 | | 18 | [mockup](./mockup.md) | 25 | 14% | 29 | 0 | -| 19 | [textarea](./textarea.md) | 25 | 0% | 28 | 0 | -| 20 | [aura](./aura.md) | 24 | 31% | 35 | 24 | +| 19 | [aura](./aura.md) | 24 | 31% | 35 | 24 | +| 20 | [password-field](./password-field.md) | 24 | 31% | 36 | 23 | -## Sin ningún token público propio (48) +## Sin ningún token público propio (47) 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) · [`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) · [`field-langs`](./field-langs.md) · [`heading`](./heading.md) · [`highlight`](./highlight.md) · [`kbd`](./kbd.md) · [`knob`](./knob.md) · [`label`](./label.md) · [`link`](./link.md) · [`link-preview`](./link-preview.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-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) · [`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) · [`field-langs`](./field-langs.md) · [`heading`](./heading.md) · [`highlight`](./highlight.md) · [`kbd`](./kbd.md) · [`knob`](./knob.md) · [`label`](./label.md) · [`link`](./link.md) · [`link-preview`](./link-preview.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) · [`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) @@ -108,7 +108,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [month-grid](./month-grid.md) | 0% | 61 | 0 | 7 | 0 | 5 | 49 | 0 | – | | [year-grid](./year-grid.md) | 0% | 61 | 0 | 7 | 0 | 5 | 49 | 0 | – | | [field-langs](./field-langs.md) | 0% | 41 | 0 | 0 | 31 | 8 | 2 | 0 | – | -| [textarea](./textarea.md) | 0% | 28 | 0 | 16 | 5 | 4 | 3 | 0 | y | | [tree-view](./tree-view.md) | 0% | 28 | 0 | 8 | 14 | 4 | 2 | 0 | y | | [code-block](./code-block.md) | 0% | 26 | 0 | 2 | 24 | 0 | 0 | 0 | – | | [card-group](./card-group.md) | 0% | 24 | 0 | 0 | 18 | 4 | 2 | 0 | y | @@ -226,6 +225,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [radio-group](./radio-group.md) | 77% | 75 | 56 | 10 | 7 | 0 | 2 | 61 | y | | [chat-log](./chat-log.md) | 77% | 42 | 30 | 0 | 4 | 5 | 3 | 31 | – | | [chat-composer](./chat-composer.md) | 79% | 44 | 31 | 0 | 3 | 5 | 5 | 31 | – | +| [textarea](./textarea.md) | 80% | 28 | 20 | 2 | 0 | 3 | 3 | 37 | – | | [gradient-builder](./gradient-builder.md) | 80% | 60 | 45 | 7 | 1 | 3 | 4 | 72 | – | | [chat-message](./chat-message.md) | 82% | 84 | 63 | 2 | 6 | 6 | 7 | 75 | y | | [fab](./fab.md) | 82% | 17 | 14 | 0 | 2 | 1 | 0 | 10 | – | diff --git a/docs/audit/theming/textarea.md b/docs/audit/theming/textarea.md index 911ef7652..fd41aac3b 100644 --- a/docs/audit/theming/textarea.md +++ b/docs/audit/theming/textarea.md @@ -5,52 +5,31 @@ > 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 25 knobs por token público -- **Knobs de apariencia**: 28 — público 0 · privado 16 · global 5 · literal 4 · sistema 3 _(fuera del ratio)_ -- **Contrato hoy** (`lib/recipes/base.ts`): 0 pública(s) · 1 privada(s) forward — `_palette-solid` -- **Eje `size`**: sí · **ficheros**: `textarea.css` +- **Medido**: 2026-08-21 · **Alcance**: **80%** — 20 de 25 knobs por token público +- **Knobs de apariencia**: 28 — público 20 · privado 2 · global 0 · literal 3 · sistema 3 _(fuera del ratio)_ +- **Contrato hoy** (`lib/recipes/base.ts`): 37 pública(s) — `input-font-size-xs`, `input-font-size-sm`, `input-font-size-md`, `input-font-size-lg`, `input-font-size-xl`, `input-padding-inline-xs`, `input-padding-inline-sm`, `input-padding-inline-md`, `input-padding-inline-lg`, `input-padding-inline-xl`, `input-padding-block-xs`, `input-padding-block-sm`, `input-padding-block-md`, `input-padding-block-lg`, `input-padding-block-xl`, `input-font-size`, `input-padding-inline`, `input-padding-block`, `gap`, `input-radius`, `input-border-width`, `input-border`, `hover-input-border`, `focus-input-border`, `invalid-input-border`, `input-bg`, `input-fg`, `input-placeholder-fg`, `input-font-family`, `input-font-weight`, `input-line-height`, `count-font-size`, `count-gap`, `count-fg`, `count-font-family`, `overflow-count-fg`, `overflow-count-font-weight` · 1 privada(s) forward — `_palette-solid` +- **Eje `size`**: no · **ficheros**: `textarea.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 | `textarea.css:88` | `[data-textarea-input]` | `font-family` | `var(--font-family-primary)` | -| 2 | `textarea.css:89` | `[data-textarea-input]` | `font-weight` | `var(--font-weight-medium)` | -| 3 | `textarea.css:111` | `[data-textarea-input][data-invalid]` | `border-color` | `var(--color-threat-solid)` | -| 4 | `textarea.css:142` | `[data-textarea-count]` | `font-family` | `var(--font-family-mono, ui-monospace, monospace)` | -| 5 | `textarea.css:150` | `[data-textarea-count][data-overflow]` | `font-weight` | `var(--font-weight-semibold)` | +_Ninguno._ -### 1.2 A través de un privado (16) +### 1.2 A través de un privado (2) | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `textarea.css:29` | `[data-textarea]` | `gap` | `var(--_textarea-gap)` | -| 2 | `textarea.css:82` | `[data-textarea-input]` | `padding-block` | `var(--_textarea-padding-block)` | -| 3 | `textarea.css:83` | `[data-textarea-input]` | `padding-inline` | `var(--_textarea-padding-inline)` | -| 4 | `textarea.css:84` | `[data-textarea-input]` | `border` | `var(--border-width) solid var(--_textarea-border)` | -| 5 | `textarea.css:85` | `[data-textarea-input]` | `border-radius` | `var(--_textarea-radius)` | -| 6 | `textarea.css:86` | `[data-textarea-input]` | `background` | `var(--_textarea-bg)` | -| 7 | `textarea.css:87` | `[data-textarea-input]` | `color` | `var(--_textarea-color)` | -| 8 | `textarea.css:90` | `[data-textarea-input]` | `font-size` | `var(--_textarea-font-size)` | -| 9 | `textarea.css:91` | `[data-textarea-input]` | `line-height` | `var(--_textarea-line-height)` | -| 10 | `textarea.css:99` | `[data-textarea-input]:hover:not([data-disabled]):not([data-readonly])` | `border-color` | `var(--_textarea-border-hover)` | -| 11 | `textarea.css:103` | `[data-textarea][data-focused] [data-textarea-input]` | `border-color` | `var(--_textarea-border-focus)` | -| 12 | `textarea.css:123` | `[data-textarea][data-variant='ghost'][data-focused] [data-textarea-input]` | `border-color` | `var(--_textarea-border-focus)` | -| 13 | `textarea.css:132` | `[data-textarea-input]::placeholder` | `color` | `var(--_textarea-placeholder)` | -| 14 | `textarea.css:141` | `[data-textarea-count]` | `color` | `var(--_textarea-count-color)` | -| 15 | `textarea.css:143` | `[data-textarea-count]` | `font-size` | `calc(var(--_textarea-font-size) * 0.85)` | -| 16 | `textarea.css:149` | `[data-textarea-count][data-overflow]` | `color` | `var(--_textarea-count-overflow)` | - -### 1.3 Literales (4) +| 1 | `textarea.css:71` | `[data-textarea][data-focused] [data-textarea-input]` | `border-color` | `var(--_textarea-border-focus)` | +| 2 | `textarea.css:91` | `[data-textarea][data-variant='ghost'][data-focused] [data-textarea-input]` | `border-color` | `var(--_textarea-border-focus)` | + +### 1.3 Literales (3) | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `textarea.css:30` | `[data-textarea]` | `inline-size` | `100%` | -| 2 | `textarea.css:79` | `[data-textarea-input]` | `inline-size` | `100%` | -| 3 | `textarea.css:139` | `[data-textarea-count]` | `gap` | `0.25em` | -| 4 | `textarea.css:145` | `[data-textarea-count]` | `line-height` | `1` | +| 1 | `textarea.css:19` | `[data-textarea]` | `inline-size` | `100%` | +| 2 | `textarea.css:47` | `[data-textarea-input]` | `inline-size` | `100%` | +| 3 | `textarea.css:113` | `[data-textarea-count]` | `line-height` | `1` | ## 2. Sistema transversal (3) — informativo, fuera del ratio @@ -58,37 +37,22 @@ Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2). | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `textarea.css:106` | `[data-textarea][data-focused] [data-textarea-input]` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` | -| 2 | `textarea.css:128` | `[data-textarea-input][data-disabled]` | `opacity` | `var(--opacity-disabled)` | -| 3 | `textarea.css:154` | `[data-textarea-count-sep]` | `opacity` | `var(--opacity-muted)` | +| 1 | `textarea.css:74` | `[data-textarea][data-focused] [data-textarea-input]` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` | +| 2 | `textarea.css:96` | `[data-textarea-input][data-disabled]` | `opacity` | `var(--opacity-disabled)` | +| 3 | `textarea.css:122` | `[data-textarea-count-sep]` | `opacity` | `var(--opacity-muted)` | ## 3. Privados de la receta — ¿de dónde sale su valor? | privado | declaraciones | valor(es) | origen | ¿deriva de un público? | | --- | ---: | --- | --- | :-: | -| `--_textarea-padding-inline` | 5 | `var(--space-3)`, `var(--space-2)`, `var(--space-4)`, `var(--space-5)` | global | no | -| `--_textarea-padding-block` | 5 | `var(--space-2)`, `var(--space-1)`, `var(--space-1-5)`, `var(--space-3)`, `var(--space-4)` | global | no | -| `--_textarea-gap` | 1 | `var(--space-2)` | global | no | -| `--_textarea-font-size` | 5 | `var(--font-size-md)`, `var(--font-size-xs)`, `var(--font-size-sm)`, `var(--font-size-lg)`, `var(--font-size-xl)` | global | no | -| `--_textarea-line-height` | 1 | `var(--leading-normal)` | global | no | -| `--_textarea-radius` | 1 | `var(--radius-default)` | global | no | -| `--_textarea-border` | 1 | `var(--color-neutral-border)` | global | no | -| `--_textarea-border-hover` | 1 | `var(--color-border-strong)` | global | no | -| `--_textarea-border-focus` | 2 | `var(--focus-ring-color)`, `var(--_textarea-palette-solid)` | private, system | no | -| `--_textarea-bg` | 1 | `var(--color-surface-default)` | global | no | -| `--_textarea-color` | 1 | `var(--color-content-primary)` | global | no | -| `--_textarea-placeholder` | 1 | `var(--color-content-muted)` | global | no | -| `--_textarea-count-color` | 1 | `var(--color-content-muted)` | global | no | -| `--_textarea-count-overflow` | 1 | `var(--color-threat-text)` | global | no | +| `--_textarea-border-focus` | 2 | `var(--textarea-focus-input-border)`, `var(--_textarea-palette-solid)` | private, public | no | | `--_textarea-resize` | 4 | `vertical`, `none`, `horizontal`, `both` | literal | no | Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inline del wrapper): `--_textarea-palette-solid`. ## 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` (35) +### 4.1 Tokens a declarar en `lib/recipes/base.ts` (2) 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,63 +62,17 @@ no distingue lo que debería — se marca `⚠`. | token (`--textarea-…`) | scope TSC | valor propuesto | usos | | --- | --- | --- | ---: | -| `input-fg` | `root` | ⚠ `var(--color-content-primary)` / `var(--color-content-muted)` | 2 | -| `input-border` | `root` | ⚠ `var(--focus-ring-color)` / `var(--_textarea-palette-solid)` | 2 | -| `ghost-input-border` | `root` | ⚠ `var(--focus-ring-color)` / `var(--_textarea-palette-solid)` | 2 | -| `count-fg` | `root` | ⚠ `var(--color-content-muted)` / `var(--color-threat-text)` | 2 | -| `gap` | `root` | `var(--space-2)` | 1 | -| `input-padding-block-md` | `size:md` | `var(--space-2)` | 1 | -| `input-padding-block-xs` | `size:xs` | `var(--space-1)` | 1 | -| `input-padding-block-sm` | `size:sm` | `var(--space-1-5)` | 1 | -| `input-padding-block-lg` | `size:lg` | `var(--space-3)` | 1 | -| `input-padding-block-xl` | `size:xl` | `var(--space-4)` | 1 | -| `input-padding-inline-md` | `size:md` | `var(--space-3)` | 1 | -| `input-padding-inline-xs` | `size:xs` | `var(--space-2)` | 1 | -| `input-padding-inline-sm` | `size:sm` | `var(--space-2)` | 1 | -| `input-padding-inline-lg` | `size:lg` | `var(--space-4)` | 1 | -| `input-padding-inline-xl` | `size:xl` | `var(--space-5)` | 1 | -| `input-radius` | `root` | `var(--radius-default)` | 1 | -| `input-bg` | `root` | `var(--color-surface-default)` | 1 | -| `input-font-family` | `root` | `var(--font-family-primary)` | 1 | -| `input-font-weight` | `root` | `var(--font-weight-medium)` | 1 | -| `input-font-size-md` | `size:md` | `var(--font-size-md)` | 1 | -| `input-font-size-xs` | `size:xs` | `var(--font-size-xs)` | 1 | -| `input-font-size-sm` | `size:sm` | `var(--font-size-sm)` | 1 | -| `input-font-size-lg` | `size:lg` | `var(--font-size-lg)` | 1 | -| `input-font-size-xl` | `size:xl` | `var(--font-size-xl)` | 1 | -| `input-line-height` | `root` | `var(--leading-normal)` | 1 | -| `hover-input-border` | `root` | `var(--color-border-strong)` | 1 | -| `invalid-input-border` | `root` | `var(--color-threat-solid)` | 1 | -| `count-gap` | `root` | `0.25em` | 1 | -| `count-font-family` | `root` | `var(--font-family-mono, ui-monospace, monospace)` | 1 | -| `count-font-size-md` | `size:md` | `var(--font-size-md)` | 1 | -| `count-font-size-xs` | `size:xs` | `var(--font-size-xs)` | 1 | -| `count-font-size-sm` | `size:sm` | `var(--font-size-sm)` | 1 | -| `count-font-size-lg` | `size:lg` | `var(--font-size-lg)` | 1 | -| `count-font-size-xl` | `size:xl` | `var(--font-size-xl)` | 1 | -| `count-font-weight` | `root` | `var(--font-weight-semibold)` | 1 | - -### 4.2 Sin nombre mecánico (4) +| `input-border` _(ya existe)_ | `root` | ⚠ `var(--textarea-focus-input-border)` / `var(--_textarea-palette-solid)` | 2 | +| `ghost-input-border` | `root` | ⚠ `var(--textarea-focus-input-border)` / `var(--_textarea-palette-solid)` | 2 | + +### 4.2 Sin nombre mecánico (3) - **⚠ 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)** — 1: `border`. - **⚠ 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: `line-height`. -### 4.3 Avisos sobre los tokens propuestos (13) - -- **el privado `--_textarea-gap` debe pasar a leer este público (o desaparecer)** — `--textarea-gap` -- **el privado `--_textarea-padding-block` debe pasar a leer este público (o desaparecer)** — `--textarea-input-padding-block-md`, `--textarea-input-padding-block-xs`, `--textarea-input-padding-block-sm`, `--textarea-input-padding-block-lg`, `--textarea-input-padding-block-xl` -- **el privado `--_textarea-padding-inline` debe pasar a leer este público (o desaparecer)** — `--textarea-input-padding-inline-md`, `--textarea-input-padding-inline-xs`, `--textarea-input-padding-inline-sm`, `--textarea-input-padding-inline-lg`, `--textarea-input-padding-inline-xl` -- **el privado `--_textarea-radius` debe pasar a leer este público (o desaparecer)** — `--textarea-input-radius` -- **el privado `--_textarea-bg` debe pasar a leer este público (o desaparecer)** — `--textarea-input-bg` -- **el privado `--_textarea-color` debe pasar a leer este público (o desaparecer)** — `--textarea-input-fg` -- **el privado `--_textarea-font-size` debe pasar a leer este público (o desaparecer)** — `--textarea-input-font-size-md`, `--textarea-input-font-size-xs`, `--textarea-input-font-size-sm`, `--textarea-input-font-size-lg`, `--textarea-input-font-size-xl`, `--textarea-count-font-size-md`, `--textarea-count-font-size-xs`, `--textarea-count-font-size-sm`, `--textarea-count-font-size-lg`, `--textarea-count-font-size-xl` -- **el privado `--_textarea-line-height` debe pasar a leer este público (o desaparecer)** — `--textarea-input-line-height` -- **el privado `--_textarea-border-hover` debe pasar a leer este público (o desaparecer)** — `--textarea-hover-input-border` +### 4.3 Avisos sobre los tokens propuestos (1) + - **el privado `--_textarea-border-focus` debe pasar a leer este público (o desaparecer)** — `--textarea-input-border`, `--textarea-ghost-input-border` -- **el privado `--_textarea-placeholder` debe pasar a leer este público (o desaparecer)** — `--textarea-input-fg` -- **el privado `--_textarea-count-color` debe pasar a leer este público (o desaparecer)** — `--textarea-count-fg` -- **el privado `--_textarea-count-overflow` debe pasar a leer este público (o desaparecer)** — `--textarea-count-fg` ### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4) @@ -168,6 +86,67 @@ no distingue lo que debería — se marca `⚠`. -_(pendiente — lo escribe el autor; se conserva al regenerar)_ +**Medido 2026-08-21** (sonda ×2 sobre el mismo código = 0 diffs en 1.247 +valores · 8 estados). La §4 propone 35 tokens y **cuatro de ellos son +colisiones que hay que partir o retirar**, más un error que cambiaría el +default. Corregido queda en 37 claves. + +1. **`count-font-size-{k}` × 5 BORRARÍA el factor 0.85 — es el error grave de + la propuesta.** El CSS calcula `calc(var(--_textarea-font-size) * 0.85)` + (con su anotación `/* literal: */`), y la §4 propone cinco tokens con el + font-size del INPUT crudo: el contador crecería un 18 % en las cinco + tallas. D-TH.5 lo prohíbe. La corrección: **UN** token + `count-font-size` que conserva el `calc` sobre el público del input, así + sigue la talla sin repetir la escala. Mismo reparto que `tree-grid.guide-fg` + — el knob es el valor, la construcción se queda en la receta. +2. **`input-fg` ⚠ son DOS knobs**: la tinta del texto + (`--color-content-primary`) y la del placeholder + (`--color-content-muted`). El clasificador las fundió por caer ambas sobre + la parte `input`. → `input-fg` + `input-placeholder-fg` (precedente + exacto: `command.input-fg` / `command.input-placeholder-fg`). +3. **`count-fg` ⚠ son DOS knobs**: reposo (`content-muted`) y rebasado + (`threat-text`). → `count-fg` + `overflow-count-fg`; `overflow` es valor + de `data-overflow`, declarado en el morfo (`textarea.ts:163`), así que + entra como modificador delante. +4. **`input-border` ⚠ NO son dos knobs, y su nombre está mal.** Los dos + valores son UNA variable (`--_textarea-border-focus`) con su swap de + paleta: `--focus-ring-color` de base y `--_textarea-palette-solid` cuando + hay `data-color`. El swap es el forward THM-2 y **se queda privado**. Y el + knob es el borde de FOCO, no el de reposo → `focus-input-border`. El de + reposo (`--color-neutral-border`) es otro token, que la §4 ni listó porque + viaja dentro del atajo `border:`. +5. **`ghost-input-border` ⚠ se RETIRA de la propuesta**: la regla ghost lee + exactamente la misma `--_textarea-border-focus` que la regla base. Un token + propio sería un duplicado con dos nombres para un valor. +6. **`border` shorthand se parte** en `input-border-width` + `input-border` + (color) — molde `command.input-border` / `input-border-width`. +7. **Qué casa con el bundle, medido** (`generated/base.css:409-450`): + - `font-size` casa **1:1 en las cinco** (`font-size-{k}` ES + `--size-{k}-font-size`) → van al bundle. + - `padding-inline` casa en sm/md/lg/xl pero **NO en xs**: el textarea pone + `--space-2` (8px) donde el bundle dice `--space-1-5` (6px). + - `padding-block` **no casa en ninguna salvo xs**: sm 6px vs 4px, md 8px vs + 6px, lg 12px vs 8px. El campo multi-línea respira más que un control de + una línea, y es correcto. + Los dos ejes de spacing van **verbatim en `--space-*`**, con la desviación + escrita: el guard del bundle gobierna control-height / font-size / + icon-size, no el spacing (precedente `table.cell-padding-*` y + `tree-grid.row-padding-*`). Los resueltos **sin `parts`** — `data-size` se + estampa en `[data-textarea]` y el input hereda; molde `command`. +8. **Fuera del contrato, a propósito**: los dos `inline-size: 100%` y el + `line-height: 1` del count (identidad, D-TH.2); `--_textarea-resize`, que + no es apariencia sino el enum de comportamiento que el morfo conduce; y el + forward de paleta. +9. **Alcance final 80 %** (20 de 25) — mi propia predicción de «≈100 %» era + errónea y la medición la corrigió. **Cero globales** y cero privados de + valor: los 16 de la propuesta leen ya su público. Lo que queda son las + DOS declaraciones que consumen `--_textarea-border-focus` (base y ghost), + que el censo cuenta como `private` porque el privado sobrevive **a + propósito** — es el conmutador de una variable con dos fuentes (el token + público y el forward de paleta), y aplanarlo exigiría duplicar la regla + por color. Es el mismo techo de `listbox` (68 % por consumir bien su capa) + y el mismo que §13 registra para los forwards THM-2: la métrica penaliza + hacer lo correcto. Más los tres literales de identidad (dos `100%` y el + `line-height: 1` del contador), fuera del perímetro por D-TH.2. diff --git a/docs/next-features.md b/docs/next-features.md index 3476e8244..b29076455 100644 --- a/docs/next-features.md +++ b/docs/next-features.md @@ -464,10 +464,13 @@ existiera. Se anotan porque el eje entero se apoya en esas mediciones. viejo: **22 falsos negativos de 26 «no effect»** en F2-A, con tres causas (el clic-foco, escribir sólo en `[data-{c}]` cuando las partes cuelgan de `{c}-root`, y no leer `::before`/`::after`). Las tres, arregladas. -- **El centinela no ve `::placeholder`.** `::before`/`::after` ya se leen - (arreglo 2026-08-21 — el aro de buffering de media-player y el spinner de - feed adjudican solos); `::placeholder` sigue fuera de `getComputedStyle` y - queda como excepción adjudicada por token en el ledger. +- ~~**El centinela no ve `::placeholder`.**~~ **ERA FALSO, y ARREGLADO + 2026-08-21** (al tokenizar `textarea`): `getComputedStyle(el, '::placeholder')` + SÍ devuelve el valor — medido, el color centinela volvió tal cual. La + afirmación anterior nunca se comprobó y le costó a + `command.input-placeholder-fg` una entrada de ledger «verificada a mano» que + ha salido STALE y se ha borrado. El pseudo entra ahora en el snapshot del + guard, junto a `::before`/`::after`. - **El centinela no ve lo que pinta un componente COMPUESTO.** Los tokens que media-player reenvía al Slider (`--media-player-track` → `--slider-track-bg`) pintan en nodos `[data-slider-*]`, fuera del espacio de @@ -613,5 +616,30 @@ Lo que `grid-list` añadió (2026-08-21): contra los selectores reales de cada capa (`listbox`, `menubar` y `navigation-menu` están en la misma entrada y tampoco aparecen en el fichero). +Lo que `textarea` añadió (2026-08-21): + +- **⚠ El borde de foco de `textarea` no se ve al hacer CLIC — sólo con Tab.** + Medido: la regla de hover `[data-textarea-input]:hover:not([data-disabled]):not([data-readonly])` + pesa **(0,4,0)** y la de foco `[data-textarea][data-focused] [data-textarea-input]` + **(0,3,0)**, así que con foco y puntero a la vez gana el hover — y hacer clic + deja el puntero encima por definición. Verificado con las dos reglas + apuntando a colores distintos: sale el del hover. Debajo, `[data-invalid]` + pesa **(0,2,0)**: un campo inválido pierde su borde rojo tanto al enfocarlo + como al pasar el ratón. El anillo de foco del SISTEMA (`outline`) sí se ve + siempre, así que no es un fallo de accesibilidad, pero el orden de las tres + reglas de `border-color` está invertido respecto a lo que significan. + Arreglarlo mueve píxel: no entra en el commit de tokenización. +- ~~**El guard mide con el PUNTERO encima del componente.**~~ **ARREGLADO + 2026-08-21**: `reopen()` corre antes de CADA token y hace clic en + `[data-{c}-input]` cuando el componente no tiene parte `content` — el + `blur()` que añadió la revisión adversarial quita el foco pero **no el + puntero**, así que `:hover` seguía casando durante toda la corrida y toda + regla de hover pisaba a sus vecinas de reposo. Es la mitad que aquel arreglo + dejó atrás. En `textarea` costaba tres falsos negativos + (`input-border`, `focus-input-border`, `invalid-input-border`); con + `page.mouse.move(0, 0)` tras cada blur, `input-border` revive solo. Corridos + los diez componentes con ledger: ningún otro cambia, un único STALE + (`command.input-placeholder-fg`, por el arreglo del `::placeholder`). + **Deps**: ninguna. Son mejoras del instrumental del eje, ejecutables cuando estorben. diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index e694a8ab5..ec8ed0343 100644 --- a/scripts/theming-sentinel-exceptions.ts +++ b/scripts/theming-sentinel-exceptions.ts @@ -21,8 +21,6 @@ export const SENTINEL_EXCEPTIONS: Record> = { command: { 'focus-input-border': 'consumed only under :focus-visible, which the guard blurs away ON PURPOSE (the click-focus poisoning fix); focused by hand → reaches (rgb(9,9,9))', - 'input-placeholder-fg': - '::placeholder — getComputedStyle cannot read it; verified by hand 2026-08-20 (oklch muted → rgb(1,2,3))', 'loading-height': 'the loading bar is conditional and the demo does not mount it', 'loading-fg': 'the loading bar is conditional and the demo does not mount it', 'viewport-padding': 'the non-ScrollArea viewport variant is not mounted in the demo', @@ -116,6 +114,16 @@ export const SENTINEL_EXCEPTIONS: Record> = { 'sentinel-spinner-reduced-duration': 'behind prefers-reduced-motion; emulated reduce context → reaches (4s → 11.5s)' }, + textarea: { + 'focus-input-border': + 'consumed only under [data-focused], which the guard blurs away ON PURPOSE (the click-focus poisoning fix); focused by hand with the pointer parked → reaches (rgb(4,5,6))', + 'invalid-input-border': + 'consumed only under [data-invalid]; the demo boots valid. Forced by hand 2026-08-21 → reaches (rgb(7,8,9))', + 'overflow-count-fg': + 'the count only carries [data-overflow] past maxLength (280 chars); forced → reaches (rgb(13,14,15))', + 'overflow-count-font-weight': + 'the count only carries [data-overflow] past maxLength (280 chars); forced → reaches (font-weight 333)' + }, 'grid-list': { 'invalid-border': 'consumed only under [data-invalid]; the demo boots with its invalid control off, so the rule never matches at measurement. Toggled by hand 2026-08-21 → reaches (oklch(0.9555 0.0207 13.86) → rgb(1,2,3))' diff --git a/scripts/theming-sentinel.ts b/scripts/theming-sentinel.ts index d1389c5c3..f509748d1 100644 --- a/scripts/theming-sentinel.ts +++ b/scripts/theming-sentinel.ts @@ -157,9 +157,21 @@ async function main() { } } await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur?.()); + // ...and PARK THE POINTER. blur() drops the focus but Playwright leaves the + // cursor where it clicked, so `:hover` keeps matching — and a hover rule + // usually outweighs the rest / focus / invalid ones it shares a node with + // (measured on textarea 2026-08-21: hover (0,4,0) beats focus (0,3,0) beats + // invalid (0,2,0) beats rest (0,1,0), so THREE rest-state tokens read dead). + // Same class as the click-focus false negative above, and the half that fix + // left behind. The per-token hover pass re-hovers on purpose further down. + await page.mouse.move(0, 0); const dead: string[] = []; const live: string[] = []; + // Runs before EVERY token. A component with no `content` part (textarea, + // any flat control) falls through to the click branch on every single key, + // so the pointer parking below is not belt-and-braces — without it the + // cursor sits on the input for the whole run. const reopen = async () => { const open = await page.locator(`[data-${component}-content]`).count(); if (open) return; @@ -170,6 +182,7 @@ async function main() { await el.click({ timeout: 1000 }); await page.waitForTimeout(250); await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur?.()); + await page.mouse.move(0, 0); return; } catch { /* keep trying */ @@ -200,8 +213,13 @@ async function main() { const own = (props as string[]).map((p) => cs[p as never]).join('|'); const b = getComputedStyle(n, '::before'); const a = getComputedStyle(n, '::after'); + // ::placeholder IS readable through getComputedStyle - measured + // 2026-08-21 on textarea (the sentinel colour came straight back). + // The next-features §13 note saying it is not was wrong, and it had + // already cost command.input-placeholder-fg a hand-checked entry. + const ph = getComputedStyle(n, '::placeholder'); const pseudo = (props as string[]) - .map((p) => `${b[p as never]}~${a[p as never]}`) + .map((p) => `${b[p as never]}~${a[p as never]}~${ph[p as never]}`) .join('|'); return own + '#' + pseudo; }) diff --git a/src/uix/eidos/components/textarea/README.md b/src/uix/eidos/components/textarea/README.md index 84314e050..674970847 100644 --- a/src/uix/eidos/components/textarea/README.md +++ b/src/uix/eidos/components/textarea/README.md @@ -94,6 +94,61 @@ Referencias: [Ant Input.TextArea](https://ant.design/components/input#inputtexta - **Radio desacoplado de la talla** (modelo Radix): `--radius-default` constante entre tallas. +## Talla y tema + +**Guard R-5.4** (`npm run theming:sentinel -- textarea `): 33/37 tokens +mueven un computed en vivo; los cuatro que callan están adjudicados uno a uno +con su porqué medido en `scripts/theming-sentinel-exceptions.ts` (los cuatro +son estados que la demo no monta: foco, invalid y las dos del contador +rebasado). Corrido 2026-08-21. + +Contrato en `lib/recipes/base.ts > textarea` (37 claves). Alcance del censo: +**80 %** (era 0 % antes del 2026-08-21), con **cero globales y cero literales +de valor** — los 16 privados que había leen ya su público. + +**El eje `size`** (xs..xl) se estampa en `[data-textarea]` y el input hereda, +así que los tres nombres resueltos van SIN `parts`; el CSS ya no lleva bloques +`[data-size]`. Qué casa con el bundle, medido: la tipografía **sí, 1:1 en las +cinco** (por eso lee `--size-{k}-font-size`); el spacing **no** — +`padding-inline` casa salvo en xs (8px aquí, 6px allí) y `padding-block` sólo +en xs; de sm en adelante este campo es más suelto (6/8/12/16). Es correcto: un +campo multi-línea respira más que un control de una línea, y el spacing queda +fuera del guard del bundle, que gobierna control-height / font-size / +icon-size. El `radius` está DESACOPLADO de la talla a propósito (modelo Radix). + +**El contador es UN token, no cinco.** Su tipo es proporcional al campo +(`calc(var(--textarea-input-font-size) * 0.85)`), y como el font-size del input +ya resuelve por talla, la escala se escribe una sola vez. La propuesta generada +pedía cinco `count-font-size-{k}` con el tipo del input crudo: habría **borrado +el 0.85** y agrandado el contador un 18 % en las cinco tallas. El default no se +mueve (D-TH.5). El token necesita `scope: 'host'` porque lee un resuelto que +sólo existe en `[data-textarea]` — emitido en `:root` el `var()` resolvería +donde su dependencia no está, y el contrato de scope de la receta lo caza. + +**Dos privados sobreviven, y no son deuda**: `--_textarea-resize` no es +apariencia sino el enum de comportamiento que conduce el morfo, y +`--_textarea-border-focus` es el conmutador de una variable con DOS fuentes — +el token público `focus-input-border` de base, y el forward de paleta THM-2 +cuando hay `data-color`. Aplanarlo exigiría duplicar la regla por color. Ese +privado es todo el 20 % que falta: el censo lo cuenta como `private`, el mismo +techo que `listbox` paga por consumir bien su capa (`next-features.md` §13). +No hay `ghost-input-border`: la regla ghost lee ese mismo conmutador, y un +token propio sería un valor con dos nombres. + +### El borde de foco se ve con Tab, no con el clic + +Medido el 2026-08-21, **anterior a la tokenización**: las tres reglas que +pintan `border-color` sobre el input están ordenadas al revés de lo que +significan. El hover pesa **(0,4,0)**, el foco **(0,3,0)** y el invalid +**(0,2,0)**, así que con el puntero encima —que es exactamente donde queda tras +hacer clic— gana el hover, y un campo inválido pierde su borde rojo tanto al +enfocarlo como al pasar el ratón. El anillo de foco del SISTEMA (`outline`) sí +se ve siempre, así que no es un fallo de accesibilidad. Arreglarlo mueve píxel +y no pertenece a este eje: registrado en `next-features.md` §13. + +`R-5.1 exception: inline-size: 100%` (×2) y el `line-height: 1` del contador — +identidad de layout, fuera del perímetro de knob (D-TH.2). + ## Gaps | Gap | Disposición | Detalle | diff --git a/src/uix/eidos/components/textarea/textarea.css b/src/uix/eidos/components/textarea/textarea.css index 141ca2ce8..626cdfffe 100644 --- a/src/uix/eidos/components/textarea/textarea.css +++ b/src/uix/eidos/components/textarea/textarea.css @@ -1,57 +1,25 @@ /* TextArea — multi-line text input. Skin layered over `