uix(toggle): el ledger y el guard del commit anterior, que un índice COMPARTIDO se llevó

Reparación, no trabajo nuevo. `edc019515` salió con la mitad del componente:
la receta, el CSS, el README, la demo y la ficha entraron; el ledger R-5.4, el
guard y la entrada del §8 se quedaron fuera, y en su lugar viajaron fragmentos
de OTRA sesión (rating-group). Causa medida: en esta rama el ÍNDICE de git es
un recurso compartido — entre el `git add` verificado y el `git commit` otra
sesión hizo su propio `reset` + `add`, y el commit fotografió su índice, no el
mío. Verificar el árbol indexado no basta si alguien puede reescribirlo
DESPUÉS de mirarlo; la verificación y el commit tienen que ir en la misma
invocación. Sin esto `npm run theming:sentinel -- toggle` falla: el contrato
tiene 126 claves y el ledger no tenía ninguna.

Lo que entra:

- **Ledger** (`theming-sentinel-exceptions.ts`): las 8 adjudicaciones medidas
  de toggle (los cinco pasos de `rounded` que la demo no expone,
  `disabled-opacity`, y el par de transición que ES lo que el guard congela)
  y la excepción POR PATRÓN de sus 48 claves de tono — la cascada de paleta,
  medida aquí: con `data-color=primary` `--toggle-primary-solid` no mueve nada
  y `--palette-solid` sobre el mismo nodo repinta; quita `data-color` y
  `--toggle-neutral-solid` pinta otra vez.
- **Guard** (`theming-sentinel.ts`): el barrido de VARIOS ejes como producto
  cartesiano (un solo eje da la misma secuencia de siempre), el valor `null`
  que QUITA el atributo, el mismo barrido en el pase de HOVER, y el override
  de toggle. 36/126 → 70/126 sin adjudicar nada. Los 12 componentes con
  `sweepAttr` re-verificados uno a uno: cero regresiones, cero STALE.
- **`PLAN-theming.md` §8**: la entrada de toggle.

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

@ -486,6 +486,30 @@ commits de corrección.
## 8. Registro
- 2026-08-23 — **`toggle` 81 % → 100 %** (COMMIT), contrato 119 → **126**
claves, centinela **70/126** con 56 adjudicadas. Tres knobs y una firma: el
**radio** (`radius` + los cinco pasos de `rounded`) leía `--radius-default` y
la escala global A PELO —el agujero de `navigation-menu`, y el único de los
CINCO componentes con escala `rounded` que no tenía la suya (`button`,
`badge`, `image`, `color-swatch` la declaran)—; el **tinte inválido** iba a
`--color-threat-element` a pelo (la costura §2-A); y los tres literales
quedan FIRMADOS (identidad del `block`, técnica `sr-only`). El desacople de
`size` se conserva: son pasos de `rounded`, no de talla. **Lo que enseñó**:
el centinela empezó en 36/126 y llegó a 70 **sin adjudicar nada** — las
claves `{variante}-*` y `{variante}-on-*` son las mismas tres variantes en
dos ESTADOS, y forzar el estado con `prepareWith` cambiaba una mitad por la
otra (36 → 46). El guard aprende a barrer el **producto cartesiano** de
varios ejes, a QUITAR un atributo con `null` (sellar `data-invalid` toda la
corrida hacía que el borde inválido ganase a los otros seis) y a aplicar el
barrido también en el **pase de hover**, que no barría nada — que es
literalmente la razón escrita en el ledger de
`radio-group.hover-segmented-segment-fg`. Los 12 componentes con `sweepAttr`
re-verificados uno a uno: **cero regresiones, cero STALE** (y tres de ellos
dieron exit=1 falso corriendo doce seguidos: a solas, verdes). Diff de
computed vacío (416 valores × 7 estados + 1.150 × 25 celdas de barrido
dirigido), capturas 2× antes/después. **Anotado y FUERA por D-TH.5**: el
`:hover` tapa el borde inválido (mismo peso, declarado antes) — mueve píxel
en los cuatro controles de formulario.
- 2026-08-23 — **`rating-group` 80 % → 100 %** (COMMIT), contrato 25 → **29**
claves, centinela **23/29** con 6 adjudicadas. Tres RESUELTAS por talla
(`gap`, `item-size`, `font-size`) jubilan sus tres privados y los cuatro

@ -1180,7 +1180,26 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
'the guard freezes transitions and this token IS the transition; measured unfrozen on a real item -> reaches (0.12s -> 11.5s on colour and transform)',
'transition-ease':
'same freeze; measured unfrozen -> reaches (cubic-bezier(0.4, 0, 0.2, 1) -> steps(3))'
},
},,
toggle: {
// 2026-08-23. Eight, each forced on the REAL stage toggle. The forty-eight
// tone keys are the palette cascade, adjudicated BY PATTERN below; the
// twelve hover keys and the thirty variant keys stopped being dead when the
// guard learnt to sweep SEVERAL axes (variant x state x invalid) in both
// passes — they are not adjudicated, they are measured.
'radius-sm':
'only the step in force paints (the demo has no `rounded` control at all, so the resting corner is the only one mounted); forced data-rounded=sm -> reaches (4px -> 1234px)',
'radius-md': 'same scale; forced data-rounded=md -> reaches (6px -> 1234px)',
'radius-lg': 'same scale; forced data-rounded=lg -> reaches (10px -> 1234px)',
'radius-xl': 'same scale; forced data-rounded=xl -> reaches (16px -> 1234px)',
'radius-full': 'same scale; forced data-rounded=full -> reaches (9999px -> 1234px)',
'disabled-opacity':
'the demo boots enabled, and switching `disabled` on for the whole run would drop pointer-events and revert the hover pair, killing six other keys; forced data-disabled on the real toggle -> reaches (opacity 0.4 -> 0.123)',
'transition-duration':
'the guard freezes transitions and this token IS the transition; measured unfrozen -> reaches (0.12s -> 11.5s on all four transitioned properties)',
'transition-ease':
'same freeze; measured unfrozen -> reaches (cubic-bezier(0.4, 0, 0.2, 1) -> steps(4))'
}
};
/**
@ -1275,6 +1294,14 @@ export const SENTINEL_PATTERN_EXCEPTIONS: Array<{
PALETTE_SUPERSEDED +
' — and here NEUTRAL falls with the rest: soma always stamps data-color (it is the resolved colour, neutral by default), so the generic block matches on every instance and the fallback the other recipes keep alive never applies. Measured checked 2026-08-23: --switch-neutral-solid moves nothing, --palette-solid on the node repaints (oklch(0.5556 …) -> rgb(4,5,6))'
},
{
component: 'toggle',
pattern:
/^(primary|secondary|neutral|affirm|risk|threat)-(track|element|hover|border|solid|solid-hover|text|contrast)$/,
reason:
PALETTE_SUPERSEDED +
' — the six tones toggle forwards x eight slots, 48 of its 126 keys (it excludes fulfill/loss by doctrine: a toggle is neither completion nor irreversible loss). Measured 2026-08-23 on the real pressed toggle with transitions frozen: with data-color=primary, --toggle-primary-solid moves NOTHING (the fill stays oklch(0.5556 0.1829 305.86)) while --palette-solid on the same node repaints it (rgb(1, 2, 3)); strip data-color and --toggle-neutral-solid paints again (oklch(0.6434 0 0) -> rgb(1, 2, 3)). The neutral eight fall with the rest for a second reason worth writing down: the guard STAMPS data-color=neutral to test them, which is exactly what turns the generic block on'
},
{
component: 'select',
pattern: /^(primary|secondary|neutral|affirm|fulfill|risk|threat|loss)-(track|border|text)$/,

@ -167,8 +167,24 @@ const COMPONENT_OVERRIDES: Record<
* mounts ONE instance (button: a single solid/primary stage driven by chips)
* hides every token that only paints under another variant — 71 of buttons
* 105 read dead without this.
*
* SEVERAL axes may be given, and then the sweep is their CARTESIAN PRODUCT
* (toggle, 2026-08-23: its `{variant}-*` keys and its `{variant}-on-*` keys
* are the same 3 variants in two different STATES, and forcing the state
* with `prepareWith` traded one half of them for the other — the "mounting
* more can measure less" trap, this time inside one component). One axis
* yields the same sequence it always did, so nothing that had a single
* `sweepAttr` changes behaviour.
*
* A `null` value REMOVES the attribute, which is the only way to sweep an
* axis whose off position is the attribute's ABSENCE (`data-invalid`,
* `data-rounded`): stamping `data-invalid=""` for the whole run makes the
* invalid border out-rank every other border token on the node, and six
* of them read dead (measured on toggle, 2026-08-23).
*/
sweepAttr?: { attr: string; values: string[] };
sweepAttr?:
| { attr: string; values: (string | null)[] }
| { attr: string; values: (string | null)[] }[];
}
> = {
// Two thirds of its surface is OPT-IN or lives on another route: the badge is
@ -517,6 +533,20 @@ const COMPONENT_OVERRIDES: Record<
extraNodes:
'[data-month-grid], [data-month-grid] *, [data-range-calendar], [data-range-calendar] *',
sweepAttr: { attr: 'data-variant', values: ['surface', 'outline', 'ghost'] }
},
// ONE instance driven by chips, and the whole palette hangs off the PRESSED
// state: the 8 `palette-*` slots — and the 48 tone forwarders behind them —
// are consumed only by the `*-on-*` keys, so an OFF toggle leaves 56 tokens
// without a node to paint. `invalid` is a second opt-in the stage does not
// boot with. With the variant swept on top, the guard sees solid / outline /
// ghost, pressed, invalid (measured 2026-08-23: 36/126 → 70/126, and the
// remaining 56 are the palette cascade plus six states the demo cannot mount).
toggle: {
sweepAttr: [
{ attr: 'data-variant', values: ['solid', 'outline', 'ghost'] },
{ attr: 'data-state', values: ['off', 'on'] },
{ attr: 'data-invalid', values: [null, ''] }
]
}
};
@ -711,7 +741,7 @@ async function main() {
const staticPass = (key: string, value: string) =>
page.evaluate(
([kebab, token, val, props, prefix, extra, sweepAttr, sweepVals]) => {
([kebab, token, val, props, prefix, extra, sweeps]) => {
const all = () =>
[...document.querySelectorAll<HTMLElement>('*')].filter(
(n) =>
@ -727,11 +757,19 @@ async function main() {
// A tone token (`risk-solid`, `affirm-track`…) only paints on an instance
// wearing that tone: the palette forward routes the slots through
// `--{c}-palette-*` per [data-color]. Stamp the tone the key names.
// The attribute this component sweeps while measuring (variant, look…), or a
// single null pass when it has none.
const swept = sweepAttr ? all() : [];
const sweptWas = swept.map((n) => n.getAttribute(sweepAttr as string));
const sweepValues: (string | null)[] = sweepAttr ? (sweepVals as string[]) : [null];
// The attributes this component sweeps while measuring (variant, look,
// state…), or a single null pass when it has none. Several axes sweep
// as their CARTESIAN PRODUCT; with one axis the sequence is the same
// list it has always been.
const axes = sweeps as { attr: string; values: (string | null)[] }[];
const swept = axes.length ? all() : [];
const sweptWas = swept.map((n) => axes.map((ax) => n.getAttribute(ax.attr)));
const combos: ((string | null)[] | null)[] = axes.length
? axes.reduce<(string | null)[][]>(
(acc, ax) => acc.flatMap((c) => ax.values.map((v) => [...c, v])),
[[]]
)
: [null];
const TONES = ['primary','secondary','neutral','affirm','fulfill','risk','threat','loss'];
const tone = TONES.find((t) => (token as string).startsWith(t + '-'));
const toned = tone ? all() : [];
@ -766,8 +804,14 @@ async function main() {
})
.join('@');
let moved = false;
for (const sweep of sweepValues) {
if (sweep !== null) for (const n of swept) n.setAttribute(sweepAttr as string, sweep);
for (const combo of combos) {
if (combo !== null)
for (const n of swept)
axes.forEach((ax, i2) =>
combo[i2] === null
? n.removeAttribute(ax.attr)
: n.setAttribute(ax.attr, combo[i2]!)
);
for (const size of ['md', 'xs', 'sm', 'lg', 'xl']) {
for (const n of sized) n.setAttribute('data-size', size);
const before = snap();
@ -783,7 +827,9 @@ async function main() {
}
sized.forEach((n, i) => (original[i] ? n.setAttribute('data-size', original[i]!) : null));
swept.forEach((n, i2) =>
sweptWas[i2] ? n.setAttribute(sweepAttr as string, sweptWas[i2]!) : n.removeAttribute(sweepAttr as string)
axes.forEach((ax, j) =>
sweptWas[i2][j] ? n.setAttribute(ax.attr, sweptWas[i2][j]!) : n.removeAttribute(ax.attr)
)
);
toned.forEach((n, i2) => {
toneWas[i2] ? n.setAttribute('data-color', toneWas[i2]!) : n.removeAttribute('data-color');
@ -800,8 +846,11 @@ async function main() {
PROPS,
attrPrefix,
override.extraNodes ?? null,
override.sweepAttr?.attr ?? null,
override.sweepAttr?.values ?? null
override.sweepAttr
? Array.isArray(override.sweepAttr)
? override.sweepAttr
: [override.sweepAttr]
: []
] as const
);
@ -836,7 +885,7 @@ async function main() {
continue;
}
const moved = await page.evaluate(
([prefix, token, val, props, idx, kebab, extra]) => {
([prefix, token, val, props, idx, kebab, extra, sweeps]) => {
const all = () =>
[...document.querySelectorAll<HTMLElement>('*')].filter(
(n) =>
@ -849,15 +898,61 @@ async function main() {
const cs = getComputedStyle(node);
return (props as string[]).map((p) => cs[p as never]).join('|');
};
const before = snap();
for (const h of [document.documentElement, ...all()])
h.style.setProperty(`--${kebab}-${token}`, val as string);
const after = snap();
for (const h of [document.documentElement, ...all()])
h.style.removeProperty(`--${kebab}-${token}`);
return before !== after;
// The hover pass swept NOTHING, so a `{variant}-hover-*` key could
// only ever move on the variant the demo happens to mount — ten of
// toggle's twelve hover keys read dead for that reason alone
// (measured 2026-08-23). Same axes as the static pass; the pointer
// does not move, so re-stamping attributes is safe.
const axes = sweeps as { attr: string; values: (string | null)[] }[];
const swept = axes.length ? all() : [];
const sweptWas = swept.map((n) => axes.map((ax) => n.getAttribute(ax.attr)));
const combos: ((string | null)[] | null)[] = axes.length
? axes.reduce<(string | null)[][]>(
(acc, ax) => acc.flatMap((c) => ax.values.map((v) => [...c, v])),
[[]]
)
: [null];
let moved = false;
for (const combo of combos) {
if (combo !== null)
for (const n of swept)
axes.forEach((ax, j) =>
combo[j] === null
? n.removeAttribute(ax.attr)
: n.setAttribute(ax.attr, combo[j]!)
);
const before = snap();
for (const h of [document.documentElement, ...all()])
h.style.setProperty(`--${kebab}-${token}`, val as string);
const after = snap();
for (const h of [document.documentElement, ...all()])
h.style.removeProperty(`--${kebab}-${token}`);
if (before !== after) {
moved = true;
break;
}
}
swept.forEach((n, i2) =>
axes.forEach((ax, j) =>
sweptWas[i2][j] ? n.setAttribute(ax.attr, sweptWas[i2][j]!) : n.removeAttribute(ax.attr)
)
);
return moved;
},
[attrPrefix, key, value, PROPS, i, component, override.extraNodes ?? null] as const
[
attrPrefix,
key,
value,
PROPS,
i,
component,
override.extraNodes ?? null,
override.sweepAttr
? Array.isArray(override.sweepAttr)
? override.sweepAttr
: [override.sweepAttr]
: []
] as const
);
if (moved) return true;
}

Loading…
Cancel
Save

Powered by TurnKey Linux.