uix(waveform): temable — 55 % → 86 %, y el playhead era el ÚNICO primitivo crudo

UNA clave nueva, cuatro literales firmados, un préstamo adjudicado. Diff de
computed 0 sobre 1.600 valores en 7 estados; R-5.4 11/11, CERO adjudicaciones.

LA CLAVE NUEVA NO LA PIDIÓ EL CENSO, LA PIDIÓ EL FICHERO

`playhead-bg`. Todas las demás líneas del re-tinte del Slider embebido leen ya
un token propio (--slider-secondary-bg: var(--waveform-buffered-fg)) o una
identidad (transparent / none / 0px / 1); el thumb era la ÚNICA que pasaba un
primitivo CRUDO (--color-content-primary) a un componente compuesto. Con eso,
repintar el playhead exigía mover --slider-thumb-bg (todos los sliders de la
app) o --color-content-primary (todo). Ahora es el par natural de
`playhead-width`, que ya existía. El molde es el re-tinte de media-player, que
reenvía --media-player-* en cada línea.

El censo no lo veía: una custom property reenviada a un componente compuesto no
entra en KNOB_PROPS (D-TH.2). La clase entera queda anotada en §13 — toda receta
que re-tinta un embebido puede pasarle primitivos crudos sin que nada lo vea.

EL 14 % QUE QUEDA ES UNA FILA, Y ES DOCTRINA

La onda deshabilitada lee var(--slider-disabled-opacity). La prueba de si un
token es tuyo no es qué valor tiene sino QUÉ DECIDE: aquí decide cuánto se apaga
un control deshabilitado, y eso es del Slider — la onda vive fuera de su caja
pero es la misma superficie a la vista. Un --waveform-disabled-opacity daría un
segundo mando para UNA decisión y permitiría desincronizar cromo y pintura.
Préstamo con dueño: se tema en la ficha de slider, como el 82 % de time-picker
se tema en la de field.

LOS CUATRO LITERALES SON IDENTIDAD, Y AHORA LO DICEN

La pintura ES la caja del provider (100% sobre un position:absolute; inset:0),
el área de arrastre ES la caja entera («scrubbing means grabbing anywhere on
it»), y un playhead abarca todo el alto por definición. Anotación
/* literal: */ en su declaración: desviación firmada, fuera del ratio.

EL INSTRUMENTO, CIEGO POR UNA CAUSA NUEVA: EL COMPONENTE COMPUESTO

La sonda medía 4 nodos —provider, svg y los dos paths— y NINGUNO era el
playhead: el cromo entero es un Slider embebido cuyos nodos llevan data-slider*.
Con extraNodes, 8. Es la TERCERA cara del mismo agujero: HTML crudo (prose),
partes que compone el consumidor (navigation-menu) y partes de un componente
compuesto (waveform). En §13, con la pregunta de fondo: hoy hay que sospecharlo
y contar los nodos, el instrumento no lo detecta solo.

VERIFICACIÓN

Sonda antes/después 0 diffs (1.600 valores · 7 estados · 8 nodos) · R-5.4 11/11
sin adjudicaciones · component:audit PASS · censo 86 % con la única fila
restante explicada · eidos-lint 4 morfo-backed / 16 eidos-only / 0 invalid / 0
class-hooks · rtl:check 0 · docs:check 0 · suite eidos 439/440 (el rojo es
skin-media-player, el conocido) · Chrome: playhead 2px × 40px, onda 544 × 40,
trazo 0.6 — idénticos.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent 129e11fa75
commit 11b92ded6f

@ -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**: **65%** — 3014 de 4606 knobs pasan por un token público del componente
- **Reparto**: público 3014 · privado 318 · global 845 · literal 429 · sistema transversal 509 · excepción firmada 106 _(los dos últimos, fuera del ratio)_
- **Alcance global**: **65%** — 3014 de 4602 knobs pasan por un token público del componente
- **Reparto**: público 3014 · privado 318 · global 845 · literal 425 · sistema transversal 509 · excepción firmada 110 _(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
@ -132,7 +132,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [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 |
| [toggle-group](./toggle-group.md) | 50% | 2 | 1 | 0 | 1 | 0 | 0 | 1 | – |
| [waveform](./waveform.md) | 55% | 11 | 6 | 0 | 1 | 4 | 0 | 10 | y |
| [sidebar](./sidebar.md) | 55% | 45 | 23 | 0 | 14 | 5 | 3 | 50 | – |
| [audio-player](./audio-player.md) | 56% | 18 | 10 | 0 | 8 | 0 | 0 | 8 | – |
| [color-field](./color-field.md) | 56% | 16 | 9 | 0 | 2 | 5 | 0 | 22 | y |
@ -195,6 +194,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [chat-message](./chat-message.md) | 84% | 84 | 65 | 0 | 6 | 6 | 7 | 75 | y |
| [grid-list](./grid-list.md) | 85% | 31 | 22 | 2 | 1 | 1 | 5 | 38 | – |
| [combobox](./combobox.md) | 85% | 97 | 76 | 4 | 3 | 6 | 8 | 106 | – |
| [waveform](./waveform.md) | 86% | 7 | 6 | 0 | 1 | 0 | 0 | 11 | y |
| [table](./table.md) | 86% | 46 | 37 | 3 | 0 | 3 | 3 | 45 | – |
| [context-menu](./context-menu.md) | 87% | 33 | 26 | 0 | 1 | 3 | 3 | 28 | – |
| [kbd](./kbd.md) | 87% | 15 | 13 | 0 | 1 | 1 | 0 | 13 | – |

@ -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-23 · **Alcance**: **55%** — 6 de 11 knobs por token público
- **Knobs de apariencia**: 11 — público 6 · privado 0 · global 1 · literal 4 · sistema 0 · excepción 0 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 10 pública(s) — `height-xs`, `height-sm`, `height-md`, `height-lg`, `height-xl`, `bar-width`, `fg`, `played-fg`, `buffered-fg`, `playhead-width`
- **Medido**: 2026-08-23 · **Alcance**: **86%** — 6 de 7 knobs por token público
- **Knobs de apariencia**: 7 — público 6 · privado 0 · global 1 · literal 0 · sistema 0 · excepción 4 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 11 pública(s) — `height-xs`, `height-sm`, `height-md`, `height-lg`, `height-xl`, `bar-width`, `fg`, `played-fg`, `buffered-fg`, `playhead-width`, `playhead-bg`
- **Eje `size`**: sí · **ficheros**: `waveform.css`
## 1. Knobs fuera de alcance
@ -22,23 +22,23 @@
_Ninguno._
### 1.3 Literales (4)
### 1.3 Literales (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `waveform.css:47` | `[data-waveform-wave]` | `inline-size` | `100%` |
| 2 | `waveform.css:48` | `[data-waveform-wave]` | `block-size` | `100%` |
| 3 | `waveform.css:114` | `[data-waveform] [data-slider][data-orientation='horizontal']` | `block-size` | `100%` |
| 4 | `waveform.css:119` | `[data-waveform] [data-slider-thumb]` | `block-size` | `100%` |
_Ninguno._
### 1.4 Excepciones firmadas (0) — fuera del ratio
### 1.4 Excepciones firmadas (4) — 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 | `waveform.css:47` | `[data-waveform-wave]` | `inline-size` | `100%` |
| 2 | `waveform.css:48` | `[data-waveform-wave]` | `block-size` | `100%` |
| 3 | `waveform.css:114` | `[data-waveform] [data-slider][data-orientation='horizontal']` | `block-size` | `100%` |
| 4 | `waveform.css:119` | `[data-waveform] [data-slider-thumb]` | `block-size` | `100%` |
## 2. Sistema transversal (0) — informativo, fuera del ratio
Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
@ -84,6 +84,47 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**EJECUTADO 2026-08-23 — 55 % → 86 %.** UNA clave nueva, cuatro literales
firmados, un préstamo adjudicado. Diff de computed **0 sobre 1.600 valores en 7
estados**; guard R-5.4 **11/11, cero adjudicaciones**.
**El 14 % que queda es UNA fila, y es doctrina.** La onda deshabilitada lee
`var(--slider-disabled-opacity)`. La prueba de si un token es tuyo no es qué
valor tiene sino **qué decide**: aquí decide *cuánto se apaga un control
deshabilitado*, y eso es del Slider — la onda vive fuera de su caja pero es la
misma superficie a la vista. Un `--waveform-disabled-opacity` daría un segundo
mando para UNA decisión y permitiría desincronizar el cromo y la pintura. Es
préstamo con dueño: se tema en la ficha de `slider`, como el 82 % de
`time-picker` se tema en la de `field`.
**Los cuatro literales son IDENTIDAD** y quedan firmados con su anotación
(recipe-contract §3), fuera del ratio: la pintura ES la caja del provider
(`inline-size`/`block-size: 100%` sobre un `position:absolute; inset:0`), el
área de arrastre ES la caja entera («scrubbing a waveform means grabbing
anywhere on it»), y un playhead abarca **todo** el alto por definición. Ninguno
es un knob: son la clase 4 de F2-B.
**La clave nueva — `playhead-bg`.** No la pedía el censo (una custom property
reenviada a un componente compuesto no entra en `KNOB_PROPS`), la pedía el
propio fichero: **todas** las demás líneas del re-tinte del Slider leen ya un
token propio (`--slider-secondary-bg: var(--waveform-buffered-fg)`) o una
identidad (`transparent` / `none` / `0px` / `1`), y el thumb era la única que
pasaba un primitivo CRUDO (`--color-content-primary`). Consecuencia medible:
repintar el playhead exigía mover `--slider-thumb-bg` —todos los sliders de la
app— o `--color-content-primary` —todo—. Ahora es su par natural de
`playhead-width`, que ya existía. El molde es el re-tinte de `media-player`,
que reenvía `--media-player-*` en cada línea.
**Y el instrumento, ciego otra vez por una causa nueva: el componente COMPUESTO.**
La sonda medía **4 nodos** —el provider, el svg y los dos paths— y ninguno era
el playhead, porque el cromo entero es un `Slider` embebido cuyos nodos llevan
`data-slider*`. Con `extraNodes` pasa a **8**. Es la tercera cara del mismo
punto ciego: `prose` (HTML crudo), `navigation-menu` (partes del consumidor) y
ahora **partes de un componente compuesto**. Registrado en next-features §13.
**Verificación**: sonda antes/después **0 diffs** (1.600 valores · 7 estados · 8
nodos) · R-5.4 **11/11** sin adjudicaciones · `component:audit` PASS · censo 86 %
con la única fila restante explicada · `eidos-lint` 4 morfo-backed / 16
eidos-only / **0 invalid, 0 class-hooks**.
<!-- veredicto:end -->

@ -1265,6 +1265,23 @@ Lo que `chart` destapó (2026-08-22):
`open` de la sonda, que no los honraba). **Queda la pregunta general**: una
receta que estiliza nodos sin atributo propio no es medible por defecto, y
nada avisa — el gate sale verde.
- **…y la TERCERA cara: las partes de un componente COMPUESTO** (medido
2026-08-23 en `waveform`). Su cromo entero es un `Slider` embebido que la
receta re-tinta —el playhead ES el thumb del slider—, así que esos nodos
llevan `data-slider*` y el filtro `data-waveform*` medía 4 nodos sin ninguno
de ellos. Con `extraNodes`, 8. Las tres caras del mismo agujero son ya: HTML
crudo (`prose`), partes que compone el CONSUMIDOR (`navigation-menu`) y partes
de un componente COMPUESTO (`waveform`, `picker-shell`). **Lo que falta es que
el instrumento lo DETECTE solo** — hoy hay que sospecharlo y contar los nodos.
- **El censo no ve los knobs que una receta reenvía a un componente compuesto.**
`--slider-thumb-bg: var(--color-content-primary)` en `waveform.css` es un
primitivo CRUDO pintando el playhead, y no aparece en ninguna columna: las
custom properties no están en `KNOB_PROPS` (D-TH.2). El caso se arregló a
mano al tokenizar (`playhead-bg`), pero **el perímetro deja fuera una clase
entera**: toda receta que re-tinta un componente embebido (media-player,
waveform, chronos, los pickers) puede pasarle primitivos crudos sin que el
censo ni `recipe-css-contract` lo vean. Medir cuántas hay antes de decidir si
D-TH.2 se ensancha.
- **Un panel de HOVER se cierra por el propio guard.** `navigation-menu` abre
con `pointerenter` y `pointerleave` programa el cierre; el guard abre con
clic y luego aparca el puntero en (0,0), que es exactamente el gesto que lo

@ -86,7 +86,12 @@ const EXTRA_NODES: Record<string, string> = {
// filter saw 10 nodes and NONE of them was the panel or a row — 9 of its 19
// knobs were outside the diff. They only exist while the panel is open, so
// the `open` pass below honours this selector too.
'navigation-menu': '[data-navigation-menu-content] a'
'navigation-menu': '[data-navigation-menu-content] a',
// Its interaction chrome IS an embedded canonical Slider that the recipe
// re-tints (the thumb becomes the playhead): those nodes carry
// `data-slider*`, so the attribute filter saw 4 nodes and none of them was
// the playhead the recipe paints.
waveform: '[data-waveform] [data-slider], [data-waveform] [data-slider] *'
};
/**

@ -199,6 +199,10 @@ const COMPONENT_OVERRIDES: Record<
// pointer parking the guard does after a click (`mouse.move(0, 0)`) shut it
// MID-RUN — two runs of the same code disagreed on one token.
'navigation-menu': { openBy: 'hover', extraNodes: '[data-navigation-menu-content] a' },
// Same class: its chrome is an embedded Slider the recipe re-tints, and the
// playhead IS that slider's thumb — `data-waveform*` matched 4 nodes and none
// of them was it.
waveform: { extraNodes: '[data-waveform] [data-slider], [data-waveform] [data-slider] *' },
// No route of its own (`/uix/components/audio-player` = 404, the FOURTH canon
// component without a demo). It is the audio skin of the media-player chassis:
// its parts are `data-media-player-*` and it only renders after the demo's

@ -61,6 +61,23 @@ en el gate, ambos declarados: `playhead-width` (el thumb cuadrado del Slider,
`buffered-fg` (el tercer estado, consumido como `--slider-secondary-bg` en
scope). Los tres son publicos: el disenador los repinta sin tocar el componente.
**`playhead-bg` (2026-08-23)** completa el par que a `playhead-width` le
faltaba. Todas las demas lineas del re-tinte del Slider embebido leen ya un
token PROPIO (`--slider-secondary-bg: var(--waveform-buffered-fg)`) o una
identidad (`transparent` / `none` / `0px`); el thumb era la unica que pasaba un
primitivo CRUDO (`--color-content-primary`) a un componente compuesto, asi que
repintar el playhead exigia mover todos los Slider de la app. Mismo patron que
el re-tinte de `media-player`, que reenvia `--media-player-*` en cada linea.
**Lo que NO se acuna, y por que.** La onda deshabilitada lee
`var(--slider-disabled-opacity)`: la pintura vive FUERA del slider, pero
apagarse es una decision del CONTROL, no de la pintura — un
`--waveform-disabled-opacity` permitiria desincronizar el cromo y la onda, que
son una sola superficie a la vista. Es prestamo con dueno: se tema en la ficha
de `slider`. Y los cuatro `100%` de la receta son IDENTIDAD (la pintura ES la
caja; el area de arrastre ES la caja; un playhead abarca todo el alto), con su
anotacion `/* literal: */` firmada.
## Passive justification
Cero eventos POR DISEÑO, no por pasividad real (`expression: 'delegated'`,

@ -44,8 +44,8 @@
[data-waveform-wave] {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
inline-size: 100%; /* literal: identity — the painting IS the provider's box */
block-size: 100%; /* literal: identity — the painting IS the provider's box */
pointer-events: none;
}
@ -99,7 +99,7 @@
the signal's silhouette makes it unreadable as a distinct state whatever
its colour. A straight band reads instantly because it is not the wave. */
--slider-secondary-bg: var(--waveform-buffered-fg);
--slider-thumb-bg: var(--color-content-primary);
--slider-thumb-bg: var(--waveform-playhead-bg);
--slider-thumb-border-width: 0px;
--slider-thumb-shadow: none;
--slider-active-thumb-shadow: none;
@ -111,12 +111,12 @@
3-attr specificity so it beats the slider recipe's hit-size rule
independently of CSS load order. */
[data-waveform] [data-slider][data-orientation='horizontal'] {
block-size: 100%;
block-size: 100%; /* literal: identity — scrubbing grabs the whole box, by definition */
}
[data-waveform] [data-slider-thumb] {
inline-size: var(--waveform-playhead-width);
block-size: 100%;
block-size: 100%; /* literal: identity — a playhead spans the full height, by definition */
}
/* Disabled: the wave lives OUTSIDE the slider, so the slider's own dimming

@ -3500,6 +3500,7 @@
--waveform-played-fg: var(--color-primary-solid);
--waveform-buffered-fg: color-mix(in srgb, var(--color-primary-solid) 55%, var(--color-content-primary) 20%);
--waveform-playhead-width: 2px;
--waveform-playhead-bg: var(--color-content-primary);
--background-pattern-rule: var(--color-border-subtle);
--background-pattern-cell: 2.5rem;
--background-pattern-dot-size: 1.5px;

@ -5425,7 +5425,15 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
'color-mix(in srgb, var(--color-primary-solid) 55%, var(--color-content-primary) 20%)',
// The embedded Slider's thumb restyled as a thin full-height playhead —
// a shape its square `--slider-thumb-size-*` tokens cannot express.
'playhead-width': '2px'
'playhead-width': '2px',
// …and its colour, the PAIR `playhead-width` was missing. Every other
// line of the re-tint already reads a token of this component's own
// (`--slider-secondary-bg: var(--waveform-buffered-fg)`) or an identity
// (`transparent` / `none`); the thumb was the one passing a RAW primitive
// (`--color-content-primary`) into a composed component, so the only way
// to repaint the playhead was to move every Slider in the app. Same shape
// as media-player's re-tint, which forwards `--media-player-*` throughout.
'playhead-bg': 'var(--color-content-primary)'
},
background: {
// ── Patterns — the knobs `Backdrop` kept as inline fallbacks, promoted to

@ -4,8 +4,9 @@
import { waveformMorfo } from '@/uix/morfo/components/waveform';
import { sliderMorfo } from '@/uix/morfo/components/slider';
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 Direction = 'ltr' | 'rtl';
type TraceEntry = { event: string; family: string; intent?: string; at: number };
@ -265,6 +266,9 @@
<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>11</span>
</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
@ -568,6 +572,10 @@
</section>
{/if}
{#if tab === 'tokens'}
<TokensPanel component="waveform" stage={stageRef ?? undefined} />
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>

Loading…
Cancel
Save

Powered by TurnKey Linux.