From 15eb9c56c94bf14ba9a4f944a29fc5aa8b02bfeb Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 22 Aug 2026 18:57:09 +0200 Subject: [PATCH] =?UTF-8?q?uix(chart):=20temable=20=E2=80=94=209=20%=20?= =?UTF-8?q?=E2=86=92=2049=20%,=20y=20la=20ficha=20ped=C3=ADa=2071=20claves?= =?UTF-8?q?=20de=20las=20que=2020=20llevaban=20el=20mismo=20valor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 22 claves nuevas fusionadas en el bloque existente (17 → 39). Censo chart 9 % → **49 %**; global **56 % → 57 %** (2.638 → 2.675 públicos; <20 % 19 → 18). LO QUE DECIDIÓ LA MEDICIÓN, no la ficha: Las 84 filas fuera de alcance colapsaban a **26 valores distintos** — 9× `content-secondary`, 9× `content-primary`, 9× `100%`, 6× `font-size-sm`, 6× `font-size-xs`, 6× `font-weight-medium`, 5× `surface-default`, 4× `radius-sm`. Nombrar por parte habría dado cinco claves para el pelo que separa marcas y nueve para la tinta de anotación: la inflación de `spinner` con otra cara. **El texto no recibe tokens de chart.** Toma su ROL DE TAMAÑO de los estilos con nombre del canon —`caption` (xs) para las anotaciones, `label` (sm) para el texto de cromo— y su tinta y su peso de las ranuras del sistema. Acuñar `--chart-label-font-size` sería tipografía por componente, prohibido el mismo día para text-effects. Precedente de consumir la capa: `code-block`, `kbd`, `announce`. Doce declaraciones cambian de `var(--font-size-xs|sm)` a `var(--style-caption|label-font-size)`, mismo valor. **La receta sólo posee la tinta de lo que NO es texto**, cinco claves y cada una un concepto que el CSS repite: `axis-fg` (eje + rejilla smith + conector del funnel), `grid-fg` (rejilla + radar), `separator-fg` (el pelo de superficie entre marcas contiguas: porción, punto activo, punto smith, pétalo, segmento), `smith-boundary-fg` y `track-bg` (raíl del bar-list y del gauge). Más 17 de geometría de cromo (gap, los pares row/column-gap, swatch, track, bar-segment). **Incumplimiento heredado, corregido**: el CSS leía la coordenada CRUDA `var(--control-height-sm, var(--space-7))` —lo que `recipe-css-contract` prohíbe— con un respaldo muerto. El contrato guarda el bundle `var(--size-sm-control-height)`, 1:1 con él. **Identidad fuera de contrato**: los nueve `100%` («ocupa a tu padre») y el `min-inline-size: max-content` del tooltip, misma clase ya adjudicada en `date-picker`. Por eso el techo es 49 %, y está escrito en el veredicto §5. EL INSTRUMENTO ESTABA CIEGO DE TRES MANERAS, arregladas ANTES de medir: 1. **La superficie de chart vive en DIECISÉIS rutas de demo**, una por tipo: `/chart` monta 58 nodos de los 302 de la página y el resto vive en rutas hermanas. El guard gana `urls`: un token está VIVO si ALGUNA superficie del componente lo sigue, y cada URL sólo re-prueba lo pendiente (el caso de una sola ruta cuesta exactamente lo que costaba). De paso `picker-shell` recibe la suya: su ruta propia es un 404 y se mide dentro del date-picker — pasa de 0/31 con seis sin adjudicar a 6/31 sin tener que saberlo. 2. **El guard no leía NINGUNA propiedad SVG.** Sin `fill` / `stroke` / `strokeWidth` / `fillOpacity` / `r` / `rx`, doce de las 39 claves de un componente que dibuja en SVG leían muertas estando vivas. Misma clase que el hueco de `filter` / `backdrop-filter` de ayer. 3. **El valor centinela se elegía por SUBCADENA**: `slice-stroke-width` recibía `rgb(1, 2, 3)` por contener «stroke» —inválido para una longitud— y leía muerta. La dimensión se decide ahora por el FINAL del nombre. Los 28 componentes con ledger re-verificados tras los tres cambios: cero regresiones, cero STALE. (Ojo: doce a la vez contra el mismo dev server dan falsos exit=1 por timeout; repetir a solas antes de creerse una regresión.) Gates: sonda **0 diffs en 161.850 valores computados** (16 rutas × 7 estados, 892 nodos en reposo) · centinela **37/39**, con `transition-duration` y `-ease` adjudicadas midiéndolas sin la congelación (0,12s → 11,5s · cubic-bezier → steps(4)) · censo --only 49 % · --names 39/39 conformes · component-audit PASS · eidos-lint 0 invalid · suite eidos con el único rojo conocido (`skin-media-player`) · rtl:check 0 · docs:check 0 · `check` con 72 errores globales y 0 en los ficheros tocados. Abre (→ next-features §13): el censo **penaliza consumir la capa tipográfica** si no eres un primitivo —las doce declaraciones correctas siguen contando `global`—, que es la nota de `listbox` con una segunda ocurrencia; y el número del gauge no tiene estilo con nombre que lo represente (un readout de dato no es un `h2`), así que sigue leyendo `--font-size-xl`. Co-Authored-By: Claude Opus 5 --- docs/audit/theming/README.md | 18 +- docs/audit/theming/chart.md | 271 ++++++++------- docs/next-features.md | 22 ++ docs/process/CONTINUE-theming.md | 28 +- docs/process/PLAN-theming.md | 55 +++ scripts/theming-sentinel-exceptions.ts | 6 + scripts/theming-sentinel.ts | 417 +++++++++++++---------- src/uix/eidos/components/chart/README.md | 48 ++- src/uix/eidos/components/chart/chart.css | 109 +++--- src/uix/eidos/generated/base.css | 22 ++ src/uix/eidos/lib/recipes/base.ts | 40 ++- 11 files changed, 662 insertions(+), 374 deletions(-) diff --git a/docs/audit/theming/README.md b/docs/audit/theming/README.md index 28a2fbef5..24b8060ba 100644 --- a/docs/audit/theming/README.md +++ b/docs/audit/theming/README.md @@ -10,9 +10,9 @@ - **Medido**: 2026-08-22 · **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**: **56%** — 2638 de 4679 knobs pasan por un token público del componente -- **Reparto**: público 2638 · privado 338 · global 1145 · literal 558 · sistema transversal 509 _(fuera del ratio)_ -- **Sin token público propio**: 24 · **alcance < 20 %**: 19 · **alcance 100 %**: 17 · **con eje `size`**: 58 +- **Alcance global**: **57%** — 2675 de 4682 knobs pasan por un token público del componente +- **Reparto**: público 2675 · privado 338 · global 1115 · literal 554 · sistema transversal 509 _(fuera del ratio)_ +- **Sin token público propio**: 24 · **alcance < 20 %**: 18 · **alcance 100 %**: 17 · **con eje `size`**: 58 ## Cómo se lee @@ -72,11 +72,11 @@ theming §6.7 lo pone delante (`hover-bg`). Las fichas proponen la forma | ---: | --- | ---: | ---: | ---: | ---: | | 1 | [palabras](./palabras.md) | 363 | 22% | 471 | 0 | | 2 | [chronos](./chronos.md) | 235 | 1% | 245 | 9 | -| 3 | [chart](./chart.md) | 80 | 9% | 92 | 17 | -| 4 | [proof-of-human](./proof-of-human.md) | 77 | 15% | 95 | 20 | -| 5 | [prose](./prose.md) | 73 | 1% | 74 | 2 | -| 6 | [time-range-picker](./time-range-picker.md) | 67 | 20% | 88 | 22 | -| 7 | [time-picker](./time-picker.md) | 47 | 18% | 59 | 16 | +| 3 | [proof-of-human](./proof-of-human.md) | 77 | 15% | 95 | 20 | +| 4 | [prose](./prose.md) | 73 | 1% | 74 | 2 | +| 5 | [time-range-picker](./time-range-picker.md) | 67 | 20% | 88 | 22 | +| 6 | [time-picker](./time-picker.md) | 47 | 18% | 59 | 16 | +| 7 | [chart](./chart.md) | 46 | 49% | 95 | 39 | | 8 | [color-picker](./color-picker.md) | 39 | 51% | 82 | 54 | | 9 | [field-langs](./field-langs.md) | 39 | 0% | 41 | 0 | | 10 | [date-range-picker](./date-range-picker.md) | 30 | 40% | 53 | 22 | @@ -118,7 +118,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [chronos](./chronos.md) | 1% | 245 | 3 | 21 | 202 | 12 | 7 | 9 | y | | [prose](./prose.md) | 1% | 74 | 1 | 0 | 43 | 30 | 0 | 2 | y | | [nav-tree](./nav-tree.md) | 4% | 25 | 1 | 0 | 17 | 6 | 1 | 1 | – | -| [chart](./chart.md) | 9% | 92 | 8 | 0 | 66 | 14 | 4 | 17 | – | | [result](./result.md) | 14% | 22 | 3 | 0 | 15 | 4 | 0 | 8 | – | | [proof-of-human](./proof-of-human.md) | 15% | 95 | 14 | 17 | 29 | 31 | 4 | 20 | – | | [menubar](./menubar.md) | 15% | 27 | 4 | 6 | 16 | 0 | 1 | 5 | y | @@ -148,6 +147,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [navigation-menu](./navigation-menu.md) | 44% | 38 | 15 | 1 | 13 | 5 | 4 | 26 | y | | [skeleton](./skeleton.md) | 45% | 11 | 5 | 5 | 0 | 1 | 0 | 9 | – | | [popover](./popover.md) | 46% | 51 | 22 | 4 | 22 | 0 | 3 | 36 | y | +| [chart](./chart.md) | 49% | 95 | 45 | 0 | 36 | 10 | 4 | 39 | – | | [text-scramble](./text-scramble.md) | 50% | 4 | 2 | 0 | 0 | 2 | 0 | 0 | – | | [split-button](./split-button.md) | 50% | 2 | 1 | 0 | 1 | 0 | 0 | 1 | y | | [color-picker](./color-picker.md) | 51% | 82 | 40 | 6 | 22 | 11 | 3 | 54 | y | diff --git a/docs/audit/theming/chart.md b/docs/audit/theming/chart.md index 3e8def3dd..e67b9dca5 100644 --- a/docs/audit/theming/chart.md +++ b/docs/audit/theming/chart.md @@ -5,106 +5,72 @@ > 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**: **9%** — 8 de 88 knobs por token público -- **Knobs de apariencia**: 92 — público 8 · privado 0 · global 66 · literal 14 · sistema 4 _(fuera del ratio)_ -- **Contrato hoy** (`lib/recipes/base.ts`): 17 pública(s) — `line-width`, `axis-width`, `grid-width`, `bar-radius`, `point-radius`, `area-opacity`, `slice-stroke-width`, `transition-duration`, `transition-ease`, `tooltip-bg`, `tooltip-border`, `tooltip-radius`, `tooltip-shadow`, `tooltip-padding`, `tooltip-gap`, `crosshair-fg`, `crosshair-width` +- **Medido**: 2026-08-22 · **Alcance**: **49%** — 45 de 91 knobs por token público +- **Knobs de apariencia**: 95 — público 45 · privado 0 · global 36 · literal 10 · sistema 4 _(fuera del ratio)_ +- **Contrato hoy** (`lib/recipes/base.ts`): 39 pública(s) — `line-width`, `axis-width`, `grid-width`, `bar-radius`, `point-radius`, `area-opacity`, `slice-stroke-width`, `transition-duration`, `transition-ease`, `tooltip-bg`, `tooltip-border`, `tooltip-radius`, `tooltip-shadow`, `tooltip-padding`, `tooltip-gap`, `crosshair-fg`, `crosshair-width`, `axis-fg`, `grid-fg`, `separator-fg`, `smith-boundary-fg`, `track-bg`, `track-height`, `track-radius`, `gap`, `legend-row-gap`, `legend-column-gap`, `legend-item-gap`, `swatch-size`, `swatch-radius`, `tooltip-swatch-size`, `bar-list-row-gap`, `bar-list-column-gap`, `bar-segment-gap`, `bar-segment-track-gap`, `bar-segment-track-radius`, `bar-segment-fill-min-inline-size`, `bar-segment-legend-row-gap`, `bar-segment-legend-column-gap` - **Eje `size`**: no · **ficheros**: `chart.css` ## 1. Knobs fuera de alcance -### 1.1 Directo a primitivo global (66) +### 1.1 Directo a primitivo global (36) | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `chart.css:14` | `[data-chart-frame]` | `gap` | `var(--space-2)` | -| 2 | `chart.css:36` | `[data-chart-empty]` | `color` | `var(--color-content-secondary)` | -| 3 | `chart.css:37` | `[data-chart-empty]` | `font-size` | `var(--font-size-sm)` | -| 4 | `chart.css:43` | `[data-chart-axis-line], [data-chart-axis-tick]` | `stroke` | `var(--color-border-default)` | -| 5 | `chart.css:48` | `[data-chart-axis-label]` | `fill` | `var(--color-content-secondary)` | -| 6 | `chart.css:49` | `[data-chart-axis-label]` | `font-size` | `var(--font-size-xs)` | -| 7 | `chart.css:54` | `[data-chart-grid-line]` | `stroke` | `var(--color-border-subtle)` | -| 8 | `chart.css:82` | `[data-chart-slice]` | `stroke` | `var(--color-surface-default)` | -| 9 | `chart.css:98` | `[data-chart-active-dot]` | `stroke` | `var(--color-surface-default)` | -| 10 | `chart.css:123` | `[data-chart-tooltip]` | `font-size` | `var(--font-size-xs)` | -| 11 | `chart.css:124` | `[data-chart-tooltip]` | `color` | `var(--color-content-primary)` | -| 12 | `chart.css:131` | `[data-chart-tooltip-x]` | `font-weight` | `var(--font-weight-medium)` | -| 13 | `chart.css:132` | `[data-chart-tooltip-x]` | `color` | `var(--color-content-primary)` | -| 14 | `chart.css:138` | `[data-chart-tooltip-row]` | `color` | `var(--color-content-secondary)` | -| 15 | `chart.css:143` | `[data-chart-tooltip-swatch]` | `border-radius` | `var(--radius-sm)` | -| 16 | `chart.css:149` | `[data-chart-tooltip-value]` | `color` | `var(--color-content-primary)` | -| 17 | `chart.css:160` | `[data-chart-figure]` | `gap` | `var(--space-2)` | -| 18 | `chart.css:166` | `[data-chart-title]` | `font-size` | `var(--font-size-sm)` | -| 19 | `chart.css:167` | `[data-chart-title]` | `font-weight` | `var(--font-weight-medium)` | -| 20 | `chart.css:168` | `[data-chart-title]` | `color` | `var(--color-content-primary)` | -| 21 | `chart.css:174` | `[data-chart-legend]` | `gap` | `var(--space-1-5) var(--space-4)` | -| 22 | `chart.css:181` | `[data-chart-legend-item]` | `gap` | `var(--space-1-5)` | -| 23 | `chart.css:182` | `[data-chart-legend-item]` | `font-size` | `var(--font-size-xs)` | -| 24 | `chart.css:183` | `[data-chart-legend-item]` | `color` | `var(--color-content-secondary)` | -| 25 | `chart.css:189` | `[data-chart-swatch]` | `border-radius` | `var(--radius-sm)` | -| 26 | `chart.css:202` | `[data-chart-bar-list]` | `gap` | `var(--space-2) var(--space-3)` | -| 27 | `chart.css:212` | `[data-chart-bar-list-label]` | `font-size` | `var(--font-size-sm)` | -| 28 | `chart.css:213` | `[data-chart-bar-list-label]` | `color` | `var(--color-content-primary)` | -| 29 | `chart.css:219` | `[data-chart-bar-list-track]` | `block-size` | `var(--control-height-sm, var(--space-7))` | -| 30 | `chart.css:220` | `[data-chart-bar-list-track]` | `background` | `var(--color-surface-muted)` | -| 31 | `chart.css:221` | `[data-chart-bar-list-track]` | `border-radius` | `var(--radius-sm)` | -| 32 | `chart.css:226` | `[data-chart-bar-list-bar]` | `border-radius` | `var(--radius-sm)` | -| 33 | `chart.css:230` | `[data-chart-bar-list-value]` | `font-size` | `var(--font-size-sm)` | -| 34 | `chart.css:231` | `[data-chart-bar-list-value]` | `color` | `var(--color-content-secondary)` | -| 35 | `chart.css:241` | `[data-chart-bar-segment]` | `gap` | `var(--space-3)` | -| 36 | `chart.css:246` | `[data-chart-bar-segment-track]` | `gap` | `var(--border-width-medium)` | -| 37 | `chart.css:247` | `[data-chart-bar-segment-track]` | `block-size` | `var(--control-height-sm, var(--space-7))` | -| 38 | `chart.css:248` | `[data-chart-bar-segment-track]` | `border-radius` | `var(--radius-md)` | -| 39 | `chart.css:253` | `[data-chart-bar-segment-fill]` | `min-inline-size` | `var(--border-width-thick)` | -| 40 | `chart.css:264` | `[data-chart-bar-segment-legend]` | `gap` | `var(--space-2) var(--space-4)` | -| 41 | `chart.css:269` | `[data-chart-bar-segment-legend-item]` | `gap` | `var(--space-1-5)` | -| 42 | `chart.css:270` | `[data-chart-bar-segment-legend-item]` | `font-size` | `var(--font-size-xs)` | -| 43 | `chart.css:271` | `[data-chart-bar-segment-legend-item]` | `color` | `var(--color-content-secondary)` | -| 44 | `chart.css:274` | `[data-chart-bar-segment-legend-value]` | `color` | `var(--color-content-primary)` | -| 45 | `chart.css:282` | `[data-chart-radar-ring], [data-chart-radar-spoke]` | `stroke` | `var(--color-border-subtle)` | -| 46 | `chart.css:313` | `[data-chart-smith-boundary]` | `stroke` | `var(--color-content-secondary)` | -| 47 | `chart.css:318` | `[data-chart-smith-grid]` | `stroke` | `var(--color-border-default)` | -| 48 | `chart.css:322` | `[data-chart-smith-label]` | `fill` | `var(--color-content-secondary)` | -| 49 | `chart.css:323` | `[data-chart-smith-label]` | `font-size` | `var(--font-size-xs)` | -| 50 | `chart.css:328` | `[data-chart-smith-point]` | `stroke` | `var(--color-surface-default)` | -| 51 | `chart.css:332` | `[data-chart-smith-point-label]` | `fill` | `var(--color-content-primary)` | -| 52 | `chart.css:333` | `[data-chart-smith-point-label]` | `font-size` | `var(--font-size-xs)` | -| 53 | `chart.css:334` | `[data-chart-smith-point-label]` | `font-weight` | `var(--font-weight-medium)` | -| 54 | `chart.css:340` | `[data-chart-polar-petal]` | `stroke` | `var(--color-surface-default)` | -| 55 | `chart.css:353` | `[data-chart-funnel-segment]` | `stroke` | `var(--color-surface-default)` | -| 56 | `chart.css:357` | `[data-chart-funnel-label]` | `font-size` | `var(--font-size-sm)` | -| 57 | `chart.css:358` | `[data-chart-funnel-label]` | `font-weight` | `var(--font-weight-medium)` | -| 58 | `chart.css:363` | `[data-chart-funnel-connector]` | `stroke` | `var(--color-border-default)` | -| 59 | `chart.css:367` | `[data-chart-funnel-side]` | `font-size` | `var(--font-size-sm)` | -| 60 | `chart.css:370` | `[data-chart-funnel-name]` | `fill` | `var(--color-content-primary)` | -| 61 | `chart.css:371` | `[data-chart-funnel-name]` | `font-weight` | `var(--font-weight-medium)` | -| 62 | `chart.css:374` | `[data-chart-funnel-meta]` | `fill` | `var(--color-content-secondary)` | -| 63 | `chart.css:380` | `[data-chart-gauge-track]` | `fill` | `var(--color-surface-muted)` | -| 64 | `chart.css:386` | `[data-chart-gauge-label]` | `fill` | `var(--color-content-primary)` | -| 65 | `chart.css:387` | `[data-chart-gauge-label]` | `font-size` | `var(--font-size-xl)` | -| 66 | `chart.css:388` | `[data-chart-gauge-label]` | `font-weight` | `var(--font-weight-medium)` | +| 1 | `chart.css:46` | `[data-chart-empty]` | `color` | `var(--color-content-secondary)` | +| 2 | `chart.css:47` | `[data-chart-empty]` | `font-size` | `var(--style-label-font-size)` | +| 3 | `chart.css:58` | `[data-chart-axis-label]` | `fill` | `var(--color-content-secondary)` | +| 4 | `chart.css:59` | `[data-chart-axis-label]` | `font-size` | `var(--style-caption-font-size)` | +| 5 | `chart.css:133` | `[data-chart-tooltip]` | `font-size` | `var(--style-caption-font-size)` | +| 6 | `chart.css:134` | `[data-chart-tooltip]` | `color` | `var(--color-content-primary)` | +| 7 | `chart.css:141` | `[data-chart-tooltip-x]` | `font-weight` | `var(--font-weight-medium)` | +| 8 | `chart.css:142` | `[data-chart-tooltip-x]` | `color` | `var(--color-content-primary)` | +| 9 | `chart.css:148` | `[data-chart-tooltip-row]` | `color` | `var(--color-content-secondary)` | +| 10 | `chart.css:159` | `[data-chart-tooltip-value]` | `color` | `var(--color-content-primary)` | +| 11 | `chart.css:176` | `[data-chart-title]` | `font-size` | `var(--style-label-font-size)` | +| 12 | `chart.css:177` | `[data-chart-title]` | `font-weight` | `var(--font-weight-medium)` | +| 13 | `chart.css:178` | `[data-chart-title]` | `color` | `var(--color-content-primary)` | +| 14 | `chart.css:193` | `[data-chart-legend-item]` | `font-size` | `var(--style-caption-font-size)` | +| 15 | `chart.css:194` | `[data-chart-legend-item]` | `color` | `var(--color-content-secondary)` | +| 16 | `chart.css:224` | `[data-chart-bar-list-label]` | `font-size` | `var(--style-label-font-size)` | +| 17 | `chart.css:225` | `[data-chart-bar-list-label]` | `color` | `var(--color-content-primary)` | +| 18 | `chart.css:242` | `[data-chart-bar-list-value]` | `font-size` | `var(--style-label-font-size)` | +| 19 | `chart.css:243` | `[data-chart-bar-list-value]` | `color` | `var(--color-content-secondary)` | +| 20 | `chart.css:283` | `[data-chart-bar-segment-legend-item]` | `font-size` | `var(--style-caption-font-size)` | +| 21 | `chart.css:284` | `[data-chart-bar-segment-legend-item]` | `color` | `var(--color-content-secondary)` | +| 22 | `chart.css:287` | `[data-chart-bar-segment-legend-value]` | `color` | `var(--color-content-primary)` | +| 23 | `chart.css:335` | `[data-chart-smith-label]` | `fill` | `var(--color-content-secondary)` | +| 24 | `chart.css:336` | `[data-chart-smith-label]` | `font-size` | `var(--style-caption-font-size)` | +| 25 | `chart.css:345` | `[data-chart-smith-point-label]` | `fill` | `var(--color-content-primary)` | +| 26 | `chart.css:346` | `[data-chart-smith-point-label]` | `font-size` | `var(--style-caption-font-size)` | +| 27 | `chart.css:347` | `[data-chart-smith-point-label]` | `font-weight` | `var(--font-weight-medium)` | +| 28 | `chart.css:370` | `[data-chart-funnel-label]` | `font-size` | `var(--style-label-font-size)` | +| 29 | `chart.css:371` | `[data-chart-funnel-label]` | `font-weight` | `var(--font-weight-medium)` | +| 30 | `chart.css:380` | `[data-chart-funnel-side]` | `font-size` | `var(--style-label-font-size)` | +| 31 | `chart.css:383` | `[data-chart-funnel-name]` | `fill` | `var(--color-content-primary)` | +| 32 | `chart.css:384` | `[data-chart-funnel-name]` | `font-weight` | `var(--font-weight-medium)` | +| 33 | `chart.css:387` | `[data-chart-funnel-meta]` | `fill` | `var(--color-content-secondary)` | +| 34 | `chart.css:399` | `[data-chart-gauge-label]` | `fill` | `var(--color-content-primary)` | +| 35 | `chart.css:400` | `[data-chart-gauge-label]` | `font-size` | `var(--font-size-xl)` | +| 36 | `chart.css:401` | `[data-chart-gauge-label]` | `font-weight` | `var(--font-weight-medium)` | ### 1.2 A través de un privado (0) _Ninguno._ -### 1.3 Literales (14) +### 1.3 Literales (10) | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `chart.css:15` | `[data-chart-frame]` | `inline-size` | `100%` | -| 2 | `chart.css:19` | `[data-chart-plot]` | `inline-size` | `100%` | -| 3 | `chart.css:25` | `[data-chart]` | `inline-size` | `100%` | -| 4 | `chart.css:35` | `[data-chart-empty]` | `block-size` | `100%` | -| 5 | `chart.css:125` | `[data-chart-tooltip]` | `min-inline-size` | `max-content` | -| 6 | `chart.css:141` | `[data-chart-tooltip-swatch]` | `inline-size` | `0.6em` | -| 7 | `chart.css:142` | `[data-chart-tooltip-swatch]` | `block-size` | `0.6em` | -| 8 | `chart.css:161` | `[data-chart-figure]` | `inline-size` | `100%` | -| 9 | `chart.css:187` | `[data-chart-swatch]` | `inline-size` | `0.75em` | -| 10 | `chart.css:188` | `[data-chart-swatch]` | `block-size` | `0.75em` | -| 11 | `chart.css:203` | `[data-chart-bar-list]` | `inline-size` | `100%` | -| 12 | `chart.css:225` | `[data-chart-bar-list-bar]` | `block-size` | `100%` | -| 13 | `chart.css:242` | `[data-chart-bar-segment]` | `inline-size` | `100%` | -| 14 | `chart.css:252` | `[data-chart-bar-segment-fill]` | `block-size` | `100%` | +| 1 | `chart.css:25` | `[data-chart-frame]` | `inline-size` | `100%` | +| 2 | `chart.css:29` | `[data-chart-plot]` | `inline-size` | `100%` | +| 3 | `chart.css:35` | `[data-chart]` | `inline-size` | `100%` | +| 4 | `chart.css:45` | `[data-chart-empty]` | `block-size` | `100%` | +| 5 | `chart.css:135` | `[data-chart-tooltip]` | `min-inline-size` | `max-content` | +| 6 | `chart.css:171` | `[data-chart-figure]` | `inline-size` | `100%` | +| 7 | `chart.css:215` | `[data-chart-bar-list]` | `inline-size` | `100%` | +| 8 | `chart.css:237` | `[data-chart-bar-list-bar]` | `block-size` | `100%` | +| 9 | `chart.css:254` | `[data-chart-bar-segment]` | `inline-size` | `100%` | +| 10 | `chart.css:264` | `[data-chart-bar-segment-fill]` | `block-size` | `100%` | ## 2. Sistema transversal (4) — informativo, fuera del ratio @@ -112,10 +78,10 @@ Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2). | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `chart.css:259` | `[data-chart-bar-segment-fill][data-dim='true']` | `opacity` | `var(--opacity-scrim)` | -| 2 | `chart.css:300` | `[data-chart-heat-cell][data-dim='true']` | `opacity` | `var(--opacity-scrim)` | -| 3 | `chart.css:308` | `[data-chart-cal-cell][data-dim='true']` | `opacity` | `var(--opacity-scrim)` | -| 4 | `chart.css:348` | `[data-chart-polar-petal][data-dim='true']` | `opacity` | `var(--opacity-scrim)` | +| 1 | `chart.css:271` | `[data-chart-bar-segment-fill][data-dim='true']` | `opacity` | `var(--opacity-scrim)` | +| 2 | `chart.css:313` | `[data-chart-heat-cell][data-dim='true']` | `opacity` | `var(--opacity-scrim)` | +| 3 | `chart.css:321` | `[data-chart-cal-cell][data-dim='true']` | `opacity` | `var(--opacity-scrim)` | +| 4 | `chart.css:361` | `[data-chart-polar-petal][data-dim='true']` | `opacity` | `var(--opacity-scrim)` | ## 3. Privados de la receta — ¿de dónde sale su valor? @@ -123,7 +89,7 @@ _La receta no declara privados propios en su CSS._ ## 4. Propuesta de corrección -### 4.1 Tokens a declarar en `lib/recipes/base.ts` (71) +### 4.1 Tokens a declarar en `lib/recipes/base.ts` (37) 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 @@ -133,74 +99,40 @@ no distingue lo que debería — se marca `⚠`. | token (`--chart-…`) | scope TSC | valor propuesto | usos | | --- | --- | --- | ---: | -| `frame-gap` | `root` | `var(--space-2)` | 1 | | `empty-fg` | `root` | `var(--color-content-secondary)` | 1 | -| `empty-font-size` | `root` | `var(--font-size-sm)` | 1 | -| `axis-line-stroke` | `root` | `var(--color-border-default)` | 1 | +| `empty-font-size` | `root` | `var(--style-label-font-size)` | 1 | | `axis-label-fill` | `root` | `var(--color-content-secondary)` | 1 | -| `axis-label-font-size` | `root` | `var(--font-size-xs)` | 1 | -| `grid-line-stroke` | `root` | `var(--color-border-subtle)` | 1 | -| `slice-stroke` | `root` | `var(--color-surface-default)` | 1 | -| `active-dot-stroke` | `root` | `var(--color-surface-default)` | 1 | -| `tooltip-font-size` | `root` | `var(--font-size-xs)` | 1 | +| `axis-label-font-size` | `root` | `var(--style-caption-font-size)` | 1 | +| `tooltip-font-size` | `root` | `var(--style-caption-font-size)` | 1 | | `tooltip-fg` | `root` | `var(--color-content-primary)` | 1 | | `tooltip-width` | `root` | `max-content` | 1 | | `tooltip-x-font-weight` | `root` | `var(--font-weight-medium)` | 1 | | `tooltip-x-fg` | `root` | `var(--color-content-primary)` | 1 | | `tooltip-row-fg` | `root` | `var(--color-content-secondary)` | 1 | -| `tooltip-swatch-width` | `root` | `0.6em` | 1 | -| `tooltip-swatch-height` | `root` | `0.6em` | 1 | -| `tooltip-swatch-radius` | `root` | `var(--radius-sm)` | 1 | | `tooltip-value-fg` | `root` | `var(--color-content-primary)` | 1 | -| `figure-gap` | `root` | `var(--space-2)` | 1 | -| `title-font-size` | `root` | `var(--font-size-sm)` | 1 | +| `title-font-size` | `root` | `var(--style-label-font-size)` | 1 | | `title-font-weight` | `root` | `var(--font-weight-medium)` | 1 | | `title-fg` | `root` | `var(--color-content-primary)` | 1 | -| `legend-gap` | `root` | `var(--space-1-5) var(--space-4)` | 1 | -| `legend-item-gap` | `root` | `var(--space-1-5)` | 1 | -| `legend-item-font-size` | `root` | `var(--font-size-xs)` | 1 | +| `legend-item-font-size` | `root` | `var(--style-caption-font-size)` | 1 | | `legend-item-fg` | `root` | `var(--color-content-secondary)` | 1 | -| `swatch-width` | `root` | `0.75em` | 1 | -| `swatch-height` | `root` | `0.75em` | 1 | -| `swatch-radius` | `root` | `var(--radius-sm)` | 1 | -| `bar-list-gap` | `root` | `var(--space-2) var(--space-3)` | 1 | -| `bar-list-label-font-size` | `root` | `var(--font-size-sm)` | 1 | +| `bar-list-label-font-size` | `root` | `var(--style-label-font-size)` | 1 | | `bar-list-label-fg` | `root` | `var(--color-content-primary)` | 1 | -| `bar-list-track-height` | `root` | `var(--control-height-sm, var(--space-7))` | 1 | -| `bar-list-track-bg` | `root` | `var(--color-surface-muted)` | 1 | -| `bar-list-track-radius` | `root` | `var(--radius-sm)` | 1 | -| `bar-list-bar-radius` | `root` | `var(--radius-sm)` | 1 | -| `bar-list-value-font-size` | `root` | `var(--font-size-sm)` | 1 | +| `bar-list-value-font-size` | `root` | `var(--style-label-font-size)` | 1 | | `bar-list-value-fg` | `root` | `var(--color-content-secondary)` | 1 | -| `bar-segment-gap` | `root` | `var(--space-3)` | 1 | -| `bar-segment-track-gap` | `root` | `var(--border-width-medium)` | 1 | -| `bar-segment-track-height` | `root` | `var(--control-height-sm, var(--space-7))` | 1 | -| `bar-segment-track-radius` | `root` | `var(--radius-md)` | 1 | -| `bar-segment-fill-width` | `root` | `var(--border-width-thick)` | 1 | -| `bar-segment-legend-gap` | `root` | `var(--space-2) var(--space-4)` | 1 | -| `bar-segment-legend-item-gap` | `root` | `var(--space-1-5)` | 1 | -| `bar-segment-legend-item-font-size` | `root` | `var(--font-size-xs)` | 1 | +| `bar-segment-legend-item-font-size` | `root` | `var(--style-caption-font-size)` | 1 | | `bar-segment-legend-item-fg` | `root` | `var(--color-content-secondary)` | 1 | | `bar-segment-legend-value-fg` | `root` | `var(--color-content-primary)` | 1 | -| `radar-ring-stroke` | `root` | `var(--color-border-subtle)` | 1 | -| `smith-boundary-stroke` | `root` | `var(--color-content-secondary)` | 1 | -| `smith-grid-stroke` | `root` | `var(--color-border-default)` | 1 | | `smith-label-fill` | `root` | `var(--color-content-secondary)` | 1 | -| `smith-label-font-size` | `root` | `var(--font-size-xs)` | 1 | -| `smith-point-stroke` | `root` | `var(--color-surface-default)` | 1 | +| `smith-label-font-size` | `root` | `var(--style-caption-font-size)` | 1 | | `smith-point-label-fill` | `root` | `var(--color-content-primary)` | 1 | -| `smith-point-label-font-size` | `root` | `var(--font-size-xs)` | 1 | +| `smith-point-label-font-size` | `root` | `var(--style-caption-font-size)` | 1 | | `smith-point-label-font-weight` | `root` | `var(--font-weight-medium)` | 1 | -| `polar-petal-stroke` | `root` | `var(--color-surface-default)` | 1 | -| `funnel-segment-stroke` | `root` | `var(--color-surface-default)` | 1 | -| `funnel-label-font-size` | `root` | `var(--font-size-sm)` | 1 | +| `funnel-label-font-size` | `root` | `var(--style-label-font-size)` | 1 | | `funnel-label-font-weight` | `root` | `var(--font-weight-medium)` | 1 | -| `funnel-connector-stroke` | `root` | `var(--color-border-default)` | 1 | -| `funnel-side-font-size` | `root` | `var(--font-size-sm)` | 1 | +| `funnel-side-font-size` | `root` | `var(--style-label-font-size)` | 1 | | `funnel-name-fill` | `root` | `var(--color-content-primary)` | 1 | | `funnel-name-font-weight` | `root` | `var(--font-weight-medium)` | 1 | | `funnel-meta-fill` | `root` | `var(--color-content-secondary)` | 1 | -| `gauge-track-fill` | `root` | `var(--color-surface-muted)` | 1 | | `gauge-label-fill` | `root` | `var(--color-content-primary)` | 1 | | `gauge-label-font-size` | `root` | `var(--font-size-xl)` | 1 | | `gauge-label-font-weight` | `root` | `var(--font-weight-medium)` | 1 | @@ -221,6 +153,71 @@ no distingue lo que debería — se marca `⚠`. -_(pendiente — lo escribe el autor; se conserva al regenerar)_ +**EJECUTADO 2026-08-22 — 9 % → 49 %**, 22 claves nuevas fusionadas en el bloque +existente (17 → 39). El 49 % es el techo de este diseño, no un trabajo a medias: +lo que queda fuera está medido y cada clase tiene su razón escrita abajo. + +**La propuesta §4 pedía 71 claves y habría escrito ~20 con el MISMO valor.** La +medición que lo decidió: las 84 filas fuera de alcance colapsaban a **26 valores +distintos** — 9× `content-secondary`, 9× `content-primary`, 9× `100%`, 6× +`font-size-sm`, 6× `font-size-xs`, 6× `font-weight-medium`, 5× +`surface-default`, 4× `radius-sm`, 3× `border-default`. Nombrar por parte +habría dado cinco claves para el pelo que separa marcas y nueve para la tinta +de anotación. Es la inflación que F2-B ya cazó en `spinner`, con otra forma. + +**El texto NO recibe tokens de chart, por doctrina.** Toma su ROL DE TAMAÑO de +los estilos con nombre —`caption` (xs) para las anotaciones y `label` (sm) para +el texto de cromo— y su TINTA y PESO de las ranuras del sistema +(`--color-content-*`, `--font-weight-*`). Acuñar `--chart-label-font-size` +sería inventar tipografía por componente, que es exactamente lo que el autor +prohibió el mismo día para la familia text-effects. Precedente de consumir la +capa: `code-block`, `kbd`, `announce`. Doce declaraciones cambian de +`var(--font-size-xs|sm)` a `var(--style-caption|label-font-size)`, mismo valor. + +**Consecuencia medida en la métrica**: esas doce y las veinte de tinta/peso +siguen contando como `global`, porque el censo sólo exime `--style-*` a los +PRIMITIVOS tipográficos y chart no lo es. El censo penaliza aquí hacer lo +correcto — la misma nota que dejó `listbox` en F2-A (§13). + +**Lo que la receta sí posee: la tinta de lo que no es texto** — cinco claves, +cada una un concepto que el CSS repite: `axis-fg` (eje + rejilla smith + +conector del funnel), `grid-fg` (rejilla + radar), `separator-fg` (el pelo de +superficie entre marcas contiguas: porción, punto activo, punto smith, pétalo, +segmento), `smith-boundary-fg` (el borde del disco, más fuerte a propósito) y +`track-bg` (raíl del bar-list y del gauge). Más 17 de geometría de cromo. + +**Un incumplimiento heredado, corregido**: el CSS leía +`var(--control-height-sm, var(--space-7))` — coordenada CRUDA, que +`recipe-css-contract` prohíbe en un token de receta, con un respaldo muerto (el +primitivo existe). El contrato guarda `var(--size-sm-control-height)`, el +bundle, que es 1:1 con él. + +**Fuera de contrato por identidad** (los 10 literales): nueve `100%` que +significan «ocupa a tu padre» y el `min-inline-size: max-content` del tooltip, +que es la corrección de composición de la misma clase ya adjudicada en +`date-picker`. Moverlos no es tematizar, es romper el layout. + +**Una escotilla sin cerrar**: el `--font-size-xl` del número del gauge. Ningún +estilo con nombre juega ese papel (un readout de dato no es un `h2`) y acuñar +`gauge-label-font-size` sería tipografía por componente. Anotado en §13. + +**El instrumento estaba ciego de tres maneras, y las tres se arreglaron ANTES +de medir** (ninguna era culpa de los tokens): + +1. **La superficie de chart vive en DIECISÉIS rutas de demo**, una por tipo de + gráfico: `/chart` monta una línea con área y nada más (58 nodos de 302 en la + página). El guard ganó `urls` — un token está VIVO si alguna superficie del + componente lo sigue— y la sonda se corrió por las dieciséis. +2. **El guard no leía NINGUNA propiedad SVG.** Sin `fill` / `stroke` / + `strokeWidth` / `fillOpacity` / `r` / `rx`, doce de las 39 claves de un + componente que dibuja en SVG leían muertas estando vivas. Misma clase que el + hueco de `filter` / `backdrop-filter` del día anterior. +3. **El valor centinela se elegía por SUBCADENA**: `slice-stroke-width` recibía + `rgb(1, 2, 3)` por contener «stroke», valor inválido para una longitud, y + leía muerta. Ahora la dimensión se decide por el final del nombre. + +Con eso: sonda **0 diffs en 161.850 valores computados** (16 rutas × 7 estados) +y centinela **37/39**, con `transition-duration` y `-ease` adjudicadas y +medidas sin la congelación (0.12s → 11.5s · cubic-bezier → steps(4)). diff --git a/docs/next-features.md b/docs/next-features.md index d5d4ce903..1575d6b4e 100644 --- a/docs/next-features.md +++ b/docs/next-features.md @@ -958,5 +958,27 @@ para resolver después** (autor, 2026-08-22): ser un guard**: la lógica es la misma que la del test de fantasmas sin fallback que ya existe, con el filtro de vocabulario de sistema encima. +Lo que `chart` destapó (2026-08-22): + +- **⚠ El censo penaliza consumir la capa tipográfica cuando no eres un + primitivo.** `chart` pasa doce declaraciones de `var(--font-size-xs|sm)` a + `var(--style-caption|label-font-size)` —lo que la doctrina manda: el rol de + tamaño lo poseen los estilos con nombre— y las doce siguen contando como + `global`, porque `TYPOGRAPHIC_PRIMITIVES` sólo exime a los seis primitivos + y `LAYER_VOCABULARY` no cubre `--style-*` para el resto. Es la misma nota que + dejó `listbox` en F2-A, ahora con una segunda ocurrencia y una forma clara: + **leer la capa que posee el eje debería contar como alcance en CUALQUIER + componente, no sólo en los primitivos.** Cambiarlo mueve la cifra de varios + componentes de golpe, así que es decisión, no arreglo. + +- **⚠ El número del gauge no tiene estilo con nombre que lo represente.** + `[data-chart-gauge-label]` es un READOUT de dato a `--font-size-xl`; el canon + tiene `h2` en ese tamaño, pero un dato no es un encabezado, y acuñar + `--chart-gauge-label-font-size` sería tipografía por componente (prohibido, + 2026-08-22). Queda leyendo el primitivo. Si aparecen más readouts —el valor + del `Gauge`, el total de un `Funnel`, el número de un `Stat`— la respuesta + es un estilo con nombre nuevo en el módulo de tipografía, que es firma del + autor. + **Deps**: ninguna. Son mejoras del instrumental del eje, ejecutables cuando estorben. diff --git a/docs/process/CONTINUE-theming.md b/docs/process/CONTINUE-theming.md index 3d0096d03..fc24a000a 100644 --- a/docs/process/CONTINUE-theming.md +++ b/docs/process/CONTINUE-theming.md @@ -4,14 +4,14 @@ con su revisión adversarial pasada (2026-08-22); la cola sigue abierta y `text-gradient` ya está dentro (2026-08-22).** Alcance global **56 %** (33 % al abrir el eje, 45 % tras F2-A, 50 % tras F2-B, y **+6 puntos al arreglar la MEDICIÓN**, no el código: ver abajo). Sin contrato -**24**; al 100 % **17**; alcance <20 % **19**. +**24**; al 100 % **17**; alcance <20 % **18**. Último: `chart` 9 % → 49 %. ## LO PRIMERO AL ENTRAR (cinco minutos, en este orden) 1. **Comprueba que el suelo no se ha movido**: ```bash - node --import tsx/esm scripts/theming-census.ts # 162 · 5.188 · 2.638 (56 %) · 24 sin contrato + node --import tsx/esm scripts/theming-census.ts # 162 · 5.191 · 2.675 (57 %) · 24 sin contrato node --import tsx/esm scripts/theming-census.ts --names # DESVIADAS 0 — la gramática se mantiene ``` @@ -134,6 +134,28 @@ declarados). Tabla en next-features §13. La más grave no es de esta familia: `--style-*-font-variation-settings` de `text.css` significan que **el eje de fuente variable no llega nunca al primitivo `Text`**. Ninguna se arregla aquí. +### 3.quater `chart` 9 % → 49 % (2026-08-22) — y el instrumento, tres veces ciego + +Lo que hay que llevarse de aquí: + +- **La ficha pedía 71 claves; ~20 habrían llevado el mismo valor.** Antes de + aceptar una propuesta larga, **agrupa las filas por VALOR**: las 84 de chart + colapsaban a 26. Nombrar por parte lo que es un concepto es la inflación de + `spinner` con otra cara. +- **El texto de un componente no recibe tokens propios**: rol de tamaño de los + estilos con nombre (`caption` / `label`), tinta y peso de las ranuras del + sistema. Sólo se acuña la tinta de lo que NO es texto. +- **El guard ya sabe medir en VARIAS rutas** (`urls` en `COMPONENT_OVERRIDES`): + un token vive si alguna superficie del componente lo sigue. Chart tiene 16; + `picker-shell` ya lleva la del date-picker (su ruta propia es 404). +- **El guard ya lee SVG** (`fill`, `stroke`, `strokeWidth`, `fillOpacity`, `r`, + `rx`…) y **elige el valor centinela por el FINAL del nombre**, no por + subcadena — `slice-stroke-width` recibía un color por contener «stroke». +- Tras cualquier cambio del guard: **re-verificar los 28 componentes con + ledger** (se hizo: cero regresiones, cero STALE). Y ojo, correr doce a la vez + contra el mismo dev server da falsos exit=1 por timeout: repite el que falle + a solas antes de creerte una regresión. + ### 4. La cola que queda, por alcance | componente | knobs | nota | @@ -142,6 +164,8 @@ fuente variable no llega nunca al primitivo `Text`**. Ninguna se arregla aquí. | `field-langs` | 41 | espera el **mandato Field** (firma 2), que NO es tokenización: manda que los x-field incorporen el wrapper Field — cambia composición y mueve píxel | | `mockup` | 29 (38 %) | ⚠ **sin ruta de demo (404)**, como `picker-shell`: sin sonda ni guard. Y sale NEEDS-WORK en el audit, preexistente | | `chronos` | 245 (1 %) | el segundo mayor; sin abrir | +| `proof-of-human` | 95 (15 %) | escena de tono fijo firmada; lo demás son hooks de clase (eje aparte) | +| `prose` | 74 (1 %) | su superficie es la capa tipográfica: mismo caso que chart, un piso más arriba | | `result` · `banner` · … | varios | ver el censo, `reach < 20 %` = 19 | `avatar` ya está al **75 %** y `knob` al 75 % (ambos hechos). `heading` / diff --git a/docs/process/PLAN-theming.md b/docs/process/PLAN-theming.md index c77f6442a..3167f2ce0 100644 --- a/docs/process/PLAN-theming.md +++ b/docs/process/PLAN-theming.md @@ -455,6 +455,61 @@ de commit. Sin artefacto, el paso no se ha hecho. ## 8. Registro +- 2026-08-22 — **`chart` 9 % → 49 %** (22 claves nuevas fusionadas en su bloque, + 17 → 39). El mayor de los de alcance <20 % que no estaba bloqueado. + + **La ficha pedía 71 claves y ~20 habrían llevado el mismo valor.** Lo decidió + la medición: las 84 filas fuera de alcance colapsan a **26 valores distintos** + (9× `content-secondary`, 9× `content-primary`, 9× `100%`, 6× `font-size-sm`, + 6× `font-size-xs`, 6× `font-weight-medium`, 5× `surface-default`). La + inflación de `spinner` con otra forma: nombrar por parte lo que es un + concepto. + + **El texto no recibe tokens de chart**: toma su ROL DE TAMAÑO de los estilos + con nombre (`caption` para anotaciones, `label` para el cromo) y su tinta y + peso de las ranuras del sistema. Acuñarlos sería tipografía por componente, + lo que el autor prohibió el mismo día. Precedente de consumir la capa: + `code-block`, `kbd`, `announce`. **La receta sólo posee la tinta de lo que no + es texto** — `axis-fg`, `grid-fg`, `separator-fg` (el pelo de superficie que + separa marcas contiguas: cinco partes, una idea), `smith-boundary-fg`, + `track-bg` — más 17 de geometría de cromo. Identidad fuera de contrato: los + nueve `100%` y el `max-content` del tooltip. + + **Incumplimiento heredado corregido**: el CSS leía la coordenada CRUDA + `var(--control-height-sm, var(--space-7))` (respaldo muerto); el contrato + guarda el bundle `var(--size-sm-control-height)`, 1:1. + + **El instrumento estaba ciego de TRES maneras, arregladas antes de medir:** + + 1. **La superficie de chart vive en 16 rutas de demo**, una por tipo: + `/chart` monta 58 nodos de los 302 de la página y el resto vive en rutas + hermanas. El guard gana `urls`: un token está VIVO si ALGUNA superficie del + componente lo sigue, y cada URL sólo re-prueba lo que sigue pendiente. + De paso `picker-shell` recibe la suya (su ruta propia es un 404, se mide + dentro del date-picker: 0/31 → 6/31 sin pasar la URL a mano). + 2. **El guard no leía NINGUNA propiedad SVG** — sin `fill`/`stroke`/ + `strokeWidth`/`fillOpacity`/`r`/`rx`, doce claves de un componente que + dibuja en SVG leían muertas estando vivas. Misma clase que el hueco de + `filter` del día anterior. + 3. **El valor centinela se elegía por SUBCADENA**: `slice-stroke-width` + recibía `rgb(1, 2, 3)` por contener «stroke» — inválido para una longitud, + así que leía muerta. Ahora la dimensión se decide por el final del nombre. + + Gates: sonda **0 diffs en 161.850 valores** (16 rutas × 7 estados, 892 nodos + en reposo) · centinela **37/39** con dos adjudicaciones medidas sin la + congelación (`transition-duration` 0.12s → 11.5s, `-ease` → steps(4)) · + censo `--only` 49 % y global **56 % → 57 %** (2.638 → 2.675 públicos, + <20 % 19 → 18) · `--names` 39/39 conformes · audit PASS · eidos-lint 0 + invalid · suite eidos con el único rojo conocido · rtl 0 · docs 0 · `check` + sin errores en los ficheros tocados. **Los 28 componentes con ledger + re-verificados tras los tres cambios de instrumento: cero regresiones, cero + STALE.** + + Abre (→ §13): el censo **penaliza consumir la capa tipográfica** si no eres + un primitivo (las doce declaraciones correctas siguen contando `global`) — la + nota de `listbox` con una segunda ocurrencia; y el número del gauge no tiene + estilo con nombre que lo represente (un readout de dato no es un `h2`). + - 2026-08-22 — **`text-scramble` ADJUDICADO: sin contrato que escribir** (y el barrido de fantasmas, hecho). Sin cambios de código: su veredicto §5. diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index 01dd69969..fcc43c116 100644 --- a/scripts/theming-sentinel-exceptions.ts +++ b/scripts/theming-sentinel-exceptions.ts @@ -314,6 +314,12 @@ export const SENTINEL_EXCEPTIONS: Record> = { 'disabled-row-fg': 'no disabled row in the demo (0 mounted, counted); forced data-disabled → reaches (oklch(0.61 0 0) → rgb(1,2,3))' }, + chart: { + 'transition-duration': + "the guard FREEZES transitions on purpose (a transitioned property returns its starting value), which is exactly what hides a token whose whole job IS the transition — same class as text-circular.char-transition. Measured without the freeze 2026-08-22 on [data-chart-line]: 0.12s -> 11.5s", + 'transition-ease': + "same freeze, same measurement 2026-08-22 on [data-chart-line]: cubic-bezier(0, 0, 0.2, 1) -> steps(4)" + }, 'text-circular': { 'char-transition': "the guard FREEZES transitions on purpose (a transitioned property returns its starting value), which is exactly what hides a token whose whole job is the transition. Measured without the freeze 2026-08-22 -> reaches (0.5s cubic-bezier -> 9.5s linear)" diff --git a/scripts/theming-sentinel.ts b/scripts/theming-sentinel.ts index bf87c4c2f..9680fc43f 100644 --- a/scripts/theming-sentinel.ts +++ b/scripts/theming-sentinel.ts @@ -82,6 +82,21 @@ const PROPS = [ 'outlineWidth', 'outlineOffset', 'zIndex', + // SVG paint + geometry. Without these EVERY token of an SVG recipe reads + // dead: chart's axis / grid / separator ink, its stroke widths, the point + // radius and the area's fill-opacity are all painted through presentation + // attributes the box properties above never see (measured 2026-08-22 — + // 12 of chart's 39 tokens). Same class as the `filter` / `backdrop-filter` + // gap fixed the same day. + 'fill', + 'fillOpacity', + 'stroke', + 'strokeWidth', + 'strokeOpacity', + 'strokeDasharray', + 'r', + 'rx', + 'ry', 'left', 'right', 'top', @@ -101,11 +116,16 @@ const PROPS = [ */ const COMPONENT_OVERRIDES: Record< string, - { attrPrefix?: string; openWith?: string[]; openBy?: 'click' | 'hover' } + { attrPrefix?: string; openWith?: string[]; openBy?: 'click' | 'hover'; urls?: string[] } > = { 'picker-shell': { attrPrefix: 'data-picker', - openWith: ['[data-uix-stage-area] [data-popover-trigger]'] + openWith: ['[data-uix-stage-area] [data-popover-trigger]'], + // It has no route of its own — `/uix/components/picker-shell` is a 404 — + // so the default URL measured NOTHING and the guard reported 0/31 with six + // unadjudicated. Named here so nobody has to know: it is measured inside a + // host picker (6/31, the figure its own commit `67b4c810c` recorded). + urls: ['/uix/components/date-picker'] }, // A hover card opens on POINTER-OVER, not on click: clicking its trigger // (an ``) navigates instead of revealing the panel, so the portaled @@ -115,6 +135,38 @@ const COMPONENT_OVERRIDES: Record< // `showBorder=false`, and the frame owns three of the seven tokens. The // chip is outside the component, so clicking it cannot poison a hover // state (the pointer is parked right after, as for every other opener). + // Chart is ONE recipe whose surface is spread over SIXTEEN demo routes, one + // per chart type: `/chart` mounts a line+area chart and NOTHING else, so + // two thirds of the contract (bar list, stacked bar, radar, smith, gauge, + // funnel, polar, heat, pie…) reads dead on it. Measured 2026-08-22: 58 + // chart nodes on `/chart` against 302 in the whole page and the rest in + // sibling routes. + chart: { + // The tooltip and the crosshair only exist WHILE the pointer is over the + // plot — there is no trigger to click and no state to latch. Hovering the + // plot is what mounts them, and `openBy: 'hover'` also keeps the pointer + // parked there instead of moving it away before each token. + openBy: 'hover', + openWith: ['[data-chart-plot]'], + urls: [ + '/uix/components/chart', + '/uix/components/line-chart', + '/uix/components/area-chart', + '/uix/components/bar-chart', + '/uix/components/scatter-chart', + '/uix/components/bubble-chart', + '/uix/components/pie-chart', + '/uix/components/sparkline', + '/uix/components/bar-list', + '/uix/components/bar-segment', + '/uix/components/radar-chart', + '/uix/components/smith-chart', + '/uix/components/polar-area', + '/uix/components/funnel', + '/uix/components/gauge', + '/uix/components/heatmap' + ] + }, 'text-gradient': { openWith: [ '[data-uix-control]:has([data-uix-control-label]:text-is("showBorder")) [data-uix-chip]:text-is("true")' @@ -130,6 +182,11 @@ function sentinelFor(key: string): string { if (/opacity|scale/.test(key)) return '0.123'; if (/duration/.test(key)) return '11.5s'; if (/letter-spacing/.test(key)) return '4.5px'; + // BEFORE the colour test on purpose: a DIMENSION whose name merely contains a + // colour word was getting `rgb(1, 2, 3)` and, being invalid for a length, + // moved nothing and read dead — measured on `chart.slice-stroke-width` + // (2026-08-22), which the substring `stroke` was capturing. + if (/(width|size|radius|gap|height|padding|offset|thickness|inset)$/.test(key)) return '1234px'; if ( /color|bg$|fg$|border$|ring$|separator|-bg-|fill|stroke|outline$|glass|scrim$|track$/.test(key) ) @@ -159,205 +216,221 @@ async function main() { .map((m) => m[1]) .filter((k) => !k.startsWith('_')); + // A component whose surface is spread over SEVERAL demo routes cannot be + // judged on one page: what that page does not mount reads dead. A token is + // LIVE when ANY of the component's surfaces follows it; only what nothing + // moves anywhere is dead. Each URL only re-tests what is still pending, so + // the common case (one route) costs exactly what it did before. + const origin = new URL(url).origin; + const urls = override.urls ? override.urls.map((path) => origin + path) : [url]; + const browser = await chromium.launch(); - const page = await browser.newPage({ viewport: { width: 1440, height: 1200 } }); - await page.goto(url, { waitUntil: 'networkidle' }); - await page.waitForTimeout(600); - // A [data-busy] demo is still mutating — measuring it is measuring an instant. - await page - .waitForFunction(() => !document.querySelector('[data-busy]'), null, { timeout: 15000 }) - .catch(() => {}); + const live: string[] = []; + let pending = keys; - // Freeze transitions: a transitioned property reads its STARTING value right - // after the write, so a live token looked dead (measured on combobox's - // `box-shadow`, which transitions on `--duration-fast`). - await page.addStyleTag({ - content: '*, *::before, *::after { transition: none !important; }' - }); + for (const target of urls) { + if (!pending.length) break; + const page = await browser.newPage({ viewport: { width: 1440, height: 1200 } }); + await page.goto(target, { waitUntil: 'networkidle' }); + await page.waitForTimeout(600); + // A [data-busy] demo is still mutating — measuring it is measuring an instant. + await page + .waitForFunction(() => !document.querySelector('[data-busy]'), null, { timeout: 15000 }) + .catch(() => {}); - // Open whatever can be opened, so portaled parts are in the document — then - // BLUR: a mouse click on a text input matches `:focus-visible`, and the - // focus rule repaints the rest-state chrome (the input-border false - // negative). Popovers dismiss on outside pointerdown, not on blur, so the - // open state survives. - for (const sel of [ - ...(override.openWith ?? []), - `[data-${component}-trigger]`, - `[data-${component}-input]`, - `[data-${component}-stop]` - ]) { - const el = page.locator(sel).first(); - if (await el.count()) { - try { - if (override.openBy === 'hover') await el.hover({ timeout: 1500 }); - else await el.click({ timeout: 1500 }); - await page.waitForTimeout(400); - break; - } catch { - /* not clickable */ - } - } - } - if (override.openBy !== 'hover') - await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur?.()); - // ...and PARK THE POINTER (never for a hover-opened panel: moving the cursor - // away is exactly what dismisses it). 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. - if (override.openBy !== 'hover') await page.mouse.move(0, 0); + // Freeze transitions: a transitioned property reads its STARTING value right + // after the write, so a live token looked dead (measured on combobox's + // `box-shadow`, which transitions on `--duration-fast`). + await page.addStyleTag({ + content: '*, *::before, *::after { transition: none !important; }' + }); - 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( - override.attrPrefix ? `[data-${component}]` : `[data-${component}-content]` - ).count(); - if (open) return; + // Open whatever can be opened, so portaled parts are in the document — then + // BLUR: a mouse click on a text input matches `:focus-visible`, and the + // focus rule repaints the rest-state chrome (the input-border false + // negative). Popovers dismiss on outside pointerdown, not on blur, so the + // open state survives. for (const sel of [ ...(override.openWith ?? []), `[data-${component}-trigger]`, - `[data-${component}-input]` + `[data-${component}-input]`, + `[data-${component}-stop]` ]) { const el = page.locator(sel).first(); if (await el.count()) { try { - if (override.openBy === 'hover') await el.hover({ timeout: 1000 }); - else await el.click({ timeout: 1000 }); - await page.waitForTimeout(250); - if (override.openBy !== 'hover') { - await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur?.()); - await page.mouse.move(0, 0); - } - return; + if (override.openBy === 'hover') await el.hover({ timeout: 1500 }); + else await el.click({ timeout: 1500 }); + await page.waitForTimeout(400); + break; } catch { - /* keep trying */ + /* not clickable */ } } } - }; + if (override.openBy !== 'hover') + await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur?.()); + // ...and PARK THE POINTER (never for a hover-opened panel: moving the cursor + // away is exactly what dismisses it). 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. + if (override.openBy !== 'hover') await page.mouse.move(0, 0); - const staticPass = (key: string, value: string) => - page.evaluate( - ([kebab, token, val, props, prefix]) => { - const all = () => - [...document.querySelectorAll('*')].filter((n) => - [...n.attributes].some((a) => a.name.startsWith(prefix)) - ); - // Write on :root AND on every component node: resolved names are - // declared per part (root/host), and a portaled panel never sees the - // component root. Writing the var everywhere cannot fake a win at the - // PROPERTY level — a declaration another rule (or an inline style) - // beats stays beaten. - const hosts = () => [document.documentElement, ...all()]; - const sized = all().filter((n) => n.hasAttribute('data-size')); - const original = sized.map((n) => n.getAttribute('data-size')); - const snap = () => - all() - .map((n) => { - const cs = getComputedStyle(n); - 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]}~${ph[p as never]}`) - .join('|'); - return own + '#' + pseudo; - }) - .join('@'); - let moved = false; - for (const size of ['md', 'xs', 'sm', 'lg', 'xl']) { - for (const n of sized) n.setAttribute('data-size', size); - const before = snap(); - for (const h of hosts()) h.style.setProperty(`--${kebab}-${token}`, val as string); - const after = snap(); - for (const h of hosts()) h.style.removeProperty(`--${kebab}-${token}`); - if (before !== after) { - moved = true; - break; + // 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( + override.attrPrefix ? `[data-${component}]` : `[data-${component}-content]` + ).count(); + if (open) return; + for (const sel of [ + ...(override.openWith ?? []), + `[data-${component}-trigger]`, + `[data-${component}-input]` + ]) { + const el = page.locator(sel).first(); + if (await el.count()) { + try { + if (override.openBy === 'hover') await el.hover({ timeout: 1000 }); + else await el.click({ timeout: 1000 }); + await page.waitForTimeout(250); + if (override.openBy !== 'hover') { + await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur?.()); + await page.mouse.move(0, 0); + } + return; + } catch { + /* keep trying */ } } - sized.forEach((n, i) => (original[i] ? n.setAttribute('data-size', original[i]!) : null)); - return moved; - }, - [component, key, value, PROPS, attrPrefix] as const - ); - - // Hover pass — a `hover-*` token can only move a computed value while some - // node is really hovered (tree-grid's hover-row-bg, gradient-picker's - // hover-preset-border were false negatives without it). - const hoverPass = async (key: string, value: string) => { - const count = await page.evaluate( - (prefix) => - [...document.querySelectorAll('*')].filter((n) => - [...n.attributes].some((a) => a.name.startsWith(prefix)) - ).length, - attrPrefix - ); - for (let i = 0; i < Math.min(count, 30); i++) { - const handle = await page.evaluateHandle( - ([prefix, idx]) => - [...document.querySelectorAll('*')].filter((n) => - [...n.attributes].some((a) => a.name.startsWith(prefix)) - )[idx as number] ?? null, - [attrPrefix, i] as const - ); - const el = handle.asElement(); - if (!el) continue; - try { - await el.hover({ timeout: 800 }); - } catch { - continue; } - const moved = await page.evaluate( - ([prefix, token, val, props, idx, kebab]) => { + }; + + const staticPass = (key: string, value: string) => + page.evaluate( + ([kebab, token, val, props, prefix]) => { const all = () => [...document.querySelectorAll('*')].filter((n) => [...n.attributes].some((a) => a.name.startsWith(prefix)) ); - const node = all()[idx as number]; - if (!node) return false; - const snap = () => { - const cs = getComputedStyle(node); - return (props as string[]).map((p) => cs[p as never]).join('|'); - }; - const before = snap(); - for (const h of [document.documentElement, ...all()]) - h.style.setProperty(`--${kebab}-${token}`, val as string); - const after = snap(); - for (const h of [document.documentElement, ...all()]) - h.style.removeProperty(`--${kebab}-${token}`); - return before !== after; + // Write on :root AND on every component node: resolved names are + // declared per part (root/host), and a portaled panel never sees the + // component root. Writing the var everywhere cannot fake a win at the + // PROPERTY level — a declaration another rule (or an inline style) + // beats stays beaten. + const hosts = () => [document.documentElement, ...all()]; + const sized = all().filter((n) => n.hasAttribute('data-size')); + const original = sized.map((n) => n.getAttribute('data-size')); + const snap = () => + all() + .map((n) => { + const cs = getComputedStyle(n); + 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]}~${ph[p as never]}`) + .join('|'); + return own + '#' + pseudo; + }) + .join('@'); + let moved = false; + for (const size of ['md', 'xs', 'sm', 'lg', 'xl']) { + for (const n of sized) n.setAttribute('data-size', size); + const before = snap(); + for (const h of hosts()) h.style.setProperty(`--${kebab}-${token}`, val as string); + const after = snap(); + for (const h of hosts()) h.style.removeProperty(`--${kebab}-${token}`); + if (before !== after) { + moved = true; + break; + } + } + sized.forEach((n, i) => (original[i] ? n.setAttribute('data-size', original[i]!) : null)); + return moved; }, - [attrPrefix, key, value, PROPS, i, component] as const + [component, key, value, PROPS, attrPrefix] as const ); - if (moved) return true; - } - return false; - }; - for (const key of keys) { - await reopen(); - const value = sentinelFor(key); - let moved = await staticPass(key, value); - if (!moved && /hover/.test(key)) moved = await hoverPass(key, value); - if (moved) live.push(key); - else dead.push(key); + // Hover pass — a `hover-*` token can only move a computed value while some + // node is really hovered (tree-grid's hover-row-bg, gradient-picker's + // hover-preset-border were false negatives without it). + const hoverPass = async (key: string, value: string) => { + const count = await page.evaluate( + (prefix) => + [...document.querySelectorAll('*')].filter((n) => + [...n.attributes].some((a) => a.name.startsWith(prefix)) + ).length, + attrPrefix + ); + for (let i = 0; i < Math.min(count, 30); i++) { + const handle = await page.evaluateHandle( + ([prefix, idx]) => + [...document.querySelectorAll('*')].filter((n) => + [...n.attributes].some((a) => a.name.startsWith(prefix)) + )[idx as number] ?? null, + [attrPrefix, i] as const + ); + const el = handle.asElement(); + if (!el) continue; + try { + await el.hover({ timeout: 800 }); + } catch { + continue; + } + const moved = await page.evaluate( + ([prefix, token, val, props, idx, kebab]) => { + const all = () => + [...document.querySelectorAll('*')].filter((n) => + [...n.attributes].some((a) => a.name.startsWith(prefix)) + ); + const node = all()[idx as number]; + if (!node) return false; + const snap = () => { + const cs = getComputedStyle(node); + return (props as string[]).map((p) => cs[p as never]).join('|'); + }; + const before = snap(); + for (const h of [document.documentElement, ...all()]) + h.style.setProperty(`--${kebab}-${token}`, val as string); + const after = snap(); + for (const h of [document.documentElement, ...all()]) + h.style.removeProperty(`--${kebab}-${token}`); + return before !== after; + }, + [attrPrefix, key, value, PROPS, i, component] as const + ); + if (moved) return true; + } + return false; + }; + + const stillDead: string[] = []; + for (const key of pending) { + await reopen(); + const value = sentinelFor(key); + let moved = await staticPass(key, value); + if (!moved && /hover/.test(key)) moved = await hoverPass(key, value); + if (moved) live.push(key); + else stillDead.push(key); + } + pending = stillDead; + await page.close(); } await browser.close(); + const dead = pending; const ledger = SENTINEL_EXCEPTIONS[component] ?? {}; const adjudicated = dead.filter((k) => k in ledger); diff --git a/src/uix/eidos/components/chart/README.md b/src/uix/eidos/components/chart/README.md index 59dfd01c8..d7fb1a75c 100644 --- a/src/uix/eidos/components/chart/README.md +++ b/src/uix/eidos/components/chart/README.md @@ -159,8 +159,46 @@ tick measures as "no overlap" while reading as contact. ## Theming -Series: `var(--color-{role}-solid)`. Axis lines: `--color-border-default`; -grid: `--color-border-subtle`; labels: `--color-content-secondary`; title: -`--color-content-primary`. Pie slice separators: `--color-surface-default`. -Geometry + tooltip surface: the `--chart-*` recipe tokens (`lib/recipes/base.ts`). -`chart.css` is code-split (imported from `chart.svelte`). +Contrato en `lib/recipes/base.ts > chart` — **39 claves** (17 + 22 el +2026-08-22). Alcance del censo: **49 %** (era 9 %). **Guard R-5.4** +(`npm run theming:sentinel -- chart `): **37/39** en vivo; las dos que +callan son `transition-duration` / `-ease`, adjudicadas con su medición (el +guard congela transiciones a propósito). + +**Series**: `var(--color-{role}-solid)`, inline por serie. + +**El texto no tiene tokens de chart, y es doctrina**: toma su ROL DE TAMAÑO de +los estilos con nombre del canon —`caption` para las anotaciones (etiquetas de +eje, leyenda, tooltip, etiquetas smith) y `label` para el texto de cromo +(título, filas del bar-list, funnel)— y su TINTA y su PESO de las ranuras del +sistema (`--color-content-*`, `--font-weight-*`), que es para lo que existen. +La receta no acuña tipografía: mismo precedente que `code-block`, `kbd` y +`announce`. Un `Chart` dentro de un tema que retoca `caption` le sigue. + +**Lo que la receta SÍ posee es la tinta de lo que no es texto**, porque nada +más puede nombrarlo, y cada clave es UN concepto que el CSS repite: + +``` +--chart-axis-fg eje + rejilla del smith + conector del funnel +--chart-grid-fg rejilla + anillos y radios del radar +--chart-separator-fg el pelo de superficie que separa marcas contiguas: + porción, punto activo, punto smith, pétalo, segmento +--chart-smith-boundary-fg el borde del disco RF, más fuerte que su rejilla +--chart-track-bg el raíl del bar-list y el del gauge +``` + +Más la geometría del cromo (`gap`, los pares `row-gap`/`column-gap` de leyenda +y listas, `swatch-size`, `track-height`, radios y separaciones del bar-segment) +y la geometría de marca que ya existía (grosores, radios, `area-opacity`, +timing, superficie del tooltip, crosshair). + +`track-height` lee **el bundle de talla** (`--size-sm-control-height`), no el +primitivo crudo `--control-height-sm` que el CSS traía: el crudo es lo que +`recipe-css-contract` prohíbe, y su `var(--space-7)` de respaldo estaba muerto. + +**Fuera de contrato por identidad**: los nueve `100%` («ocupa a tu padre») y el +`min-inline-size: max-content` del tooltip. Y una escotilla sin cerrar: el +`--font-size-xl` del número del gauge, que no tiene estilo con nombre que juegue +ese papel (un readout de dato no es un `h2`) — anotado en next-features §13. + +`chart.css` es code-split (se importa desde `chart.svelte`). diff --git a/src/uix/eidos/components/chart/chart.css b/src/uix/eidos/components/chart/chart.css index d8891ed04..49f2152b6 100644 --- a/src/uix/eidos/components/chart/chart.css +++ b/src/uix/eidos/components/chart/chart.css @@ -4,14 +4,24 @@ * geometric constant (stroke widths, radii, opacities, transition timing, * tooltip surface) comes from a `--chart-*` recipe token sourced from canonical * primitives — see `lib/recipes/base.ts`. Series fills/strokes are themed per - * series inline (`var(--color-{role}-solid)`); colours for chrome come from the - * global content/border/surface tokens. + * series inline (`var(--color-{role}-solid)`). + * + * TEXT takes its SIZE ROLE from the canon's named styles — `caption` for the + * annotations (axis labels, legend, tooltip, smith labels) and `label` for the + * chrome text (title, bar-list rows, funnel) — and its INK and WEIGHT from the + * system's content / weight slots, which is what those slots are for. The + * recipe mints NO typography of its own (theming §6, D-TH.2 precedent: + * code-block / kbd / announce read the same layer). + * + * What the recipe DOES own, because nothing else can name it: the ink of the + * lines and fills that are not text — axis, grid, the hairline that separates + * adjacent marks, the rails — plus its geometry. */ [data-chart-frame] { display: flex; flex-direction: column; - gap: var(--space-2); + gap: var(--chart-gap); inline-size: 100%; } [data-chart-plot] { @@ -34,24 +44,24 @@ place-items: center; block-size: 100%; color: var(--color-content-secondary); - font-size: var(--font-size-sm); + font-size: var(--style-label-font-size); } /* ── Axes ─────────────────────────────────────────────────────────────── */ [data-chart-axis-line], [data-chart-axis-tick] { - stroke: var(--color-border-default); + stroke: var(--chart-axis-fg); stroke-width: var(--chart-axis-width); } [data-chart-axis-label] { fill: var(--color-content-secondary); - font-size: var(--font-size-xs); + font-size: var(--style-caption-font-size); } /* ── Grid ─────────────────────────────────────────────────────────────── */ [data-chart-grid-line] { - stroke: var(--color-border-subtle); + stroke: var(--chart-grid-fg); stroke-width: var(--chart-grid-width); } @@ -79,7 +89,7 @@ /* Pie / donut slices, separated by a hairline of the page surface. */ [data-chart-slice] { - stroke: var(--color-surface-default); + stroke: var(--chart-separator-fg); stroke-width: var(--chart-slice-stroke-width); /* So the `bloom` enter scales each slice from the chart (viewport) centre — a radial grow. */ transform-box: view-box; @@ -95,7 +105,7 @@ } [data-chart-active-dot] { r: var(--chart-point-radius); - stroke: var(--color-surface-default); + stroke: var(--chart-separator-fg); stroke-width: var(--chart-axis-width); pointer-events: none; } @@ -120,7 +130,7 @@ border: var(--chart-axis-width) solid var(--chart-tooltip-border); border-radius: var(--chart-tooltip-radius); box-shadow: var(--chart-tooltip-shadow); - font-size: var(--font-size-xs); + font-size: var(--style-caption-font-size); color: var(--color-content-primary); min-inline-size: max-content; } @@ -138,9 +148,9 @@ color: var(--color-content-secondary); } [data-chart-tooltip-swatch] { - inline-size: 0.6em; - block-size: 0.6em; - border-radius: var(--radius-sm); + inline-size: var(--chart-tooltip-swatch-size); + block-size: var(--chart-tooltip-swatch-size); + border-radius: var(--chart-swatch-radius); flex: none; } [data-chart-tooltip-value] { @@ -157,13 +167,13 @@ position: relative; /* anchor for the pie's hover tooltip */ display: flex; flex-direction: column; - gap: var(--space-2); + gap: var(--chart-gap); inline-size: 100%; } [data-chart-title] { margin: 0; - font-size: var(--font-size-sm); + font-size: var(--style-label-font-size); font-weight: var(--font-weight-medium); color: var(--color-content-primary); } @@ -171,22 +181,23 @@ [data-chart-legend] { display: flex; flex-wrap: wrap; - gap: var(--space-1-5) var(--space-4); + row-gap: var(--chart-legend-row-gap); + column-gap: var(--chart-legend-column-gap); align-items: center; } [data-chart-legend-item] { display: inline-flex; align-items: center; - gap: var(--space-1-5); - font-size: var(--font-size-xs); + gap: var(--chart-legend-item-gap); + font-size: var(--style-caption-font-size); color: var(--color-content-secondary); } [data-chart-swatch] { - inline-size: 0.75em; - block-size: 0.75em; - border-radius: var(--radius-sm); + inline-size: var(--chart-swatch-size); + block-size: var(--chart-swatch-size); + border-radius: var(--chart-swatch-radius); flex: none; } @@ -199,7 +210,8 @@ display: grid; grid-template-columns: minmax(0, max-content) 1fr auto; align-items: center; - gap: var(--space-2) var(--space-3); + row-gap: var(--chart-bar-list-row-gap); + column-gap: var(--chart-bar-list-column-gap); inline-size: 100%; } [data-chart-bar-list-item] { @@ -209,25 +221,25 @@ align-items: center; } [data-chart-bar-list-label] { - font-size: var(--font-size-sm); + font-size: var(--style-label-font-size); color: var(--color-content-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } [data-chart-bar-list-track] { - block-size: var(--control-height-sm, var(--space-7)); - background: var(--color-surface-muted); - border-radius: var(--radius-sm); + block-size: var(--chart-track-height); + background: var(--chart-track-bg); + border-radius: var(--chart-track-radius); overflow: hidden; } [data-chart-bar-list-bar] { block-size: 100%; - border-radius: var(--radius-sm); + border-radius: var(--chart-bar-radius); transition: inline-size var(--chart-transition-duration) var(--chart-transition-ease); } [data-chart-bar-list-value] { - font-size: var(--font-size-sm); + font-size: var(--style-label-font-size); color: var(--color-content-secondary); font-variant-numeric: tabular-nums; text-align: end; @@ -238,19 +250,19 @@ position: relative; /* anchor for the hover tooltip */ display: flex; flex-direction: column; - gap: var(--space-3); + gap: var(--chart-bar-segment-gap); inline-size: 100%; } [data-chart-bar-segment-track] { display: flex; - gap: var(--border-width-medium); - block-size: var(--control-height-sm, var(--space-7)); - border-radius: var(--radius-md); + gap: var(--chart-bar-segment-track-gap); + block-size: var(--chart-track-height); + border-radius: var(--chart-bar-segment-track-radius); overflow: hidden; } [data-chart-bar-segment-fill] { block-size: 100%; - min-inline-size: var(--border-width-thick); + min-inline-size: var(--chart-bar-segment-fill-min-inline-size); transition: inline-size var(--chart-transition-duration) var(--chart-transition-ease), opacity var(--chart-transition-duration) var(--chart-transition-ease); @@ -261,13 +273,14 @@ [data-chart-bar-segment-legend] { display: flex; flex-wrap: wrap; - gap: var(--space-2) var(--space-4); + row-gap: var(--chart-bar-segment-legend-row-gap); + column-gap: var(--chart-bar-segment-legend-column-gap); } [data-chart-bar-segment-legend-item] { display: inline-flex; align-items: center; - gap: var(--space-1-5); - font-size: var(--font-size-xs); + gap: var(--chart-legend-item-gap); + font-size: var(--style-caption-font-size); color: var(--color-content-secondary); } [data-chart-bar-segment-legend-value] { @@ -279,7 +292,7 @@ [data-chart-radar-ring], [data-chart-radar-spoke] { fill: none; - stroke: var(--color-border-subtle); + stroke: var(--chart-grid-fg); stroke-width: var(--chart-grid-width); } [data-chart-radar-area] { @@ -310,34 +323,34 @@ /* Smith chart (RF reflection-coefficient disk). */ [data-chart-smith-boundary] { fill: none; - stroke: var(--color-content-secondary); + stroke: var(--chart-smith-boundary-fg); stroke-width: var(--border-width-medium); } [data-chart-smith-grid] { fill: none; - stroke: var(--color-border-default); + stroke: var(--chart-axis-fg); stroke-width: var(--border-width-thin); } [data-chart-smith-label] { fill: var(--color-content-secondary); - font-size: var(--font-size-xs); + font-size: var(--style-caption-font-size); font-variant-numeric: tabular-nums; pointer-events: none; } [data-chart-smith-point] { - stroke: var(--color-surface-default); + stroke: var(--chart-separator-fg); stroke-width: var(--border-width-medium); } [data-chart-smith-point-label] { fill: var(--color-content-primary); - font-size: var(--font-size-xs); + font-size: var(--style-caption-font-size); font-weight: var(--font-weight-medium); pointer-events: none; } /* ── Polar area (Nightingale / coxcomb petals) ────────────────────────────── */ [data-chart-polar-petal] { - stroke: var(--color-surface-default); + stroke: var(--chart-separator-fg); stroke-width: var(--chart-slice-stroke-width); fill-opacity: 0.9; /* So the `bloom` enter scales each petal from the chart centre (radial grow). */ @@ -350,21 +363,21 @@ /* ── Funnel (tapering stages) ─────────────────────────────────────────────── */ [data-chart-funnel-segment] { - stroke: var(--color-surface-default); + stroke: var(--chart-separator-fg); stroke-width: var(--chart-slice-stroke-width); } [data-chart-funnel-label] { - font-size: var(--font-size-sm); + font-size: var(--style-label-font-size); font-weight: var(--font-weight-medium); pointer-events: none; } /* Right-side labels (always legible — theme text colour, never on a coloured fill). */ [data-chart-funnel-connector] { - stroke: var(--color-border-default); + stroke: var(--chart-axis-fg); stroke-width: var(--chart-grid-width); } [data-chart-funnel-side] { - font-size: var(--font-size-sm); + font-size: var(--style-label-font-size); } [data-chart-funnel-name] { fill: var(--color-content-primary); @@ -377,7 +390,7 @@ /* ── Gauge (radial single value) ──────────────────────────────────────────── */ [data-chart-gauge-track] { - fill: var(--color-surface-muted); + fill: var(--chart-track-bg); } [data-chart-gauge-value] { transition: opacity var(--chart-transition-duration) var(--chart-transition-ease); diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index f2dcc48c9..fe05a184e 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -2486,6 +2486,28 @@ --chart-tooltip-gap: var(--space-1-5); --chart-crosshair-fg: var(--color-border-strong); --chart-crosshair-width: var(--border-width-thin); + --chart-axis-fg: var(--color-border-default); + --chart-grid-fg: var(--color-border-subtle); + --chart-separator-fg: var(--color-surface-default); + --chart-smith-boundary-fg: var(--color-content-secondary); + --chart-track-bg: var(--color-surface-muted); + --chart-track-height: var(--size-sm-control-height); + --chart-track-radius: var(--radius-sm); + --chart-gap: var(--space-2); + --chart-legend-row-gap: var(--space-1-5); + --chart-legend-column-gap: var(--space-4); + --chart-legend-item-gap: var(--space-1-5); + --chart-swatch-size: 0.75em; + --chart-swatch-radius: var(--radius-sm); + --chart-tooltip-swatch-size: 0.6em; + --chart-bar-list-row-gap: var(--space-2); + --chart-bar-list-column-gap: var(--space-3); + --chart-bar-segment-gap: var(--space-3); + --chart-bar-segment-track-gap: var(--border-width-medium); + --chart-bar-segment-track-radius: var(--radius-md); + --chart-bar-segment-fill-min-inline-size: var(--border-width-thick); + --chart-bar-segment-legend-row-gap: var(--space-2); + --chart-bar-segment-legend-column-gap: var(--space-4); --qr-code-fg: #18181b; --qr-code-bg: #ffffff; --qr-code-size-xs: 96px; diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index 3e204bea8..c36565409 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -3746,7 +3746,45 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ 'tooltip-padding': 'var(--space-2)', 'tooltip-gap': 'var(--space-1-5)', 'crosshair-fg': 'var(--color-border-strong)', - 'crosshair-width': 'var(--border-width-thin)' + 'crosshair-width': 'var(--border-width-thin)', + // ── The ink of everything that is NOT text (2026-08-22) ────────────── + // Text takes its size role from the named styles (`caption` / `label`) + // and its ink from the content slots — the recipe mints no typography. + // These four are lines and fills nothing else can name, and each is ONE + // concept the CSS repeats: the axis ink also draws the smith grid and the + // funnel connector; the grid ink also draws the radar rings; and + // `separator-fg` is the hairline of page surface that keeps adjacent + // marks apart — pie slice, active dot, smith point, polar petal, funnel + // segment: five parts, one idea. Naming them per part would be five keys + // carrying one value. + 'axis-fg': 'var(--color-border-default)', + 'grid-fg': 'var(--color-border-subtle)', + 'separator-fg': 'var(--color-surface-default)', + // The RF disk outline is deliberately stronger than the grid it encloses. + 'smith-boundary-fg': 'var(--color-content-secondary)', + // ── Rails (bar list, stacked bar, gauge) ───────────────────────────── + // `track-height` reads the size BUNDLE, not `--control-height-sm`: the raw + // coordinate is what `recipe-css-contract` forbids, and the CSS carried it + // (behind a dead `--space-7` fallback — the primitive does exist). + 'track-bg': 'var(--color-surface-muted)', + 'track-height': 'var(--size-sm-control-height)', + 'track-radius': 'var(--radius-sm)', + // ── Chrome geometry ────────────────────────────────────────────────── + gap: 'var(--space-2)', + 'legend-row-gap': 'var(--space-1-5)', + 'legend-column-gap': 'var(--space-4)', + 'legend-item-gap': 'var(--space-1-5)', + 'swatch-size': '0.75em', + 'swatch-radius': 'var(--radius-sm)', + 'tooltip-swatch-size': '0.6em', + 'bar-list-row-gap': 'var(--space-2)', + 'bar-list-column-gap': 'var(--space-3)', + 'bar-segment-gap': 'var(--space-3)', + 'bar-segment-track-gap': 'var(--border-width-medium)', + 'bar-segment-track-radius': 'var(--radius-md)', + 'bar-segment-fill-min-inline-size': 'var(--border-width-thick)', + 'bar-segment-legend-row-gap': 'var(--space-2)', + 'bar-segment-legend-column-gap': 'var(--space-4)' }, 'qr-code': { fg: '#18181b',