uix(image-adjustments): temable — 67 % → 100 %, y medir de MÁS midió menos

Sus cuatro knobs fuera de alcance eran LA COSTURA (PLAN §2-A) y ninguno una
clave nueva de diseño: el valor se queda donde estaba, el knob pasa a ser del
componente.

Entran cuatro claves (contrato 12 → 16), valor verbatim:

- `head-gap` = `var(--space-2)` y `label-line-height` = `var(--leading-ui)`,
  que la receta leía a pelo — un tema no podía apretar la línea de cabecera sin
  mover esos primitivos en toda la app.
- `hover-reset-fg` = `var(--color-content-primary)`. NO es un hover inventado
  por componente: es TINTA, y el velo del sistema es `background-image`, que un
  control sin superficie no puede recibir. Molde del catálogo: once
  `hover-{parte}-fg` ya escritos así.
- `focus-reset-radius` = `var(--radius-sm)`. Precedente exacto
  `rating-group.focus-radius`, la misma declaración dentro de la misma regla
  `:focus-visible`. La §4 generada proponía `reset-radius` (el control no tiene
  radio en reposo) e `item-label-line-height` (el bloque ya habla `label-*`):
  el catálogo decide el nombre, no la propuesta.

El literal que quedaba se FIRMA, no se acuña: `outline-color: Highlight` bajo
`@media (forced-colors: active)` es la palabra clave del sistema — la paleta del
SO manda ahí. Pasa a `exception`, fuera del ratio.

El canal de VALOR sigue sin acuñarse y aquí no había ninguno que confundir: la
salida del panel es la CADENA `filter` que soma entrega por `onFilterChange`,
nunca toca el CSS de la receta.

Censo: 18 knobs · 10 públicos · 4 global · 1 literal · 3 sistema · **67 %**
    →  17 knobs · 14 públicos · 0 global · 0 literal · 3 sistema · 1 excepción
       · **100 %**.

Diff de computed **VACÍO**: 5.440 valores, 7 estados, 26 nodos, sonda antes y
después. Capturas 2× antes/después, miradas: idénticas.

Píxel hacia arriba (§7.4-12, obligatorio porque toca una regla `:hover`): el
píxel bajo el puntero ES el `<button data-image-adjustments-reset>`; el hover
mueve `color` y SÓLO `color` en ese nodo (oklch(0.5032 0 0) → oklch(0.2435 0 0))
y ni él ni un solo ancestro pintan `background-image`, ni en reposo ni en hover.

Centinela R-5.4 **15/16**, 1 adjudicada — y el instrumento enseñó dos cosas:

1. `hue-track` leía muerto (11/12) y estaba vivísimo: pinta en el `::before` del
   `<Slider>` COMPUESTO, cuyo nodo lleva `data-slider*`. Clase de `waveform`.
   `extraNodes` en `COMPONENT_OVERRIDES`.
2. Ese selector apunta SÓLO a la fila `hue`, y la precisión es funcional: con
   las seis filas el conjunto medido pasó a 32 nodos y el reset se volvió el
   nº 32, fuera del tope de 30 del pase de hover — `hover-reset-fg` leía muerto
   por una razón ajena al token. **Medir de más midió menos.**
   La adjudicación restante es de estado: `focus-reset-radius` sólo pinta bajo
   `:focus-visible` y el guard desenfoca a propósito; medido a mano sobre el
   reset real, 4px → 1234px.

Guards: censo `--only image-adjustments` 100 % · `component-audit` PASS ·
`eidos-lint` invalid 0 · `vitest run src/uix/eidos` 35/36 ficheros (el rojo
conocido `skin-media-player`) · `rtl:check` 0 · `docs:check` 0 · `check` sin un
solo error atribuible a este componente · `--names` sin desviadas suyas ·
prettier limpio con el contenido normalizado a LF.

Más el README con su sección «Talla y tema», la pestaña `Tokens` de la demo
(16 claves, verificada en navegador) y el veredicto §5 de su ficha.

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

@ -5,40 +5,35 @@
> 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**: **67%** — 10 de 15 knobs por token público
- **Knobs de apariencia**: 18 — público 10 · privado 0 · global 4 · literal 1 · sistema 3 · excepción 0 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 12 pública(s) — `gap`, `row-gap`, `label-fg`, `label-font-size`, `label-font-weight`, `value-fg`, `value-font-size`, `value-font-family`, `reset-gap`, `reset-fg`, `reset-font-size`, `hue-track`
- **Medido**: 2026-08-24 · **Alcance**: **100%** — 14 de 14 knobs por token público
- **Knobs de apariencia**: 17 — público 14 · privado 0 · global 0 · literal 0 · sistema 3 · excepción 1 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 16 pública(s) — `gap`, `row-gap`, `head-gap`, `label-fg`, `label-font-size`, `label-font-weight`, `label-line-height`, `value-fg`, `value-font-size`, `value-font-family`, `reset-gap`, `reset-fg`, `reset-font-size`, `hover-reset-fg`, `focus-reset-radius`, `hue-track`
- **Eje `size`**: no · **ficheros**: `image-adjustments.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (4)
### 1.1 Directo a primitivo global (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `image-adjustments.css:33` | `[data-image-adjustments-head]` | `gap` | `var(--space-2)` |
| 2 | `image-adjustments.css:40` | `[data-image-adjustments-item-label]` | `line-height` | `var(--leading-ui)` |
| 3 | `image-adjustments.css:67` | `[data-image-adjustments-reset]:hover:not([disabled])` | `color` | `var(--color-content-primary)` |
| 4 | `image-adjustments.css:73` | `[data-image-adjustments-reset]:focus-visible` | `border-radius` | `var(--radius-sm)` |
_Ninguno._
### 1.2 A través de un privado (0)
_Ninguno._
### 1.3 Literales (1)
### 1.3 Literales (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `image-adjustments.css:95` | `[data-image-adjustments-reset]:focus-visible` | `outline-color` | `Highlight` |
_Ninguno._
### 1.4 Excepciones firmadas (0) — fuera del ratio
### 1.4 Excepciones firmadas (1) — 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 | `image-adjustments.css:95` | `[data-image-adjustments-reset]:focus-visible` | `outline-color` | `Highlight` |
## 2. Sistema transversal (3) — informativo, fuera del ratio
Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
@ -55,7 +50,7 @@ _La receta no declara privados propios en su CSS._
## 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` (0)
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
@ -65,10 +60,6 @@ no distingue lo que debería — se marca `⚠`.
| token (`--image-adjustments-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `head-gap` | `root` | `var(--space-2)` | 1 |
| `item-label-line-height` | `root` | `var(--leading-ui)` | 1 |
| `hover-reset-fg` | `root` | `var(--color-content-primary)` | 1 |
| `reset-radius` | `root` | `var(--radius-sm)` | 1 |
### 4.2 Sin nombre mecánico (1)
@ -86,6 +77,61 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**Ejecutado 2026-08-24 · 67 % → 100 %.** Contrato **12 → 16 claves**. Censo:
público 10 → **14**, **global 4 → 0**, **literal 1 → 0** (pasa a `exception`),
privado 0, sistema 3. Diff de computed **VACÍO** (5.440 valores, 7 estados, 26
nodos); capturas 2× antes/después idénticas. Centinela **15/16**, 1 adjudicada.
**Las cuatro que entran son LA COSTURA (PLAN §2-A), no cuatro inventos.** Los
cuatro knobs fuera de alcance leían un primitivo del sistema a pelo, que es
media doctrina: el valor se queda donde está y el KNOB pasa a ser del
componente. `head-gap` (`--space-2`) y `label-line-height` (`--leading-ui`)
dejan de obligar a mover un primitivo global para apretar una línea de
cabecera; `hover-reset-fg` (`--color-content-primary`) y `focus-reset-radius`
(`--radius-sm`) hacen lo propio con el cromo del reset. Los nombres los decide
el catálogo, no la §4 generada: ésta proponía `item-label-line-height` cuando el
bloque ya habla `label-*` para esa misma parte, y `reset-radius` cuando el radio
sólo existe bajo `:focus-visible` — `focus-reset-radius` es la forma del
precedente `rating-group.focus-radius`, acuñada un día antes sobre la misma
declaración dentro de la misma regla.
**El hover del reset NO es una invención por componente.** Es TINTA, y el velo
del sistema es `background-image`: un control sin superficie que velar no puede
recibir la capa de `archetypes.css`. Verificado desde el píxel hacia arriba
(§7.4-12, obligatorio porque el commit toca una regla `:hover`): el píxel bajo
el puntero ES el `<button data-image-adjustments-reset>`, el hover mueve
`color` **y sólo `color`** en ese nodo (oklch(0.5032 0 0) → oklch(0.2435 0 0)),
y ni él ni un solo ancestro tienen `background-image` en reposo ni en hover.
**El canal de VALOR no se acuña, y aquí no había ninguno que confundir.** Un
panel de ajustes escribe valores por instancia (brillo, contraste, tono), pero
en éste esa salida es la CADENA `filter` que soma calcula y entrega al
consumidor por `onFilterChange` — nunca toca el CSS de la receta. Lo único que
la receta re-apunta a otro componente es `--slider-track-bg` en la fila `hue`, y
eso ya era un token público (`hue-track`) desde que nació.
**El único literal que queda está FIRMADO**: `outline-color: Highlight` bajo
`@media (forced-colors: active)`. La paleta del SO manda en ese modo; un token
ahí sería un knob que el navegador ignora.
**Dos lecciones del instrumento, las dos medidas:**
1. **`hue-track` leía muerto (11/12) y estaba vivísimo.** Su pintura cae en el
`::before` del `<Slider>` COMPUESTO, cuyo nodo lleva `data-slider*` y no
`data-image-adjustments-*`: el filtro del centinela nunca lo veía. Es la
clase de `waveform` (su playhead es el thumb de un Slider embebido). Se
arregla con `extraNodes`.
2. **El selector de `extraNodes` apunta SÓLO a la fila `hue`, y esa precisión es
funcional.** Con `[data-image-adjustments-item] [data-slider]` —las seis
filas— el conjunto medido pasó a 32 nodos y el reset se convirtió en el
nº 32, fuera del tope de 30 del pase de hover: `hover-reset-fg` leía muerto
por una razón que no tenía NADA que ver con el token. Acotando el selector a
la fila que la receta realmente re-tinta, el reset vuelve al rango y la clave
lee viva. **Un instrumento que mide de más puede medir menos.**
**La única adjudicación es de estado, no deuda**: `focus-reset-radius` sólo
pinta bajo `:focus-visible` y el guard desenfoca a propósito (el arreglo F2-A
contra el envenenamiento por clic-foco). Medido a mano sobre el reset real, con
`:focus-visible` confirmado: 4px → 1234px.
<!-- veredicto:end -->

@ -1249,6 +1249,15 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
'same :active rule, which also sets transition-duration: var(--card-press-duration); read in the live sheet next to press-scale',
'disabled-opacity':
'the card dims through `filter: opacity()` (not `opacity` — its own mount animation would outrank a plain one, the comment in card.css records the measurement), and the demo boots enabled; forced data-disabled on the real card -> reaches (filter opacity(0.4) -> opacity(0.123))'
},
'image-adjustments': {
// 2026-08-24. One, measured on the REAL stage reset, transitions frozen.
// Its sibling `hover-reset-fg` looked like the same class and was NOT: the
// reset boots disabled (nothing adjusted yet) so `:hover:not([disabled])`
// could not match, and the `disabled` sweep in COMPONENT_OVERRIDES makes it
// read live. Only the focus corner has no mechanical route.
'focus-reset-radius':
'the corner only rounds under :focus-visible and the guard blurs the active element on purpose (the F2-A fix for click-focus poisoning); focused the real reset from script and confirmed :focus-visible matched -> reaches (4px -> 1234px). Same cause and same shape as rating-group.focus-radius'
}
};

@ -573,6 +573,14 @@ const COMPONENT_OVERRIDES: Record<
{ attr: 'data-state', values: ['off', 'on'] },
{ attr: 'data-invalid', values: [null, ''] }
]
},
// The selector names the HUE row alone on purpose: it is the only row the
// recipe re-tints, and widening it to every row pushed the reset out of the
// hover pass's 30-node cap (it became the 32nd match, so its ink hover read
// dead for a reason that had nothing to do with the token).
'image-adjustments': {
extraNodes: "[data-image-adjustments-item][data-adjustment='hue'] [data-slider]",
sweepAttr: { attr: 'disabled', values: [null] }
}
};

@ -63,17 +63,42 @@ own Slider.
- **`temperature` is a CSS approximation** (warm→sepia, cool→hue-rotate+saturate);
there is no native white-balance primitive.
## Recipe tokens
## Talla y tema
Public, themeable — `--image-adjustments-*`:
**16 claves públicas** en `lib/recipes/base.ts`, todas `--image-adjustments-*`.
No hay eje `size` en la receta: la talla viaja al `<Slider>` compuesto, que trae
la suya. La pestaña **Tokens** de su demo las resuelve en vivo.
| Token | Default |
| --- | --- |
| `gap` / `row-gap` | space-3 / space-1.5 |
| `label-*` | content-secondary · sm · medium |
| `value-*` | content-tertiary · xs · mono (tabular) |
| `reset-*` | space-2 · content-secondary · xs |
| `hue-track` | the rainbow gradient |
| Rol | Claves | Default |
| --- | --- | --- |
| Ritmo vertical | `gap`, `row-gap` | `--space-3`, `--space-1-5` |
| Línea de cabecera | `head-gap` | `--space-2` |
| Etiqueta | `label-fg`, `label-font-size`, `label-font-weight`, `label-line-height` | content-secondary · `--size-sm-font-size` · medium · `--leading-ui` |
| Lectura en vivo | `value-fg`, `value-font-size`, `value-font-family` | tertiary-text · `--size-xs-font-size` · mono (tabular) |
| Reset | `reset-gap`, `reset-fg`, `reset-font-size`, `hover-reset-fg`, `focus-reset-radius` | `--space-2` · content-secondary · `--size-xs-font-size` · content-primary · `--radius-sm` |
| Fila `hue` | `hue-track` | el degradado de tono, re-apuntado a `--slider-track-bg` |
Lo que entró el **2026-08-24** (67 % → 100 %), las cuatro por LA COSTURA — el
valor sigue siendo del sistema, el knob pasa a ser del componente:
- **`head-gap`**, **`label-line-height`** — leían `var(--space-2)` y
`var(--leading-ui)` a pelo, así que un tema no podía apretar la línea de
cabecera sin mover esos primitivos en toda la app.
- **`hover-reset-fg`** — el hover del reset es TINTA, no la capa de estado (el
velo es `background-image` y este control no tiene superficie que velar), así
que es knob del componente. Verificado desde el píxel hacia arriba: el hover
sólo mueve `color` en el propio `<button>`, y ni él ni un solo ancestro
pintan `background-image`.
- **`focus-reset-radius`** — redondea el aro de foco de un control que en
reposo no tiene radio. Mismo molde que `rating-group.focus-radius`.
**El único literal que queda está FIRMADO**: `outline-color: Highlight` bajo
`@media (forced-colors: active)` — es la palabra clave del sistema, la paleta
del SO manda ahí y un token no llegaría.
**Lo que el guard da por silencioso**: `focus-reset-radius`, porque sólo pinta
bajo `:focus-visible` y el centinela desenfoca a propósito. Medido a mano sobre
el reset real: 4px → 1234px.
`eidos-lint image-adjustments` → invalid 0. CSS imported from the wrapper
(code-split) — intentionally NOT in `eidos/index.css`.

@ -30,14 +30,14 @@
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-2);
gap: var(--image-adjustments-head-gap);
}
[data-image-adjustments-item-label] {
color: var(--image-adjustments-label-fg);
font-size: var(--image-adjustments-label-font-size);
font-weight: var(--image-adjustments-label-font-weight);
line-height: var(--leading-ui);
line-height: var(--image-adjustments-label-line-height);
}
[data-image-adjustments-item-value] {
@ -64,13 +64,13 @@
}
[data-image-adjustments-reset]:hover:not([disabled]) {
color: var(--color-content-primary);
color: var(--image-adjustments-hover-reset-fg);
}
[data-image-adjustments-reset]:focus-visible {
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: 0;
border-radius: var(--radius-sm);
border-radius: var(--image-adjustments-focus-reset-radius);
}
[data-image-adjustments-reset][disabled] {
@ -92,6 +92,6 @@
@media (forced-colors: active) {
[data-image-adjustments-reset]:focus-visible {
outline-color: Highlight;
outline-color: Highlight; /* literal: forced-colors system keyword, the OS palette owns it */
}
}

@ -1487,15 +1487,19 @@
--float-panel-transition-ease: var(--ease-default);
--image-adjustments-gap: var(--space-3);
--image-adjustments-row-gap: var(--space-1-5);
--image-adjustments-head-gap: var(--space-2);
--image-adjustments-label-fg: var(--color-content-secondary);
--image-adjustments-label-font-size: var(--size-sm-font-size);
--image-adjustments-label-font-weight: var(--font-weight-medium);
--image-adjustments-label-line-height: var(--leading-ui);
--image-adjustments-value-fg: var(--color-tertiary-text);
--image-adjustments-value-font-size: var(--size-xs-font-size);
--image-adjustments-value-font-family: var(--font-family-mono);
--image-adjustments-reset-gap: var(--space-2);
--image-adjustments-reset-fg: var(--color-content-secondary);
--image-adjustments-reset-font-size: var(--size-xs-font-size);
--image-adjustments-hover-reset-fg: var(--color-content-primary);
--image-adjustments-focus-reset-radius: var(--radius-sm);
--image-adjustments-hue-track: linear-gradient(to right, hsl(0 85% 60%), hsl(60 85% 60%), hsl(120 85% 60%), hsl(180 85% 60%), hsl(240 85% 60%), hsl(300 85% 60%), hsl(360 85% 60%));
--image-picker-gap: var(--space-3);
--image-picker-preview-radius: var(--radius-lg);

@ -1675,9 +1675,14 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
// (label/value line → slider).
gap: 'var(--space-3)',
'row-gap': 'var(--space-1-5)',
// Label ↔ read-out on the head line. The VALUE is the system's spacing
// step; the KNOB is the component's, so a theme can tighten the head line
// without moving `--space-2` app-wide (PLAN §2-A, the seam).
'head-gap': 'var(--space-2)',
'label-fg': 'var(--color-content-secondary)',
'label-font-size': 'var(--size-sm-font-size)',
'label-font-weight': 'var(--font-weight-medium)',
'label-line-height': 'var(--leading-ui)',
// Read-out: tabular numerals so the value doesn't jitter while dragging.
// The TERTIARY ACCENT ROLE's text slot (audit B.3, 2026-07-07): the
// original `--color-content-tertiary` mixed the content namespace with
@ -1689,6 +1694,15 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
'reset-gap': 'var(--space-2)',
'reset-fg': 'var(--color-content-secondary)',
'reset-font-size': 'var(--size-xs-font-size)',
// The reset's ink hover is a TINT, not the state layer (the veil is
// `background-image`, and this control has no surface to veil), so it is
// the component's knob — same shape as the eleven `hover-{part}-fg` keys
// already in the catalogue.
'hover-reset-fg': 'var(--color-content-primary)',
// Rounds the focus outline of a control that has no radius at rest.
// Precedent: `rating-group.focus-radius`, the same declaration inside the
// same `:focus-visible` rule.
'focus-reset-radius': 'var(--radius-sm)',
// The `hue` row paints its slider track as a hue wheel so the control
// reads as a hue picker. A literal rainbow (the hues ARE the value) — kept
// as a token so it's themeable and stays out of the component CSS.

@ -7,10 +7,11 @@
import { compileMorfo } from '$uix/morfo';
import { imageAdjustmentsMorfo } from '@/uix/morfo/components/image-adjustments';
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';
let tab = $state<Tab>('live');
// ── Live state ────────────────────────────────────────────────────────
@ -240,9 +241,16 @@
<span data-uix-tab-count>{events.length}</span>
</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>16</span>
</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
{#if tab === 'tokens'}
<TokensPanel component="image-adjustments" stage={stageRef ?? undefined} />
{/if}
{#if tab === 'live'}
<section data-uix-section>
<h2 data-uix-section-title>Controls</h2>

Loading…
Cancel
Save

Powered by TurnKey Linux.