diff --git a/docs/audit/theming/README.md b/docs/audit/theming/README.md index 3b08aeec4..46d4f7253 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%** — 2617 de 4678 knobs pasan por un token público del componente -- **Reparto**: público 2617 · privado 341 · global 1156 · literal 564 · sistema transversal 509 _(fuera del ratio)_ -- **Sin token público propio**: 29 · **alcance < 20 %**: 20 · **alcance 100 %**: 14 · **con eje `size`**: 59 +- **Alcance global**: **56%** — 2628 de 4678 knobs pasan por un token público del componente +- **Reparto**: público 2628 · privado 338 · global 1148 · literal 564 · sistema transversal 509 _(fuera del ratio)_ +- **Sin token público propio**: 28 · **alcance < 20 %**: 20 · **alcance 100 %**: 14 · **con eje `size`**: 58 ## Cómo se lee @@ -91,12 +91,12 @@ theming §6.7 lo pone delante (`hover-bg`). Las fichas proponen la forma | 19 | [float-panel](./float-panel.md) | 20 | 59% | 53 | 28 | | 20 | [field](./field.md) | 19 | 72% | 73 | 60 | -## Sin ningún token público propio (29) +## Sin ningún token público propio (28) 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) · [`aspect-ratio`](./aspect-ratio.md) · [`auto-grid`](./auto-grid.md) · [`avatar`](./avatar.md) · [`cascade`](./cascade.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) · [`knob`](./knob.md) · [`mask-field`](./mask-field.md) · [`mockup`](./mockup.md) · [`month-grid`](./month-grid.md) · [`motion`](./motion.md) · [`number-field`](./number-field.md) · [`palabras`](./palabras.md) · [`range-calendar`](./range-calendar.md) · [`skin-media-player`](./skin-media-player.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) · [`wrap`](./wrap.md) · [`year-grid`](./year-grid.md) +[`alert-dialog`](./alert-dialog.md) · [`aspect-ratio`](./aspect-ratio.md) · [`auto-grid`](./auto-grid.md) · [`avatar`](./avatar.md) · [`cascade`](./cascade.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) · [`mask-field`](./mask-field.md) · [`mockup`](./mockup.md) · [`month-grid`](./month-grid.md) · [`motion`](./motion.md) · [`number-field`](./number-field.md) · [`palabras`](./palabras.md) · [`range-calendar`](./range-calendar.md) · [`skin-media-player`](./skin-media-player.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) · [`wrap`](./wrap.md) · [`year-grid`](./year-grid.md) ## Tabla completa (162 recetas, por alcance ascendente) @@ -131,7 +131,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [time-range-field](./time-range-field.md) | 25% | 9 | 2 | 0 | 4 | 2 | 1 | 3 | – | | [surface](./surface.md) | 25% | 4 | 1 | 3 | 0 | 0 | 0 | 25 | – | | [callout](./callout.md) | 27% | 15 | 4 | 3 | 7 | 1 | 0 | 28 | – | -| [knob](./knob.md) | 29% | 27 | 7 | 4 | 9 | 4 | 3 | 0 | y | | [onion-menu](./onion-menu.md) | 29% | 27 | 7 | 0 | 8 | 9 | 3 | 12 | – | | [empty-state](./empty-state.md) | 29% | 17 | 5 | 7 | 5 | 0 | 0 | 6 | y | | [banner](./banner.md) | 30% | 10 | 3 | 5 | 1 | 1 | 0 | 42 | y | @@ -196,6 +195,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [metrics](./metrics.md) | 74% | 50 | 37 | 0 | 9 | 4 | 0 | 68 | y | | [editable](./editable.md) | 75% | 44 | 30 | 7 | 1 | 2 | 4 | 56 | y | | [avatar](./avatar.md) | 75% | 36 | 27 | 3 | 0 | 6 | 0 | 0 | y | +| [knob](./knob.md) | 75% | 27 | 18 | 1 | 1 | 4 | 3 | 33 | – | | [switch](./switch.md) | 76% | 22 | 16 | 4 | 1 | 0 | 1 | 51 | y | | [file-upload](./file-upload.md) | 76% | 61 | 45 | 9 | 1 | 4 | 2 | 98 | y | | [carousel](./carousel.md) | 77% | 35 | 23 | 2 | 0 | 5 | 5 | 27 | – | diff --git a/docs/audit/theming/knob.md b/docs/audit/theming/knob.md index 66a092f44..1bf6d0d3c 100644 --- a/docs/audit/theming/knob.md +++ b/docs/audit/theming/knob.md @@ -5,44 +5,33 @@ > 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**: **29%** — 7 de 24 knobs por token público -- **Knobs de apariencia**: 27 — público 7 · privado 4 · global 9 · literal 4 · sistema 3 _(fuera del ratio)_ -- **Contrato hoy** (`lib/recipes/base.ts`): **sin entrada en `base.ts`** -- **Eje `size`**: sí · **ficheros**: `knob.css` +- **Medido**: 2026-08-22 · **Alcance**: **75%** — 18 de 24 knobs por token público +- **Knobs de apariencia**: 27 — público 18 · privado 1 · global 1 · literal 4 · sistema 3 _(fuera del ratio)_ +- **Contrato hoy** (`lib/recipes/base.ts`): 33 pública(s) — `gap-sm`, `gap-md`, `gap-lg`, `gap-xl`, `diameter-sm`, `diameter-md`, `diameter-lg`, `diameter-xl`, `label-font-size-sm`, `label-font-size-md`, `label-font-size-lg`, `label-font-size-xl`, `value-font-size-sm`, `value-font-size-md`, `value-font-size-lg`, `value-font-size-xl`, `gap`, `diameter`, `label-font-size`, `value-font-size`, `radius`, `shadow`, `dragging-shadow`, `track-bg`, `face-bg`, `arc-width`, `pointer-inset`, `pointer-width`, `pointer-bg`, `indicator-radius`, `value-fg`, `label-fg`, `label-line-height` +- **Eje `size`**: no · **ficheros**: `knob.css` ## 1. Knobs fuera de alcance -### 1.1 Directo a primitivo global (9) +### 1.1 Directo a primitivo global (1) | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `knob.css:59` | `[data-knob-control]` | `border-radius` | `var(--radius-full, 9999px)` | -| 2 | `knob.css:67` | `[data-knob-control]` | `box-shadow` | `var(--shadow-2)` | -| 3 | `knob.css:92` | `[data-knob-value-text]` | `color` | `var(--color-neutral-text)` | -| 4 | `knob.css:141` | `[data-knob-value-field] [data-spin-field-input]` | `color` | `var(--color-neutral-text)` | -| 5 | `knob.css:147` | `[data-knob-value-field] [data-spin-field-input]:focus-visible` | `border-radius` | `var(--radius-sm)` | -| 6 | `knob.css:168` | `[data-knob-indicator]::before` | `border-radius` | `var(--radius-sm)` | -| 7 | `knob.css:187` | `[data-knob][data-dragging] [data-knob-control]` | `box-shadow` | `var(--shadow-3)` | -| 8 | `knob.css:227` | `[data-knob-label]` | `line-height` | `var(--leading-ui)` | -| 9 | `knob.css:228` | `[data-knob-label]` | `color` | `var(--color-neutral-text)` | - -### 1.2 A través de un privado (4) +| 1 | `knob.css:127` | `[data-knob-value-field] [data-spin-field-input]:focus-visible` | `border-radius` | `var(--radius-sm)` | + +### 1.2 A través de un privado (1) | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `knob.css:90` | `[data-knob-value-text]` | `font-size` | `var(--_knob-value-font)` | -| 2 | `knob.css:134` | `[data-knob-value-field] [data-spin-field-input]` | `inline-size` | `calc(var(--_knob-value-digits, 3) * 1ch)` | -| 3 | `knob.css:139` | `[data-knob-value-field] [data-spin-field-input]` | `font-size` | `var(--_knob-value-font)` | -| 4 | `knob.css:226` | `[data-knob-label]` | `font-size` | `var(--_knob-label-font)` | +| 1 | `knob.css:114` | `[data-knob-value-field] [data-spin-field-input]` | `inline-size` | `calc(var(--_knob-value-digits, 3) * 1ch)` | ### 1.3 Literales (4) | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `knob.css:91` | `[data-knob-value-text]` | `line-height` | `1` | -| 2 | `knob.css:140` | `[data-knob-value-field] [data-spin-field-input]` | `line-height` | `1` | -| 3 | `knob.css:167` | `[data-knob-indicator]::before` | `block-size` | `28%` | -| 4 | `knob.css:177` | `[data-knob-control]:hover` | `filter` | `brightness(1.08)` | +| 1 | `knob.css:71` | `[data-knob-value-text]` | `line-height` | `1` | +| 2 | `knob.css:120` | `[data-knob-value-field] [data-spin-field-input]` | `line-height` | `1` | +| 3 | `knob.css:149` | `[data-knob-indicator]::before` | `block-size` | `28%` | +| 4 | `knob.css:159` | `[data-knob-control]:hover` | `filter` | `brightness(1.08)` | ## 2. Sistema transversal (3) — informativo, fuera del ratio @@ -50,27 +39,21 @@ Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2). | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `knob.css:145` | `[data-knob-value-field] [data-spin-field-input]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` | -| 2 | `knob.css:181` | `[data-knob-control]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` | -| 3 | `knob.css:192` | `[data-knob][data-disabled] [data-knob-control]` | `opacity` | `var(--opacity-disabled)` | +| 1 | `knob.css:125` | `[data-knob-value-field] [data-spin-field-input]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` | +| 2 | `knob.css:163` | `[data-knob-control]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` | +| 3 | `knob.css:174` | `[data-knob][data-disabled] [data-knob-control]` | `opacity` | `var(--opacity-disabled)` | ## 3. Privados de la receta — ¿de dónde sale su valor? -| privado | declaraciones | valor(es) | origen | ¿deriva de un público? | -| --- | ---: | --- | --- | :-: | -| `--_knob-gap` | 4 | `var(--size-md-gap)`, `var(--size-sm-gap)`, `var(--size-lg-gap)`, `var(--size-xl-gap)` | global | no | -| `--_knob-diameter` | 4 | `calc(var(--size-md-control-height) * 2)`, `calc(var(--size-sm-control-height) * 2)`, `calc(var(--size-lg-control-height) * 2)`, `calc(var(--size-xl-control-height) * 2)` | global | no | -| `--_knob-label-font` | 4 | `var(--size-md-font-size)`, `var(--size-sm-font-size)`, `var(--size-lg-font-size)`, `var(--size-xl-font-size)` | global | no | -| `--_knob-value-font` | 4 | `var(--size-md-font-size)`, `var(--size-sm-font-size)`, `var(--size-lg-font-size)`, `var(--size-xl-font-size)` | global | no | +_La receta no declara privados propios en su CSS._ Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inline del wrapper): `--_knob-value-digits`. ## 4. Propuesta de corrección - **Consume la capa compartida `spin-field`.** Un eje que la capa posee se consume como `var(--_x, var(--x))`; el consumidor **no acuña** `--knob-{eje}` para él — sería un vocabulario paralelo (README de `eidos/components`, «Capas compartidas» regla 2). -- **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` (22) +### 4.1 Tokens a declarar en `lib/recipes/base.ts` (3) 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 @@ -80,39 +63,15 @@ no distingue lo que debería — se marca `⚠`. | token (`--knob-…`) | scope TSC | valor propuesto | usos | | --- | --- | --- | ---: | -| `control-shadow` | `root` | ⚠ `var(--shadow-2)` / `var(--shadow-3)` | 2 | -| `control-radius` | `root` | `var(--radius-full, 9999px)` | 1 | -| `value-text-font-size-md` | `size:md` | `var(--size-md-font-size)` | 1 | -| `value-text-font-size-sm` | `size:sm` | `var(--size-sm-font-size)` | 1 | -| `value-text-font-size-lg` | `size:lg` | `var(--size-lg-font-size)` | 1 | -| `value-text-font-size-xl` | `size:xl` | `var(--size-xl-font-size)` | 1 | -| `value-text-fg` | `root` | `var(--color-neutral-text)` | 1 | -| `value-field-font-size-md` | `size:md` | `var(--size-md-font-size)` | 1 | -| `value-field-font-size-sm` | `size:sm` | `var(--size-sm-font-size)` | 1 | -| `value-field-font-size-lg` | `size:lg` | `var(--size-lg-font-size)` | 1 | -| `value-field-font-size-xl` | `size:xl` | `var(--size-xl-font-size)` | 1 | -| `value-field-fg` | `root` | `var(--color-neutral-text)` | 1 | | `value-field-radius` | `root` | `var(--radius-sm)` | 1 | | `indicator-height` | `root` | `28%` | 1 | -| `indicator-radius` | `root` | `var(--radius-sm)` | 1 | | `hover-control-filter` | `root` | `brightness(1.08)` | 1 | -| `label-font-size-md` | `size:md` | `var(--size-md-font-size)` | 1 | -| `label-font-size-sm` | `size:sm` | `var(--size-sm-font-size)` | 1 | -| `label-font-size-lg` | `size:lg` | `var(--size-lg-font-size)` | 1 | -| `label-font-size-xl` | `size:xl` | `var(--size-xl-font-size)` | 1 | -| `label-line-height` | `root` | `var(--leading-ui)` | 1 | -| `label-fg` | `root` | `var(--color-neutral-text)` | 1 | ### 4.2 Sin nombre mecánico (3) - **⚠ 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** — 2: `line-height`. - **⚠ decisión: el privado que alimenta este knob no se declara en el CSS (viene de `base.ts` o de un estilo inline) — hay que resolverlo antes de nombrarlo** — 1: `inline-size`. -### 4.3 Avisos sobre los tokens propuestos (2) - -- **el privado `--_knob-value-font` debe pasar a leer este público (o desaparecer)** — `--knob-value-text-font-size-md`, `--knob-value-text-font-size-sm`, `--knob-value-text-font-size-lg`, `--knob-value-text-font-size-xl`, `--knob-value-field-font-size-md`, `--knob-value-field-font-size-sm`, `--knob-value-field-font-size-lg`, `--knob-value-field-font-size-xl` -- **el privado `--_knob-label-font` debe pasar a leer este público (o desaparecer)** — `--knob-label-font-size-md`, `--knob-label-font-size-sm`, `--knob-label-font-size-lg`, `--knob-label-font-size-xl` - ### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4) - [ ] **Privado que no deriva de un público** — §3 lo marca; el privado debe leer el público o desaparecer. @@ -125,6 +84,33 @@ no distingue lo que debería — se marca `⚠`. -_(pendiente — lo escribe el autor; se conserva al regenerar)_ +**Medido 2026-08-22** (sonda ×2 = 0 diffs en 754 valores · 7 estados). El knob +ya tenía siete públicos «de facto» —consumidos con la forma +`var(--knob-x, )`— pero **ninguno declarado en el contrato**, así que +un tema no los ve en `getRecipeTokens()` y el censo los cuenta bien por +casualidad. La tokenización consiste sobre todo en DECLARAR lo que ya se +consume, y en separar dos cosas que compartían nombre. + +1. **Cuatro `--knob-*` NO son de tema: son canal de valor.** El provider los + escribe INLINE desde el estado del control + (`knob-provider.svelte.ts:358`): `--knob-progress`, `--knob-angle`, + `--knob-start-angle` y `--knob-sweep`. Ningún contrato puede ganarles y + ninguno debe intentarlo — es la clase de `carousel.item-gap` y de la `z` del + preview de `drag-drop`. +2. **⚠ `--knob-arc-width` significa DOS cosas con DOS defaults distintos**: el + inset de la cara del dial (`--space-3`, líneas 80 y 113) y la separación + superior del puntero (`--space-2`, línea 165). Un tema que lo escriba mueve + las tres a la vez, pero sin tema cada una vale algo distinto — «un nombre + que no distingue lo que debería», que es la definición de colisión. Se + separan: `arc-width` (la cara) y `pointer-inset` (el puntero), cada uno con + su valor verbatim. **Cambia la superficie, no el píxel.** +3. **Las cuatro coordenadas por talla suben al TSC.** Hoy el default por talla + vive en un privado y el público es un OVERRIDE que va DELANTE + (`var(--knob-gap, var(--_knob-gap))`), así que declarar `--knob-gap` en el + contrato **mataría la escala**: ganaría siempre. La forma correcta es la del + resto del eje — coordenadas `{eje}-{k}` + nombre resuelto por `data-size`, y + la receta lee el resuelto a secas. El eje es **sm|md|lg|xl**, sin xs. +4. El diámetro es `control-height × 2` por talla: una PROPORCIÓN del bundle, + que se conserva verbatim en cada coordenada. diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index c54235db6..5d7771db8 100644 --- a/scripts/theming-sentinel-exceptions.ts +++ b/scripts/theming-sentinel-exceptions.ts @@ -142,6 +142,24 @@ export const SENTINEL_EXCEPTIONS: Record> = { 'underline-offset': "same: no underline at rest under data-underline='hover'; forced → reaches (2 → 11px). textUnderlineOffset added to the guard prop list too" }, + knob: { + 'label-font-size-sm': + 'the demo mounts no [data-knob-label] (0 nodes, counted); forced → reaches (per-size coordinate; the resolved name moves with it)', + 'label-font-size-md': + 'the demo mounts no [data-knob-label] (0 nodes, counted); forced → reaches (16 → 99px)', + 'label-font-size-lg': + 'the demo mounts no [data-knob-label] (0 nodes, counted); forced → reaches (per-size coordinate)', + 'label-font-size-xl': + 'the demo mounts no [data-knob-label] (0 nodes, counted); forced → reaches (per-size coordinate)', + 'label-font-size': + 'the demo mounts no [data-knob-label] (0 nodes, counted); forced → reaches (16 → 99px)', + 'label-fg': + 'the demo mounts no [data-knob-label] (0 nodes, counted); forced → reaches (rgb(1,2,3))', + 'label-line-height': + 'the demo mounts no [data-knob-label] (0 nodes, counted); forced → reaches (20px → 3.77em)', + 'dragging-shadow': + 'consumed only under [data-dragging], and the demo knob is at rest (0 nodes, counted); forced → reaches (0 0 0 7px rgb(4,5,6))' + }, label: { 'disabled-fg': 'the demo mounts no disabled label (0 [data-disabled] nodes, counted); forced → reaches (rgb(1,2,3))' diff --git a/src/uix/eidos/components/knob/README.md b/src/uix/eidos/components/knob/README.md index df40d7645..d4d8b6671 100644 --- a/src/uix/eidos/components/knob/README.md +++ b/src/uix/eidos/components/knob/README.md @@ -104,6 +104,41 @@ Referencias: [APG Slider](https://www.w3.org/WAI/ARIA/apg/patterns/slider/) · - **`Indicator` es decorativo** (`role="presentation"`, excepción A2.3 documentada en el morfo): se posiciona solo con las vars publicadas. +## Talla y tema + +**Guard R-5.4** (`npm run theming:sentinel -- knob `): 25/33 tokens mueven +un computed en vivo; los ocho que callan están adjudicados con su medición en +`scripts/theming-sentinel-exceptions.ts` (la demo no monta etiqueta ni estado +de arrastre). Corrido 2026-08-22. + +Contrato en `lib/recipes/base.ts > knob` (33 claves, entrada NUEVA). Alcance +del censo: **75 %** (era 29 %). + +**Cuatro `--knob-*` NO están en el contrato, y es deliberado**: `progress`, +`angle`, `start-angle` y `sweep` los escribe el provider **inline** desde el +estado del control (`knob-provider.svelte.ts:358`). Son canal de valor, no +superficie de tema, y ningún contrato puede ganar a un estilo inline — la clase +de `carousel.item-gap` y de la `z` del preview de `drag-drop`. + +**Las cuatro coordenadas por talla subieron al TSC, y había que hacerlo así.** +Antes el default por talla vivía en un privado y el público era un OVERRIDE que +iba DELANTE (`var(--knob-gap, var(--_knob-gap))`): declarar `--knob-gap` en el +contrato **habría matado la escala**, porque ganaría siempre. El eje es +sm|md|lg|xl (sin xs), y el diámetro es `control-height × 2` en cada talla — una +proporción del bundle, conservada verbatim. + +### ⚠ `--knob-arc-width` significaba dos cosas + +Medido: el mismo nombre gobernaba el inset de la cara del dial (default +`--space-3`) y la separación superior del puntero (default `--space-2`). Un +tema que lo escribiera movía las tres declaraciones a la vez, pero **sin tema +cada una valía algo distinto** — «un nombre que no distingue lo que debería». +Separados en `arc-width` (la cara) y `pointer-inset` (el puntero), cada uno con +su valor verbatim: **cambia la superficie de tema, no el píxel** (0 diffs). + +`--_knob-value-digits` sigue privado: es el número de dígitos del valor máximo, +dato del consumidor, no del tema. + ## Gaps | Gap | Disposición | Detalle | diff --git a/src/uix/eidos/components/knob/knob.css b/src/uix/eidos/components/knob/knob.css index ac2a8e98f..9e4dd241c 100644 --- a/src/uix/eidos/components/knob/knob.css +++ b/src/uix/eidos/components/knob/knob.css @@ -17,54 +17,34 @@ * diameter AND fonts scale together from the same size k (--size-{k}-*). The * public --knob-* aliases below still override per-instance; the private var * is only the size-driven default. */ - --_knob-gap: var(--size-md-gap); - --_knob-diameter: calc(var(--size-md-control-height) * 2); - --_knob-label-font: var(--size-md-font-size); - --_knob-value-font: var(--size-md-font-size); - + /* The four size coordinates (gap, diameter, label/value type) are EMITTED by + the TSC from the recipe contract. They used to live in privates behind a + `var(--knob-x, var(--_knob-x))` override, where the public went FIRST — + declaring the public in the contract would have killed the scale. */ display: inline-flex; flex-direction: column; align-items: center; - gap: var(--knob-gap, var(--_knob-gap)); -} - -[data-knob][data-size='sm'] { - --_knob-gap: var(--size-sm-gap); - --_knob-diameter: calc(var(--size-sm-control-height) * 2); - --_knob-label-font: var(--size-sm-font-size); - --_knob-value-font: var(--size-sm-font-size); -} - -[data-knob][data-size='lg'] { - --_knob-gap: var(--size-lg-gap); - --_knob-diameter: calc(var(--size-lg-control-height) * 2); - --_knob-label-font: var(--size-lg-font-size); - --_knob-value-font: var(--size-lg-font-size); + gap: var(--knob-gap); } -[data-knob][data-size='xl'] { - --_knob-gap: var(--size-xl-gap); - --_knob-diameter: calc(var(--size-xl-control-height) * 2); - --_knob-label-font: var(--size-xl-font-size); - --_knob-value-font: var(--size-xl-font-size); -} +/* Size — the TSC emits gap / diameter / label / value coordinates per size. */ [data-knob-control] { position: relative; /* Center the value readout in the dial's hole (known-knob convention). */ display: grid; place-items: center; - inline-size: var(--knob-diameter, var(--_knob-diameter)); - block-size: var(--knob-diameter, var(--_knob-diameter)); - border-radius: var(--radius-full, 9999px); /* literal: fallback for circle */ + inline-size: var(--knob-diameter); + block-size: var(--knob-diameter); + border-radius: var(--knob-radius); /* Value arc (conic pie), driven by the provider-published progress. The face * plate below carves it into a donut ring. */ background: conic-gradient( from calc(var(--knob-start-angle, -135deg)), var(--color-primary-solid) calc(var(--knob-progress, 0) * var(--knob-sweep, 270deg)), - var(--knob-track-bg, var(--color-neutral-track)) 0 + var(--knob-track-bg) 0 ); - box-shadow: var(--shadow-2); + box-shadow: var(--knob-shadow); cursor: grab; touch-action: none; user-select: none; @@ -77,9 +57,9 @@ [data-knob-control]::before { content: ''; position: absolute; - inset: var(--knob-arc-width, var(--space-3)); + inset: var(--knob-arc-width); border-radius: inherit; - background: var(--knob-face-bg, var(--color-surface-raised)); + background: var(--knob-face-bg); z-index: 0; } @@ -87,9 +67,9 @@ [data-knob-value-text] { position: relative; z-index: 2; - font-size: var(--_knob-value-font); + font-size: var(--knob-value-font-size); line-height: 1; - color: var(--color-neutral-text); + color: var(--knob-value-fg); font-variant-numeric: tabular-nums; } @@ -110,7 +90,7 @@ inline-size: auto; min-inline-size: 0; max-inline-size: calc( - var(--knob-diameter, var(--_knob-diameter)) - 2 * var(--knob-arc-width, var(--space-3)) + var(--knob-diameter) - 2 * var(--knob-arc-width) ); block-size: auto; padding: 0; @@ -136,9 +116,9 @@ max-inline-size: none; padding: 0; text-align: center; - font-size: var(--_knob-value-font); + font-size: var(--knob-value-font-size); line-height: 1; - color: var(--color-neutral-text); + color: var(--knob-value-fg); font-variant-numeric: tabular-nums; } [data-knob-value-field] [data-spin-field-input]:focus-visible { @@ -160,13 +140,15 @@ content: ''; position: absolute; inset-inline-start: calc( - 50% - (var(--knob-pointer-width, 3px) / 2) - ); /* literal: hairline pointer */ - inset-block-start: var(--knob-arc-width, var(--space-2)); - inline-size: var(--knob-pointer-width, 3px); /* literal: hairline pointer */ + 50% - (var(--knob-pointer-width) / 2) + ); + /* Its own axis: the single `--knob-arc-width` that used to sit here carried + a DIFFERENT default (--space-2) than the face inset (--space-3). */ + inset-block-start: var(--knob-pointer-inset); + inline-size: var(--knob-pointer-width); block-size: 28%; /* literal: pointer proportion of face */ - border-radius: var(--radius-sm); - background: var(--knob-pointer-bg, var(--color-neutral-text-strong)); + border-radius: var(--knob-indicator-radius); + background: var(--knob-pointer-bg); } /* ---- states (R-1.x coverage) -------------------------------------------- */ @@ -184,7 +166,7 @@ [data-knob][data-dragging] [data-knob-control] { cursor: grabbing; - box-shadow: var(--shadow-3); + box-shadow: var(--knob-dragging-shadow); } [data-knob][data-disabled] [data-knob-control] { @@ -223,7 +205,7 @@ /* ---- typography ----------------------------------------------------------- */ [data-knob-label] { - font-size: var(--_knob-label-font); - line-height: var(--leading-ui); - color: var(--color-neutral-text); + font-size: var(--knob-label-font-size); + line-height: var(--knob-label-line-height); + color: var(--knob-label-fg); } diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index b17eef167..93a058e0c 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -4084,6 +4084,35 @@ --menu-dial-gap: var(--space-3); --menu-dial-stagger-step: 28ms; --menu-dial-backdrop-bg: color-mix(in oklab, var(--color-neutral-text-strong) 40%, transparent); + --knob-gap-sm: var(--size-sm-gap); + --knob-gap-md: var(--size-md-gap); + --knob-gap-lg: var(--size-lg-gap); + --knob-gap-xl: var(--size-xl-gap); + --knob-diameter-sm: calc(var(--size-sm-control-height) * 2); + --knob-diameter-md: calc(var(--size-md-control-height) * 2); + --knob-diameter-lg: calc(var(--size-lg-control-height) * 2); + --knob-diameter-xl: calc(var(--size-xl-control-height) * 2); + --knob-label-font-size-sm: var(--size-sm-font-size); + --knob-label-font-size-md: var(--size-md-font-size); + --knob-label-font-size-lg: var(--size-lg-font-size); + --knob-label-font-size-xl: var(--size-xl-font-size); + --knob-value-font-size-sm: var(--size-sm-font-size); + --knob-value-font-size-md: var(--size-md-font-size); + --knob-value-font-size-lg: var(--size-lg-font-size); + --knob-value-font-size-xl: var(--size-xl-font-size); + --knob-radius: var(--radius-full); + --knob-shadow: var(--shadow-2); + --knob-dragging-shadow: var(--shadow-3); + --knob-track-bg: var(--color-neutral-track); + --knob-face-bg: var(--color-surface-raised); + --knob-arc-width: var(--space-3); + --knob-pointer-inset: var(--space-2); + --knob-pointer-width: 3px; + --knob-pointer-bg: var(--color-neutral-text-strong); + --knob-indicator-radius: var(--radius-sm); + --knob-value-fg: var(--color-neutral-text); + --knob-label-fg: var(--color-neutral-text); + --knob-label-line-height: var(--leading-ui); --kbd-font-size: var(--size-xs-font-size); --kbd-font-weight: var(--font-weight-medium); --kbd-letter-spacing: var(--tracking-wide); @@ -7037,6 +7066,34 @@ --virtual-grid-font-size: var(--virtual-grid-font-size-xl); } +[data-knob] { + --knob-gap: var(--knob-gap-md); + --knob-diameter: var(--knob-diameter-md); + --knob-label-font-size: var(--knob-label-font-size-md); + --knob-value-font-size: var(--knob-value-font-size-md); +} + +[data-knob][data-size='sm'] { + --knob-gap: var(--knob-gap-sm); + --knob-diameter: var(--knob-diameter-sm); + --knob-label-font-size: var(--knob-label-font-size-sm); + --knob-value-font-size: var(--knob-value-font-size-sm); +} + +[data-knob][data-size='lg'] { + --knob-gap: var(--knob-gap-lg); + --knob-diameter: var(--knob-diameter-lg); + --knob-label-font-size: var(--knob-label-font-size-lg); + --knob-value-font-size: var(--knob-value-font-size-lg); +} + +[data-knob][data-size='xl'] { + --knob-gap: var(--knob-gap-xl); + --knob-diameter: var(--knob-diameter-xl); + --knob-label-font-size: var(--knob-label-font-size-xl); + --knob-value-font-size: var(--knob-value-font-size-xl); +} + [data-link-preview-content] { --link-preview-padding-block: var(--link-preview-padding-block-md); --link-preview-padding-inline: var(--link-preview-padding-inline-md); diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index 69da5a191..b11af60ca 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -6602,6 +6602,86 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ 'stagger-step': '28ms', 'backdrop-bg': 'color-mix(in oklab, var(--color-neutral-text-strong) 40%, transparent)' }, + // Knob. FOUR `--knob-*` names are deliberately absent: `progress`, `angle`, + // `start-angle` and `sweep` are written INLINE by the provider from control + // state (`knob-provider.svelte.ts:358`) — a value channel, not a theme + // surface, and no contract can outrank an inline style. + // + // The size axis is sm|md|lg|xl (no xs). It moves here rather than staying in + // a private on purpose: the recipe used to read `var(--knob-gap, + // var(--_knob-gap))`, where the public goes FIRST — so declaring the public + // in the contract would have killed the per-size scale outright. + knob: { + 'gap-sm': 'var(--size-sm-gap)', + 'gap-md': 'var(--size-md-gap)', + 'gap-lg': 'var(--size-lg-gap)', + 'gap-xl': 'var(--size-xl-gap)', + // The dial is TWICE the control height at every size — a proportion of the + // bundle, kept verbatim per coordinate. + 'diameter-sm': 'calc(var(--size-sm-control-height) * 2)', + 'diameter-md': 'calc(var(--size-md-control-height) * 2)', + 'diameter-lg': 'calc(var(--size-lg-control-height) * 2)', + 'diameter-xl': 'calc(var(--size-xl-control-height) * 2)', + 'label-font-size-sm': 'var(--size-sm-font-size)', + 'label-font-size-md': 'var(--size-md-font-size)', + 'label-font-size-lg': 'var(--size-lg-font-size)', + 'label-font-size-xl': 'var(--size-xl-font-size)', + 'value-font-size-sm': 'var(--size-sm-font-size)', + 'value-font-size-md': 'var(--size-md-font-size)', + 'value-font-size-lg': 'var(--size-lg-font-size)', + 'value-font-size-xl': 'var(--size-xl-font-size)', + + 'gap': { + declarations: [ + { value: 'var(--knob-gap-md)', scope: 'host' }, + { value: 'var(--knob-gap-sm)', scope: 'size:sm' }, + { value: 'var(--knob-gap-lg)', scope: 'size:lg' }, + { value: 'var(--knob-gap-xl)', scope: 'size:xl' } + ] + }, + 'diameter': { + declarations: [ + { value: 'var(--knob-diameter-md)', scope: 'host' }, + { value: 'var(--knob-diameter-sm)', scope: 'size:sm' }, + { value: 'var(--knob-diameter-lg)', scope: 'size:lg' }, + { value: 'var(--knob-diameter-xl)', scope: 'size:xl' } + ] + }, + 'label-font-size': { + declarations: [ + { value: 'var(--knob-label-font-size-md)', scope: 'host' }, + { value: 'var(--knob-label-font-size-sm)', scope: 'size:sm' }, + { value: 'var(--knob-label-font-size-lg)', scope: 'size:lg' }, + { value: 'var(--knob-label-font-size-xl)', scope: 'size:xl' } + ] + }, + 'value-font-size': { + declarations: [ + { value: 'var(--knob-value-font-size-md)', scope: 'host' }, + { value: 'var(--knob-value-font-size-sm)', scope: 'size:sm' }, + { value: 'var(--knob-value-font-size-lg)', scope: 'size:lg' }, + { value: 'var(--knob-value-font-size-xl)', scope: 'size:xl' } + ] + }, + + // The dial face and its ring. + radius: 'var(--radius-full)', + shadow: 'var(--shadow-2)', + 'dragging-shadow': 'var(--shadow-3)', + 'track-bg': 'var(--color-neutral-track)', + 'face-bg': 'var(--color-surface-raised)', + // SPLIT from a single `--knob-arc-width` that carried two meanings with two + // different defaults: the face inset (--space-3) and the pointer's top + // offset (--space-2). One name that does not distinguish what it should. + 'arc-width': 'var(--space-3)', + 'pointer-inset': 'var(--space-2)', + 'pointer-width': '3px', + 'pointer-bg': 'var(--color-neutral-text-strong)', + 'indicator-radius': 'var(--radius-sm)', + 'value-fg': 'var(--color-neutral-text)', + 'label-fg': 'var(--color-neutral-text)', + 'label-line-height': 'var(--leading-ui)' + }, kbd: { 'font-size': 'var(--size-xs-font-size)', 'font-weight': 'var(--font-weight-medium)', diff --git a/web/routes/uix/components/knob/+page.svelte b/web/routes/uix/components/knob/+page.svelte index 42d1151cd..c7e65a4ac 100644 --- a/web/routes/uix/components/knob/+page.svelte +++ b/web/routes/uix/components/knob/+page.svelte @@ -3,8 +3,9 @@ import { compileMorfo } from '$uix/morfo'; import { knobMorfo } from '@/uix/morfo/components/knob'; import { getActiveUix } from '$active-uix'; + import TokensPanel from '../../lib/TokensPanel.svelte'; - type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y'; + type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'tokens' | 'a11y'; type Intent = 'neutral' | 'affirm' | 'risk' | 'threat'; type TraceEntry = { event: string; family: string; intent?: string; at: number }; @@ -178,6 +179,9 @@ + @@ -503,6 +507,10 @@ {/if} + {#if tab === 'tokens'} + + {/if} + {#if tab === 'a11y'}

Accessibility