uix(s-text-virtual-list): temable — 58 % → 70 %, y su techo es ése porque el triple es un CANAL

CERO claves nuevas: dos literales firmados y tres declaraciones adjudicadas por
doctrina. Diff de computed 0 sobre 2.240 valores en 7 estados; R-5.4 9/9 sin una
sola adjudicación — su contrato ya era sano.

LOS TRES QUE FALTAN SON CANAL DE VALOR, NO DEUDA

font-family, font-size y font-weight leen var(--_s-tvl-*, <primitivo>), y el
privado lo escribe el componente INLINE en cada render: eidos.resolve(size,'md')
y sus dos hermanos SIEMPRE producen un valor, así que el estilo inline siempre
está y el fallback del CSS sólo cubre el render previo a que aterrice. Un
--s-text-virtual-list-font-* quedaría pisado por ese inline y MENTIRÍA — la
clase de --gp-current-gradient, del preview-z de drag-drop y del canal de rect
del indicador de navigation-menu.

El mando aquí ES la prop (size / family / weight), y el tema llega por donde
viven esos tokens: la capa tipográfica. Y hay una razón dura para que el triple
viaje por una variable y no por un token de receta: el canvas que mide el texto
lee getComputedStyle del contenedor, así que la fuente pintada y la medida
tienen que ser la MISMA — una indirección que pudiera divergir rompería el
cálculo de anchuras de la virtualización.

LOS DOS LITERALES SON IDENTIDAD

La lista ES el ancho de su contenedor; el espaciador ES el del scroller.
Firmados con su anotación, fuera del ratio.

Su techo real es el 70 % y está medido: mismo caso que listbox (68 % a
propósito) o waveform (86 %). Lo que queda fuera no es deriva, es una decisión
con dueño escrita.

VERIFICACIÓN

Sonda antes/después 0 diffs (2.240 valores · 7 estados · 10 nodos) · R-5.4 9/9 ·
component:audit PASS · censo 70 % con las tres filas restantes explicadas ·
eidos-lint 4 morfo-backed / 0 invalid / 0 class-hooks · rtl:check 0 ·
docs:check 0 · suite eidos 439/440 (el rojo es skin-media-player, el conocido).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent 9bdc5a5d5d
commit 1f785b6f2d

@ -10,8 +10,8 @@
- **Medido**: 2026-08-23 · **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**: **66%** — 3025 de 4598 knobs pasan por un token público del componente
- **Reparto**: público 3025 · privado 318 · global 838 · literal 417 · sistema transversal 509 · excepción firmada 114 _(los dos últimos, fuera del ratio)_
- **Alcance global**: **66%** — 3025 de 4596 knobs pasan por un token público del componente
- **Reparto**: público 3025 · privado 318 · global 838 · literal 415 · sistema transversal 509 · excepción firmada 116 _(los dos últimos, fuera del ratio)_
- **Sin token público propio**: 24 · **alcance < 20 %**: 12 · **alcance 100 %**: 26 · **con eje `size`**: 57
## Cómo se lee
@ -134,7 +134,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [toggle-group](./toggle-group.md) | 50% | 2 | 1 | 0 | 1 | 0 | 0 | 1 | – |
| [sidebar](./sidebar.md) | 55% | 45 | 23 | 0 | 14 | 5 | 3 | 50 | – |
| [audio-player](./audio-player.md) | 56% | 18 | 10 | 0 | 8 | 0 | 0 | 8 | – |
| [s-text-virtual-list](./s-text-virtual-list.md) | 58% | 12 | 7 | 0 | 3 | 2 | 0 | 9 | – |
| [badge](./badge.md) | 59% | 22 | 13 | 6 | 1 | 2 | 0 | 65 | y |
| [drawer](./drawer.md) | 60% | 74 | 43 | 8 | 16 | 5 | 2 | 54 | y |
| [cropper](./cropper.md) | 60% | 27 | 15 | 0 | 3 | 7 | 2 | 15 | – |
@ -156,6 +155,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [listbox](./listbox.md) | 68% | 37 | 23 | 9 | 1 | 1 | 3 | 21 | – |
| [progress](./progress.md) | 69% | 35 | 24 | 2 | 3 | 6 | 0 | 41 | y |
| [color-swatch](./color-swatch.md) | 70% | 10 | 7 | 0 | 3 | 0 | 0 | 12 | y |
| [s-text-virtual-list](./s-text-virtual-list.md) | 70% | 10 | 7 | 0 | 3 | 0 | 0 | 9 | – |
| [form](./form.md) | 71% | 52 | 36 | 8 | 5 | 2 | 1 | 54 | y |
| [image-picker](./image-picker.md) | 71% | 20 | 12 | 0 | 1 | 4 | 3 | 13 | – |
| [search-field](./search-field.md) | 71% | 15 | 10 | 0 | 0 | 4 | 1 | 17 | y |

@ -5,8 +5,8 @@
> Vista de conjunto: [README](./README.md) · método y protocolo:
> [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7.
- **Medido**: 2026-08-23 · **Alcance**: **58%** — 7 de 12 knobs por token público
- **Knobs de apariencia**: 12 — público 7 · privado 0 · global 3 · literal 2 · sistema 0 · excepción 0 _(los dos últimos, fuera del ratio)_
- **Medido**: 2026-08-23 · **Alcance**: **70%** — 7 de 10 knobs por token público
- **Knobs de apariencia**: 10 — público 7 · privado 0 · global 3 · literal 0 · sistema 0 · excepción 2 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 9 pública(s) — `line-height`, `fg`, `bg`, `border-width`, `border-fg`, `radius`, `row-padding-inline`, `row-border-width`, `row-border-fg`
- **Eje `size`**: no · **ficheros**: `s-text-virtual-list.css`
@ -16,29 +16,29 @@
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `s-text-virtual-list.css:15` | `[data-s-text-virtual-list]` | `font-family` | `var(--_s-tvl-font-family, var(--font-family-primary))` |
| 2 | `s-text-virtual-list.css:16` | `[data-s-text-virtual-list]` | `font-size` | `var(--_s-tvl-font-size, var(--font-size-md))` |
| 3 | `s-text-virtual-list.css:17` | `[data-s-text-virtual-list]` | `font-weight` | `var(--_s-tvl-font-weight, var(--font-weight-regular))` |
| 1 | `s-text-virtual-list.css:23` | `[data-s-text-virtual-list]` | `font-family` | `var(--_s-tvl-font-family, var(--font-family-primary))` |
| 2 | `s-text-virtual-list.css:24` | `[data-s-text-virtual-list]` | `font-size` | `var(--_s-tvl-font-size, var(--font-size-md))` |
| 3 | `s-text-virtual-list.css:25` | `[data-s-text-virtual-list]` | `font-weight` | `var(--_s-tvl-font-weight, var(--font-weight-regular))` |
### 1.2 A través de un privado (0)
_Ninguno._
### 1.3 Literales (2)
### 1.3 Literales (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `s-text-virtual-list.css:14` | `[data-s-text-virtual-list]` | `inline-size` | `100%` |
| 2 | `s-text-virtual-list.css:33` | `[data-s-text-virtual-list-spacer]` | `inline-size` | `100%` |
_Ninguno._
### 1.4 Excepciones firmadas (0) — fuera del ratio
### 1.4 Excepciones firmadas (2) — fuera del ratio
Literales que llevan su anotación `/* literal: <razón> */` en la propia
declaración: la válvula de recipe-contract §3, la misma que honra
`component-audit`. **Una desviación firmada no es deuda** — se listan para que la
razón se lea, no para acuñarlas.
_Ninguno._
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `s-text-virtual-list.css:22` | `[data-s-text-virtual-list]` | `inline-size` | `100%` |
| 2 | `s-text-virtual-list.css:41` | `[data-s-text-virtual-list-spacer]` | `inline-size` | `100%` |
## 2. Sistema transversal (0) — informativo, fuera del ratio
Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
@ -81,6 +81,38 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**EJECUTADO 2026-08-23 — 58 % → 70 %.** CERO claves nuevas: dos literales
firmados y tres declaraciones adjudicadas por doctrina. Diff de computed **0
sobre 2.240 valores en 7 estados**; guard R-5.4 **9/9**, sin una sola
adjudicación — su contrato ya era sano.
**Los tres que faltan son un CANAL DE VALOR, no deuda.** `font-family`,
`font-size` y `font-weight` leen `var(--_s-tvl-*, <primitivo>)`, y el privado lo
escribe el componente INLINE en cada render:
`eidos.resolve(size, 'md')` / `(family, 'primary')` / `(weight, 'regular')`
siempre producen un valor, así que el estilo inline siempre está y el fallback
del CSS sólo cubre el render previo a que aterrice. Un
`--s-text-virtual-list-font-*` quedaría pisado por ese inline y **mentiría** —
la clase de `--gp-current-gradient`, del `preview-z` de drag-drop y del canal de
rect del indicador de `navigation-menu`.
El mando aquí ES la prop (`size` / `family` / `weight`), y el tema llega por
donde viven esos tokens: la capa tipográfica. Hay además una razón dura para que
el triple viaje por una variable y no por un token de receta: **el canvas que
mide el texto lee `getComputedStyle` del contenedor**, así que la fuente pintada
y la medida tienen que ser la MISMA — cualquier indirección que pudiera
divergir rompería el cálculo de anchuras de la virtualización.
**Los dos literales son identidad**: la lista ES el ancho de su contenedor y el
espaciador ES el del scroller. Firmados con su anotación, fuera del ratio.
**Su techo real es el 70 %**, y está medido. Mismo caso que `listbox` (68 % a
propósito) o `waveform` (86 %): lo que queda fuera no es deriva, es una decisión
con dueño escrita.
**Verificación**: sonda antes/después **0 diffs** (2.240 valores · 7 estados · 10
nodos) · R-5.4 **9/9** · `component:audit` PASS · censo 70 % con las tres filas
restantes explicadas · `eidos-lint` 4 morfo-backed / **0 invalid, 0
class-hooks**.
<!-- veredicto:end -->

@ -45,6 +45,26 @@ layout; aquí la altura sale del canvas antes de montar).
texto pintado usen exactamente la misma fuente, más `overscan` y el
vocabulario visual de SText.
## Talla y tema
Nueve claves públicas en `lib/recipes/base.ts`: `line-height`, `fg`, `bg`,
`border-width`, `border-fg`, `radius` y las tres de la fila
(`row-padding-inline`, `row-border-width`, `row-border-fg`). El guard R-5.4 las
alcanza **todas** (9/9, 2026-08-23).
**El triple tipográfico NO se acuña, y no es deuda.** `font-family`, `font-size`
y `font-weight` los escribe el componente INLINE en cada render, desde las props
`family` / `size` / `weight` resueltas por `eidos.resolve` (que siempre produce
un valor). Un `--s-text-virtual-list-font-*` quedaría pisado por ese inline y
mentiría — es la clase de `--gp-current-gradient` y del canal de rect del
indicador de `navigation-menu`. El mando aquí ES la prop, y el tema llega por
donde viven esos tokens: la capa tipográfica. Además el triple tiene que ser
EXACTO: el canvas que mide el texto lee `getComputedStyle` del contenedor, así
que la fuente pintada y la medida han de ser la misma.
Los dos `100%` de la receta son identidad (la lista ES el ancho de su
contenedor; el espaciador ES el del scroller), firmados con su anotación.
## Comparativa
| Capacidad | UIX | TanStack Virtual (dynamic) | react-window (VariableSize) |

@ -8,10 +8,18 @@
* `--_s-tvl-font-weight` inline from resolved tokens; the recipe
* applies them so the canvas (which reads `getComputedStyle`) and the
* rendered text use the exact same font triple.
*
* That triple is a VALUE CHANNEL, not a theming surface: `s-text-virtual-list
* .svelte` writes it inline on EVERY render (`eidos.resolve(size, 'md')` and
* friends always produce a value), so a `--s-text-virtual-list-font-*` public
* would be overwritten and would lie. The knob is the PROP — `size`, `family`,
* `weight` — and the theme reaches it where those tokens live, in the
* typographic layer. The fallbacks below only cover the render before the
* inline style lands.
*/
[data-s-text-virtual-list] {
inline-size: 100%;
inline-size: 100%; /* literal: identity — the list IS its container's width */
font-family: var(--_s-tvl-font-family, var(--font-family-primary));
font-size: var(--_s-tvl-font-size, var(--font-size-md));
font-weight: var(--_s-tvl-font-weight, var(--font-weight-regular));
@ -30,7 +38,7 @@
[data-s-text-virtual-list-spacer] {
position: relative;
inline-size: 100%;
inline-size: 100%; /* literal: identity — the spacer IS the scroller's width */
}
[data-s-text-virtual-list-row] {

@ -2,8 +2,9 @@
import { STextVirtualList } from '$uix/eidos/components/s-text-virtual-list';
import { compileMorfo } from '$uix/morfo';
import { sTextVirtualListMorfo } from '@/uix/morfo/components/s-text-virtual-list';
import TokensPanel from '../../lib/TokensPanel.svelte';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'tokens' | 'a11y';
let tab = $state<Tab>('live');
// ── Live state ────────────────────────────────────────────────────────
@ -111,6 +112,9 @@
</button>
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button
>
<button data-uix-tab data-active={tab === 'tokens'} onclick={() => (tab = 'tokens')}>
Tokens <span data-uix-tab-count>9</span>
</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
@ -246,6 +250,10 @@
</section>
{/if}
{#if tab === 'tokens'}
<TokensPanel component="s-text-virtual-list" />
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>

Loading…
Cancel
Save

Powered by TurnKey Linux.