From 1f692629e5bacfc7a6c8272533b5c4ffb65fec02 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 22 Aug 2026 01:06:15 +0200 Subject: [PATCH] =?UTF-8?q?uix(scroll-frames):=20temable=20=E2=80=94=200?= =?UTF-8?q?=20%=20=E2=86=92=2033=20%,=20porque=20es=20LAYOUT=20y=20no=20cr?= =?UTF-8?q?omo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → 33 %. **Es layout, no cromo** — la receta lo dice de entrada y la medición lo confirma: una pista alta cuyo viewport pegajoso fija el medio a sangre mientras la pista rueda. Lo único que pinta es el rótulo de carga, y ése es el contrato entero: su separación del borde inferior, el tipo, el tracking y la tinta. **`--_scroll-frames-viewport-block-size` se queda privado**: es la escotilla que el consumidor sobrescribe para encajar el viewport en un contenedor (`100%`) en vez de en la ventana (`100vh`), y su fallback es geometría de LAYOUT, no un valor de tema — quien lo mueve es el sitio de uso, no el tema. Con los `100%` del medio y de la pista (identidad, D-TH.2), el `line-height: 1` del rótulo y el `opacity` del sistema, forman el 67 % que no alcanza, y es correcto que no lo haga. Artefactos y gates: - Sonda antes/después = 0 diffs en 580 valores × 7 estados. - R-5.4: 0/4, los cuatro adjudicados con su medición — el rótulo sólo existe mientras se decodifican los fotogramas (0 nodos una vez cargado, contados) y forzándolo los cuatro mueven (24→1234px, 12→99px, 1.2→9px, rgb(1,2,3)). - censo --only 33 % · 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 (4 claves). Co-Authored-By: Claude Fable 5 --- docs/audit/theming/README.md | 12 +++---- docs/audit/theming/scroll-frames.md | 32 +++++++++++-------- scripts/theming-sentinel-exceptions.ts | 10 ++++++ .../eidos/components/scroll-frames/README.md | 24 ++++++++++++++ .../scroll-frames/scroll-frames.css | 8 ++--- src/uix/eidos/generated/base.css | 4 +++ src/uix/eidos/lib/recipes/base.ts | 12 +++++++ .../uix/components/scroll-frames/+page.svelte | 8 ++++- 8 files changed, 86 insertions(+), 24 deletions(-) diff --git a/docs/audit/theming/README.md b/docs/audit/theming/README.md index abf67f048..83af524cc 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%** — 2323 de 4676 knobs pasan por un token público del componente -- **Reparto**: público 2323 · privado 627 · global 1160 · literal 566 · sistema transversal 509 _(fuera del ratio)_ -- **Sin token público propio**: 32 · **alcance < 20 %**: 25 · **alcance 100 %**: 10 · **con eje `size`**: 59 +- **Alcance global**: **50%** — 2326 de 4676 knobs pasan por un token público del componente +- **Reparto**: público 2326 · privado 627 · global 1157 · literal 566 · sistema transversal 509 _(fuera del ratio)_ +- **Sin token público propio**: 31 · **alcance < 20 %**: 24 · **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 (32) +## Sin ningún token público propio (31) 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) · [`mark`](./mark.md) · [`mask-field`](./mask-field.md) · [`menu-dial`](./menu-dial.md) · [`mockup`](./mockup.md) · [`month-grid`](./month-grid.md) · [`motion`](./motion.md) · [`number-field`](./number-field.md) · [`palabras`](./palabras.md) · [`range-calendar`](./range-calendar.md) · [`scroll-frames`](./scroll-frames.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) · [`mark`](./mark.md) · [`mask-field`](./mask-field.md) · [`menu-dial`](./menu-dial.md) · [`mockup`](./mockup.md) · [`month-grid`](./month-grid.md) · [`motion`](./motion.md) · [`number-field`](./number-field.md) · [`palabras`](./palabras.md) · [`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 | -| [scroll-frames](./scroll-frames.md) | 0% | 9 | 0 | 1 | 4 | 4 | 0 | 0 | – | | [menu-dial](./menu-dial.md) | 0% | 8 | 0 | 3 | 1 | 4 | 0 | 0 | y | | [mark](./mark.md) | 0% | 5 | 0 | 2 | 1 | 2 | 0 | 0 | – | | [aspect-ratio](./aspect-ratio.md) | 0% | 3 | 0 | 0 | 1 | 2 | 0 | 0 | – | @@ -145,6 +144,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [search-field](./search-field.md) | 31% | 14 | 4 | 5 | 0 | 4 | 1 | 17 | y | | [password-field](./password-field.md) | 31% | 36 | 11 | 5 | 9 | 10 | 1 | 23 | y | | [aura](./aura.md) | 31% | 35 | 11 | 4 | 14 | 6 | 0 | 24 | y | +| [scroll-frames](./scroll-frames.md) | 33% | 9 | 3 | 1 | 1 | 4 | 0 | 4 | – | | [toggle-group](./toggle-group.md) | 33% | 3 | 1 | 0 | 1 | 1 | 0 | 1 | – | | [background](./background.md) | 35% | 26 | 8 | 6 | 6 | 3 | 3 | 31 | – | | [avatar](./avatar.md) | 36% | 36 | 13 | 17 | 0 | 6 | 0 | 0 | y | diff --git a/docs/audit/theming/scroll-frames.md b/docs/audit/theming/scroll-frames.md index ca7c2ada9..f958d690f 100644 --- a/docs/audit/theming/scroll-frames.md +++ b/docs/audit/theming/scroll-frames.md @@ -5,21 +5,18 @@ > 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 9 knobs por token público -- **Knobs de apariencia**: 9 — público 0 · privado 1 · global 4 · literal 4 · sistema 0 _(fuera del ratio)_ -- **Contrato hoy** (`lib/recipes/base.ts`): **sin entrada en `base.ts`** +- **Medido**: 2026-08-21 · **Alcance**: **33%** — 3 de 9 knobs por token público +- **Knobs de apariencia**: 9 — público 3 · privado 1 · global 1 · literal 4 · sistema 0 _(fuera del ratio)_ +- **Contrato hoy** (`lib/recipes/base.ts`): 4 pública(s) — `loader-inset-block-end`, `loader-font-size`, `loader-letter-spacing`, `loader-fg` - **Eje `size`**: no · **ficheros**: `scroll-frames.css` ## 1. Knobs fuera de alcance -### 1.1 Directo a primitivo global (4) +### 1.1 Directo a primitivo global (1) | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `scroll-frames.css:34` | `[data-scroll-frames-loader]` | `font-size` | `var(--font-size-xs)` | -| 2 | `scroll-frames.css:36` | `[data-scroll-frames-loader]` | `letter-spacing` | `var(--tracking-widest)` | -| 3 | `scroll-frames.css:37` | `[data-scroll-frames-loader]` | `color` | `var(--color-neutral-text)` | -| 4 | `scroll-frames.css:38` | `[data-scroll-frames-loader]` | `opacity` | `var(--opacity-50)` | +| 1 | `scroll-frames.css:38` | `[data-scroll-frames-loader]` | `opacity` | `var(--opacity-50)` | ### 1.2 A través de un privado (1) @@ -50,7 +47,7 @@ Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inl ## 4. Propuesta de corrección -### 4.1 Tokens a declarar en `lib/recipes/base.ts` (4) +### 4.1 Tokens a declarar en `lib/recipes/base.ts` (1) 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 @@ -60,9 +57,6 @@ no distingue lo que debería — se marca `⚠`. | token (`--scroll-frames-…`) | scope TSC | valor propuesto | usos | | --- | --- | --- | ---: | -| `loader-font-size` | `root` | `var(--font-size-xs)` | 1 | -| `loader-letter-spacing` | `root` | `var(--tracking-widest)` | 1 | -| `loader-fg` | `root` | `var(--color-neutral-text)` | 1 | | `loader-opacity` | `root` | `var(--opacity-50)` | 1 | ### 4.2 Sin nombre mecánico (5) @@ -83,6 +77,18 @@ no distingue lo que debería — se marca `⚠`. -_(pendiente — lo escribe el autor; se conserva al regenerar)_ +**Medido 2026-08-22.** La receta lo dice de entrada y la medición lo confirma: +es **layout, no cromo**. Una pista alta cuyo viewport pegajoso fija el medio a +sangre mientras la pista rueda; lo único que pinta es el rótulo de carga. + +1. **Los cuatro knobs del loader entran**: su separación del borde inferior, el + tipo, el tracking y la tinta. Ése es el contrato entero. +2. **`--_scroll-frames-viewport-block-size` se queda privado.** Es la escotilla + que el consumidor sobrescribe para encajar el viewport en un contenedor + (`100%`) en vez de en la ventana (`100vh`), y su fallback es geometría de + LAYOUT, no un valor de tema: quien lo mueve es el sitio de uso, no el tema. +3. **Fuera del contrato**: los `100%` del medio y de la pista (identidad de + layout, D-TH.2), el `line-height: 1` del rótulo (una línea suelta no tiene + interlineado) y el `opacity` del sistema. diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index 3b7f13247..203338647 100644 --- a/scripts/theming-sentinel-exceptions.ts +++ b/scripts/theming-sentinel-exceptions.ts @@ -206,6 +206,16 @@ export const SENTINEL_EXCEPTIONS: Record> = { 'time-label-font-size-lg': 'the time row is CLASS-hooked (.picker-time-row) and the date-picker demo does not render it; the guard filters nodes by attribute, so it cannot see it either way (§13 class-hook debt). Forced → reaches (gap/padding → 1234px, border 1→9px + rgb(7,8,9), label → rgb(10,11,12) / 99px)' }, + 'scroll-frames': { + 'loader-inset-block-end': + 'the loading caption only exists while frames are decoding (0 nodes once loaded, counted); forced → reaches (24 → 1234px)', + 'loader-font-size': + 'the loading caption only exists while frames are decoding (0 nodes once loaded, counted); forced → reaches (12 → 99px)', + 'loader-letter-spacing': + 'the loading caption only exists while frames are decoding (0 nodes once loaded, counted); forced → reaches (1.2 → 9px)', + 'loader-fg': + 'the loading caption only exists while frames are decoding (0 nodes once loaded, counted); forced → reaches (rgb(1,2,3))' + }, skeleton: { 'line-radius': "only the `text` shape renders [data-skeleton-line]; the demo mounts `rect` (0 lines, counted). Forced shape='text' → reaches (4 → 1234px)" diff --git a/src/uix/eidos/components/scroll-frames/README.md b/src/uix/eidos/components/scroll-frames/README.md index fad5f73b2..ed9dc5468 100644 --- a/src/uix/eidos/components/scroll-frames/README.md +++ b/src/uix/eidos/components/scroll-frames/README.md @@ -113,6 +113,30 @@ ninguna integrada a un sistema de tokens/disciplina DOM; la sección - **`smooth` solo en video**: el easing del `currentTime` evita el stutter del seek frame-a-frame; los frames de canvas no lo necesitan. +## Talla y tema + +**Guard R-5.4** (`npm run theming:sentinel -- scroll-frames `): 0/4 en +vivo, los cuatro adjudicados con su medición en +`scripts/theming-sentinel-exceptions.ts` — el rótulo de carga sólo existe +mientras se decodifican los fotogramas (0 nodos una vez cargado, contados), y +forzándolo los cuatro mueven su propiedad. Corrido 2026-08-22. + +Contrato en `lib/recipes/base.ts > scroll-frames` (4 claves, entrada NUEVA). +Alcance del censo: **33 %** (era 0 %). + +**Es LAYOUT, no cromo** — la receta lo dice de entrada y la medición lo +confirma. Una pista alta cuyo viewport pegajoso fija el medio a sangre mientras +la pista rueda; lo único que pinta es el rótulo de carga, y ése es el contrato +entero. + +**`--_scroll-frames-viewport-block-size` se queda privado**: es la escotilla que +el consumidor sobrescribe para encajar el viewport en un contenedor (`100%`) en +vez de en la ventana (`100vh`), y su fallback es geometría de LAYOUT, no un +valor de tema — quien lo mueve es el sitio de uso, no el tema. Con los `100%` +del medio y de la pista (identidad, D-TH.2), el `line-height: 1` del rótulo y +el `opacity` del sistema, forman el 67 % que no alcanza, y es correcto que no +lo haga. + ## Gaps | Gap | Disposición | Detalle | diff --git a/src/uix/eidos/components/scroll-frames/scroll-frames.css b/src/uix/eidos/components/scroll-frames/scroll-frames.css index 68ed28131..09aee3993 100644 --- a/src/uix/eidos/components/scroll-frames/scroll-frames.css +++ b/src/uix/eidos/components/scroll-frames/scroll-frames.css @@ -30,10 +30,10 @@ [data-scroll-frames-loader] { position: absolute; - inset-block-end: 1.5rem; - font-size: var(--font-size-xs); + inset-block-end: var(--scroll-frames-loader-inset-block-end); + font-size: var(--scroll-frames-loader-font-size); line-height: 1; - letter-spacing: var(--tracking-widest); - color: var(--color-neutral-text); + letter-spacing: var(--scroll-frames-loader-letter-spacing); + color: var(--scroll-frames-loader-fg); opacity: var(--opacity-50); } diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index bf036de6c..2646b92df 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -4074,6 +4074,10 @@ --announce-bg: var(--color-surface-default); --announce-font-size: var(--size-sm-font-size); --announce-line-height: var(--leading-ui); + --scroll-frames-loader-inset-block-end: 1.5rem; + --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); --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 9b6189aaf..b659c98ae 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -6570,6 +6570,18 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ 'font-size': 'var(--size-sm-font-size)', 'line-height': 'var(--leading-ui)' }, + // ScrollFrames is LAYOUT, not chrome: a tall track whose sticky viewport pins + // the media full-bleed while the track scrolls. The only thing it paints is + // the loading caption, so that is the whole contract. + // `--_scroll-frames-viewport-block-size` stays private on purpose: it is the + // hatch a consumer overrides to fit a container (100%) instead of the window + // (100vh) — layout geometry the SITE sets, not a theme value. + 'scroll-frames': { + 'loader-inset-block-end': '1.5rem', + 'loader-font-size': 'var(--size-xs-font-size)', + 'loader-letter-spacing': 'var(--tracking-widest)', + 'loader-fg': 'var(--color-neutral-text)' + }, kbd: { 'font-size': 'var(--size-xs-font-size)', 'font-weight': 'var(--font-weight-medium)', diff --git a/web/routes/uix/components/scroll-frames/+page.svelte b/web/routes/uix/components/scroll-frames/+page.svelte index 0472773b6..b10050409 100644 --- a/web/routes/uix/components/scroll-frames/+page.svelte +++ b/web/routes/uix/components/scroll-frames/+page.svelte @@ -4,10 +4,11 @@ import { compileMorfo } from '$uix/morfo'; import { scrollFramesMorfo } from '@/uix/morfo/components/scroll-frames'; 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([]); @@ -199,6 +200,9 @@ {events.length} + @@ -365,6 +369,8 @@ intent/variant selectors — the component is structural.

+ {:else if tab === 'tokens'} + {:else if tab === 'a11y'}

Accessibility