From 471ba96d25d0812d83a99d45409be3d486fc18a1 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 22 Aug 2026 01:22:15 +0200 Subject: [PATCH] =?UTF-8?q?uix(menu-dial):=20temable=20=E2=80=94=200=20%?= =?UTF-8?q?=20=E2=86=92=2025=20%,=20y=20las=20ranuras=20de=20la=20capa=20q?= =?UTF-8?q?ue=20NO=20se=20acu=C3=B1an?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → 25 %. **La receta posee sólo el cromo del dial**, como dice su propia cabecera: las acciones son `Button` compuestos y la colocación flotante es de la capa `viewport-placement`. Lo suyo son tres cosas: el hueco del abanico, el paso del escalonado de entrada y el tinte del velo. **`--_viewport-placement-offset` y `-z` NO se acuñan, y ésa es la decisión del commit.** Son las RANURAS DE OVERRIDE que la capa compartida publica, y esta receta escribe en ellas su decisión — 20px en vez de los 16px por defecto de la capa, y el comentario del CSS deja claro que es decisión y no deriva. Acuñar `menu-dial-placement-offset` sería el vocabulario paralelo que la regla 2 de capas compartidas prohíbe: el eje es de la capa, y un tema lo mueve ahí. Es el mismo techo que `listbox` paga por consumir bien `list-surface` — el grueso del 75 % que no alcanza, y es correcto que no lo haga (§13). **El escalonado es tema, no motion**: `stagger-step` no es una duración de animación sino el INTERVALO entre hermanos (multiplica el índice de cada acción), y es lo que da carácter al abanico. Por eso entra al contrato. Artefactos y gates: - Sonda antes/después = 0 diffs en 1.856 valores × 8 estados. - R-5.4: 1/3, los dos mudos adjudicados con su medición — el velo sólo existe con el dial abierto (0 nodos en reposo, contados) y el escalonado sólo se aplica bajo `[data-stagger]` con la lista abierta; forzando ambos atributos con índice 3, alcanza (0.084s → 2.331s = 3 × 777ms). - censo --only 25 % · component:audit PASS · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · check por fichero limpio. - README con «Talla y tema» y tab `Tokens` en la demo (3 claves). Co-Authored-By: Claude Fable 5 --- docs/audit/theming/README.md | 12 ++--- docs/audit/theming/menu-dial.md | 47 ++++++++++++------- scripts/theming-sentinel-exceptions.ts | 6 +++ src/uix/eidos/components/menu-dial/README.md | 29 ++++++++++++ .../eidos/components/menu-dial/menu-dial.css | 8 ++-- src/uix/eidos/generated/base.css | 3 ++ src/uix/eidos/lib/recipes/base.ts | 13 +++++ .../uix/components/menu-dial/+page.svelte | 10 +++- 8 files changed, 99 insertions(+), 29 deletions(-) diff --git a/docs/audit/theming/README.md b/docs/audit/theming/README.md index 5a0b825c4..af5721993 100644 --- a/docs/audit/theming/README.md +++ b/docs/audit/theming/README.md @@ -10,9 +10,9 @@ - **Medido**: 2026-08-21 · **162 recetas** con CSS + **8 componentes sin receta** = 170 fichas, el árbol entero de `eidos/components/` - **La pregunta**: ¿cuánto de la apariencia de cada componente puede cambiar un tema **sin tocar el sistema ni la receta**? -- **Alcance global**: **50%** — 2329 de 4676 knobs pasan por un token público del componente -- **Reparto**: público 2329 · privado 627 · global 1156 · literal 564 · sistema transversal 509 _(fuera del ratio)_ -- **Sin token público propio**: 30 · **alcance < 20 %**: 23 · **alcance 100 %**: 10 · **con eje `size`**: 59 +- **Alcance global**: **50%** — 2331 de 4676 knobs pasan por un token público del componente +- **Reparto**: público 2331 · privado 625 · global 1156 · literal 564 · sistema transversal 509 _(fuera del ratio)_ +- **Sin token público propio**: 29 · **alcance < 20 %**: 22 · **alcance 100 %**: 10 · **con eje `size`**: 59 ## Cómo se lee @@ -91,12 +91,12 @@ theming §6.7 lo pone delante (`hover-bg`). Las fichas proponen la forma | 19 | [timeline](./timeline.md) | 24 | 51% | 49 | 45 | | 20 | [avatar](./avatar.md) | 23 | 36% | 36 | 0 | -## Sin ningún token público propio (30) +## Sin ningún token público propio (29) 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) · [`menu-dial`](./menu-dial.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) · [`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) ## Tabla completa (162 recetas, por alcance ascendente) @@ -109,7 +109,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [year-grid](./year-grid.md) | 0% | 61 | 0 | 7 | 0 | 5 | 49 | 0 | – | | [field-langs](./field-langs.md) | 0% | 41 | 0 | 0 | 31 | 8 | 2 | 0 | – | | [skin-media-player](./skin-media-player.md) | 0% | 11 | 0 | 0 | 2 | 7 | 2 | 0 | y | -| [menu-dial](./menu-dial.md) | 0% | 8 | 0 | 3 | 1 | 4 | 0 | 0 | y | | [aspect-ratio](./aspect-ratio.md) | 0% | 3 | 0 | 0 | 1 | 2 | 0 | 0 | – | | [date-picker](./date-picker.md) | 0% | 2 | 0 | 0 | 0 | 2 | 0 | 0 | – | | [sticky](./sticky.md) | 0% | 2 | 0 | 1 | 0 | 1 | 0 | 1 | – | @@ -134,6 +133,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [toggle](./toggle.md) | 22% | 28 | 6 | 17 | 1 | 3 | 1 | 119 | y | | [date-range-field](./date-range-field.md) | 25% | 9 | 2 | 0 | 4 | 2 | 1 | 3 | – | | [time-range-field](./time-range-field.md) | 25% | 9 | 2 | 0 | 4 | 2 | 1 | 3 | – | +| [menu-dial](./menu-dial.md) | 25% | 8 | 2 | 1 | 1 | 4 | 0 | 3 | y | | [surface](./surface.md) | 25% | 4 | 1 | 3 | 0 | 0 | 0 | 25 | – | | [callout](./callout.md) | 27% | 15 | 4 | 3 | 7 | 1 | 0 | 28 | – | | [meter](./meter.md) | 29% | 14 | 4 | 6 | 2 | 2 | 0 | 28 | y | diff --git a/docs/audit/theming/menu-dial.md b/docs/audit/theming/menu-dial.md index 0c0a853bd..40a438ab5 100644 --- a/docs/audit/theming/menu-dial.md +++ b/docs/audit/theming/menu-dial.md @@ -5,9 +5,9 @@ > Vista de conjunto: [README](./README.md) · método y protocolo: > [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7. -- **Medido**: 2026-08-20 · **Alcance**: **0%** — 0 de 8 knobs por token público -- **Knobs de apariencia**: 8 — público 0 · privado 3 · global 1 · literal 4 · sistema 0 _(fuera del ratio)_ -- **Contrato hoy** (`lib/recipes/base.ts`): **sin entrada en `base.ts`** +- **Medido**: 2026-08-21 · **Alcance**: **25%** — 2 de 8 knobs por token público +- **Knobs de apariencia**: 8 — público 2 · privado 1 · global 1 · literal 4 · sistema 0 _(fuera del ratio)_ +- **Contrato hoy** (`lib/recipes/base.ts`): 3 pública(s) — `gap`, `stagger-step`, `backdrop-bg` - **Eje `size`**: sí · **ficheros**: `menu-dial.css` ## 1. Knobs fuera de alcance @@ -18,13 +18,11 @@ | ---: | --- | --- | --- | --- | | 1 | `menu-dial.css:273` | `.menu-dial-glyph::before, .menu-dial-glyph::after` | `border-radius` | `var(--radius-full, 9999px)` | -### 1.2 A través de un privado (3) +### 1.2 A través de un privado (1) | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `menu-dial.css:20` | `[data-menu-dial]` | `gap` | `var(--_menu-dial-gap, var(--space-3))` | -| 2 | `menu-dial.css:50` | `[data-menu-dial-list]` | `gap` | `var(--_menu-dial-gap, var(--space-3))` | -| 3 | `menu-dial.css:291` | `[data-menu-dial-backdrop]` | `background` | `var(--_menu-dial-backdrop-bg)` | +| 1 | `menu-dial.css:291` | `[data-menu-dial-backdrop]` | `background` | `var(--_menu-dial-backdrop-bg)` | ### 1.3 Literales (4) @@ -45,8 +43,8 @@ _Ninguno._ | privado | declaraciones | valor(es) | origen | ¿deriva de un público? | | --- | ---: | --- | --- | :-: | -| `--_menu-dial-stagger-step` | 1 | `28ms` | literal | no | -| `--_menu-dial-backdrop-bg` | 1 | `color-mix(in oklab, var(--color-neutral-text-strong) 40%, transparent)` | global | no | +| `--_menu-dial-stagger-step` | 1 | `var(--menu-dial-stagger-step)` | public | **sí** | +| `--_menu-dial-backdrop-bg` | 1 | `var(--menu-dial-backdrop-bg)` | public | **sí** | | `--_menu-dial-arc-start` | 13 | `270deg`, `0deg`, `180deg`, `90deg` | literal | no | | `--_menu-dial-arc-span` | 13 | `90deg`, `180deg`, `360deg` | literal | no | | `--_menu-dial-radius` | 1 | `max(var(--_dial-r-clear), var(--_dial-r-spread))` | global | no | @@ -59,7 +57,7 @@ Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inl - **Consume la capa compartida `viewport-placement`.** Un eje que la capa posee se consume como `var(--_x, var(--x))`; el consumidor **no acuña** `--menu-dial-{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` (4) +### 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 @@ -72,16 +70,11 @@ no distingue lo que debería — se marca `⚠`. | `height` | `root` | ⚠ `1em` / `2px` | 2 | | `width` | `root` | `1em` | 1 | | `radius` | `root` | `var(--radius-full, 9999px)` | 1 | -| `backdrop-bg` | `root` | `color-mix(in oklab, var(--color-neutral-text-strong) 40%, transparent)` | 1 | -### 4.2 Sin nombre mecánico (3) +### 4.2 Sin nombre mecánico (2) -- **⚠ 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** — 2: `gap`. - **⚠ decisión: `1` es un valor identidad o geometría de layout, no un knob de tema — el perímetro de «knob» es D-TH.2, sin firmar** — 1: `opacity`. - -### 4.3 Avisos sobre los tokens propuestos (1) - -- **el privado `--_menu-dial-backdrop-bg` debe pasar a leer este público (o desaparecer)** — `--menu-dial-backdrop-bg` +- **ya alcanzable: el privado `--_menu-dial-backdrop-bg` deriva de un público (`var(--menu-dial-backdrop-bg)`) — sin acción; el censo lo cuenta como no alcanzable por el límite del regex** — 1: `background`. ### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4) @@ -95,6 +88,24 @@ no distingue lo que debería — se marca `⚠`. -_(pendiente — lo escribe el autor; se conserva al regenerar)_ +**Medido 2026-08-22.** Su propia cabecera acota el encargo: la receta «owns +only the dial-specific chrome». Las acciones son `Button` compuestos y la +colocación flotante es de la capa `viewport-placement`, así que lo suyo son +tres cosas. + +1. **Tres knobs propios**: el hueco del abanico, el paso del escalonado de + entrada (`28ms`, que multiplica el índice de cada acción) y el tinte del + velo de fondo. +2. **`--_viewport-placement-offset` y `-z` NO se acuñan.** Son las RANURAS DE + OVERRIDE que la capa compartida publica, y el componente escribe en ellas su + decisión (20px en vez de los 16px por defecto de la capa; el comentario de + la receta deja claro que es decisión, no deriva). Acuñar + `menu-dial-placement-offset` sería el vocabulario paralelo que la regla 2 de + capas compartidas prohíbe: el eje es de la capa, y un tema lo mueve ahí. Es + el mismo techo que `listbox` paga por consumir bien `list-surface` + (`next-features.md` §13). +3. **El escalonado es tema, no motion**: `--_menu-dial-stagger-step` no es una + duración de animación sino el INTERVALO entre hermanos, que define el + carácter del abanico. Entra al contrato. diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index 203338647..b82864650 100644 --- a/scripts/theming-sentinel-exceptions.ts +++ b/scripts/theming-sentinel-exceptions.ts @@ -154,6 +154,12 @@ export const SENTINEL_EXCEPTIONS: Record> = { 'trigger-underline': 'same structural conflict: the hover rule sets `text-decoration-color: currentColor` over the resting mix, and the panel cannot be opened without hovering' }, + 'menu-dial': { + 'backdrop-bg': + 'the backdrop only exists while the dial is open (0 nodes at rest, counted); forced → reaches (rgb(1,2,3))', + 'stagger-step': + "consumed only by `[data-stagger] [data-state='open'] .menu-dial-action`, and the demo dial is closed and un-staggered; forced both attrs with index 3 → reaches (0.084s → 2.331s = 3 × 777ms)" + }, 'picker-shell': { 'section-gap-xs': 'its size axis is `data-picker-size` on the PORTALED [data-popover-content], not `data-size` on itself, and the guard only cycles `data-size`; the demo popover sits at md. Cycled by hand across the four picker-sizes 2026-08-21 → reaches (section/row/footer spacing follows the picker size)', diff --git a/src/uix/eidos/components/menu-dial/README.md b/src/uix/eidos/components/menu-dial/README.md index 096872911..1b8d5e3fd 100644 --- a/src/uix/eidos/components/menu-dial/README.md +++ b/src/uix/eidos/components/menu-dial/README.md @@ -133,6 +133,35 @@ the actions `role="menuitem"`, the trigger `aria-haspopup="menu"` + `aria-expand the button's `contact-activate` (tap) AND the dial's `commit-select` (affirm). If the layered feedback is too much, a sema cascade can mute the action's contact. +## Talla y tema + +**Guard R-5.4** (`npm run theming:sentinel -- menu-dial `): 1/3 en vivo; +los dos que callan están adjudicados con su medición en +`scripts/theming-sentinel-exceptions.ts` — el velo sólo existe con el dial +abierto y el escalonado sólo se aplica bajo `[data-stagger]` con la lista +abierta. Corrido 2026-08-22. + +Contrato en `lib/recipes/base.ts > menu-dial` (3 claves, entrada NUEVA). +Alcance del censo: **25 %** (era 0 %). + +**La receta posee sólo el cromo del dial**, como dice su propia cabecera: las +acciones son `Button` compuestos y la colocación flotante es de la capa +`viewport-placement`. Lo suyo son tres cosas: el hueco del abanico, el paso del +escalonado de entrada y el tinte del velo. + +**`--_viewport-placement-offset` y `-z` NO se acuñan.** Son las RANURAS DE +OVERRIDE que la capa compartida publica, y esta receta escribe en ellas su +decisión —20px en vez de los 16px por defecto de la capa, y el comentario deja +claro que es decisión y no deriva. Acuñar `menu-dial-placement-offset` sería el +vocabulario paralelo que la regla 2 de capas compartidas prohíbe: el eje es de +la capa y un tema lo mueve ahí. Es el mismo techo que `listbox` paga por +consumir bien `list-surface`, y el grueso del 75 % que no alcanza +(`next-features.md` §13). + +**El escalonado es tema, no motion**: `stagger-step` no es una duración de +animación sino el INTERVALO entre hermanos —multiplica el índice de cada +acción— y es lo que da carácter al abanico. Por eso entra al contrato. + ## Gaps - No demo page yet (deferred to the demo/docs web phase) — disposition: implementar. diff --git a/src/uix/eidos/components/menu-dial/menu-dial.css b/src/uix/eidos/components/menu-dial/menu-dial.css index 489ad11ea..7f797b689 100644 --- a/src/uix/eidos/components/menu-dial/menu-dial.css +++ b/src/uix/eidos/components/menu-dial/menu-dial.css @@ -17,12 +17,12 @@ [data-menu-dial] { display: inline-flex; align-items: center; - gap: var(--_menu-dial-gap, var(--space-3)); + gap: var(--_menu-dial-gap, var(--menu-dial-gap)); /* Per-action stagger step (private — internal animation timing). */ - --_menu-dial-stagger-step: 28ms; + --_menu-dial-stagger-step: var(--menu-dial-stagger-step); /* Scrim behind the open fan. */ - --_menu-dial-backdrop-bg: color-mix(in oklab, var(--color-neutral-text-strong) 40%, transparent); + --_menu-dial-backdrop-bg: var(--menu-dial-backdrop-bg); } /* ── Fan layout per direction ────────────────────────────────────────── */ @@ -47,7 +47,7 @@ [data-menu-dial-list] { display: inline-flex; align-items: center; - gap: var(--_menu-dial-gap, var(--space-3)); + gap: var(--_menu-dial-gap, var(--menu-dial-gap)); } /* ── Floating placement — BRIDGED to the shared layer ────────────────── */ diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 272eff1c1..b0062f38c 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -4081,6 +4081,9 @@ --scroll-frames-loader-font-size: var(--size-xs-font-size); --scroll-frames-loader-letter-spacing: var(--tracking-widest); --scroll-frames-loader-fg: var(--color-neutral-text); + --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); --kbd-font-size: var(--size-xs-font-size); --kbd-font-weight: var(--font-weight-medium); --kbd-letter-spacing: var(--tracking-wide); diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index d7bd6ba47..f1ab43021 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -6589,6 +6589,19 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ 'loader-letter-spacing': 'var(--tracking-widest)', 'loader-fg': 'var(--color-neutral-text)' }, + 'menu-dial': { + // The dial owns ONLY its own chrome: the fan gap, the entrance stagger step + // and the backdrop tint. Actions are composed Buttons, and the floating + // placement belongs to the `viewport-placement` layer — whose override + // slots (`--_viewport-placement-offset` / `-z`) the recipe writes into. + // Those are NOT re-coined here: the axis is the layer's, and coining + // `placement-offset` would be the parallel vocabulary rule 2 forbids. + gap: 'var(--space-3)', + // The interval BETWEEN siblings, not an animation duration: it is what + // gives the fan its character. + 'stagger-step': '28ms', + 'backdrop-bg': 'color-mix(in oklab, var(--color-neutral-text-strong) 40%, transparent)' + }, kbd: { 'font-size': 'var(--size-xs-font-size)', 'font-weight': 'var(--font-weight-medium)', diff --git a/web/routes/uix/components/menu-dial/+page.svelte b/web/routes/uix/components/menu-dial/+page.svelte index 66bbd8e1b..f34218753 100644 --- a/web/routes/uix/components/menu-dial/+page.svelte +++ b/web/routes/uix/components/menu-dial/+page.svelte @@ -12,10 +12,11 @@ import { compileMorfo } from '$uix/morfo'; import { menuDialMorfo } from '@/uix/morfo/components/menu-dial'; import { getActiveUix } from '$active-uix'; + import TokensPanel from '../../lib/TokensPanel.svelte'; const uix = getActiveUix(); - type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y'; + type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'tokens' | 'a11y'; type TraceEntry = { event: string; family: string; intent?: string; at: number }; let tab = $state('live'); let trace = $state([]); @@ -216,6 +217,9 @@ {events.length} + @@ -566,6 +570,10 @@ {/if} + {#if tab === 'tokens'} + + {/if} + {#if tab === 'a11y'}

Accessibility