feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
---
title: Next features — initiative registry
type: registry
audience: human + agent
authority: backlog of user-decided future initiatives — records scope, sequencing and dependencies; it does NOT record verdicts (those live in the RFCs / changelog / book-deviations when executed)
status: living — items enter by explicit user decision during audits and sessions; remove an item when it ships (and link where it landed)
created: 2026-07-06
---
# Next features — initiative registry
Rules of this file: one entry per initiative, dated, with its origin (which
audit/session raised it), its scope in one paragraph, and its dependencies.
Entries are **registered, not started** — starting one is a session decision.
Detail lives in the linked doc, never copied here.
color: contraste Stage 2 — contrato §40 como datos + guard CI; solver descartado
La ejecución refutó la premisa del plan: el morph HEREDA el contraste del texto
(steps 11/12 = curva-L del donante verbatim; ~invariante al croma bajo
gamut-mapping), así que toda escala desde donantes §40-compliant cumple por
construcción. Medido en 3 bancos (base, leave-one-out, 45 semillas OOD): 0 fallos
del gate duro text-strong·12, min WCAG 9.7:1. El solver de luminancia no tenía
nada que resolver → descartado por especulativo. D2 = base verbatim (sin
migración base→seeds).
- $color: CONTRAST_PAIRS (arts/color/contrast-contract.ts) — tabla §40 como datos
compartidos: floor duro text-strong·12, banda text·11 (tope relacional, sin nº
mágico), border·7 exento
- scripts/contrast-audit.ts: muere la PAIRS pre-veredicto (4.5 duro contra §40),
consume CONTRAST_PAIRS + banco de regresión morph-generado
- eidos/lib/contrast-invariant.test.ts: guard de CI (4 tests) que bloquea la herencia
- docs: reference.md §40, next-features.md §1, changelog.md §45, plan (OUTCOME)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
## 1. Tonal-ramp contrast parity (measure, then construct) — 2026-07-06 · Stage 1 DONE 2026-07-19 · Stage 2 CLOSED 2026-07-20
> **Stage 1 SHIPPED (2026-07-19).** Measured with `scripts/contrast-audit.ts`
> (33 scales × 2 modes); drift + user verdicts ratified as doctrine in
> [`theming/reference.md §40`](./theming/reference.md) + [`changelog.md §44`](./theming/changelog.md).
> Outcome: `text·11` = secondary tier, `text-strong` = AA-guaranteed; the border
> vocabulary is Radix-subtle by design (only `solid·9` clears 3:1 by
> construction) with a decorative-vs-load-bearing split; the focus ring is a
> config axis (`primitives.focusRing` + `color.focus`, single `outline` per §32)
> whose shipped default is soft — hardening is a config-value choice, kept as-is.
color: contraste Stage 2 — contrato §40 como datos + guard CI; solver descartado
La ejecución refutó la premisa del plan: el morph HEREDA el contraste del texto
(steps 11/12 = curva-L del donante verbatim; ~invariante al croma bajo
gamut-mapping), así que toda escala desde donantes §40-compliant cumple por
construcción. Medido en 3 bancos (base, leave-one-out, 45 semillas OOD): 0 fallos
del gate duro text-strong·12, min WCAG 9.7:1. El solver de luminancia no tenía
nada que resolver → descartado por especulativo. D2 = base verbatim (sin
migración base→seeds).
- $color: CONTRAST_PAIRS (arts/color/contrast-contract.ts) — tabla §40 como datos
compartidos: floor duro text-strong·12, banda text·11 (tope relacional, sin nº
mágico), border·7 exento
- scripts/contrast-audit.ts: muere la PAIRS pre-veredicto (4.5 duro contra §40),
consume CONTRAST_PAIRS + banco de regresión morph-generado
- eidos/lib/contrast-invariant.test.ts: guard de CI (4 tests) que bloquea la herencia
- docs: reference.md §40, next-features.md §1, changelog.md §45, plan (OUTCOME)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
> **Stage 2 CLOSED (2026-07-20) — no solver needed.** The 6 decisions were
> locked (D1 band · D2 measured-pass · D3 flip-only · D4 opt-in · D5 runtime-first
> · D6 now), then **execution disproved the premise**: the template morph does not
> _compute_ text contrast, it **inherits** it (steps 11/12 copy the donor's L-curve
color: contraste Stage 2 — contrato §40 como datos + guard CI; solver descartado
La ejecución refutó la premisa del plan: el morph HEREDA el contraste del texto
(steps 11/12 = curva-L del donante verbatim; ~invariante al croma bajo
gamut-mapping), así que toda escala desde donantes §40-compliant cumple por
construcción. Medido en 3 bancos (base, leave-one-out, 45 semillas OOD): 0 fallos
del gate duro text-strong·12, min WCAG 9.7:1. El solver de luminancia no tenía
nada que resolver → descartado por especulativo. D2 = base verbatim (sin
migración base→seeds).
- $color: CONTRAST_PAIRS (arts/color/contrast-contract.ts) — tabla §40 como datos
compartidos: floor duro text-strong·12, banda text·11 (tope relacional, sin nº
mágico), border·7 exento
- scripts/contrast-audit.ts: muere la PAIRS pre-veredicto (4.5 duro contra §40),
consume CONTRAST_PAIRS + banco de regresión morph-generado
- eidos/lib/contrast-invariant.test.ts: guard de CI (4 tests) que bloquea la herencia
- docs: reference.md §40, next-features.md §1, changelog.md §45, plan (OUTCOME)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
> verbatim; L-driven contrast is ~chroma-invariant under gamut-mapping), so any
> scale generated from a §40-compliant donor library clears the ratified text
> floors **by construction**. Verified on three banks — authored base, leave-one-out
> regeneration, and 45 out-of-distribution seeds — **0 hard-gate failures**
> (min WCAG 9.7:1). The luminance solver would have had nothing to resolve for
> realistic inputs → dropped as speculative. **Delivered instead:** the ratified
> pair table as shared data (`$color` → `CONTRAST_PAIRS`), the audit refactored to
> consume it + a morph-generated regression bank (`scripts/contrast-audit.ts`), and
> a CI guard that locks the inheritance (`eidos/lib/contrast-invariant.test.ts` —
> fails if a future donor/generator breaks it). With D2, the base stays verbatim
> ground-truth; **no base→seeds migration**.
> **Plan + decision + outcome record:**
> [`process/contrast-stage2-plan-2026-07.md`](./process/contrast-stage2-plan-2026-07.md);
> chronicle [`changelog.md §45`](./theming/changelog.md).
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
**Origin**: A.7 follow-up (theming audit F). The on-solid pair is now computed
at generation with a single criterion ([`rfcs/rfc-color-engine.md`](./rfcs/rfc-color-engine.md)
§8, `eidos/lib/on-solid.ts` ). M3 still guarantees more: contrast **by
construction across its whole tonal ramp** (container/on-container pairs at
fixed tone deltas — ΔT 40 ≈ 3:1, ΔT 50 ≈ 4.5:1). Our equivalent promises
(text-11 on bg-2/element-3, border-7 vs bg-2 at the 3:1 non-text floor, text
over composited soft surfaces a2/a3) rest on Radix-style curation, unmeasured.
**Stage 1 — pair-contract verification** (the A.7 pattern generalized): a
canonical table of slot pairs + floors (a user-decided doctrine session),
a measurement pass over pairs × 33 scales × 2 modes at generation
(`on-solid.ts` generalizes; the compositing math exists in `$color` ), and the
resulting drift list goes to user verdicts (retune vs annotated exception).
Infrastructure is small; the fallout is the unknown.
color: contraste Stage 2 — contrato §40 como datos + guard CI; solver descartado
La ejecución refutó la premisa del plan: el morph HEREDA el contraste del texto
(steps 11/12 = curva-L del donante verbatim; ~invariante al croma bajo
gamut-mapping), así que toda escala desde donantes §40-compliant cumple por
construcción. Medido en 3 bancos (base, leave-one-out, 45 semillas OOD): 0 fallos
del gate duro text-strong·12, min WCAG 9.7:1. El solver de luminancia no tenía
nada que resolver → descartado por especulativo. D2 = base verbatim (sin
migración base→seeds).
- $color: CONTRAST_PAIRS (arts/color/contrast-contract.ts) — tabla §40 como datos
compartidos: floor duro text-strong·12, banda text·11 (tope relacional, sin nº
mágico), border·7 exento
- scripts/contrast-audit.ts: muere la PAIRS pre-veredicto (4.5 duro contra §40),
consume CONTRAST_PAIRS + banco de regresión morph-generado
- eidos/lib/contrast-invariant.test.ts: guard de CI (4 tests) que bloquea la herencia
- docs: reference.md §40, next-features.md §1, changelog.md §45, plan (OUTCOME)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
**Stage 2 — by-construction generator (CLOSED 2026-07-20, solver dropped).** The
plan proposed a luminance solver that resolves each step to satisfy Stage 1's
pair table for a seed. Execution disproved its premise: the morph **inherits**
text contrast from the donor (steps 11/12 copy the donor's L-curve verbatim;
L-driven contrast is ~chroma-invariant under gamut-mapping), so every scale from
a §40-compliant donor library clears the ratified floors by construction. Measured
across three banks (authored base, leave-one-out regeneration, 45 out-of-distribution
seeds): **0 hard-gate failures, min WCAG 9.7:1.** The solver had nothing to
resolve for realistic inputs → dropped (speculative). What shipped: the pair
table as shared data (`$color` `CONTRAST_PAIRS` ), the audit consuming it + a
morph-generated regression bank, and a CI guard (`eidos/lib/contrast-invariant.test.ts`)
that locks the inheritance. The BASE stays verbatim ground-truth (D2); no
base→seeds migration.
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
**Sequence**: Stage 1 first (it is the spec for Stage 2). No M3-style `on-*`
vocabulary expansion — our slots already encode the pairs; the table
formalizes them without inflating the namespace.
## 2. Per-theme palette-contrast cascade emission — 2026-07-06
**Origin**: A.7 execution. The per-instance `palette-contrast` cascade lives
in the STATIC foundation — one polarity for all themes. Today polarity is
computed per configured theme and unanimous (33/33 scales agree across
base-light/base-dark); on disagreement the `*-light` theme wins
(`computeLightSolidScales`, documented in
[`rfcs/rfc-color-engine.md` ](./rfcs/rfc-color-engine.md ) §8). **Trigger** : the
day a real configured theme disagrees on a scale's polarity, emit the cascade
per-theme instead of statically. Not before — no speculative machinery.
## 3. Component audit (post-system) — 2026-07-06
**Origin**: user decision during the theming audit — "de momento no tocamos
los componentes, merecen una auditoría adicional una vez que se haya resuelto
estas". Gate: the system-level audit (F/G) closes first. Registered items so
far, to seed its scope:
- **Named-styles adoption by controls** (from A.3): controls/recipes that
hand-declare typography instead of consuming `--style-*` /
size-bundle typography — likely by error, to be confirmed per component.
- **Letter-spacing bundle adoption**: ~34 recipes consume `font-size` /
`line-height` from the size bundle but not `letter-spacing` ; plus ~7 raw
letter-spacing literals in component CSS.
- **Tier-A `data-depth` stamping coverage**: overlays that should stamp their
plane (dialog / drawer / tooltip / menubar / combobox) per
[`rfcs/rfc-depth.md` ](./rfcs/rfc-depth.md ) §5.
- **Calendar family → shared layer/archetype** (user design verdict,
2026-07-07): the calendar-grid vocabulary (accent set, day geometry, the
holiday/event marks fixed in audit B.2) becomes a FIRST-CLASS shared layer
— today the family borrows it informally (measured: range-calendar
consumes 79 `--calendar-*` tokens, month-grid 65, year-grid 65,
date-range-picker 38, date-picker 4 — ~250 cross-component borrows), and
**chronos will draw from it too** when its track lands. Materialization to
decide at execution: a morfo part-archetype for the day cell (the 26-set
grows) and/or a `calendar-surface` shared recipe à la list-surface
(`--list-item-*` precedent). This supersedes "per-component composition
repairs" for the B.2 borrowers: the marks reach the whole family through
the shared layer instead.
- **Field family composition — segments under `[data-field]` ** (user design
verdict, 2026-07-07, audit B.1): segment height derives from the SIZE axis
at the FIELD family level — `field` recipe declares `segment-height` with
size-scoped declarations (`calc(var(--field-control-height-{k}) -
var(--space-2))`; the TSC `size:` scope axis exists, unused so far), and
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
the per-component copies die (each x-field today re-declares the whole
`height-{xs..xl}` family the field recipe already has). **MANDATED by the
B.1-(i) verdict**: date/time/color-field MUST incorporate the Field
wrapper (segments under `[data-field][data-size]` — probed live: today
they don't) as part of the component audit; the three broken
per-component `segment-height` tokens and their consumers were removed
(they had computed `auto` since birth — zero visual change).
- Whatever F/G registers as "component-level adoption gap" in
`docs/audit/theming-audit.md` when that report lands (deliverable G of the
2026-07 theming audit).
## 4. `architecture/sema.md` hold-sync — 2026-07-06
**Origin**: A.4/A.5 execution (holds single-source). `SEMA_HOLDS_BY_INTENT`
(`src/uix/sema/holds.ts`) is now THE only hold table (resolver + visual
channel consume it; `sema-map.ts` 's per-family hold column died). The chapter
[`architecture/sema.md` ](./architecture/sema.md ) still narrates per-family
holds in ~34 places. **Blocked** : the file is the author's WIP (uncommitted
edits) — sync it when the author's pass lands, not before.
## 6. THM-2 rollout — per-instance palette to all surfaces — SHIPPED 2026-07-12; scope boundaries REVERTED by the open-color-cage initiative (2026-07-18/19)
Shipped in `05f0031c` (media-player Batch-4 + 14 surfaces + vocab + 4 props)
and `cc7e9a16` (avatar root). Every per-component palette cascade now collapses
into the shared `--palette-*` layer (size win, behavior-preserving for roles);
the vocabulary gained the alpha `surface` /`surface-hover` + `hover` slots.
> **REVERSED (2026-07-18/19) — "abrir la jaula del color".** THM-2 originally
> kept semantic controls (checkbox/radio/stepper/editable/select/file-upload)
> and `Avatar.Badge` role-restricted "by design". A later user decision reverses
> that: `color` accepts the FULL system (role / intent / 33 scales / raw CSS =
> `ComponentColorProp`) on **every** component, no exceptions — content-ink
> primitives keep their axis as an additive union. Enforced by two guards in
> `recipe-css-contract.test.ts`. Full record + the closed queue (incl. the
> chart SVG resolver and the Card/Avatar custom-color fix):
> [`process/open-color-cage-2026-07.md`](./process/open-color-cage-2026-07.md).
> Only `qr-code` stays out (its `color` is the QR module ink, not a scale).
feat(theming+soma): media-player Batch-4 (SOM-4) + THM-2 shared-palette rollout to 14 surfaces
media-player Batch-4 (closes SOM-4): the 10 sub-parts migrate to
renderProps() (35 attr-dup hits -> 0). Register the sources the morfo
reads (label propRef, on stateRef, direction/type) so renderProps emits
type/role/aria-*/data-state; keep only contract-only soma-owned attrs.
Pin the soma-attr-audit ceiling to 0.
THM-2 rollout - collapse each per-component palette cascade into the
shared per-instance layer (size win, behavior-preserving for roles,
adds the 33 scales):
- Rename palette tokens to canonical _palette-{slot} + route via the
presence-guarded forward: tags-input, stepper, badge, card, select
(parts-aware trigger+content), file-upload, editable, tag-group.
- Extend the shared vocabulary with alpha surface(a2)/surface-hover(a3)
slots (soft variant of variant-aware recipes) and hover(4) - the
latter also fixes Toggle's latent scale-hover.
- tag-group: variant-aware migration (solid/outline -> canonical,
soft -> alpha surface; merge outline-fg into text, prune orphans).
- toggle-group: replace the 4-role cross-recipe composition with a
hand-authored presence-scoped route to the shared layer (the
composition DSL can't presence-scope); covers roles + 33 scales.
- Widen color to ComponentColor (roles + 33 scales) where arbitrary
color is meaningful: badge, tag-group, tags-input, toggle-group, and
add the scale picker to their demos. Semantic controls stay restricted.
Verified end-to-end with the REAL components (drive the demo color
control -> the component emits data-color -> the shared layer resolves
the exact scale). Gates: recipe-css-contract/config/contracts/
soma-attr-audit/media-player suites green; 0 orphan tokens.
avatar remains for a focused pass: its variant-resolved matrix and badge
sub-part don't fit a mechanical migration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## 5. Book editorial note — "el hold es suelo, no tijera" — 2026-07-06
**Origin**: A.4/A.5 verdict. The mechanism (hold = floor for the expression,
never a scissor; expression waits animation-finish after the hold, capped by
an absolute `MAX_EXPRESSION_WAIT_MS` , never derived from the hold) is recorded
in [`decisions/book-deviations.md` ](./decisions/book-deviations.md ) D.12 as a
candidate **editorial appendix** for the book (_Diseñando lo que ocurre_,
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
HOMOGENEIZADO edition) — the book gives qualitative regions and this is the
materialization doctrine worth feeding back to the source.
uix(chat): bloque de mensajería chat-* v1 (log · message · composer · typing)
Familia chat-* completa, componiendo el ecosistema (Feed + VirtualList
anclado, Textarea, FileUpload, Avatar, Popover, Icon). Transporte-agnóstica:
el modelo de datos es de la app, los componentes reciben props + snippets.
- chat-log: Feed + VirtualList(anchor:'end') en triple registro; day
separators, unread divider, filas de sistema (Feed.Article), pill "N
nuevos" + notifyIncoming (announce a live-region), to-latest distance-aware.
- chat-message: raíz Feed.Article; runs ($libs/chat), ticks de entrega,
reply embebido, reacciones (chip pressed + tapback panel), acciones hover.
- chat-composer: Textarea (doble registro) + FileUpload; teclado
Enter/Shift+Enter/Esc/ArrowUp; typing saliente debounced (uix.timers);
barra reply/edit.
- chat-typing: sustain-streaming (visual-only, loop del sistema);
aggregateTypers 1/2/3+ con plantillas plurales.
- $libs/chat: groupIntoRuns (ventana 8 min estilo Discord) + aggregateTypers.
- VirtualList: extensión anchor:'end' (followOutput, prepend por clave
estable, sticky monótono, onAtEndChange) — sin scroller paralelo.
Diseño de referencia (WhatsApp/Telegram, pase lado-a-lado): burbujas suaves
(out = primary-element + texto normal; in = surface card + sombra) sobre
lienzo muted del log, meta dentro de la burbuja, tapback bar horizontal,
iconos lucide del ecosistema (nada de SVG a mano). Contrato de tamaños:
size="md" = 16px canónico.
Fixes de framework (afectaban más allá de chat):
- EngineLangs.t() ahora pela el prefijo '#?': los idlangrefs imperativos
(pagination/file-upload/chronos/chat) caían al fallback EN en locale es.
Fix parseLangRef ?? parsePathFallback + test de regresión (132/132).
- virtual-list commit-set-resize con channels:[]: es bookkeeping de
ResizeObserver, no gesto → dejaba de emitir beep/vibrate pre-interacción
(interventions de navigator.vibrate / AudioContext).
Escaparate messenger en /uix/demos/chat (bot de eco con uix.timers;
variante $connection señalada). Gaps v2 registrados en next-features §7.
Pase: component:audit 4×PASS · eidos-lint invalid=0 · recipe-contract
29/29 · Playwright escaparate PASS end-to-end · consola limpia ·
npm run check en baseline (76, cero errores nuevos).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## 7. Chat block v2 — 2026-07-18
**Origin**: the `chat-*` v1 initiative (comparative study + plan, session
2026-07-18; user scoped v1 to the room core). v1 shipped `chat-log`
(Feed + end-anchored VirtualList), `chat-message` , `chat-composer`
(Textarea + FileUpload composition) and `chat-typing` , plus the
`$libs/chat` helpers and the VirtualList `anchor: 'end'` extension. The
deferred surface, registered with disposition (each item also lives in its
component README §Gaps):
uix(chat): v2 · chat-list — panel de conversaciones (compone Listbox)
Componente nuevo del bloque chat v2: el panel de lista de salas/DMs.
Compone el Listbox del ecosistema (doble registro id/ref compartido):
selección única + roving-tabindex + typeahead + role=option vienen de
Listbox; chat-list aporta la disposición de fila (avatar · título/preview ·
meta) y el estado visual data-unread (negrita + Badge compuesto).
- morfo: expression 'delegated', sin eventos propios (D.1 — el actor es el
Listbox compuesto); parts provider/item(data-unread)/avatar/title/preview/meta.
- soma: ChatListProvider crea ListboxProvider dentro (value string <-> string[]);
ChatListItemProvider per-instance (unread = boolean crudo); 4 clases de
contenido explicitas.
- eidos: rejilla avatar | (titulo/preview) | meta, avatar y meta abarcan las
dos filas; truncado; unread -> titulo en negrita. No neutraliza el arquetipo
item (la conversacion ES una opcion seleccionable). Sin @keyframes; ejes
logicos.
- demo: 5 salas, seleccion limpia unread, typing-en-lista (compone ChatTyping),
Badge de no-leidos; tabs live/system/api/morfo/a11y.
- READMEs dobles (eidos con Passive justification, delegated); next-features 7
marca chat-list SHIPPED.
Verificacion: check baseline 76 (0 nuevos) · component:audit PASS ·
eidos-lint invalid=0 · recipe-contract 30/30 · morfo:vocabulary 0 · Playwright
PASS (role=listbox, 5 salas, aria-selected, unread=2, badges=2, typing-en-lista,
click + teclado seleccionan).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
- ~~**`chat-list`** — the conversation-list pane (rooms, unread badges,
typing-in-list).~~ ** ✅ SHIPPED 2026-07-19** — composes `Listbox`
(single-select + keyboard) with an avatar/title/preview/meta row; unread
bold + `Badge` , typing-in-list composes `ChatTyping` . Virtualization for
very long lists deferred (pairs with the VirtualList bidirectional item).
uix(chat): v2 · emoji-picker (picker completo) + escaparate rico del bloque
Nuevo componente emoji-picker y rediseño del escaparate de mensajeria.
emoji-picker (ruta 9 fases):
- Compone Popover (open/close/foco/portal) + Command (busqueda + grid + nav de
teclado + a11y combobox/listbox, su "grid mode" documentado) + dos ToggleGroup
(categorias + tono de piel). Delegated: sin eventos propios (D.1); el grid y
las celdas doble-registran Command.List/Item, conservando la a11y pero con
contrato visual propio (gana especificidad con selectores de 2 niveles, sin
tocar attrs [data-command-*] ajenos — eidos-lint limpio).
- Datos: $libs/emoji, dataset vendorizado de emojibase (MIT) horneado por
scripts/generate-emoji-data.ts (precedente lucide: JSON crudo en static/ +
script generador). ~1906 emoji con nombre CLDR + keywords, 316 con tono de
piel single-person. Grid acotado (categoria activa o resultados de busqueda),
sin virtualizacion (refinamiento v2). Helpers puros + test (12/12).
- morfo delegated (7 partes + header/footer de layout); soma (root + trigger +
content + grid + option + preview, provider-test 4/4); eidos (panel
empaquetado, receta con tokens --emoji-picker-*); langs es/en; demo 5 tabs
(excepcion delegated documentada); READMEs dobles.
Escaparate del bloque (web/routes/uix/demos/chat) — rehecho rico y correcto:
- Avatares por-autor con color solido (Avatar del ecosistema): antes gris
neutro indefinido, ahora teal/ambar/verde/indigo/primary bien contrastados.
- Dos columnas: chat-list (sidebar con badges + typing-en-lista) + sala.
- Conversacion rica: separador de dia, run agrupado, reaccion, reply citado,
recibos de lectura, typing con avatar, y el emoji-picker integrado en el
composer (inserta en el draft).
- Solape de burbujas (transitorio, estimateSize=48 con mensajes ~90-126px)
mitigado subiendo el estimate a 76.
Auditoria de tamanos/espacios: recipes del bloque limpios (todo con tokens
--space-*/--size-*/--color-*); unico literal (punto tono-capable) tokenizado.
Verificacion: check baseline 76 (0 nuevos) · component:audit emoji-picker PASS ·
eidos-lint invalid=0 · recipe-contract 30/30 · morfo:vocabulary 0 · emoji lib
12/12 · provider-test 4/4 · Playwright PASS (busqueda 'rocket'->rocket+
astronautas, click->onSelect+cierra, teclado, tono oscuro 👋->👋🏿, picker abre
en el composer). Capturas claro miradas.
next-features §7 marca emoji-picker SHIPPED.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
- ~~**`emoji-picker`** — a real picker component (color-picker scale:
categories, search, skin tones).~~ ** ✅ SHIPPED 2026-07-21** — composes
`Popover` + `Command` (search + grid keyboard + combobox/listbox a11y, its
documented "grid mode") + two `ToggleGroup` s (categories + skin tone). Data is
the vendored `$libs/emoji` (baked from emojibase, MIT — `scripts/generate-emoji-data.ts` ):
~1,900 emoji, 316 tone-capable. The grid double-registers Command's
List/Item so it keeps the a11y but wears its own grid-cell styling. Delegated
(no own events). Full-grid virtualization + per-emoji tone + image emoji
(Twemoji/Noto) deferred. The quick-set Popover in `ChatMessage.ReactionAdd`
stays as the inline tapback bar (different UX).
uix(chat): bloque de mensajería chat-* v1 (log · message · composer · typing)
Familia chat-* completa, componiendo el ecosistema (Feed + VirtualList
anclado, Textarea, FileUpload, Avatar, Popover, Icon). Transporte-agnóstica:
el modelo de datos es de la app, los componentes reciben props + snippets.
- chat-log: Feed + VirtualList(anchor:'end') en triple registro; day
separators, unread divider, filas de sistema (Feed.Article), pill "N
nuevos" + notifyIncoming (announce a live-region), to-latest distance-aware.
- chat-message: raíz Feed.Article; runs ($libs/chat), ticks de entrega,
reply embebido, reacciones (chip pressed + tapback panel), acciones hover.
- chat-composer: Textarea (doble registro) + FileUpload; teclado
Enter/Shift+Enter/Esc/ArrowUp; typing saliente debounced (uix.timers);
barra reply/edit.
- chat-typing: sustain-streaming (visual-only, loop del sistema);
aggregateTypers 1/2/3+ con plantillas plurales.
- $libs/chat: groupIntoRuns (ventana 8 min estilo Discord) + aggregateTypers.
- VirtualList: extensión anchor:'end' (followOutput, prepend por clave
estable, sticky monótono, onAtEndChange) — sin scroller paralelo.
Diseño de referencia (WhatsApp/Telegram, pase lado-a-lado): burbujas suaves
(out = primary-element + texto normal; in = surface card + sombra) sobre
lienzo muted del log, meta dentro de la burbuja, tapback bar horizontal,
iconos lucide del ecosistema (nada de SVG a mano). Contrato de tamaños:
size="md" = 16px canónico.
Fixes de framework (afectaban más allá de chat):
- EngineLangs.t() ahora pela el prefijo '#?': los idlangrefs imperativos
(pagination/file-upload/chronos/chat) caían al fallback EN en locale es.
Fix parseLangRef ?? parsePathFallback + test de regresión (132/132).
- virtual-list commit-set-resize con channels:[]: es bookkeeping de
ResizeObserver, no gesto → dejaba de emitir beep/vibrate pre-interacción
(interventions de navigator.vibrate / AudioContext).
Escaparate messenger en /uix/demos/chat (bot de eco con uix.timers;
variante $connection señalada). Gaps v2 registrados en next-features §7.
Pase: component:audit 4×PASS · eidos-lint invalid=0 · recipe-contract
29/29 · Playwright escaparate PASS end-to-end · consola limpia ·
npm run check en baseline (76, cero errores nuevos).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- **Mention autocomplete (`@`)** — combobox-in-textarea for
`chat-composer` ; pairs with the future `words` /`palabras` integration.
- **Threads UI** — the side-panel composition over the already-shipped
`Feed.Thread` semantics.
- ~~**Jump-to-message highlight** — `signal-emphasize-target` on
`chat-message` (the morfo event was deliberately left out of v1).~~
** ✅ SHIPPED 2026-07-19** — `emphasized` prop → `data-emphasized` row flash
(visual-only, no sema event: reference apps flash silently; the app scrolls
via a reply's `onJump` then toggles `emphasized` for ~1.4s).
- ~~**Read-by list** — "seen by N" avatar row (Teams/Messenger pattern) on
`chat-message` .~~ ** ✅ SHIPPED 2026-07-19** — `ChatMessage.ReadBy` part
(the app composes `AvatarGroup` ; the sender's `onMessageSeen` feeds it).
uix(chat): bloque de mensajería chat-* v1 (log · message · composer · typing)
Familia chat-* completa, componiendo el ecosistema (Feed + VirtualList
anclado, Textarea, FileUpload, Avatar, Popover, Icon). Transporte-agnóstica:
el modelo de datos es de la app, los componentes reciben props + snippets.
- chat-log: Feed + VirtualList(anchor:'end') en triple registro; day
separators, unread divider, filas de sistema (Feed.Article), pill "N
nuevos" + notifyIncoming (announce a live-region), to-latest distance-aware.
- chat-message: raíz Feed.Article; runs ($libs/chat), ticks de entrega,
reply embebido, reacciones (chip pressed + tapback panel), acciones hover.
- chat-composer: Textarea (doble registro) + FileUpload; teclado
Enter/Shift+Enter/Esc/ArrowUp; typing saliente debounced (uix.timers);
barra reply/edit.
- chat-typing: sustain-streaming (visual-only, loop del sistema);
aggregateTypers 1/2/3+ con plantillas plurales.
- $libs/chat: groupIntoRuns (ventana 8 min estilo Discord) + aggregateTypers.
- VirtualList: extensión anchor:'end' (followOutput, prepend por clave
estable, sticky monótono, onAtEndChange) — sin scroller paralelo.
Diseño de referencia (WhatsApp/Telegram, pase lado-a-lado): burbujas suaves
(out = primary-element + texto normal; in = surface card + sombra) sobre
lienzo muted del log, meta dentro de la burbuja, tapback bar horizontal,
iconos lucide del ecosistema (nada de SVG a mano). Contrato de tamaños:
size="md" = 16px canónico.
Fixes de framework (afectaban más allá de chat):
- EngineLangs.t() ahora pela el prefijo '#?': los idlangrefs imperativos
(pagination/file-upload/chronos/chat) caían al fallback EN en locale es.
Fix parseLangRef ?? parsePathFallback + test de regresión (132/132).
- virtual-list commit-set-resize con channels:[]: es bookkeeping de
ResizeObserver, no gesto → dejaba de emitir beep/vibrate pre-interacción
(interventions de navigator.vibrate / AudioContext).
Escaparate messenger en /uix/demos/chat (bot de eco con uix.timers;
variante $connection señalada). Gaps v2 registrados en next-features §7.
Pase: component:audit 4×PASS · eidos-lint invalid=0 · recipe-contract
29/29 · Playwright escaparate PASS end-to-end · consola limpia ·
npm run check en baseline (76, cero errores nuevos).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- **VirtualList chat-mode refinements** — bidirectional sparse ranges for
jump-into-history far from the loaded window.
- ~~**Typing avatars** — faces next to the `chat-typing` dots (Messenger).~~
** ✅ SHIPPED 2026-07-19** — `ChatTyping.Avatars` slot (compose `AvatarGroup` ).
uix(chat): bloque de mensajería chat-* v1 (log · message · composer · typing)
Familia chat-* completa, componiendo el ecosistema (Feed + VirtualList
anclado, Textarea, FileUpload, Avatar, Popover, Icon). Transporte-agnóstica:
el modelo de datos es de la app, los componentes reciben props + snippets.
- chat-log: Feed + VirtualList(anchor:'end') en triple registro; day
separators, unread divider, filas de sistema (Feed.Article), pill "N
nuevos" + notifyIncoming (announce a live-region), to-latest distance-aware.
- chat-message: raíz Feed.Article; runs ($libs/chat), ticks de entrega,
reply embebido, reacciones (chip pressed + tapback panel), acciones hover.
- chat-composer: Textarea (doble registro) + FileUpload; teclado
Enter/Shift+Enter/Esc/ArrowUp; typing saliente debounced (uix.timers);
barra reply/edit.
- chat-typing: sustain-streaming (visual-only, loop del sistema);
aggregateTypers 1/2/3+ con plantillas plurales.
- $libs/chat: groupIntoRuns (ventana 8 min estilo Discord) + aggregateTypers.
- VirtualList: extensión anchor:'end' (followOutput, prepend por clave
estable, sticky monótono, onAtEndChange) — sin scroller paralelo.
Diseño de referencia (WhatsApp/Telegram, pase lado-a-lado): burbujas suaves
(out = primary-element + texto normal; in = surface card + sombra) sobre
lienzo muted del log, meta dentro de la burbuja, tapback bar horizontal,
iconos lucide del ecosistema (nada de SVG a mano). Contrato de tamaños:
size="md" = 16px canónico.
Fixes de framework (afectaban más allá de chat):
- EngineLangs.t() ahora pela el prefijo '#?': los idlangrefs imperativos
(pagination/file-upload/chronos/chat) caían al fallback EN en locale es.
Fix parseLangRef ?? parsePathFallback + test de regresión (132/132).
- virtual-list commit-set-resize con channels:[]: es bookkeeping de
ResizeObserver, no gesto → dejaba de emitir beep/vibrate pre-interacción
(interventions de navigator.vibrate / AudioContext).
Escaparate messenger en /uix/demos/chat (bot de eco con uix.timers;
variante $connection señalada). Gaps v2 registrados en next-features §7.
Pase: component:audit 4×PASS · eidos-lint invalid=0 · recipe-contract
29/29 · Playwright escaparate PASS end-to-end · consola limpia ·
npm run check en baseline (76, cero errores nuevos).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- **Delegate-family ↔ agent chat** — the Aura reservation (§F6 of the
scene/pack initiative) once agent conversations land.
**Deps**: none to start `chat-list` /`emoji-picker`; mentions depend on the
`words` integration decision. The transport stays app-land (`$connection`).
feat(eidos): `Barcode` — el código de barras 1D, con encoder propio y cero dependencias
Ocho simbologías desde su estándar ISO publicado (Code 128 con auto-switching
A/B/C, EAN-13/8, UPC-A/E, Code 39, ITF/ITF-14) en `$libs/barcode`: tablas
transcritas de la especificación o derivadas de su regla de construcción, sin
una sola dependencia npm añadida. JsBarcode y bwip-js son referencia de
corrección, jamás import — la misma doctrina con la que `$libs/qr` usó a Nayuki.
Ningún headless del mercado trae barcode: el terreno lo ocupan encoders
imperativos sin anatomía ni a11y.
- **La física manda sobre la ergonomía visual.** La dimensión X (`moduleWidth`)
es el ancla de escaneabilidad y la altura de barra un eje libre — un escáner
lee una franja horizontal. Por eso la geometría es px absolutos en el viewBox
(precedente: la familia `chart`) y no el token `size` cuadrado del QR:
colapsar los dos ejes en uno haría que la X dependiera de la altura, que es
justo lo que no puede flotar. La zona muda va en módulos, con el mínimo de
cada estándar.
- **Lo que hace reconocible a la familia.** Las guardas EAN/UPC bajan hasta la
línea base y los dígitos se imprimen dígito-bajo-dígito sobre su celda de 7
módulos, en mono (la HRI del estándar es OCR-B, de ancho fijo). ITF-14 lleva
su barra portadora por fuera de la zona muda, que no puede comerse.
- **Un valor no codificable es un ESTADO, no una excepción.** `encode` lanza un
`BarcodeError` tipado; el wrapper lo captura, marca `data-invalid`, avisa por
`onInvalid` (deduplicado por razón: `encoded` es un objeto nuevo por
pulsación) y deja un marco placeholder decorativo. Un EAN a medio teclear es
un paso normal de edición; dejarlo reventar dentro de un `$derived` tumbaría
la página.
- **`'text'` entra en `MorfoElement`** — y también en el schema de sium, que
tiene su propia lista literal. Añadir solo la unión de TypeScript compila y
falla en runtime; lo cazó `morfo:check`, no `npm run check`.
Verificación: 48 tests de encoder en tres capas (invariantes estructurales de
cada tabla · vectores publicados transcritos aparte · decoders independientes
que releen la retícula), la lectura real confirmada con escáner por el usuario,
y la retícula decodificada desde el SVG ya pintado en navegador. `component:audit`
PASS con 0 errores; los 2 warnings (D-1.5 / D-4.3) son los mismos que da
`button`, la demo canaria: sus regex buscan la forma v1 inline del
MutationObserver y del `emit`, que la v2 movió al harness compartido.
Demo v2 canónica de 9 pestañas. El alcance diferido (Codabar/MSI/Pharmacode,
GS1-128, add-ons EAN-2/5, y las 2D como componentes aparte) queda registrado en
`next-features.md` §10 — el hueco de numeración se cierra cuando el track de
blocks commitee sus §8/§9, hoy sin commitear.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## 8. `blocks` tier — reusable page-function compositions — 2026-07-21
**Origin**: ecosystem analysis session 2026-07-21; user decision: a new tier
for **blocks** — named compositions of canon components performing a page
function (sticky site header, hero, footer, app shell, docs shell…). Scope:
tier infrastructure (`src/uix/blocks/` + `$blocks` , doctrine at
`architecture/blocks.md` , B contract, `blocks-check` guard), **7 canon base
components first** (`sticky` · `empty-state` · `result` · `callout` ·
`prose` · `anchor-nav` · `sidebar` — full 9-phase route each), then the
block catalog (10 site + 10 app + 3 docs). Admission rule mirrors packs:
any behavior with contract surface is promoted to canon BEFORE the block
composes it. Detail + per-item fiches: the execution plan at
[`process/PLAN-blocks.md` ](./process/PLAN-blocks.md ).
**Status**: D-BLK decisions SIGNED 2026-07-21 (D-BLK.1 amended by the user:
the tier lives at `src/uix/blocks/` , not `src/blocks/` ). **F0 SHIPPED
2026-07-21** (`069759604`: doctrine + `$blocks` + `blocks:check` + gallery).
**Reference study SHIPPED 2026-07-21** (`aff42fee0`:
[`process/RESEARCH-blocks-references.md` ](./process/RESEARCH-blocks-references.md ),
6 research tracks; every phase-0 now checks against it). Amendments E-1…E-5
SIGNED 2026-07-21: F1 = **8** canon components (+`nav-tree`, data-driven docs
navigation tree); parity-floor = v1 scope policy; F2 = **14** site blocks
(+banner · team · contact · content-section); distribution registered as §9;
⌘K stays app-land.
**F3 STARTED 2026-08-19** — `app-shell` shipped with a signed phase 0 (four
decisions: the block wires no services, two scroll models by prop, the top bar
inside the inset, generated bypass links) and a new canon component in front of
it, `SkipLink` (WCAG 2.4.1). It closes ledger row **A-95** , the one thing the
tier could not fix from inside any block: with the shell owning the page's
heights, an in-flow notice and a pinned header overlap by **0px** (measured;
before: 49px, with the hit test landing on the strip).
**Deps**: `scheduler` block gated on `chronos` landing; chat-room block
explicitly NOT here (canon `chat-*` family, §7 above).
## 9. Blocks distribution — registry + llms.txt + MCP — 2026-07-21
**Origin**: the blocks reference study (E-4 signed 2026-07-21; dossier
[`process/RESEARCH-blocks-references.md` ](./process/RESEARCH-blocks-references.md )
§P6). The ecosystem's 2026 table stakes for component/block distribution are
agent-enumerable catalogs: a registry protocol (shadcn `registry.json` /
`registry-item.json` ), `llms.txt` (+ `llms-full.txt` / `.md` content
negotiation) over the docs corpus, and an MCP server so agents can list and
install blocks (sv-blocks precedent). Scope when started: (a) `llms.txt`
generation from the docs corpus (cheap — the corpus is already Markdown,
pairs with F4 docs blocks); (b) a blocks/components registry manifest; (c)
an MCP server exposing catalog + morfo contracts (pairs with the
contract-introspection superation angle and the palabras F-B agentive
track). Registered, not started.
**Deps**: F2 catalog existing (something to distribute); F4.3 props-table
work shares the morfo-introspection substrate.
feat(eidos): `Barcode` — el código de barras 1D, con encoder propio y cero dependencias
Ocho simbologías desde su estándar ISO publicado (Code 128 con auto-switching
A/B/C, EAN-13/8, UPC-A/E, Code 39, ITF/ITF-14) en `$libs/barcode`: tablas
transcritas de la especificación o derivadas de su regla de construcción, sin
una sola dependencia npm añadida. JsBarcode y bwip-js son referencia de
corrección, jamás import — la misma doctrina con la que `$libs/qr` usó a Nayuki.
Ningún headless del mercado trae barcode: el terreno lo ocupan encoders
imperativos sin anatomía ni a11y.
- **La física manda sobre la ergonomía visual.** La dimensión X (`moduleWidth`)
es el ancla de escaneabilidad y la altura de barra un eje libre — un escáner
lee una franja horizontal. Por eso la geometría es px absolutos en el viewBox
(precedente: la familia `chart`) y no el token `size` cuadrado del QR:
colapsar los dos ejes en uno haría que la X dependiera de la altura, que es
justo lo que no puede flotar. La zona muda va en módulos, con el mínimo de
cada estándar.
- **Lo que hace reconocible a la familia.** Las guardas EAN/UPC bajan hasta la
línea base y los dígitos se imprimen dígito-bajo-dígito sobre su celda de 7
módulos, en mono (la HRI del estándar es OCR-B, de ancho fijo). ITF-14 lleva
su barra portadora por fuera de la zona muda, que no puede comerse.
- **Un valor no codificable es un ESTADO, no una excepción.** `encode` lanza un
`BarcodeError` tipado; el wrapper lo captura, marca `data-invalid`, avisa por
`onInvalid` (deduplicado por razón: `encoded` es un objeto nuevo por
pulsación) y deja un marco placeholder decorativo. Un EAN a medio teclear es
un paso normal de edición; dejarlo reventar dentro de un `$derived` tumbaría
la página.
- **`'text'` entra en `MorfoElement`** — y también en el schema de sium, que
tiene su propia lista literal. Añadir solo la unión de TypeScript compila y
falla en runtime; lo cazó `morfo:check`, no `npm run check`.
Verificación: 48 tests de encoder en tres capas (invariantes estructurales de
cada tabla · vectores publicados transcritos aparte · decoders independientes
que releen la retícula), la lectura real confirmada con escáner por el usuario,
y la retícula decodificada desde el SVG ya pintado en navegador. `component:audit`
PASS con 0 errores; los 2 warnings (D-1.5 / D-4.3) son los mismos que da
`button`, la demo canaria: sus regex buscan la forma v1 inline del
MutationObserver y del `emit`, que la v2 movió al harness compartido.
Demo v2 canónica de 9 pestañas. El alcance diferido (Codabar/MSI/Pharmacode,
GS1-128, add-ons EAN-2/5, y las 2D como componentes aparte) queda registrado en
`next-features.md` §10 — el hueco de numeración se cierra cuando el track de
blocks commitee sus §8/§9, hoy sin commitear.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## 10. Barcode — deferred symbologies + GS1 profiles — 2026-07-29
**Origin**: the `Barcode` component (shipped 2026-07-29; design record +
dispositions in
[`eidos/components/barcode/README.md` ](../src/uix/eidos/components/barcode/README.md )
§Gaps). v1 covers the Retail + Logistics set — Code 128 (A/B/C auto), EAN-13/8,
UPC-A/E, Code 39, ITF/ITF-14 — from their published ISO standards, in the
zero-dependency `$libs/barcode` encoder.
**Shipped the same day, out of this registry: the ISBN input profile.** An ISBN
barcode IS an EAN-13 (ISO 2108, Bookland 978/979), so it entered as a profile
over `ean13` — separators tolerated in the numeric symbologies, ISBN-10 read and
converted after validating its own base-11 check digit — and explicitly NOT as a
ninth `symbology` value, which would make `data-symbology` claim a standard that
does not exist. The doctrine is recorded in the component README §ISBN.
Four dispositions stay **diferir** , each a self-contained follow-up over the
same encoder:
feat(eidos): `Barcode` — el código de barras 1D, con encoder propio y cero dependencias
Ocho simbologías desde su estándar ISO publicado (Code 128 con auto-switching
A/B/C, EAN-13/8, UPC-A/E, Code 39, ITF/ITF-14) en `$libs/barcode`: tablas
transcritas de la especificación o derivadas de su regla de construcción, sin
una sola dependencia npm añadida. JsBarcode y bwip-js son referencia de
corrección, jamás import — la misma doctrina con la que `$libs/qr` usó a Nayuki.
Ningún headless del mercado trae barcode: el terreno lo ocupan encoders
imperativos sin anatomía ni a11y.
- **La física manda sobre la ergonomía visual.** La dimensión X (`moduleWidth`)
es el ancla de escaneabilidad y la altura de barra un eje libre — un escáner
lee una franja horizontal. Por eso la geometría es px absolutos en el viewBox
(precedente: la familia `chart`) y no el token `size` cuadrado del QR:
colapsar los dos ejes en uno haría que la X dependiera de la altura, que es
justo lo que no puede flotar. La zona muda va en módulos, con el mínimo de
cada estándar.
- **Lo que hace reconocible a la familia.** Las guardas EAN/UPC bajan hasta la
línea base y los dígitos se imprimen dígito-bajo-dígito sobre su celda de 7
módulos, en mono (la HRI del estándar es OCR-B, de ancho fijo). ITF-14 lleva
su barra portadora por fuera de la zona muda, que no puede comerse.
- **Un valor no codificable es un ESTADO, no una excepción.** `encode` lanza un
`BarcodeError` tipado; el wrapper lo captura, marca `data-invalid`, avisa por
`onInvalid` (deduplicado por razón: `encoded` es un objeto nuevo por
pulsación) y deja un marco placeholder decorativo. Un EAN a medio teclear es
un paso normal de edición; dejarlo reventar dentro de un `$derived` tumbaría
la página.
- **`'text'` entra en `MorfoElement`** — y también en el schema de sium, que
tiene su propia lista literal. Añadir solo la unión de TypeScript compila y
falla en runtime; lo cazó `morfo:check`, no `npm run check`.
Verificación: 48 tests de encoder en tres capas (invariantes estructurales de
cada tabla · vectores publicados transcritos aparte · decoders independientes
que releen la retícula), la lectura real confirmada con escáner por el usuario,
y la retícula decodificada desde el SVG ya pintado en navegador. `component:audit`
PASS con 0 errores; los 2 warnings (D-1.5 / D-4.3) son los mismos que da
`button`, la demo canaria: sus regex buscan la forma v1 inline del
MutationObserver y del `emit`, que la v2 movió al harness compartido.
Demo v2 canónica de 9 pestañas. El alcance diferido (Codabar/MSI/Pharmacode,
GS1-128, add-ons EAN-2/5, y las 2D como componentes aparte) queda registrado en
`next-features.md` §10 — el hueco de numeración se cierra cuando el track de
blocks commitee sus §8/§9, hoy sin commitear.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Codabar · MSI · Pharmacode** — the niche tail JsBarcode still ships. Pure
table work: each is a symbology module + its vectors.
- **GS1-128 (FNC1 + Application Identifier parsing)** — a _profile_ of Code 128,
feat(eidos): `Barcode` — el código de barras 1D, con encoder propio y cero dependencias
Ocho simbologías desde su estándar ISO publicado (Code 128 con auto-switching
A/B/C, EAN-13/8, UPC-A/E, Code 39, ITF/ITF-14) en `$libs/barcode`: tablas
transcritas de la especificación o derivadas de su regla de construcción, sin
una sola dependencia npm añadida. JsBarcode y bwip-js son referencia de
corrección, jamás import — la misma doctrina con la que `$libs/qr` usó a Nayuki.
Ningún headless del mercado trae barcode: el terreno lo ocupan encoders
imperativos sin anatomía ni a11y.
- **La física manda sobre la ergonomía visual.** La dimensión X (`moduleWidth`)
es el ancla de escaneabilidad y la altura de barra un eje libre — un escáner
lee una franja horizontal. Por eso la geometría es px absolutos en el viewBox
(precedente: la familia `chart`) y no el token `size` cuadrado del QR:
colapsar los dos ejes en uno haría que la X dependiera de la altura, que es
justo lo que no puede flotar. La zona muda va en módulos, con el mínimo de
cada estándar.
- **Lo que hace reconocible a la familia.** Las guardas EAN/UPC bajan hasta la
línea base y los dígitos se imprimen dígito-bajo-dígito sobre su celda de 7
módulos, en mono (la HRI del estándar es OCR-B, de ancho fijo). ITF-14 lleva
su barra portadora por fuera de la zona muda, que no puede comerse.
- **Un valor no codificable es un ESTADO, no una excepción.** `encode` lanza un
`BarcodeError` tipado; el wrapper lo captura, marca `data-invalid`, avisa por
`onInvalid` (deduplicado por razón: `encoded` es un objeto nuevo por
pulsación) y deja un marco placeholder decorativo. Un EAN a medio teclear es
un paso normal de edición; dejarlo reventar dentro de un `$derived` tumbaría
la página.
- **`'text'` entra en `MorfoElement`** — y también en el schema de sium, que
tiene su propia lista literal. Añadir solo la unión de TypeScript compila y
falla en runtime; lo cazó `morfo:check`, no `npm run check`.
Verificación: 48 tests de encoder en tres capas (invariantes estructurales de
cada tabla · vectores publicados transcritos aparte · decoders independientes
que releen la retícula), la lectura real confirmada con escáner por el usuario,
y la retícula decodificada desde el SVG ya pintado en navegador. `component:audit`
PASS con 0 errores; los 2 warnings (D-1.5 / D-4.3) son los mismos que da
`button`, la demo canaria: sus regex buscan la forma v1 inline del
MutationObserver y del `emit`, que la v2 movió al harness compartido.
Demo v2 canónica de 9 pestañas. El alcance diferido (Codabar/MSI/Pharmacode,
GS1-128, add-ons EAN-2/5, y las 2D como componentes aparte) queda registrado en
`next-features.md` §10 — el hueco de numeración se cierra cuando el track de
blocks commitee sus §8/§9, hoy sin commitear.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
not a new encoder: the FNC1 code value plus an AI grammar so the HRI can print
the bracketed form. The natural pair for a logistics-facing app, and the same
shape the ISBN profile already established.
- **EAN-2 / EAN-5 add-ons + the upper `ISBN 978-…` line** — the periodical /
book supplements and the book-cover convention; they extend the EAN geometry
(a second text band, a composed add-on symbol) rather than replacing it. Hard
constraint recorded with them: **we do not hyphenate** . The hyphen positions
are not computable from the number — they need the International ISBN
Agency's prefix-range tables, versioned data with an expiry date, which is a
maintenance clock a zero-dependency library must not swallow. The hyphenated
string comes from the consumer's catalogue.
feat(eidos): `Barcode` — el código de barras 1D, con encoder propio y cero dependencias
Ocho simbologías desde su estándar ISO publicado (Code 128 con auto-switching
A/B/C, EAN-13/8, UPC-A/E, Code 39, ITF/ITF-14) en `$libs/barcode`: tablas
transcritas de la especificación o derivadas de su regla de construcción, sin
una sola dependencia npm añadida. JsBarcode y bwip-js son referencia de
corrección, jamás import — la misma doctrina con la que `$libs/qr` usó a Nayuki.
Ningún headless del mercado trae barcode: el terreno lo ocupan encoders
imperativos sin anatomía ni a11y.
- **La física manda sobre la ergonomía visual.** La dimensión X (`moduleWidth`)
es el ancla de escaneabilidad y la altura de barra un eje libre — un escáner
lee una franja horizontal. Por eso la geometría es px absolutos en el viewBox
(precedente: la familia `chart`) y no el token `size` cuadrado del QR:
colapsar los dos ejes en uno haría que la X dependiera de la altura, que es
justo lo que no puede flotar. La zona muda va en módulos, con el mínimo de
cada estándar.
- **Lo que hace reconocible a la familia.** Las guardas EAN/UPC bajan hasta la
línea base y los dígitos se imprimen dígito-bajo-dígito sobre su celda de 7
módulos, en mono (la HRI del estándar es OCR-B, de ancho fijo). ITF-14 lleva
su barra portadora por fuera de la zona muda, que no puede comerse.
- **Un valor no codificable es un ESTADO, no una excepción.** `encode` lanza un
`BarcodeError` tipado; el wrapper lo captura, marca `data-invalid`, avisa por
`onInvalid` (deduplicado por razón: `encoded` es un objeto nuevo por
pulsación) y deja un marco placeholder decorativo. Un EAN a medio teclear es
un paso normal de edición; dejarlo reventar dentro de un `$derived` tumbaría
la página.
- **`'text'` entra en `MorfoElement`** — y también en el schema de sium, que
tiene su propia lista literal. Añadir solo la unión de TypeScript compila y
falla en runtime; lo cazó `morfo:check`, no `npm run check`.
Verificación: 48 tests de encoder en tres capas (invariantes estructurales de
cada tabla · vectores publicados transcritos aparte · decoders independientes
que releen la retícula), la lectura real confirmada con escáner por el usuario,
y la retícula decodificada desde el SVG ya pintado en navegador. `component:audit`
PASS con 0 errores; los 2 warnings (D-1.5 / D-4.3) son los mismos que da
`button`, la demo canaria: sus regex buscan la forma v1 inline del
MutationObserver y del `emit`, que la v2 movió al harness compartido.
Demo v2 canónica de 9 pestañas. El alcance diferido (Codabar/MSI/Pharmacode,
GS1-128, add-ons EAN-2/5, y las 2D como componentes aparte) queda registrado en
`next-features.md` §10 — el hueco de numeración se cierra cuando el track de
blocks commitee sus §8/§9, hoy sin commitear.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **2D — DataMatrix · PDF417 · Aztec** — explicitly NOT this component: each is a
whole different encoder (own Reed– Solomon, matrix layout). They belong beside
`QrCode` as their own components, sharing the display-primitive shape.
**Deps**: none — the encoder's `BarcodeResult` (module lattice + guards + HRI
groups in module space) already generalises; the 2D entries would instead reuse
the `$libs/qr` shape.
uix(background): el fondo se mueve con el scroll, y con lo que el lector no pidió no
F3 (parallax) + F4 (demo y registro documental) + las correcciones de sus dos
auditorías, en un commit porque viven en los mismos ficheros: la demo enseña los
ejes que F3 añade, y separarlas dejaría un estado que nunca se probó.
Cinco maneras de que una capa deje de estarse quieta: `speed` (cuánto del token
de travel cubre mientras el anfitrión cruza el viewport), `bleed` (crece más
allá del anfitrión para que el viaje no arrastre su propio borde), `depth` (la
deriva contra el puntero), `spotlight` y `attach='fixed'`.
El travel es CSS: `animation-timeline: view()` lo gobierna desde la posición de
scroll, sin listener ni rAF. Sólo donde el motor no lo trae, la pila arranca
`ScrollProgress` y escribe `--background-progress`, que la rama `@supports not`
mete en la MISMA declaración; los dos caminos no pueden estar vivos a la vez
porque el JS comprueba la condición idéntica con `CSS.supports`, y ambos se
paran bajo `prefers-reduced-motion` — el parallax es movimiento atado al scroll
del propio lector, que es justo la clase que provoca síntomas vestibulares.
**La decisión que no estaba prevista.** El scroll y el puntero quieren mover la
MISMA capa, y una animación sobre `translate` gana a cualquier declaración
estática: el puntero habría dejado de existir sin más. Así que el scroll anima
una custom property REGISTRADA (`@property`, o interpolaría a saltos) y un único
`translate` compone los dos términos. Medido: parallax solo → `0px 30px`; con el
puntero arriba-derecha y `depth: 20px` → `20px 10px`. Es `translate` y nunca el
shorthand `transform`, la misma ley que sigue el lift del draggable con `scale`.
El precio, dicho porque en la primera redacción escribí lo contrario tres veces:
una custom property NO se puede compositar, así que el navegador recalcula
estilo cada frame. Para una decoración es el intercambio correcto —una property
por capa que viaja, ninguna bajo reduced motion— pero «va en el compositor» era
falso y ahora el código dice lo que ocurre.
**Dos footguns cerrados por forma, no por disciplina:**
- `attach='fixed'` se DECLARA desde la capa y la pila se recorta sola. Antes
había que escribirlo también en la pila, y olvidarlo dejaba la capa
`position: fixed` pintando a sangre por todo el viewport, detrás de todo y sin
error (un hijo fijo se escapa de `overflow: clip`; sólo un `clip-path` lo trae
de vuelta). La prop de la pila desaparece: no hay nada que olvidar.
- Un `speed` negativo —una capa que se mueve contra el scroll— invertía el
bleed: la capa ENCOGÍA y enseñaba justo los bordes que el bleed tapa. Ahora
usa la magnitud.
**Lo que costó medición**: el shorthand `animation` pone `duration: 0s` y una
línea de tiempo de progreso necesita el `auto` inicial, así que con el shorthand
la capa no se movía nunca (van longhands, con el porqué escrito) · mi listener
de puntero pedía un frame y no lo liberaba si el rect salía degenerado, matando
el puntero para el resto de la sesión (reescrito sin frame, con el rect cacheado
e invalidado por `pointerenter` y `observeResize`) · las cuatro registraciones
—`animated`, `pointer`, `scroll`, `fixed`— comparten un solo sitio,
`declare.svelte.ts`, donde vive la regla A30 y su segunda mitad: registrar desde
el init, y seguir el prop sin escribir en la primera pasada.
**La demo** (`/uix/components/background`, v2, nueve pestañas) monta un
ANFITRIÓN de verdad en el escenario, porque este componente es invisible por sí
solo y sin padre no se puede enseñar lo único que importa: que el padre se
adopta y el layout no se mueve. Los chips son uniones completas verificadas por
el TIPO (`Record<Union, 0>`): un miembro que falte es error de compilación.
Y fue la demo la que destapó que, con A30, encender `animate` en caliente no
hacía aparecer el control de pausa — el registro era un hecho de montaje.
Invisible en una sonda, obvio con un interruptor.
Registro documental (D-BG.11): `next-features.md` §11 · la frase en
`design-text-effects.md` (el mismo corte canon/pack leído desde el otro lado) ·
`PLAN-blocks-quality.md` Q0.3 → sucesor · `surface/README.md` §Gaps «scrim de
autoría» CERRADO por `Background.Scrim` · glosario con entrada `Background` y
`Aura` corregida (decía «Not built yet» y está construido) · y en
`motion-guide.md` §8 + el RFC: el travel ligado al scroll no es un preset —un
preset nombra una transición discreta CON duración, y esto es modulación
continua sin ninguna— y sólo se replantea como dominio con un segundo consumidor.
Verificado en Chrome real: el puntero mueve `depth` y `spotlight` con los
valores exactos y vuelven al centro al salir · `attach='fixed'` estampa y
retira el recorte de la pila · el bleed aguanta el speed negativo · RTL: el
`translate` del puntero se mantiene FÍSICO y el bleed en el eje de bloque · cada
control de la demo cambia algo (los de `spotlight` y `depth` no llegaban a tres
de las cuatro clases de capa hasta la segunda auditoría).
⚠️ SIN VERIFICAR, y no lo doy por bueno: el travel real al hacer scroll, los 60
fps y el detector de reflow. El panel del navegador va oculto con viewport 0×0 y
ahí las animaciones scroll-driven declaradas en CSS no se activan — comprobado
que es del ENTORNO con un caso mínimo inyectado (un `div` pelado con
`animation-timeline: view()` sale inactivo mientras una `ViewTimeline` creada
por API sobre el mismo sujeto marca 68%). Necesita una pasada con Chrome
visible.
Gates: audit `--only background` PASS 0 errores · eidos-lint invalid 0 ·
`rtl:check` 0/180 · `docs:check` 0/0 en 634 docs · `blocks:check` 0/18 ·
`morfo:check` PASS · smoke PASS · 441/442 (el fallo es el `skin-media-player` de
siempre) · `check` 0 errores propios.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## 11. Background — parallax, the scrim scale, and the pack seam — 2026-08-18
**Origin**: the `Background` component (design record, measurements and
dispositions in
[`eidos/components/background/README.md` ](../src/uix/eidos/components/background/README.md );
plan and signed decisions in
[`process/PLAN-background.md` ](./process/PLAN-background.md )). It ships as the
HOST of a surface's background layers — a child that pins itself behind its
parent's content, with the parent adopted by a foundation rule rather than
wrapped. Patterns, gradients, scrims, images and video are in; so is the WCAG
2.2.2 pause control.
**Scope of this registry entry** — what the component deliberately does NOT own
yet, each a self-contained follow-up:
- **Parallax and the scroll seam** — `animation-timeline: view()` with a
`ScrollProgress` fallback, `attach='fixed'` , the pointer `depth` axis. The
infrastructure landed with the component (`$adom`'s `ScrollProgress` and
`prefersReducedData` ); the layers that consume it did not. It is the one
follow-up with a mechanism already chosen and measured against browser
support, so it is also the one most likely to move first.
- **The scrim `strength` scale is not ordered by weight** — measured: `subtle`
veils MORE than `overlay` , and `overlay` and `muted` resolve to the same
number. The names come from the semantic opacity scale, which says how opaque
an ELEMENT is; borrowing them for a VEIL imports an ordering that means
something else. Renaming or renumbering is an API change, not a fix in
passing.
- **A bright photograph is illegible at every scrim weight** — white copy over
the heaviest veil measures below AA. Either the scale gains a step above
`--opacity-subtle` , or the doctrine becomes "the answer is a graded scrim",
stated once and demonstrated.
- **A shared `<video>` state port** — the framework has a load-state contract
for `<img>` (`ImageProvider` in `soma/layers` ) and none for `<video>` :
`ScrollFrames` listens by hand and so does `Background.Video` . Two consumers
is the threshold this codebase uses to extract a layer.
- **The `Ambient` pack honouring the pause** — the canon publishes the context
(`paused`, `reduced` , `seen` ); the pack reads it. A task OF the pack, because
the dependency runs pack → framework and never back.
**Deps**: the parallax entry needs no new service — the two `$adom` ports it
consumes already shipped. The scrim entries are the author's call on the token
vocabulary, so they block on a decision, not on code.
docs(next-features): el registro de incidencias del eje theme-reach
Encargo del autor: anotar todas las incidencias de esta clase para resolverlas
después. Van al registro canónico (`next-features.md`), no a un sitio nuevo, y
con su MEDICIÓN — no como impresión.
**§12 — El contrato de cascada del velo de estado.** Seis incidencias que
resultaron ser la misma: nadie fijó nunca cómo compone el velo de
`archetypes.css` con las reglas de una receta. Y la raíz explica por qué se
manifiestan de formas tan distintas: el velo se pinta con DOS pesos según el
arquetipo — `:where()` (0,0,0) para `trigger`, especificidad plena (0,5,0) para
`item`/`option`.
131 declaraciones-atajo en 45 componentes cancelan el velo (66 BASE ·
32 ACENTO · 23 HOVER · 10 OTRA; cero valores con gradiente, así que el paso a
longhand no tiene riesgo técnico; 32 nodos velados que hoy no reaccionan al
ratón pasarían a hacerlo) · la prop `hoverable` de table no suprime nada · en
tree-grid el empate (0,5,0) lo decide el ORDEN DE CARGA y varía entre
recargas · la banda de tree-grid mata el hover en las filas pares · un token
de tinta no puede ganar al arquetipo (`selected-row-fg` se retiró por eso) ·
el thumb de scroll-area no tiene velo al que migrar.
Esto explica de paso la adopción 21/135 que midió la auditoría del 2026-07-01:
el velo estaba escrito y estructuralmente derrotado.
**§13 — Huecos de instrumento y de demo.** Lo que hizo que una medición
mintiera o no existiera, que importa porque el eje entero se apoya en ellas:
falsos negativos del centinela sobre propiedades transicionadas, pseudo-elementos
y componentes compuestos que no ve, la sonda que no pasa el ratón por un `<tr>`,
y la lección general —**una sonda sobre una parte que la demo no monta compara
CERO valores y pasa**—, con la lista de partes condicionales afectadas.
El handoff gana la regla para las sesiones del eje: si una incidencia mueve
píxel o toca morfo, se mide, se anota ahí y se sigue; no se arregla dentro del
commit del componente.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## 12. El contrato de cascada del velo de estado — 2026-08-20
**Origen**: el eje `theme-reach` (F2-A, sesión del 2026-08-20). Al tokenizar
componente a componente aparecieron, medidas una a una, seis incidencias que
son **la misma** : nadie fijó nunca cómo compone el velo de `archetypes.css` con
las reglas de una receta. No se tocan dentro del eje de theming — mueven píxel
y son decisión aparte — pero cada una está medida y localizada.
**La raíz**: `archetypes.css` pinta el velo neutro como
`background-image: linear-gradient(var(--state-hover), var(--state-hover))` , y
lo hace con DOS pesos distintos según el arquetipo — `:where(...)` (0,0,0) para
`trigger` , y especificidad PLENA (0,5,0) para `item` / `option` , «INTENTIONAL
cross-component accent», dice su comentario. Una receta que declare fondo sobre
el mismo nodo entra en una relación que nunca se escribió.
### Las seis, con su medición
1. **El atajo `background:` cancela el velo — 131 declaraciones en 45
componentes.** El atajo fija `background-image: none` ; sobre un arquetipo
velado por `:where()` eso lo mata para siempre, no sólo en hover. Medido con
`scripts/__statelayer-analysis.ts` , que comprueba contra el morfo compilado
que el atajo cae sobre el nodo VELADO y no sobre cualquiera. Por clase de
regla: **66 BASE** (mecánicas: pasar a `background-color` no mueve el
reposo), **32 ACENTO** (selected/checked/open: §38 pide `background-color`
para que el velo COMPONGA encima), **23 HOVER** (la migración de la firma 3)
y **10 OTRA** (disabled, dragging, variantes). **Cero valores con
gradiente**, así que el único riesgo técnico del paso a longhand no existe
en este catálogo. El píxel que se mueve: **32 nodos velados que hoy no
reaccionan al ratón** pasarían a hacerlo. Esto explica la adopción 21/135
que midió la auditoría del 2026-07-01: el velo estaba escrito y
estructuralmente derrotado.
2. **La prop `hoverable` de `table` no suprime nada.** El velo de `item` es
incondicional y a (0,5,0), así que una fila se ilumina al pasar el ratón
aunque `hoverable` esté apagada — verificado con hover real, y ocurre desde
antes de tocar la receta. O el arquetipo decide y la prop sobra, o la fila
uix(theming): revisión adversarial de F2-A — 8 hallazgos de 34, y nace R-5.4
Pase escéptico del bloque (§7.7, molde Sidebar) con medición propia: worktree
con SOLO el CSS revertido a la base del bloque (DOM de demo constante — prueba
más dura que la del autor), sonda x2 por componente para fijar el suelo de
ruido. 34 hipótesis · 26 refutadas · 8 reales.
Lo que NO se pudo romper: el «default idéntico» aguanta en los ocho (0 diffs;
command 8.091 · table 13.050 · media-player 3.277 · gradient-picker 609 ·
listbox 2.813 · carousel 2.842; los 3 de tree-grid y los 5 de feed salen
idénticos corriendo DOS veces el mismo código → instrumento); 0 tokens
huérfanos de 336; el bundle ES 1:1 con los primitivos; el «verificado a mano»
de gradient-picker era verdad (15/16 alcanzan al montar); sin doble animación;
eidos-lint 0 invalid.
Los 4 de código, arreglados con diff de computed VACÍO (re-medido: command
8.091 y carousel 2.842 valores, 0 diffs):
1. command: el re-point de `--command-radius` dentro del Dialog era una
declaración muerta con comentario falso — la misma regla pone el radio a 0
y el input lee `--command-input-radius`, nunca este token (medido:
`--dialog-content-radius: 9999px` no movía NADA; `--command-radius: 77px`
movía la paleta inline y no la del dialog). Retirado; comentarios y README
reescritos con lo medido.
2. carousel: la regla vertical del indicador activo llevaba el `2.25` a pelo
mientras la horizontal leía `active-indicator-scale` — el token alcanzaba
media superficie (scale:6 → horizontal 18→48px, vertical 18→18px). Ahora
lo leen las dos.
3. carousel: el trío `item-gap*` no podía ganar NUNCA — soma estampa `gap`
INLINE desde la prop (lo necesita para el flex-basis; reglas casadas por
CDP: la de la receta y encima INLINE gap:0rem). Retirados los 3 tokens y
las 2 declaraciones muertas (la base y el gap:0 de ghost): 0 diffs, que es
la prueba. Canal de valor de soma, como --gp-current-gradient. Censo
carousel 35 knobs · 23 públicos · 77 % · 27 claves; global 43 % intacto
(5.190 · 2.102).
4. table: `striped-row-bg` no alcanza NADA en la demo publicada —
`:nth-of-type(even)` cuenta los <tr> de RowDetail y las filas de datos
quedan impares (medido tr a tr: diez inmunes). Arreglarlo mueve píxel →
PENDIENTE DE FIRMA (candidato `:nth-child(even of [data-table-row])`);
adjudicado en el ledger mientras tanto.
El instrumento cargaba 22 falsos negativos de 26 «no effect», con tres causas
medidas, y la cuarta era de la sonda:
- el paso de abrir hacía CLIC en el input, y un clic de ratón sobre un input
de texto SÍ casa `:focus-visible` — la regla de foco pintaba el borde y el
token de reposo leía muerto (era el «falso negativo sin causa» de §13);
- el override se escribía solo en `[data-{c}]`, y las partes que cuelgan de
`{c}-root` no lo recibían;
- `::before`/`::after` eran invisibles (el aro de buffering, el spinner);
- la no-determinación de feed no era una animación sin localizar: era la demo
aún CARGANDO — el auto load-more mantiene [data-busy] ~3 s y la firma
`commit-settle` anima box-shadow sobre el nodo medido. Con la espera de
asentado: 0 diffs y 0 nodos ausentes sobre 6.264 valores, dos corridas.
Con eso NACE EL GUARD R-5.4 (la deuda que §13 pedía desde gradient-picker, y
que carousel convirtió en patrón — un estilo inline de otra capa es invisible
para todo análisis estático): `npm run theming:sentinel -- <c> <url>`
(scripts/theming-sentinel.ts, promovido de `__`; blur tras abrir, override en
todos los nodos del componente, pseudos, pase de hover, settle) + ledger
scripts/theming-sentinel-exceptions.ts con UNA razón medida por token — muerto
sin adjudicar = exit 1, excepción STALE = aviso. Los ocho componentes en
verde: command 50/57 · table 41/45 · media-player 46/61 · tree-grid 45/47 ·
gradient-picker 9/25 · listbox 20/21 · carousel 26/27 · feed 43/50, con 46
excepciones adjudicadas. El guard ya me corrigió a mí: cinco entradas mías
salieron STALE (los control-height-* de media-player SÍ alcanzan) y las borró;
y dos muertos nuevos los produjeron los propios arreglos (el blur apaga
focus-input-border; el settle desmonta el spinner de feed) — adjudicados con
su porqué. Doctrina en recipe-contract §4.
También: tres cifras de especificidad de next-features §12 recontadas (la
banda de tree-grid es (0,8,0); la tinta seleccionada del arquetipo es (0,4,0);
y el `item` de table vive en la fila Y EN LA CELDA — elementsFromPoint pone el
velo en la celda), el comentario de listbox que mandaba pisar el privado
`--_listbox-max-height` apunta ahora al público, y quedan registrados en §13
los dos `header-z` con literal '2' y el spinner de media-player a 720ms
mientras el gemelo de feed lleva tokens.
Guards: censo global 43 % intacto · audit --only command+carousel PASS ·
eidos-lint 0 invalid (command 16/12, carousel 29/9, listbox 12/7) · vitest
eidos 434/435 (el rojo es el conocido skin-media-player) · rtl 0/180 · docs
0/813 · check 0 errores en tocados (72 globales de otras sesiones) · prettier
limpio en lo nuevo (los desvíos de command.css/carousel.css ya estaban en
HEAD y no se tocan).
Pendiente de firma (CONTINUE §«PENDIENTE DE FIRMA»): el striped inerte, el
arquetipo en fila+celda, y que soma lea el gap de un token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
no es un `item` para el sistema. Decisión de MORFO. **Precisión medida
2026-08-21** (revisión adversarial, `elementsFromPoint` ): en `table` el
arquetipo `item` está declarado en la fila Y EN LA CELDA
(`morfo/components/table.ts:132` y `:155` ), y el nodo que recibe el velo
bajo el puntero es la CELDA — la decisión de morfo debe contemplar los dos
portadores, no sólo la fila.
docs(next-features): el registro de incidencias del eje theme-reach
Encargo del autor: anotar todas las incidencias de esta clase para resolverlas
después. Van al registro canónico (`next-features.md`), no a un sitio nuevo, y
con su MEDICIÓN — no como impresión.
**§12 — El contrato de cascada del velo de estado.** Seis incidencias que
resultaron ser la misma: nadie fijó nunca cómo compone el velo de
`archetypes.css` con las reglas de una receta. Y la raíz explica por qué se
manifiestan de formas tan distintas: el velo se pinta con DOS pesos según el
arquetipo — `:where()` (0,0,0) para `trigger`, especificidad plena (0,5,0) para
`item`/`option`.
131 declaraciones-atajo en 45 componentes cancelan el velo (66 BASE ·
32 ACENTO · 23 HOVER · 10 OTRA; cero valores con gradiente, así que el paso a
longhand no tiene riesgo técnico; 32 nodos velados que hoy no reaccionan al
ratón pasarían a hacerlo) · la prop `hoverable` de table no suprime nada · en
tree-grid el empate (0,5,0) lo decide el ORDEN DE CARGA y varía entre
recargas · la banda de tree-grid mata el hover en las filas pares · un token
de tinta no puede ganar al arquetipo (`selected-row-fg` se retiró por eso) ·
el thumb de scroll-area no tiene velo al que migrar.
Esto explica de paso la adopción 21/135 que midió la auditoría del 2026-07-01:
el velo estaba escrito y estructuralmente derrotado.
**§13 — Huecos de instrumento y de demo.** Lo que hizo que una medición
mintiera o no existiera, que importa porque el eje entero se apoya en ellas:
falsos negativos del centinela sobre propiedades transicionadas, pseudo-elementos
y componentes compuestos que no ve, la sonda que no pasa el ratón por un `<tr>`,
y la lección general —**una sonda sobre una parte que la demo no monta compara
CERO valores y pasa**—, con la lista de partes condicionales afectadas.
El handoff gana la regla para las sesiones del eje: si una incidencia mueve
píxel o toca morfo, se mide, se anota ahí y se sigue; no se arregla dentro del
commit del componente.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
3. **En `tree-grid` el empate lo decide el ORDEN DE CARGA.** El velo de `item`
y el hover de la receta pesan EXACTAMENTE lo mismo (0,5,0), así que gana la
hoja que cargue después — y en el dev server varía entre recargas: seis
corridas de la misma configuración dieron cuatro «sin velo» y dos «con
velo». Un empate resuelto por orden es la misma fragilidad que el canon ya
documenta para las bandas de `z-index` .
4. **En `tree-grid` la banda mata el hover.** La regla de `striped` pesa
uix(theming): revisión adversarial de F2-A — 8 hallazgos de 34, y nace R-5.4
Pase escéptico del bloque (§7.7, molde Sidebar) con medición propia: worktree
con SOLO el CSS revertido a la base del bloque (DOM de demo constante — prueba
más dura que la del autor), sonda x2 por componente para fijar el suelo de
ruido. 34 hipótesis · 26 refutadas · 8 reales.
Lo que NO se pudo romper: el «default idéntico» aguanta en los ocho (0 diffs;
command 8.091 · table 13.050 · media-player 3.277 · gradient-picker 609 ·
listbox 2.813 · carousel 2.842; los 3 de tree-grid y los 5 de feed salen
idénticos corriendo DOS veces el mismo código → instrumento); 0 tokens
huérfanos de 336; el bundle ES 1:1 con los primitivos; el «verificado a mano»
de gradient-picker era verdad (15/16 alcanzan al montar); sin doble animación;
eidos-lint 0 invalid.
Los 4 de código, arreglados con diff de computed VACÍO (re-medido: command
8.091 y carousel 2.842 valores, 0 diffs):
1. command: el re-point de `--command-radius` dentro del Dialog era una
declaración muerta con comentario falso — la misma regla pone el radio a 0
y el input lee `--command-input-radius`, nunca este token (medido:
`--dialog-content-radius: 9999px` no movía NADA; `--command-radius: 77px`
movía la paleta inline y no la del dialog). Retirado; comentarios y README
reescritos con lo medido.
2. carousel: la regla vertical del indicador activo llevaba el `2.25` a pelo
mientras la horizontal leía `active-indicator-scale` — el token alcanzaba
media superficie (scale:6 → horizontal 18→48px, vertical 18→18px). Ahora
lo leen las dos.
3. carousel: el trío `item-gap*` no podía ganar NUNCA — soma estampa `gap`
INLINE desde la prop (lo necesita para el flex-basis; reglas casadas por
CDP: la de la receta y encima INLINE gap:0rem). Retirados los 3 tokens y
las 2 declaraciones muertas (la base y el gap:0 de ghost): 0 diffs, que es
la prueba. Canal de valor de soma, como --gp-current-gradient. Censo
carousel 35 knobs · 23 públicos · 77 % · 27 claves; global 43 % intacto
(5.190 · 2.102).
4. table: `striped-row-bg` no alcanza NADA en la demo publicada —
`:nth-of-type(even)` cuenta los <tr> de RowDetail y las filas de datos
quedan impares (medido tr a tr: diez inmunes). Arreglarlo mueve píxel →
PENDIENTE DE FIRMA (candidato `:nth-child(even of [data-table-row])`);
adjudicado en el ledger mientras tanto.
El instrumento cargaba 22 falsos negativos de 26 «no effect», con tres causas
medidas, y la cuarta era de la sonda:
- el paso de abrir hacía CLIC en el input, y un clic de ratón sobre un input
de texto SÍ casa `:focus-visible` — la regla de foco pintaba el borde y el
token de reposo leía muerto (era el «falso negativo sin causa» de §13);
- el override se escribía solo en `[data-{c}]`, y las partes que cuelgan de
`{c}-root` no lo recibían;
- `::before`/`::after` eran invisibles (el aro de buffering, el spinner);
- la no-determinación de feed no era una animación sin localizar: era la demo
aún CARGANDO — el auto load-more mantiene [data-busy] ~3 s y la firma
`commit-settle` anima box-shadow sobre el nodo medido. Con la espera de
asentado: 0 diffs y 0 nodos ausentes sobre 6.264 valores, dos corridas.
Con eso NACE EL GUARD R-5.4 (la deuda que §13 pedía desde gradient-picker, y
que carousel convirtió en patrón — un estilo inline de otra capa es invisible
para todo análisis estático): `npm run theming:sentinel -- <c> <url>`
(scripts/theming-sentinel.ts, promovido de `__`; blur tras abrir, override en
todos los nodos del componente, pseudos, pase de hover, settle) + ledger
scripts/theming-sentinel-exceptions.ts con UNA razón medida por token — muerto
sin adjudicar = exit 1, excepción STALE = aviso. Los ocho componentes en
verde: command 50/57 · table 41/45 · media-player 46/61 · tree-grid 45/47 ·
gradient-picker 9/25 · listbox 20/21 · carousel 26/27 · feed 43/50, con 46
excepciones adjudicadas. El guard ya me corrigió a mí: cinco entradas mías
salieron STALE (los control-height-* de media-player SÍ alcanzan) y las borró;
y dos muertos nuevos los produjeron los propios arreglos (el blur apaga
focus-input-border; el settle desmonta el spinner de feed) — adjudicados con
su porqué. Doctrina en recipe-contract §4.
También: tres cifras de especificidad de next-features §12 recontadas (la
banda de tree-grid es (0,8,0); la tinta seleccionada del arquetipo es (0,4,0);
y el `item` de table vive en la fila Y EN LA CELDA — elementsFromPoint pone el
velo en la celda), el comentario de listbox que mandaba pisar el privado
`--_listbox-max-height` apunta ahora al público, y quedan registrados en §13
los dos `header-z` con literal '2' y el spinner de media-player a 720ms
mientras el gemelo de feed lleva tokens.
Guards: censo global 43 % intacto · audit --only command+carousel PASS ·
eidos-lint 0 invalid (command 16/12, carousel 29/9, listbox 12/7) · vitest
eidos 434/435 (el rojo es el conocido skin-media-player) · rtl 0/180 · docs
0/813 · check 0 errores en tocados (72 globales de otras sesiones) · prettier
limpio en lo nuevo (los desvíos de command.css/carousel.css ya estaban en
HEAD y no se tocan).
Pendiente de firma (CONTINUE §«PENDIENTE DE FIRMA»): el striped inerte, el
arquetipo en fila+celda, y que soma lea el gap de un token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
(0,8,0) — siete atributos más el `:nth-of-type` (la cifra (0,7,0) que se
anotó primero contaba uno de menos; recontada 2026-08-21) — contra los
(0,5,0) del hover y usa el atajo: en las filas pares no ocurre NADA al
pasar el ratón. El hover del componente es inconsistente entre filas.
docs(next-features): el registro de incidencias del eje theme-reach
Encargo del autor: anotar todas las incidencias de esta clase para resolverlas
después. Van al registro canónico (`next-features.md`), no a un sitio nuevo, y
con su MEDICIÓN — no como impresión.
**§12 — El contrato de cascada del velo de estado.** Seis incidencias que
resultaron ser la misma: nadie fijó nunca cómo compone el velo de
`archetypes.css` con las reglas de una receta. Y la raíz explica por qué se
manifiestan de formas tan distintas: el velo se pinta con DOS pesos según el
arquetipo — `:where()` (0,0,0) para `trigger`, especificidad plena (0,5,0) para
`item`/`option`.
131 declaraciones-atajo en 45 componentes cancelan el velo (66 BASE ·
32 ACENTO · 23 HOVER · 10 OTRA; cero valores con gradiente, así que el paso a
longhand no tiene riesgo técnico; 32 nodos velados que hoy no reaccionan al
ratón pasarían a hacerlo) · la prop `hoverable` de table no suprime nada · en
tree-grid el empate (0,5,0) lo decide el ORDEN DE CARGA y varía entre
recargas · la banda de tree-grid mata el hover en las filas pares · un token
de tinta no puede ganar al arquetipo (`selected-row-fg` se retiró por eso) ·
el thumb de scroll-area no tiene velo al que migrar.
Esto explica de paso la adopción 21/135 que midió la auditoría del 2026-07-01:
el velo estaba escrito y estructuralmente derrotado.
**§13 — Huecos de instrumento y de demo.** Lo que hizo que una medición
mintiera o no existiera, que importa porque el eje entero se apoya en ellas:
falsos negativos del centinela sobre propiedades transicionadas, pseudo-elementos
y componentes compuestos que no ve, la sonda que no pasa el ratón por un `<tr>`,
y la lección general —**una sonda sobre una parte que la demo no monta compara
CERO valores y pasa**—, con la lista de partes condicionales afectadas.
El handoff gana la regla para las sesiones del eje: si una incidencia mueve
píxel o toca morfo, se mide, se anota ahí y se sigue; no se arregla dentro del
commit del componente.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
5. **Un token de receta no puede ganar al arquetipo.** La regla de `item` fija
uix(theming): revisión adversarial de F2-A — 8 hallazgos de 34, y nace R-5.4
Pase escéptico del bloque (§7.7, molde Sidebar) con medición propia: worktree
con SOLO el CSS revertido a la base del bloque (DOM de demo constante — prueba
más dura que la del autor), sonda x2 por componente para fijar el suelo de
ruido. 34 hipótesis · 26 refutadas · 8 reales.
Lo que NO se pudo romper: el «default idéntico» aguanta en los ocho (0 diffs;
command 8.091 · table 13.050 · media-player 3.277 · gradient-picker 609 ·
listbox 2.813 · carousel 2.842; los 3 de tree-grid y los 5 de feed salen
idénticos corriendo DOS veces el mismo código → instrumento); 0 tokens
huérfanos de 336; el bundle ES 1:1 con los primitivos; el «verificado a mano»
de gradient-picker era verdad (15/16 alcanzan al montar); sin doble animación;
eidos-lint 0 invalid.
Los 4 de código, arreglados con diff de computed VACÍO (re-medido: command
8.091 y carousel 2.842 valores, 0 diffs):
1. command: el re-point de `--command-radius` dentro del Dialog era una
declaración muerta con comentario falso — la misma regla pone el radio a 0
y el input lee `--command-input-radius`, nunca este token (medido:
`--dialog-content-radius: 9999px` no movía NADA; `--command-radius: 77px`
movía la paleta inline y no la del dialog). Retirado; comentarios y README
reescritos con lo medido.
2. carousel: la regla vertical del indicador activo llevaba el `2.25` a pelo
mientras la horizontal leía `active-indicator-scale` — el token alcanzaba
media superficie (scale:6 → horizontal 18→48px, vertical 18→18px). Ahora
lo leen las dos.
3. carousel: el trío `item-gap*` no podía ganar NUNCA — soma estampa `gap`
INLINE desde la prop (lo necesita para el flex-basis; reglas casadas por
CDP: la de la receta y encima INLINE gap:0rem). Retirados los 3 tokens y
las 2 declaraciones muertas (la base y el gap:0 de ghost): 0 diffs, que es
la prueba. Canal de valor de soma, como --gp-current-gradient. Censo
carousel 35 knobs · 23 públicos · 77 % · 27 claves; global 43 % intacto
(5.190 · 2.102).
4. table: `striped-row-bg` no alcanza NADA en la demo publicada —
`:nth-of-type(even)` cuenta los <tr> de RowDetail y las filas de datos
quedan impares (medido tr a tr: diez inmunes). Arreglarlo mueve píxel →
PENDIENTE DE FIRMA (candidato `:nth-child(even of [data-table-row])`);
adjudicado en el ledger mientras tanto.
El instrumento cargaba 22 falsos negativos de 26 «no effect», con tres causas
medidas, y la cuarta era de la sonda:
- el paso de abrir hacía CLIC en el input, y un clic de ratón sobre un input
de texto SÍ casa `:focus-visible` — la regla de foco pintaba el borde y el
token de reposo leía muerto (era el «falso negativo sin causa» de §13);
- el override se escribía solo en `[data-{c}]`, y las partes que cuelgan de
`{c}-root` no lo recibían;
- `::before`/`::after` eran invisibles (el aro de buffering, el spinner);
- la no-determinación de feed no era una animación sin localizar: era la demo
aún CARGANDO — el auto load-more mantiene [data-busy] ~3 s y la firma
`commit-settle` anima box-shadow sobre el nodo medido. Con la espera de
asentado: 0 diffs y 0 nodos ausentes sobre 6.264 valores, dos corridas.
Con eso NACE EL GUARD R-5.4 (la deuda que §13 pedía desde gradient-picker, y
que carousel convirtió en patrón — un estilo inline de otra capa es invisible
para todo análisis estático): `npm run theming:sentinel -- <c> <url>`
(scripts/theming-sentinel.ts, promovido de `__`; blur tras abrir, override en
todos los nodos del componente, pseudos, pase de hover, settle) + ledger
scripts/theming-sentinel-exceptions.ts con UNA razón medida por token — muerto
sin adjudicar = exit 1, excepción STALE = aviso. Los ocho componentes en
verde: command 50/57 · table 41/45 · media-player 46/61 · tree-grid 45/47 ·
gradient-picker 9/25 · listbox 20/21 · carousel 26/27 · feed 43/50, con 46
excepciones adjudicadas. El guard ya me corrigió a mí: cinco entradas mías
salieron STALE (los control-height-* de media-player SÍ alcanzan) y las borró;
y dos muertos nuevos los produjeron los propios arreglos (el blur apaga
focus-input-border; el settle desmonta el spinner de feed) — adjudicados con
su porqué. Doctrina en recipe-contract §4.
También: tres cifras de especificidad de next-features §12 recontadas (la
banda de tree-grid es (0,8,0); la tinta seleccionada del arquetipo es (0,4,0);
y el `item` de table vive en la fila Y EN LA CELDA — elementsFromPoint pone el
velo en la celda), el comentario de listbox que mandaba pisar el privado
`--_listbox-max-height` apunta ahora al público, y quedan registrados en §13
los dos `header-z` con literal '2' y el spinner de media-player a 720ms
mientras el gemelo de feed lleva tokens.
Guards: censo global 43 % intacto · audit --only command+carousel PASS ·
eidos-lint 0 invalid (command 16/12, carousel 29/9, listbox 12/7) · vitest
eidos 434/435 (el rojo es el conocido skin-media-player) · rtl 0/180 · docs
0/813 · check 0 errores en tocados (72 globales de otras sesiones) · prettier
limpio en lo nuevo (los desvíos de command.css/carousel.css ya estaban en
HEAD y no se tocan).
Pendiente de firma (CONTINUE §«PENDIENTE DE FIRMA»): el striped inerte, el
arquetipo en fila+celda, y que soma lea el gap de un token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
también `color` en el estado seleccionado, a (0,4,0) contra los (0,3,0) de
la receta (la cifra (0,5,0) anotada primero era la de la variante `:hover`
de la misma lista; recontada 2026-08-21): un `table.selected-row-fg` no
podía mover nada nunca. Se retiró
docs(next-features): el registro de incidencias del eje theme-reach
Encargo del autor: anotar todas las incidencias de esta clase para resolverlas
después. Van al registro canónico (`next-features.md`), no a un sitio nuevo, y
con su MEDICIÓN — no como impresión.
**§12 — El contrato de cascada del velo de estado.** Seis incidencias que
resultaron ser la misma: nadie fijó nunca cómo compone el velo de
`archetypes.css` con las reglas de una receta. Y la raíz explica por qué se
manifiestan de formas tan distintas: el velo se pinta con DOS pesos según el
arquetipo — `:where()` (0,0,0) para `trigger`, especificidad plena (0,5,0) para
`item`/`option`.
131 declaraciones-atajo en 45 componentes cancelan el velo (66 BASE ·
32 ACENTO · 23 HOVER · 10 OTRA; cero valores con gradiente, así que el paso a
longhand no tiene riesgo técnico; 32 nodos velados que hoy no reaccionan al
ratón pasarían a hacerlo) · la prop `hoverable` de table no suprime nada · en
tree-grid el empate (0,5,0) lo decide el ORDEN DE CARGA y varía entre
recargas · la banda de tree-grid mata el hover en las filas pares · un token
de tinta no puede ganar al arquetipo (`selected-row-fg` se retiró por eso) ·
el thumb de scroll-area no tiene velo al que migrar.
Esto explica de paso la adopción 21/135 que midió la auditoría del 2026-07-01:
el velo estaba escrito y estructuralmente derrotado.
**§13 — Huecos de instrumento y de demo.** Lo que hizo que una medición
mintiera o no existiera, que importa porque el eje entero se apoya en ellas:
falsos negativos del centinela sobre propiedades transicionadas, pseudo-elementos
y componentes compuestos que no ve, la sonda que no pasa el ratón por un `<tr>`,
y la lección general —**una sonda sobre una parte que la demo no monta compara
CERO valores y pasa**—, con la lista de partes condicionales afectadas.
El handoff gana la regla para las sesiones del eje: si una incidencia mueve
píxel o toca morfo, se mide, se anota ahí y se sigue; no se arregla dentro del
commit del componente.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
con su declaración muerta (un token que no mueve nada es un token que
miente). Mientras el arquetipo pinte tinta, ningún componente puede
tematizar la suya en ese estado.
6. ** `scroll-area.thumb-bg-hover` no tiene velo al que migrar.** Su thumb lleva
`archetype: 'thumb'` , que `archetypes.css` no vela, así que la firma 3 no
puede ejecutarse ahí sin cambiar el arquetipo — decisión de MORFO. Es el
único knob de los seis de la cola que sigue sin resolver.
uix(grid-list): temable — 0 % → 85 %, y una selección que nunca pintó
38 claves nuevas en el bloque `grid-list` de `recipes/base.ts` (que ya existía
con los dos forwards de paleta: se FUSIONA, no se añade un segundo). Alcance
del censo 0 % → 85 % (22 de 26 puntuables); global 45 % → 46 %, sin contrato
49 → 48.
El eje `size` se estampa en el PROVIDER —el wrapper de eidos no tiene parte
`root`—, así que los tres nombres resueltos van SIN `parts`: molde `command`,
no `table`/`tree-grid`. Fuera los cuatro bloques `[data-size]` del CSS; los
emite el TSC. Altura de fila 1:1 con el bundle, `row-padding-inline` con ritmo
propio, y la tipografía un paso por debajo de md en adelante — verbatim, como
table y tree-grid.
LO QUE LA MEDICIÓN CAMBIÓ (la §4 generada tenía un error de doctrina y dos
defectos que sólo aparecieron midiendo; el §5 Veredicto se escribió con la
sonda delante, porque estaba vacío):
1. La selección de fila NUNCA HA PINTADO. La receta seleccionaba
`[data-grid-list-row][data-selected]`, un atributo que ni el morfo declara
ni soma estampa (estampa `data-state='selected'` + `aria-selected`): la
regla no casaba jamás y su acento `--_grid-list-palette-element` no llegó
nunca al píxel — una fila seleccionada muestra el velo NEUTRO del
arquetipo, indistinguible del hover (capturado). Retirada la declaración
muerta y su forward huérfano: 0 diffs, que es la prueba. Repararlo mueve
píxel y choca con §12.5 (el arquetipo fija también `color`) → §13, espera
firma. El acento del checkbox (`_palette-solid`) sí vive y se conserva.
2. NO se acuña `hover-row-bg` aunque la propuesta lo pedía. La fila es
`archetype: 'item'`: el velo del sistema la pinta a especificidad plena y
el plano de la receta pierde — medido, el hover pinta DOS VECES sobre el
MISMO nodo (a diferencia de table/tree-grid, donde velo y plano caen en
nodos distintos). Sería un token que no puede ganar; mismo criterio que
listbox.highlighted → §12.7. Y fila Y celda llevan ambas `item`, así que
bajo el puntero se apilan tres capas.
3. «Consume la capa compartida `menu-indicator`» es FALSO. Sale de
`SHARED_LAYERS` (`theming-census.ts:432`), pero esa capa no tiene un solo
selector de grid-list, la receta no la importa y soma no renderiza
indicator: el checkbox ESPEJA el visual pintándolo por su cuenta. Entrada
desviada del censo → §13 (listbox / menubar / navigation-menu están en la
misma entrada y tampoco aparecen en el fichero).
Nombres, sobre la propuesta: `selection-checkbox-{width,height}` son UN knob →
`selection-checkbox-size`; `on-selection-checkbox-bg` → `checked-selection-checkbox-fg`
(es la tinta del glifo, y `on-` es prefijo reservado); `border` partido en
`border-width` + color (molde `command.input-border*`), que el checkbox
comparte; `padding` uniforme se queda `padding` (precedente
`command.viewport-padding`); `max-block` entra al contrato (molde
`command.viewport-max-block`).
Artefactos y gates:
- Sonda antes/después = 0 diffs en 6.090 valores × 7 estados. Determinismo
verificado: dos corridas del mismo código, 0 diffs.
- R-5.4: 37/38 tokens mueven un computed. `invalid-border` adjudicado EN EL
LEDGER con su razón medida (la demo arranca con su control en off; activado
a mano alcanza: oklch(0.9555 0.0207 13.86) → rgb(1,2,3)).
- censo --only 85 % · component:audit PASS (R-5.3 incluida) · eidos-lint
invalid 0, class-hooks 0 · suite eidos sin rojos nuevos (el único es el
conocido skin-media-player) · rtl:check 0 · docs:check 0 · check por fichero
limpio (72 globales, ninguno mío).
- README con su «Talla y tema» y tab `Tokens` en la demo (38 claves, leídas en
vivo). El tab `Recipe` documentaba la regla retirada: corregido.
Las fichas de calendar / carousel / command / month-grid / range-calendar /
table / year-grid entran porque `--report` regenera el informe entero: sólo
actualizan NÚMEROS DE LÍNEA de commits ya hechos que no lo regeneraron. Cero
cambios de alcance. Las otras 161, que sólo cambiaban la fecha, se revirtieron.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
7. **En `grid-list` el hover pinta DOS VECES sobre el MISMO nodo** (medido
2026-08-21 al tokenizarlo). A diferencia de `table` / `tree-grid` —donde el
velo cae en la celda y el plano en la fila, nodos distintos— aquí el plano
de la receta (`background:` atajo, (0,3,0) → `background-color` ) y el velo
del arquetipo ((0,5,0) → `background-image` ) caen ambos en
`[data-grid-list-row]` : el atajo fija `background-image: none` pero pierde,
así que se ven los dos. Por eso el commit de `grid-list` **no acuñó**
`hover-row-bg` : sería un token que no puede ganar (mismo criterio que
`listbox.highlighted` ). Y como en `table` , **fila Y celda llevan
`archetype: 'item'` ** (`morfo/components/grid-list.ts:118` y `:141` ), así
que bajo el puntero se apilan velo de celda + velo de fila + plano — tres
capas. El par de portadores del punto 2 aplica igual aquí.
uix(tree-view): temable — 0 % → 92 %, y el velo que tiñe la carpeta entera
46 claves en el bloque `tree-view` de `recipes/base.ts` (fusionadas con el
forward de paleta). Censo 0 % → 92 %, cero globales. Global 46 %, sin contrato
47 → 46. Molde del hermano `tree-grid` allí donde el problema es el mismo.
LA PROPUESTA GENERADA FALLABA EN EL NOMBRE DE CASI TODO LO QUE IMPORTA: el
clasificador toma el PRIMER selector de cada regla como si fuera la parte
pintada, y aquí ese primero es el root o el branch-control cuando el nodo real
es otro.
1. `root-width: 1px` / `root-bg` NO son del root: son la GUÍA de indentación
(el `::before` del branch, una línea vertical). → `guide-width` + `guide-fg`,
literalmente los nombres de tree-grid. Un `root-bg` que tiñe una línea es la
clase de nombre que miente.
2. `branch-control-padding-inline-{k}` ⚠ fundía DOS ejes que se SUMAN en el
mismo calc: el ritmo de la fila y el PASO DE PROFUNDIDAD
(`--tree-depth × indent + padding`). Fundidos, tematizar la sangría habría
movido el padding. → `row-padding-inline-{k}` + `indent-{k}`.
3. El prefijo `branch-control-` es erróneo: la regla reza
`[data-tree-view-branch-control], [data-tree-view-item]` — las DOS filas. El
CSS ya llamaba `row` a sus privados. → `row-*`, como tree-grid y como la
lección de table. Y fuera el prefijo `root-` del chasis: el wrapper ES el
componente.
4. `row-padding-block` ⚠ no eran dos valores en pugna sino cinco por talla: xs
colapsa a `0`, las otras cuatro `--space-1`.
5. `branch-indicator-width` + `-height` son UN knob (`1em`) →
`branch-indicator-size`.
Bundle: la altura de fila casa 1:1; padding e indent llevan ritmo propio; la
tipografía un paso por debajo desde lg — verbatim, como table y tree-grid. Los
resueltos con `parts: ['root']` (el `data-size` va en `[data-tree-view-root]`,
wrapper de eidos, no parte del morfo). Los `transparent` de outline/ghost y el
`padding: 0` de ghost son identidad de variante y siguen literales.
`hover-row-bg` SÍ se acuña, al revés que en grid-list, y por medición: la misma
regla pinta dos nodos con arquetipo distinto, y `branch-control` NO lleva
archetype — ahí el plano de la receta es la única pintura (velo medido ausente)
y el token es toda la superficie de tema del hover. Tener un consumidor
legítimo es lo que lo distingue del caso grid-list.
EL DEFECTO QUE LA MEDICIÓN DESTAPÓ (preexistente, NO se toca aquí →
next-features §12.8): al pasar el ratón por la fila de una carpeta abierta se
tiñe LA CARPETA ENTERA, hijos incluidos. `archetype: 'item'` está declarado en
el `<li>` branch (`morfo/components/tree-view.ts:103`), que contiene el control
Y todo el `branch-content`, mientras la fila que el usuario señala es el
`branch-control` de dentro: el nodo velado mide 336 px frente a los 36 px de la
fila — medido y capturado. Un branch anidado acumula además el velo de sus
ancestros, así que sus filas se ven más oscuras que las hermanas. Mover el
arquetipo es morfo y mueve píxel en toda la demo. Es la TERCERA variante de la
misma familia: table lo tiene en fila Y celda, grid-list en fila Y celda sobre
el mismo nodo, y tree-view en un ANCESTRO del nodo señalado.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 8.062 valores × 7 estados.
- R-5.4: 45/46; `disabled-row-fg` adjudicado en el ledger (la demo monta CERO
filas deshabilitadas, contadas; forzada alcanza oklch(0.61 0 0) → rgb(1,2,3)).
- censo --only 92 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0
· suite eidos sin rojos nuevos (el conocido skin-media-player) · rtl:check 0
· docs:check 0 · check por fichero limpio (72 globales, ninguno mío).
- README con su «Talla y tema» y tab `Tokens` en la demo (46 claves, en vivo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
8. **En `tree-view` el velo DERRAMA sobre el subárbol entero** (medido y
capturado 2026-08-21 al tokenizarlo). `archetype: 'item'` está declarado en
el ** `<li>` branch** (`morfo/components/tree-view.ts:103`), que contiene el
`branch-control` Y el `branch-content` con todos los descendientes — así que
al pasar el ratón por la fila de una carpeta abierta se tiñe **la carpeta
entera**: el nodo velado mide **336 px** (la rama completa) frente a los
36 px de la fila que el usuario señala. Un branch anidado acumula además el
velo de sus ancestros, así que sus filas se ven más oscuras que las
hermanas. Es la tercera variante de la misma familia: `table` lo tiene en
fila Y celda (punto 2), `grid-list` en fila Y celda sobre el MISMO nodo
(punto 7), y aquí en un **ANCESTRO** del nodo señalado. Mover el arquetipo
del `<li>` al control es morfo, y mueve píxel en toda la demo.
Ojo al tokenizar la familia: en `tree-view` el `branch-control` **no lleva
archetype**, así que ahí el plano de la receta es la única pintura y su
`hover-row-bg` SÍ es un token legítimo — al revés que en `grid-list` . La
misma regla CSS cubre los dos nodos.
uix(link-preview): temable — 0 % → 100 %, y el plano overlay que traía tipografía
33 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → **100 %**: no
queda un knob de apariencia fuera del contrato (los tres restantes son sistema
— anillo de foco y los dos del plano de profundidad que pintan la flecha).
Global 48 %, sin contrato 42 → 41, al 100 % 7 → 8.
1. **Los per-talla llevan `parts: ['content']`**, no el host: el panel está
PORTALADO y su `data-size` vive en el content, así que una cascada colgada
del host no lo alcanza nunca. La talla mueve sólo dimensiones físicas
(padding, min-width, tipo); familia, peso e interlineado son constantes a
propósito, como ya decía la receta.
2. **La flecha no se tokeniza**: su fill y su stroke consumen el plano
`overlay` para casar siempre con el panel; acuñar `arrow-fg` rompería justo
esa garantía. El subrayado del trigger es un `color-mix` sobre
`currentColor` — la mezcla ES el aspecto, así que el knob es la declaración
entera, no el 35 %. Y el fallback encadenado del color del trigger viaja
verbatim: la coma es parte del valor, no un segundo knob.
EL HALLAZGO — el plano `overlay` impone TIPOGRAFÍA (→ next-features §12.9):
`[data-depth='overlay']` (`generated/base.css:7088`) no declara sólo fondo,
borde y sombra: también `font-family` y `line-height`. El panel estampa ese
plano para su elevación, así que la declaración del plano empata a (0,1,0) con
la de la receta y **gana por orden de carga** (la hoja generada va después del
chunk de la receta). Medido con CDP `getMatchedStylesForNode`: el par
`font-family` / `line-height` de esta receta **no había pintado nunca**.
Retirado con **0 diffs**, que es la prueba.
Afecta a los **veinte** componentes que estampan el plano. Lo que hay que
decidir —aparte, mueve píxel— es si un plano de profundidad tiene competencia
sobre la tipografía o sólo sobre la superficie. Cuarta ocurrencia de la clase
«empate resuelto por orden de carga».
CÓMO SE MIDIÓ, porque el instrumento estándar tampoco servía aquí:
El panel abre en HOVER con retardo y está portalado → la sonda genérica ve **1
nodo**, y una sonda que compara un nodo pasa en falso. Sonda dirigida (abre por
hover, captura los cinco nodos reales en las cinco tallas): determinismo
verificado y **gate antes/después = 0 diffs sobre 550 valores**, repetido tras
la retirada.
El guard R-5.4 pasó de **3/35 a 31/33** con dos extensiones: `openBy: 'hover'`
para este componente (un hover card no se abre con clic — clicar su `<a>`
navega) y `text-decoration-color` en la lista de propiedades, que faltaba pese
a estar en `KNOB_PROPS` del censo. Verificado que los otros QUINCE componentes
con ledger no se mueven ni una cifra.
Los dos tokens que siguen mudos son un conflicto ESTRUCTURAL, no un hueco de
demo: para abrir el panel hay que tener el puntero en el trigger, y el hover es
justo el estado cuya regla pisa la tinta y el subrayado de reposo — el token de
hover lee vivo en la misma corrida. Adjudicados con esa razón.
Gates: censo --only 100 % · component:audit PASS · eidos-lint invalid 0,
class-hooks 0 · suite eidos sin rojos nuevos (el conocido skin-media-player) ·
rtl:check 0 · docs:check 0 · check por fichero limpio · README con «Talla y
tema» y tab `Tokens` en la demo (33 claves, en vivo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
9. **El plano de profundidad `overlay` impone TIPOGRAFÍA, no sólo elevación**
— medido con CDP el 2026-08-21 al tokenizar `link-preview` .
`[data-depth='overlay']` (`generated/base.css:7088`) declara
`font-family: var(--style-label-font-family)` y
`line-height: var(--leading-ui)` junto al fondo, el borde y la sombra. Un
componente que estampe el plano para su elevación —**veinte lo hacen**—
hereda además esa tipografía, y su propia declaración empata a ** (0,1,0)**
y **pierde por orden de carga** (la hoja generada va después del chunk de la
receta). En `link-preview` el par `font-family` / `line-height` de la receta
no había pintado nunca; retirarlo dio 0 diffs, que es la prueba. Cuarta
ocurrencia de la clase «empate resuelto por orden de carga» (§12.3, y las
dos del calendario y de `tree-grid` ).
Lo que hay que decidir: si un PLANO DE PROFUNDIDAD tiene competencia sobre
la tipografía o sólo sobre la superficie. Si sólo sobre la superficie, esas
dos líneas salen del plano y cada componente recupera las suyas; si el plano
manda, entonces ningún overlay del catálogo debe declarar familia ni
interlineado y hay que barrer los que lo intentan. Mueve píxel en los veinte
en cuanto se toque.
docs(next-features): el registro de incidencias del eje theme-reach
Encargo del autor: anotar todas las incidencias de esta clase para resolverlas
después. Van al registro canónico (`next-features.md`), no a un sitio nuevo, y
con su MEDICIÓN — no como impresión.
**§12 — El contrato de cascada del velo de estado.** Seis incidencias que
resultaron ser la misma: nadie fijó nunca cómo compone el velo de
`archetypes.css` con las reglas de una receta. Y la raíz explica por qué se
manifiestan de formas tan distintas: el velo se pinta con DOS pesos según el
arquetipo — `:where()` (0,0,0) para `trigger`, especificidad plena (0,5,0) para
`item`/`option`.
131 declaraciones-atajo en 45 componentes cancelan el velo (66 BASE ·
32 ACENTO · 23 HOVER · 10 OTRA; cero valores con gradiente, así que el paso a
longhand no tiene riesgo técnico; 32 nodos velados que hoy no reaccionan al
ratón pasarían a hacerlo) · la prop `hoverable` de table no suprime nada · en
tree-grid el empate (0,5,0) lo decide el ORDEN DE CARGA y varía entre
recargas · la banda de tree-grid mata el hover en las filas pares · un token
de tinta no puede ganar al arquetipo (`selected-row-fg` se retiró por eso) ·
el thumb de scroll-area no tiene velo al que migrar.
Esto explica de paso la adopción 21/135 que midió la auditoría del 2026-07-01:
el velo estaba escrito y estructuralmente derrotado.
**§13 — Huecos de instrumento y de demo.** Lo que hizo que una medición
mintiera o no existiera, que importa porque el eje entero se apoya en ellas:
falsos negativos del centinela sobre propiedades transicionadas, pseudo-elementos
y componentes compuestos que no ve, la sonda que no pasa el ratón por un `<tr>`,
y la lección general —**una sonda sobre una parte que la demo no monta compara
CERO valores y pasa**—, con la lista de partes condicionales afectadas.
El handoff gana la regla para las sesiones del eje: si una incidencia mueve
píxel o toca morfo, se mide, se anota ahí y se sigue; no se arregla dentro del
commit del componente.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### Qué habría que decidir
En una frase: **quién manda cuando el arquetipo y la receta pintan el mismo
eje**. Las tres piezas concretas: si el velo de `item` baja a `:where()` como
el de `trigger` (y entonces la receta manda, y `hoverable` vuelve a significar
algo); si las recetas pasan a longhand en bloque (66 BASE + 32 ACENTO, mecánico
y sin gradientes de por medio); y si un arquetipo puede fijar `color` sobre un
componente que quiere tematizarlo.
**Deps**: ninguna de código. Es decisión del autor sobre píxel, y el
instrumental para medirla ya existe (`__statelayer-analysis.ts` +
`__theming-probe.ts` + `__tg-rowhover.ts` ).
## 13. Huecos de instrumento y de demo del eje theme-reach — 2026-08-20
**Origen**: la misma sesión. Cosas que hicieron que una medición mintiera o no
existiera. Se anotan porque el eje entero se apoya en esas mediciones.
uix(theming): revisión adversarial de F2-A — 8 hallazgos de 34, y nace R-5.4
Pase escéptico del bloque (§7.7, molde Sidebar) con medición propia: worktree
con SOLO el CSS revertido a la base del bloque (DOM de demo constante — prueba
más dura que la del autor), sonda x2 por componente para fijar el suelo de
ruido. 34 hipótesis · 26 refutadas · 8 reales.
Lo que NO se pudo romper: el «default idéntico» aguanta en los ocho (0 diffs;
command 8.091 · table 13.050 · media-player 3.277 · gradient-picker 609 ·
listbox 2.813 · carousel 2.842; los 3 de tree-grid y los 5 de feed salen
idénticos corriendo DOS veces el mismo código → instrumento); 0 tokens
huérfanos de 336; el bundle ES 1:1 con los primitivos; el «verificado a mano»
de gradient-picker era verdad (15/16 alcanzan al montar); sin doble animación;
eidos-lint 0 invalid.
Los 4 de código, arreglados con diff de computed VACÍO (re-medido: command
8.091 y carousel 2.842 valores, 0 diffs):
1. command: el re-point de `--command-radius` dentro del Dialog era una
declaración muerta con comentario falso — la misma regla pone el radio a 0
y el input lee `--command-input-radius`, nunca este token (medido:
`--dialog-content-radius: 9999px` no movía NADA; `--command-radius: 77px`
movía la paleta inline y no la del dialog). Retirado; comentarios y README
reescritos con lo medido.
2. carousel: la regla vertical del indicador activo llevaba el `2.25` a pelo
mientras la horizontal leía `active-indicator-scale` — el token alcanzaba
media superficie (scale:6 → horizontal 18→48px, vertical 18→18px). Ahora
lo leen las dos.
3. carousel: el trío `item-gap*` no podía ganar NUNCA — soma estampa `gap`
INLINE desde la prop (lo necesita para el flex-basis; reglas casadas por
CDP: la de la receta y encima INLINE gap:0rem). Retirados los 3 tokens y
las 2 declaraciones muertas (la base y el gap:0 de ghost): 0 diffs, que es
la prueba. Canal de valor de soma, como --gp-current-gradient. Censo
carousel 35 knobs · 23 públicos · 77 % · 27 claves; global 43 % intacto
(5.190 · 2.102).
4. table: `striped-row-bg` no alcanza NADA en la demo publicada —
`:nth-of-type(even)` cuenta los <tr> de RowDetail y las filas de datos
quedan impares (medido tr a tr: diez inmunes). Arreglarlo mueve píxel →
PENDIENTE DE FIRMA (candidato `:nth-child(even of [data-table-row])`);
adjudicado en el ledger mientras tanto.
El instrumento cargaba 22 falsos negativos de 26 «no effect», con tres causas
medidas, y la cuarta era de la sonda:
- el paso de abrir hacía CLIC en el input, y un clic de ratón sobre un input
de texto SÍ casa `:focus-visible` — la regla de foco pintaba el borde y el
token de reposo leía muerto (era el «falso negativo sin causa» de §13);
- el override se escribía solo en `[data-{c}]`, y las partes que cuelgan de
`{c}-root` no lo recibían;
- `::before`/`::after` eran invisibles (el aro de buffering, el spinner);
- la no-determinación de feed no era una animación sin localizar: era la demo
aún CARGANDO — el auto load-more mantiene [data-busy] ~3 s y la firma
`commit-settle` anima box-shadow sobre el nodo medido. Con la espera de
asentado: 0 diffs y 0 nodos ausentes sobre 6.264 valores, dos corridas.
Con eso NACE EL GUARD R-5.4 (la deuda que §13 pedía desde gradient-picker, y
que carousel convirtió en patrón — un estilo inline de otra capa es invisible
para todo análisis estático): `npm run theming:sentinel -- <c> <url>`
(scripts/theming-sentinel.ts, promovido de `__`; blur tras abrir, override en
todos los nodos del componente, pseudos, pase de hover, settle) + ledger
scripts/theming-sentinel-exceptions.ts con UNA razón medida por token — muerto
sin adjudicar = exit 1, excepción STALE = aviso. Los ocho componentes en
verde: command 50/57 · table 41/45 · media-player 46/61 · tree-grid 45/47 ·
gradient-picker 9/25 · listbox 20/21 · carousel 26/27 · feed 43/50, con 46
excepciones adjudicadas. El guard ya me corrigió a mí: cinco entradas mías
salieron STALE (los control-height-* de media-player SÍ alcanzan) y las borró;
y dos muertos nuevos los produjeron los propios arreglos (el blur apaga
focus-input-border; el settle desmonta el spinner de feed) — adjudicados con
su porqué. Doctrina en recipe-contract §4.
También: tres cifras de especificidad de next-features §12 recontadas (la
banda de tree-grid es (0,8,0); la tinta seleccionada del arquetipo es (0,4,0);
y el `item` de table vive en la fila Y EN LA CELDA — elementsFromPoint pone el
velo en la celda), el comentario de listbox que mandaba pisar el privado
`--_listbox-max-height` apunta ahora al público, y quedan registrados en §13
los dos `header-z` con literal '2' y el spinner de media-player a 720ms
mientras el gemelo de feed lleva tokens.
Guards: censo global 43 % intacto · audit --only command+carousel PASS ·
eidos-lint 0 invalid (command 16/12, carousel 29/9, listbox 12/7) · vitest
eidos 434/435 (el rojo es el conocido skin-media-player) · rtl 0/180 · docs
0/813 · check 0 errores en tocados (72 globales de otras sesiones) · prettier
limpio en lo nuevo (los desvíos de command.css/carousel.css ya estaban en
HEAD y no se tocan).
Pendiente de firma (CONTINUE §«PENDIENTE DE FIRMA»): el striped inerte, el
arquetipo en fila+celda, y que soma lea el gap de un token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- ~~**El centinela da falsos negativos sobre una propiedad transicionada.**~~
**CAUSA ENCONTRADA Y ARREGLADA 2026-08-21** (revisión adversarial): no era la
transición. El paso «abrir lo que se pueda abrir» hacía CLIC en
`[data-command-input]` , y un clic de ratón sobre un input de texto SÍ casa
`:focus-visible` — la regla de foco (0,2,0) se quedaba con `border-color` y
el token de reposo leía muerto. El guard (`theming-sentinel.ts`) ahora hace
`blur()` tras abrir. La revisión midió la tasa completa del instrumento
viejo: **22 falsos negativos de 26 «no effect»** en F2-A, con tres causas
(el clic-foco, escribir sólo en `[data-{c}]` cuando las partes cuelgan de
`{c}-root` , y no leer `::before` /`::after`). Las tres, arregladas.
uix(textarea): temable — 0 % → 80 %, y el contador que casi pierde su 0.85
37 claves en el bloque `textarea` de `recipes/base.ts` (fusionadas con el
forward de paleta que ya estaba). Censo 0 % → 80 %, con **cero globales y cero
literales de valor**: los 16 privados leen ya su público. Global 46 %, sin
contrato 48 → 47.
LO QUE LA MEDICIÓN CAMBIÓ. La §4 generada proponía 35 tokens; cuatro eran
colisiones y uno habría movido el default:
1. `count-font-size-{k}` × 5 BORRABA el factor 0.85. El CSS calcula
`calc(var(--_textarea-font-size) * 0.85)` y la propuesta ponía el tipo del
input crudo en las cinco tallas: el contador habría crecido un 18 %.
D-TH.5 lo prohíbe. Queda en UN token con el `calc` sobre el público del
input — la escala se escribe una vez y la talla la resuelve el input. Pide
`scope: 'host'`: emitido en `:root` su dependencia no existe, y el contrato
de scope de la receta lo caza (lo cazó).
2. `input-fg` ⚠ eran DOS knobs (texto y placeholder) → `input-fg` +
`input-placeholder-fg`. Igual `count-fg` → `count-fg` + `overflow-count-fg`.
3. `input-border` ⚠ NO eran dos knobs sino UNA variable con su swap de paleta,
y el nombre estaba mal: es el borde de FOCO → `focus-input-border`. El de
reposo es otro token, que la propuesta ni listó por viajar en el atajo.
4. `ghost-input-border` se RETIRA: la regla ghost lee ese mismo conmutador —
sería un valor con dos nombres.
Bundle, medido: la tipografía casa 1:1 en las cinco (lee `--size-{k}-font-size`);
el spacing NO — `padding-inline` casa salvo en xs (8px vs 6px) y `padding-block`
sólo en xs. Verbatim y documentado: un campo multi-línea respira más, y el
spacing queda fuera del guard del bundle. Resueltos sin `parts` (el `data-size`
va en `[data-textarea]` y el input hereda); fuera los cuatro `[data-size]`.
DOS ARREGLOS DEL INSTRUMENTO, los dos por medición y no por sospecha:
- **El guard medía con el PUNTERO encima.** `reopen()` corre antes de CADA
token y hace clic en `[data-{c}-input]` cuando el componente no tiene parte
`content`; el `blur()` de la revisión adversarial quitaba el foco pero no el
puntero, así que `:hover` casaba toda la corrida y toda regla de hover pisaba
a sus vecinas. Costaba TRES falsos negativos aquí. Con `mouse.move(0,0)`
tras cada blur, `input-border` revive solo (31 → 32).
- **`::placeholder` SÍ se lee con `getComputedStyle`.** La nota de §13 que
decía lo contrario nunca se comprobó; medido, el color centinela vuelve tal
cual. Entra en el snapshot (32 → 33) y deja STALE la entrada
`command.input-placeholder-fg`, que el guard señaló y se ha borrado.
Corridos los DIEZ componentes con ledger tras los dos arreglos: ningún otro
cambia, un único STALE, el previsto.
Y un hallazgo de píxel que NO se toca aquí (→ next-features §13): el borde de
foco sólo se ve con Tab. Las tres reglas de `border-color` del input están
ordenadas al revés de lo que significan — hover (0,4,0) > foco (0,3,0) >
invalid (0,2,0) — y hacer clic deja el puntero encima por definición, así que
gana el hover; un campo inválido pierde su borde rojo al enfocarlo o al pasar
el ratón. El anillo de foco del sistema sí se ve siempre, así que no es fallo
de accesibilidad. Arreglarlo mueve píxel.
El 20 % que falta son las dos declaraciones que consumen
`--_textarea-border-focus`, privado a propósito: es el conmutador de una
variable con dos fuentes (el público y el forward THM-2), y aplanarlo exigiría
duplicar la regla por color. Mismo techo que `listbox` paga por consumir bien
su capa. Mi propio veredicto predecía ~100 % y la medición lo corrigió a 80 %.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 1.247 valores × 8 estados (el 0.85 intacto,
que era el riesgo). Determinismo verificado: dos corridas, 0 diffs.
- R-5.4: 33/37; los 4 mudos adjudicados en el ledger con su medición (foco,
invalid y las dos del contador rebasado — estados que la demo no monta).
- censo --only 80 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0
· suite eidos sin rojos nuevos (el conocido skin-media-player) · rtl:check 0
· docs:check 0 · check por fichero limpio (72 globales, ninguno mío).
- README con su «Talla y tema» y tab `Tokens` en la demo (37 claves; el panel
muestra el contador computando `calc(calc(1rem * 1) * 0.85)`).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
- ~~**El centinela no ve `::placeholder` .**~~ **ERA FALSO, y ARREGLADO
2026-08-21** (al tokenizar `textarea` ): `getComputedStyle(el, '::placeholder')`
SÍ devuelve el valor — medido, el color centinela volvió tal cual. La
afirmación anterior nunca se comprobó y le costó a
`command.input-placeholder-fg` una entrada de ledger «verificada a mano» que
ha salido STALE y se ha borrado. El pseudo entra ahora en el snapshot del
guard, junto a `::before` /`::after`.
docs(next-features): el registro de incidencias del eje theme-reach
Encargo del autor: anotar todas las incidencias de esta clase para resolverlas
después. Van al registro canónico (`next-features.md`), no a un sitio nuevo, y
con su MEDICIÓN — no como impresión.
**§12 — El contrato de cascada del velo de estado.** Seis incidencias que
resultaron ser la misma: nadie fijó nunca cómo compone el velo de
`archetypes.css` con las reglas de una receta. Y la raíz explica por qué se
manifiestan de formas tan distintas: el velo se pinta con DOS pesos según el
arquetipo — `:where()` (0,0,0) para `trigger`, especificidad plena (0,5,0) para
`item`/`option`.
131 declaraciones-atajo en 45 componentes cancelan el velo (66 BASE ·
32 ACENTO · 23 HOVER · 10 OTRA; cero valores con gradiente, así que el paso a
longhand no tiene riesgo técnico; 32 nodos velados que hoy no reaccionan al
ratón pasarían a hacerlo) · la prop `hoverable` de table no suprime nada · en
tree-grid el empate (0,5,0) lo decide el ORDEN DE CARGA y varía entre
recargas · la banda de tree-grid mata el hover en las filas pares · un token
de tinta no puede ganar al arquetipo (`selected-row-fg` se retiró por eso) ·
el thumb de scroll-area no tiene velo al que migrar.
Esto explica de paso la adopción 21/135 que midió la auditoría del 2026-07-01:
el velo estaba escrito y estructuralmente derrotado.
**§13 — Huecos de instrumento y de demo.** Lo que hizo que una medición
mintiera o no existiera, que importa porque el eje entero se apoya en ellas:
falsos negativos del centinela sobre propiedades transicionadas, pseudo-elementos
y componentes compuestos que no ve, la sonda que no pasa el ratón por un `<tr>`,
y la lección general —**una sonda sobre una parte que la demo no monta compara
CERO valores y pasa**—, con la lista de partes condicionales afectadas.
El handoff gana la regla para las sesiones del eje: si una incidencia mueve
píxel o toca morfo, se mide, se anota ahí y se sigue; no se arregla dentro del
commit del componente.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **El centinela no ve lo que pinta un componente COMPUESTO.** Los tokens que
media-player reenvía al Slider (`--media-player-track` →
`--slider-track-bg` ) pintan en nodos `[data-slider-*]` , fuera del espacio de
uix(theming): revisión adversarial de F2-A — 8 hallazgos de 34, y nace R-5.4
Pase escéptico del bloque (§7.7, molde Sidebar) con medición propia: worktree
con SOLO el CSS revertido a la base del bloque (DOM de demo constante — prueba
más dura que la del autor), sonda x2 por componente para fijar el suelo de
ruido. 34 hipótesis · 26 refutadas · 8 reales.
Lo que NO se pudo romper: el «default idéntico» aguanta en los ocho (0 diffs;
command 8.091 · table 13.050 · media-player 3.277 · gradient-picker 609 ·
listbox 2.813 · carousel 2.842; los 3 de tree-grid y los 5 de feed salen
idénticos corriendo DOS veces el mismo código → instrumento); 0 tokens
huérfanos de 336; el bundle ES 1:1 con los primitivos; el «verificado a mano»
de gradient-picker era verdad (15/16 alcanzan al montar); sin doble animación;
eidos-lint 0 invalid.
Los 4 de código, arreglados con diff de computed VACÍO (re-medido: command
8.091 y carousel 2.842 valores, 0 diffs):
1. command: el re-point de `--command-radius` dentro del Dialog era una
declaración muerta con comentario falso — la misma regla pone el radio a 0
y el input lee `--command-input-radius`, nunca este token (medido:
`--dialog-content-radius: 9999px` no movía NADA; `--command-radius: 77px`
movía la paleta inline y no la del dialog). Retirado; comentarios y README
reescritos con lo medido.
2. carousel: la regla vertical del indicador activo llevaba el `2.25` a pelo
mientras la horizontal leía `active-indicator-scale` — el token alcanzaba
media superficie (scale:6 → horizontal 18→48px, vertical 18→18px). Ahora
lo leen las dos.
3. carousel: el trío `item-gap*` no podía ganar NUNCA — soma estampa `gap`
INLINE desde la prop (lo necesita para el flex-basis; reglas casadas por
CDP: la de la receta y encima INLINE gap:0rem). Retirados los 3 tokens y
las 2 declaraciones muertas (la base y el gap:0 de ghost): 0 diffs, que es
la prueba. Canal de valor de soma, como --gp-current-gradient. Censo
carousel 35 knobs · 23 públicos · 77 % · 27 claves; global 43 % intacto
(5.190 · 2.102).
4. table: `striped-row-bg` no alcanza NADA en la demo publicada —
`:nth-of-type(even)` cuenta los <tr> de RowDetail y las filas de datos
quedan impares (medido tr a tr: diez inmunes). Arreglarlo mueve píxel →
PENDIENTE DE FIRMA (candidato `:nth-child(even of [data-table-row])`);
adjudicado en el ledger mientras tanto.
El instrumento cargaba 22 falsos negativos de 26 «no effect», con tres causas
medidas, y la cuarta era de la sonda:
- el paso de abrir hacía CLIC en el input, y un clic de ratón sobre un input
de texto SÍ casa `:focus-visible` — la regla de foco pintaba el borde y el
token de reposo leía muerto (era el «falso negativo sin causa» de §13);
- el override se escribía solo en `[data-{c}]`, y las partes que cuelgan de
`{c}-root` no lo recibían;
- `::before`/`::after` eran invisibles (el aro de buffering, el spinner);
- la no-determinación de feed no era una animación sin localizar: era la demo
aún CARGANDO — el auto load-more mantiene [data-busy] ~3 s y la firma
`commit-settle` anima box-shadow sobre el nodo medido. Con la espera de
asentado: 0 diffs y 0 nodos ausentes sobre 6.264 valores, dos corridas.
Con eso NACE EL GUARD R-5.4 (la deuda que §13 pedía desde gradient-picker, y
que carousel convirtió en patrón — un estilo inline de otra capa es invisible
para todo análisis estático): `npm run theming:sentinel -- <c> <url>`
(scripts/theming-sentinel.ts, promovido de `__`; blur tras abrir, override en
todos los nodos del componente, pseudos, pase de hover, settle) + ledger
scripts/theming-sentinel-exceptions.ts con UNA razón medida por token — muerto
sin adjudicar = exit 1, excepción STALE = aviso. Los ocho componentes en
verde: command 50/57 · table 41/45 · media-player 46/61 · tree-grid 45/47 ·
gradient-picker 9/25 · listbox 20/21 · carousel 26/27 · feed 43/50, con 46
excepciones adjudicadas. El guard ya me corrigió a mí: cinco entradas mías
salieron STALE (los control-height-* de media-player SÍ alcanzan) y las borró;
y dos muertos nuevos los produjeron los propios arreglos (el blur apaga
focus-input-border; el settle desmonta el spinner de feed) — adjudicados con
su porqué. Doctrina en recipe-contract §4.
También: tres cifras de especificidad de next-features §12 recontadas (la
banda de tree-grid es (0,8,0); la tinta seleccionada del arquetipo es (0,4,0);
y el `item` de table vive en la fila Y EN LA CELDA — elementsFromPoint pone el
velo en la celda), el comentario de listbox que mandaba pisar el privado
`--_listbox-max-height` apunta ahora al público, y quedan registrados en §13
los dos `header-z` con literal '2' y el spinner de media-player a 720ms
mientras el gemelo de feed lleva tokens.
Guards: censo global 43 % intacto · audit --only command+carousel PASS ·
eidos-lint 0 invalid (command 16/12, carousel 29/9, listbox 12/7) · vitest
eidos 434/435 (el rojo es el conocido skin-media-player) · rtl 0/180 · docs
0/813 · check 0 errores en tocados (72 globales de otras sesiones) · prettier
limpio en lo nuevo (los desvíos de command.css/carousel.css ya estaban en
HEAD y no se tocan).
Pendiente de firma (CONTINUE §«PENDIENTE DE FIRMA»): el striped inerte, el
arquetipo en fila+celda, y que soma lea el gap de un token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
atributos del componente medido. Sigue vivo; adjudicado por token en el
ledger de R-5.4.
- ~~**La sonda no pasa el ratón por un `<tr>` .**~~ **ARREGLADO 2026-08-21** :
`tr` entra en el filtro de hover de la sonda, y el guard R-5.4 añade un pase
de hover propio para los tokens `hover-*` (así adjudicaron
`table.hover-row-bg` y `gradient-picker.hover-preset-border` ).
- **La sonda medía una demo aún CARGANDO.** El auto load-more de `feed`
mantiene `[data-busy]` ~3 s y su firma `commit-settle` anima `box-shadow`
sobre el nodo medido — los «5 diffs entre dos corridas del mismo código» del
handoff eran esto, no una animación sin localizar. **ARREGLADO 2026-08-21** :
sonda y guard esperan a que `[data-busy]` caiga; dos corridas del mismo
código dan 0 diffs y 0 nodos ausentes (6.264 valores).
docs(next-features): el registro de incidencias del eje theme-reach
Encargo del autor: anotar todas las incidencias de esta clase para resolverlas
después. Van al registro canónico (`next-features.md`), no a un sitio nuevo, y
con su MEDICIÓN — no como impresión.
**§12 — El contrato de cascada del velo de estado.** Seis incidencias que
resultaron ser la misma: nadie fijó nunca cómo compone el velo de
`archetypes.css` con las reglas de una receta. Y la raíz explica por qué se
manifiestan de formas tan distintas: el velo se pinta con DOS pesos según el
arquetipo — `:where()` (0,0,0) para `trigger`, especificidad plena (0,5,0) para
`item`/`option`.
131 declaraciones-atajo en 45 componentes cancelan el velo (66 BASE ·
32 ACENTO · 23 HOVER · 10 OTRA; cero valores con gradiente, así que el paso a
longhand no tiene riesgo técnico; 32 nodos velados que hoy no reaccionan al
ratón pasarían a hacerlo) · la prop `hoverable` de table no suprime nada · en
tree-grid el empate (0,5,0) lo decide el ORDEN DE CARGA y varía entre
recargas · la banda de tree-grid mata el hover en las filas pares · un token
de tinta no puede ganar al arquetipo (`selected-row-fg` se retiró por eso) ·
el thumb de scroll-area no tiene velo al que migrar.
Esto explica de paso la adopción 21/135 que midió la auditoría del 2026-07-01:
el velo estaba escrito y estructuralmente derrotado.
**§13 — Huecos de instrumento y de demo.** Lo que hizo que una medición
mintiera o no existiera, que importa porque el eje entero se apoya en ellas:
falsos negativos del centinela sobre propiedades transicionadas, pseudo-elementos
y componentes compuestos que no ve, la sonda que no pasa el ratón por un `<tr>`,
y la lección general —**una sonda sobre una parte que la demo no monta compara
CERO valores y pasa**—, con la lista de partes condicionales afectadas.
El handoff gana la regla para las sesiones del eje: si una incidencia mueve
píxel o toca morfo, se mide, se anota ahí y se sigue; no se arregla dentro del
commit del componente.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Una sonda sobre una demo que no monta la parte compara CERO valores y
pasa.** Ocurrió con el skin de audio: `/uix/components/audio-player` da 404
porque la demo es la de `media-player` con el chip `media` , y la sonda cargaba
en modo vídeo. Resuelto con `scripts/__probe-audio-skin.ts` , pero la clase de
fallo es general: **una parte condicional necesita que la sonda la monte** .
Afecta al menos a captions, buffering y paneles portalados de media-player, al
estado vacío y la barra de carga de `command` , y al overview de
`date-range-picker` .
- **`date-range-picker` no expone `kind='month'` / `'year'` en su demo**
(norma N-6), así que la mitad de su API no se ve ni se mide.
feat(gradient-picker): temable — y su trigger resultó estar muerto entero
Quinto del bloque F2-A.
censo 0 % → 88 % · global 42 %
computed 609 valores en 8 estados: 0 diffs
**El hallazgo llegó por el centinela: 36 de 45 tokens no movían nada.** Con esa
cifra no se sigue adelante. El trigger es un POPOVER TRIGGER, y `popover.css` le
pinta el cromo entero —altura, padding inline, tipografía, color, fondo, borde,
radio, hover y foco— desde
`[data-popover-trigger]:not([data-archetype='field-trigger'])`, que gana a
`[data-gradient-picker-trigger]` en base (0,2,0 contra 0,1,0) y en los estados
(0,3,0 contra 0,2,0).
Esta receta re-declaraba TODO eso y nada pintaba: la altura, el padding, el
tamaño de letra y el radio venían de popover, y con valores distintos —12px
contra el `space-2-5` de la receta, 14px contra su `font-size-md`—. Llevaba
así desde que el trigger se volvió trigger de popover.
Así que no se tokeniza: **se retira**. Un token sobre una declaración muerta es
un token que miente, y este eje ya retiró uno por lo mismo en `table`.
Retirarlo dio **0 diffs**, que es la prueba de que estaba muerto. Un tema viste
este trigger por el contrato de POPOVER, que es composición funcionando.
Lo que sí es del picker se queda y alcanza (verificado a mano, porque el chip
usa un hook de CLASE que el centinela no ve): el `gap` de la fila, el chip del
degradado (tamaño por talla, radio, borde), las tres anchuras del panel, la
lista de paradas y la rejilla de presets.
**Correcciones al veredicto**, ambas medidas: `--gp-current-gradient` NO lo
estampa el wrapper sino SOMA (`gradient-picker-provider.svelte.ts:197`), así
que renombrarlo toca otra capa y no es de este commit; y no es un knob de tema
sino un canal de valor —el degradado que eligió el usuario—, sobre el que un
tema no tiene nada que decir.
Las tres anchuras del panel se quedan como tokens planos leídos por tres
reglas, no como cascada del TSC: el content va por PORTAL y dimensiona con
`data-picker-size` (el atributo de picker-shell), y el vocabulario de scopes no
tiene palabra para el atributo de otro componente.
Al registro (`next-features.md` §13) van cuatro incidencias nuevas: que un
componente compuesto pueda tener el cromo entero muerto sin que nada avise
—merece guard, y el instrumento ya existe—, el nombre abreviado en soma, los
dos hooks por CLASE que `eidos-lint` cuenta, y el foco opaco del trigger contra
la mezcla suave del sistema.
audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados ·
suite: sólo el rojo conocido
Demo con tab Tokens: 25 filas, 0 sin computar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Un componente COMPUESTO puede tener su cromo entero muerto sin que nada
avise.** `gradient-picker` re-declaraba altura, padding, tipografía, color,
fondo, borde, radio, hover y foco de su trigger, y NADA de ello pintaba:
`popover.css` gana con `[data-popover-trigger]:not([data-archetype='field-trigger'])`
(0,2,0 contra 0,1,0). Lo cazó el centinela —36 de 45 tokens sin efecto— y
uix(theming): revisión adversarial de F2-A — 8 hallazgos de 34, y nace R-5.4
Pase escéptico del bloque (§7.7, molde Sidebar) con medición propia: worktree
con SOLO el CSS revertido a la base del bloque (DOM de demo constante — prueba
más dura que la del autor), sonda x2 por componente para fijar el suelo de
ruido. 34 hipótesis · 26 refutadas · 8 reales.
Lo que NO se pudo romper: el «default idéntico» aguanta en los ocho (0 diffs;
command 8.091 · table 13.050 · media-player 3.277 · gradient-picker 609 ·
listbox 2.813 · carousel 2.842; los 3 de tree-grid y los 5 de feed salen
idénticos corriendo DOS veces el mismo código → instrumento); 0 tokens
huérfanos de 336; el bundle ES 1:1 con los primitivos; el «verificado a mano»
de gradient-picker era verdad (15/16 alcanzan al montar); sin doble animación;
eidos-lint 0 invalid.
Los 4 de código, arreglados con diff de computed VACÍO (re-medido: command
8.091 y carousel 2.842 valores, 0 diffs):
1. command: el re-point de `--command-radius` dentro del Dialog era una
declaración muerta con comentario falso — la misma regla pone el radio a 0
y el input lee `--command-input-radius`, nunca este token (medido:
`--dialog-content-radius: 9999px` no movía NADA; `--command-radius: 77px`
movía la paleta inline y no la del dialog). Retirado; comentarios y README
reescritos con lo medido.
2. carousel: la regla vertical del indicador activo llevaba el `2.25` a pelo
mientras la horizontal leía `active-indicator-scale` — el token alcanzaba
media superficie (scale:6 → horizontal 18→48px, vertical 18→18px). Ahora
lo leen las dos.
3. carousel: el trío `item-gap*` no podía ganar NUNCA — soma estampa `gap`
INLINE desde la prop (lo necesita para el flex-basis; reglas casadas por
CDP: la de la receta y encima INLINE gap:0rem). Retirados los 3 tokens y
las 2 declaraciones muertas (la base y el gap:0 de ghost): 0 diffs, que es
la prueba. Canal de valor de soma, como --gp-current-gradient. Censo
carousel 35 knobs · 23 públicos · 77 % · 27 claves; global 43 % intacto
(5.190 · 2.102).
4. table: `striped-row-bg` no alcanza NADA en la demo publicada —
`:nth-of-type(even)` cuenta los <tr> de RowDetail y las filas de datos
quedan impares (medido tr a tr: diez inmunes). Arreglarlo mueve píxel →
PENDIENTE DE FIRMA (candidato `:nth-child(even of [data-table-row])`);
adjudicado en el ledger mientras tanto.
El instrumento cargaba 22 falsos negativos de 26 «no effect», con tres causas
medidas, y la cuarta era de la sonda:
- el paso de abrir hacía CLIC en el input, y un clic de ratón sobre un input
de texto SÍ casa `:focus-visible` — la regla de foco pintaba el borde y el
token de reposo leía muerto (era el «falso negativo sin causa» de §13);
- el override se escribía solo en `[data-{c}]`, y las partes que cuelgan de
`{c}-root` no lo recibían;
- `::before`/`::after` eran invisibles (el aro de buffering, el spinner);
- la no-determinación de feed no era una animación sin localizar: era la demo
aún CARGANDO — el auto load-more mantiene [data-busy] ~3 s y la firma
`commit-settle` anima box-shadow sobre el nodo medido. Con la espera de
asentado: 0 diffs y 0 nodos ausentes sobre 6.264 valores, dos corridas.
Con eso NACE EL GUARD R-5.4 (la deuda que §13 pedía desde gradient-picker, y
que carousel convirtió en patrón — un estilo inline de otra capa es invisible
para todo análisis estático): `npm run theming:sentinel -- <c> <url>`
(scripts/theming-sentinel.ts, promovido de `__`; blur tras abrir, override en
todos los nodos del componente, pseudos, pase de hover, settle) + ledger
scripts/theming-sentinel-exceptions.ts con UNA razón medida por token — muerto
sin adjudicar = exit 1, excepción STALE = aviso. Los ocho componentes en
verde: command 50/57 · table 41/45 · media-player 46/61 · tree-grid 45/47 ·
gradient-picker 9/25 · listbox 20/21 · carousel 26/27 · feed 43/50, con 46
excepciones adjudicadas. El guard ya me corrigió a mí: cinco entradas mías
salieron STALE (los control-height-* de media-player SÍ alcanzan) y las borró;
y dos muertos nuevos los produjeron los propios arreglos (el blur apaga
focus-input-border; el settle desmonta el spinner de feed) — adjudicados con
su porqué. Doctrina en recipe-contract §4.
También: tres cifras de especificidad de next-features §12 recontadas (la
banda de tree-grid es (0,8,0); la tinta seleccionada del arquetipo es (0,4,0);
y el `item` de table vive en la fila Y EN LA CELDA — elementsFromPoint pone el
velo en la celda), el comentario de listbox que mandaba pisar el privado
`--_listbox-max-height` apunta ahora al público, y quedan registrados en §13
los dos `header-z` con literal '2' y el spinner de media-player a 720ms
mientras el gemelo de feed lleva tokens.
Guards: censo global 43 % intacto · audit --only command+carousel PASS ·
eidos-lint 0 invalid (command 16/12, carousel 29/9, listbox 12/7) · vitest
eidos 434/435 (el rojo es el conocido skin-media-player) · rtl 0/180 · docs
0/813 · check 0 errores en tocados (72 globales de otras sesiones) · prettier
limpio en lo nuevo (los desvíos de command.css/carousel.css ya estaban en
HEAD y no se tocan).
Pendiente de firma (CONTINUE §«PENDIENTE DE FIRMA»): el striped inerte, el
arquetipo en fila+celda, y que soma lea el gap de un token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
retirarlo dio 0 diffs. **EL GUARD EXISTE desde 2026-08-21: R-5.4**
(`npm run theming:sentinel -- < c > < url > `, `scripts/theming-sentinel.ts` +
ledger `theming-sentinel-exceptions.ts` , doctrina en recipe-contract §4).
Nació con la segunda ocurrencia de la clase: el `item-gap` de `carousel`
contra un estilo INLINE de soma, invisible para todo análisis estático.
Los ocho componentes de F2-A pasan con su adjudicación escrita.
feat(gradient-picker): temable — y su trigger resultó estar muerto entero
Quinto del bloque F2-A.
censo 0 % → 88 % · global 42 %
computed 609 valores en 8 estados: 0 diffs
**El hallazgo llegó por el centinela: 36 de 45 tokens no movían nada.** Con esa
cifra no se sigue adelante. El trigger es un POPOVER TRIGGER, y `popover.css` le
pinta el cromo entero —altura, padding inline, tipografía, color, fondo, borde,
radio, hover y foco— desde
`[data-popover-trigger]:not([data-archetype='field-trigger'])`, que gana a
`[data-gradient-picker-trigger]` en base (0,2,0 contra 0,1,0) y en los estados
(0,3,0 contra 0,2,0).
Esta receta re-declaraba TODO eso y nada pintaba: la altura, el padding, el
tamaño de letra y el radio venían de popover, y con valores distintos —12px
contra el `space-2-5` de la receta, 14px contra su `font-size-md`—. Llevaba
así desde que el trigger se volvió trigger de popover.
Así que no se tokeniza: **se retira**. Un token sobre una declaración muerta es
un token que miente, y este eje ya retiró uno por lo mismo en `table`.
Retirarlo dio **0 diffs**, que es la prueba de que estaba muerto. Un tema viste
este trigger por el contrato de POPOVER, que es composición funcionando.
Lo que sí es del picker se queda y alcanza (verificado a mano, porque el chip
usa un hook de CLASE que el centinela no ve): el `gap` de la fila, el chip del
degradado (tamaño por talla, radio, borde), las tres anchuras del panel, la
lista de paradas y la rejilla de presets.
**Correcciones al veredicto**, ambas medidas: `--gp-current-gradient` NO lo
estampa el wrapper sino SOMA (`gradient-picker-provider.svelte.ts:197`), así
que renombrarlo toca otra capa y no es de este commit; y no es un knob de tema
sino un canal de valor —el degradado que eligió el usuario—, sobre el que un
tema no tiene nada que decir.
Las tres anchuras del panel se quedan como tokens planos leídos por tres
reglas, no como cascada del TSC: el content va por PORTAL y dimensiona con
`data-picker-size` (el atributo de picker-shell), y el vocabulario de scopes no
tiene palabra para el atributo de otro componente.
Al registro (`next-features.md` §13) van cuatro incidencias nuevas: que un
componente compuesto pueda tener el cromo entero muerto sin que nada avise
—merece guard, y el instrumento ya existe—, el nombre abreviado en soma, los
dos hooks por CLASE que `eidos-lint` cuenta, y el foco opaco del trigger contra
la mezcla suave del sistema.
audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados ·
suite: sólo el rojo conocido
Demo con tab Tokens: 25 filas, 0 sin computar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **`--gp-current-gradient` es un nombre ABREVIADO en SOMA.** Lo estampa
`gradient-picker-provider.svelte.ts` (no el wrapper de eidos, como decía la
ficha), y viola theming §6 r5. No es knob de tema —es un canal de valor— pero
el nombre es deuda; renombrarlo toca soma, otra capa.
- **Hooks por CLASE en una receta de eidos.** `gradient-picker` usa
`.gradient-picker-trigger-swatch` y `.gradient-picker-trigger-label` en vez
de `data-*` ; `eidos-lint` los cuenta (`class-hooks: 2`) y el centinela no los
ve, porque filtra nodos por atributo.
- **El foco del trigger de `gradient-picker` es opaco** (`--color-primary-solid`)
mientras el resto del catálogo lleva la mezcla suave del sistema
(`--focus-ring-color`, 52 %). Quedó sin tocar porque alinearlo mueve píxel —
y de hecho quedó sin token, porque la regla entera está muerta bajo popover.
feat(calendar-surface): la capa que ya existía, con nombre, casa y su agujero tapado
Firma 1 del acta, diseño presentado y firmado. **La medición desmontó el
encargo**: el handoff la vendía como una capa NUEVA de ~220 knobs, y la capa ya
existía de hecho, sin nombre. `--calendar-*` se emite en `:root` (76 claves) y
sus consumidores no acuñan NADA — medido: `range-calendar` 114 referencias
prestadas y 0 propias, `month-grid` 77/0, `year-grid` 77/0, y lo único ajeno
que leen es sistema puro (`--focus-ring-*`, `--state-hover`). Su 0 % era el
artefacto de `listbox` (§13), pero total.
**`lib/calendar-surface.css`** (hook `data-calendar-surface`):
- cuatro coordenadas por talla — `padding`, `control-size`, `day-size`,
`font-size` — xs..lg, porque la familia NO tiene xl, y con la celda DOS pasos
por debajo del bundle de control. Esa desviación estaba escondida en cuatro
bloques `[data-size]` idénticos, uno por receta, cada uno puenteando a un
privado con otro nombre; ahora se lee en un sitio.
- la FORMA del anillo de evento y de la marca de festivo.
**Capa HÍBRIDA, y es lo que la distingue de sus hermanas**: `list-surface` y
`viewport-placement` componen primitivos del sistema, así que declaran sus
públicos en el fichero y no tienen entrada de receta. Ésta no puede: su
vocabulario son 76 claves SEMÁNTICAS que un tema alcanza una a una por config,
así que la entrada `calendar` de `recipes/base.ts` pasa a ser la de la FAMILIA
y la capa posee sólo lo que una entrada de receta no sabe expresar.
**El defecto que la justificaba, medido**: `--calendar-event-shadow` y
`--calendar-day-holiday-shadow` se emitían con ámbito `[data-calendar]`
(audit B.2 los host-scopeó por buenas razones) mientras `range-calendar`,
`month-grid` y `year-grid` los leían desde hosts que nunca llevan ese atributo:
variable VACÍA, `box-shadow` inválido en computed, **el anillo sema de
`commit-select` / `commit-set` no pintaba jamás en tres componentes**. Un token
prestado cuyo ÁMBITO no te cubre no es un préstamo, es un agujero silencioso, y
ningún guard lo veía. Ahora la forma vive en la capa y el acento entra por
`--_calendar-surface-accent`, que cada superficie alimenta con su propio
forward de paleta THM-2.
**Siete wrappers estampan, no cuatro** — y esto casi se me cuela: `DatePicker`
y `DateRangePicker` renderizan la superficie soma por sus PROPIOS wrappers
(`date-picker-calendar`, `-month-view`, `-year-view`,
`date-range-picker-calendar`) y un panel portalado no hereda nada del root del
picker. Con sólo los cuatro standalone sellando, ambos quedaban con
`--calendar-padding` VACÍA y el panel a padding 0 (medido). La tentación era
enganchar la capa a las cuatro identidades de componente: eso viola la regla 1
de capas compartidas, y la respuesta correcta es un sello por wrapper.
computed 0 diffs en range-calendar (19.285 valores) · month-grid (3.451) ·
year-grid (3.451) · date-picker (464) · date-range-picker (406).
`calendar` da 12, y son del INSTRUMENTO: dos corridas del MISMO
código dan 24 en los mismos nodos y las mismas dos propiedades.
Los «missing node» son el propio sello entrando en la clave.
**Una incidencia nueva, medida y NO arreglada aquí** (§13): el font-size de los
selectores month/year es una moneda al aire —
`[data-calendar-month-select][data-button]` (0,2,0) empata con
`[data-popover-trigger]:not([data-archetype='field-trigger'])` (0,2,0), la
MISMA regla de popover que dejó muerto el cromo de `gradient-picker`, y gana la
hoja que cargue después: 16px o 14px según la recarga. Arreglarlo fija el píxel
en un lado ⇒ decisión.
De paso, `calendar-select.css` deja de puentear un privado que sólo
`[data-calendar]` declaraba: en range / date-range corría SIEMPRE por el
fallback, clavado a md fuera cual fuera la talla.
**El censo deja de penalizar hacer lo correcto**: `LAYER_VOCABULARY` en
`theming-census.ts`, mismo precedente que D-TH.2-b con `--style-*`. Global
43 % → **45 %**, `calendar` 75 % → **84 %**. `list-surface` NO se registra: sus
consumidores puentean por privados, otra forma, y mueve diez componentes de
golpe. Y aparece el techo de debajo, anotado: a los tres consumidores sólo les
quedan los forwards de paleta THM-2 —que el censo cuenta como `private` en TODO
el catálogo— así que siguen leyendo 0 %.
`recipe-css-contract` aprende que una CAPA también declara públicos (antes sólo
miraba la receta, y una capa que comparte prefijo con un componente la hacía
fallar). Sin debilitarla: un nombre que no declara nadie sigue en rojo.
eidos-lint 0 invalid (calendar 29/8 · range-calendar 41/12 · los grids 23/5) ·
audit --only calendar PASS · vitest eidos 434/435 (el rojo conocido) ·
rtl 0/181 · docs 0/813 · check 0 errores en tocados · prettier: revertido el
reformateo en masa que se coló en cuatro README, el test y el censo (churn
ajeno, no mío)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- ~~**El censo PENALIZA consumir una capa compartida correctamente.**~~
**RESUELTO PARA LA FAMILIA CALENDAR 2026-08-21** y abierto para el resto: el
censo tiene ahora `LAYER_VOCABULARY` (mismo precedente que D-TH.2-b), así que
el `--calendar-*` que leen `range-calendar` / `month-grid` / `year-grid`
cuenta como `system` . ** `list-surface` NO está registrado a propósito**: sus
consumidores puentean por PRIVADOS (`--_listbox-item-*: var(--list-item-*)`),
otra forma, y activarlo mueve diez componentes de golpe — sigue siendo
decisión aparte. Y aparece un techo NUEVO debajo: con el vocabulario de la
capa fuera del denominador, a los tres sólo les quedan los **forwards de
paleta THM-2** (`--_x-palette-*`, que el censo cuenta como `private` en TODO
el catálogo) y literales de layout, así que siguen leyendo 0 %. Reclasificar
el forward de paleta es la misma pregunta, un piso más abajo, y afecta a
todos.
feat(listbox): temable hasta donde le toca — 0 % → 68 %
Sexto del bloque F2-A, y el primero cuyo techo es DELIBERADO.
censo 0 % → 68 % · global 42 %
computed 2.813 valores en 7 estados: 0 diffs
**Este componente no puede acuñar su ritmo de fila, y esa es la regla de oro de
las capas compartidas.** Altura, padding inline y block, gap y tamaño de letra
los posee `lib/list-surface.css`, por talla, para TODAS las superficies de lista
—select, combobox, command y los menús—. Acuñar `--listbox-item-height` sería
el vocabulario paralelo que las reglas de esa capa prohíben, y encima ganaría a
la capa para todas las demás. Los cinco privados ADOPTAN la capa y se quedan
privados; un tema mueve ese ritmo por `--list-*`, que es donde vive.
Consecuencia honesta: el censo cuenta esos cinco como privados y el alcance se
queda en 68 % en vez de ~86 %. **La métrica penaliza hacer lo correcto** — el
mismo defecto de medición que D-TH.2-b arregló para los primitivos tipográficos
(`--style-*` pasó a `system`). Registrado en `next-features.md` §13; extenderlo
a las capas compartidas es decisión, no corrección al paso.
**Dos cosas medidas que NO se tocaron:**
- El `highlighted` pinta DOS veces: la receta pone un `--color-surface-overlay`
plano y encima el arquetipo `item` añade su velo del 8 %. El plano es el
duplicado que §38 deprecó, así que se dejó SIN token — bendecir con un nombre
público algo condenado a morir es el error que este eje ya evitó en el
codemod. Retirarlo mueve píxel.
- La tinta de la fila seleccionada era código MUERTO y se retiró: el arquetipo
la fija a (0,4,0) contra los (0,3,0) de la receta. Mismo hallazgo que en
`table`, verificado igual, 0 diffs al quitarlo.
Lo que sí es suyo y ahora es público: la superficie que sostiene las filas
(fondo, borde, radio, padding, gap, tipografía, alto máximo), la esquina y la
tinta de la fila, el glifo de selección y el grupo con su etiqueta.
audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados ·
suite: sólo el rojo conocido
Demo con tab Tokens: 21 filas, 0 sin computar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **El censo PENALIZA consumir una capa compartida correctamente.** `listbox`
adopta los cinco ejes de ritmo que `list-surface` posee — que es exactamente
lo que la doctrina de capas manda — y el censo los cuenta como `private` , así
que su alcance queda en 68 % en vez de ~86 %. Es el mismo defecto de medición
que D-TH.2-b arregló para los primitivos tipográficos (`--style-*` pasó a
contar como `system` ): la capa ES la superficie de tema de ese eje. Afecta a
los consumidores de `list-surface` (select · combobox · command · listbox ·
los menús) y de `viewport-placement` (affix · fab · menu-dial). Extenderlo es
una decisión, no una corrección al paso.
- **`listbox` pinta el `highlighted` DOS veces**: la receta pone un
`--color-surface-overlay` plano y encima el arquetipo `item` añade su velo del
8 % — medido. El plano es el duplicado que §38 deprecó; se dejó sin token para
no bendecirlo, pero retirarlo mueve píxel.
docs(next-features): el registro de incidencias del eje theme-reach
Encargo del autor: anotar todas las incidencias de esta clase para resolverlas
después. Van al registro canónico (`next-features.md`), no a un sitio nuevo, y
con su MEDICIÓN — no como impresión.
**§12 — El contrato de cascada del velo de estado.** Seis incidencias que
resultaron ser la misma: nadie fijó nunca cómo compone el velo de
`archetypes.css` con las reglas de una receta. Y la raíz explica por qué se
manifiestan de formas tan distintas: el velo se pinta con DOS pesos según el
arquetipo — `:where()` (0,0,0) para `trigger`, especificidad plena (0,5,0) para
`item`/`option`.
131 declaraciones-atajo en 45 componentes cancelan el velo (66 BASE ·
32 ACENTO · 23 HOVER · 10 OTRA; cero valores con gradiente, así que el paso a
longhand no tiene riesgo técnico; 32 nodos velados que hoy no reaccionan al
ratón pasarían a hacerlo) · la prop `hoverable` de table no suprime nada · en
tree-grid el empate (0,5,0) lo decide el ORDEN DE CARGA y varía entre
recargas · la banda de tree-grid mata el hover en las filas pares · un token
de tinta no puede ganar al arquetipo (`selected-row-fg` se retiró por eso) ·
el thumb de scroll-area no tiene velo al que migrar.
Esto explica de paso la adopción 21/135 que midió la auditoría del 2026-07-01:
el velo estaba escrito y estructuralmente derrotado.
**§13 — Huecos de instrumento y de demo.** Lo que hizo que una medición
mintiera o no existiera, que importa porque el eje entero se apoya en ellas:
falsos negativos del centinela sobre propiedades transicionadas, pseudo-elementos
y componentes compuestos que no ve, la sonda que no pasa el ratón por un `<tr>`,
y la lección general —**una sonda sobre una parte que la demo no monta compara
CERO valores y pasa**—, con la lista de partes condicionales afectadas.
El handoff gana la regla para las sesiones del eje: si una incidencia mueve
píxel o toca morfo, se mide, se anota ahí y se sigue; no se arregla dentro del
commit del componente.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **La demo del bloque F2-A ya enseña los tokens** (`TokensPanel`), pero los
~155 componentes sin contrato siguen sin superficie donde verlos hasta que se
tokenicen.
feat(calendar-surface): la capa que ya existía, con nombre, casa y su agujero tapado
Firma 1 del acta, diseño presentado y firmado. **La medición desmontó el
encargo**: el handoff la vendía como una capa NUEVA de ~220 knobs, y la capa ya
existía de hecho, sin nombre. `--calendar-*` se emite en `:root` (76 claves) y
sus consumidores no acuñan NADA — medido: `range-calendar` 114 referencias
prestadas y 0 propias, `month-grid` 77/0, `year-grid` 77/0, y lo único ajeno
que leen es sistema puro (`--focus-ring-*`, `--state-hover`). Su 0 % era el
artefacto de `listbox` (§13), pero total.
**`lib/calendar-surface.css`** (hook `data-calendar-surface`):
- cuatro coordenadas por talla — `padding`, `control-size`, `day-size`,
`font-size` — xs..lg, porque la familia NO tiene xl, y con la celda DOS pasos
por debajo del bundle de control. Esa desviación estaba escondida en cuatro
bloques `[data-size]` idénticos, uno por receta, cada uno puenteando a un
privado con otro nombre; ahora se lee en un sitio.
- la FORMA del anillo de evento y de la marca de festivo.
**Capa HÍBRIDA, y es lo que la distingue de sus hermanas**: `list-surface` y
`viewport-placement` componen primitivos del sistema, así que declaran sus
públicos en el fichero y no tienen entrada de receta. Ésta no puede: su
vocabulario son 76 claves SEMÁNTICAS que un tema alcanza una a una por config,
así que la entrada `calendar` de `recipes/base.ts` pasa a ser la de la FAMILIA
y la capa posee sólo lo que una entrada de receta no sabe expresar.
**El defecto que la justificaba, medido**: `--calendar-event-shadow` y
`--calendar-day-holiday-shadow` se emitían con ámbito `[data-calendar]`
(audit B.2 los host-scopeó por buenas razones) mientras `range-calendar`,
`month-grid` y `year-grid` los leían desde hosts que nunca llevan ese atributo:
variable VACÍA, `box-shadow` inválido en computed, **el anillo sema de
`commit-select` / `commit-set` no pintaba jamás en tres componentes**. Un token
prestado cuyo ÁMBITO no te cubre no es un préstamo, es un agujero silencioso, y
ningún guard lo veía. Ahora la forma vive en la capa y el acento entra por
`--_calendar-surface-accent`, que cada superficie alimenta con su propio
forward de paleta THM-2.
**Siete wrappers estampan, no cuatro** — y esto casi se me cuela: `DatePicker`
y `DateRangePicker` renderizan la superficie soma por sus PROPIOS wrappers
(`date-picker-calendar`, `-month-view`, `-year-view`,
`date-range-picker-calendar`) y un panel portalado no hereda nada del root del
picker. Con sólo los cuatro standalone sellando, ambos quedaban con
`--calendar-padding` VACÍA y el panel a padding 0 (medido). La tentación era
enganchar la capa a las cuatro identidades de componente: eso viola la regla 1
de capas compartidas, y la respuesta correcta es un sello por wrapper.
computed 0 diffs en range-calendar (19.285 valores) · month-grid (3.451) ·
year-grid (3.451) · date-picker (464) · date-range-picker (406).
`calendar` da 12, y son del INSTRUMENTO: dos corridas del MISMO
código dan 24 en los mismos nodos y las mismas dos propiedades.
Los «missing node» son el propio sello entrando en la clave.
**Una incidencia nueva, medida y NO arreglada aquí** (§13): el font-size de los
selectores month/year es una moneda al aire —
`[data-calendar-month-select][data-button]` (0,2,0) empata con
`[data-popover-trigger]:not([data-archetype='field-trigger'])` (0,2,0), la
MISMA regla de popover que dejó muerto el cromo de `gradient-picker`, y gana la
hoja que cargue después: 16px o 14px según la recarga. Arreglarlo fija el píxel
en un lado ⇒ decisión.
De paso, `calendar-select.css` deja de puentear un privado que sólo
`[data-calendar]` declaraba: en range / date-range corría SIEMPRE por el
fallback, clavado a md fuera cual fuera la talla.
**El censo deja de penalizar hacer lo correcto**: `LAYER_VOCABULARY` en
`theming-census.ts`, mismo precedente que D-TH.2-b con `--style-*`. Global
43 % → **45 %**, `calendar` 75 % → **84 %**. `list-surface` NO se registra: sus
consumidores puentean por privados, otra forma, y mueve diez componentes de
golpe. Y aparece el techo de debajo, anotado: a los tres consumidores sólo les
quedan los forwards de paleta THM-2 —que el censo cuenta como `private` en TODO
el catálogo— así que siguen leyendo 0 %.
`recipe-css-contract` aprende que una CAPA también declara públicos (antes sólo
miraba la receta, y una capa que comparte prefijo con un componente la hacía
fallar). Sin debilitarla: un nombre que no declara nadie sigue en rojo.
eidos-lint 0 invalid (calendar 29/8 · range-calendar 41/12 · los grids 23/5) ·
audit --only calendar PASS · vitest eidos 434/435 (el rojo conocido) ·
rtl 0/181 · docs 0/813 · check 0 errores en tocados · prettier: revertido el
reformateo en masa que se coló en cuatro README, el test y el censo (churn
ajeno, no mío)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Lo que la capa `calendar-surface` añadió (2026-08-21):
- **⚠ El font-size de los selectores month/year del calendario es una MONEDA AL
AIRE.** `[data-calendar-month-select][data-button]` (0,2,0) empata con
`[data-popover-trigger]:not([data-archetype='field-trigger'])` (0,2,0) — la
MISMA regla de `popover.css` que dejó muerto el cromo entero del trigger de
`gradient-picker` — y gana la hoja que cargue después: dos corridas del mismo
código dan 16px o 14px en los mismos nodos (24 diffs de instrumento, medidos
con la sonda dos veces). El pin del calendario existe justamente para que
caja, etiqueta y chevron lean UNA talla, así que perderlo es visible.
Arreglarlo es subir el peso del pin de forma determinista — decisión, porque
fija el píxel en un lado. Segunda ocurrencia de la clase §12.3 (empate por
orden de carga) y segunda víctima de la misma regla de popover.
- **Los tres consumidores de la capa siguen leyendo 0 % de alcance**, ahora por
los forwards de paleta THM-2 — ver el punto del censo más arriba.
uix(theming): revisión adversarial de F2-A — 8 hallazgos de 34, y nace R-5.4
Pase escéptico del bloque (§7.7, molde Sidebar) con medición propia: worktree
con SOLO el CSS revertido a la base del bloque (DOM de demo constante — prueba
más dura que la del autor), sonda x2 por componente para fijar el suelo de
ruido. 34 hipótesis · 26 refutadas · 8 reales.
Lo que NO se pudo romper: el «default idéntico» aguanta en los ocho (0 diffs;
command 8.091 · table 13.050 · media-player 3.277 · gradient-picker 609 ·
listbox 2.813 · carousel 2.842; los 3 de tree-grid y los 5 de feed salen
idénticos corriendo DOS veces el mismo código → instrumento); 0 tokens
huérfanos de 336; el bundle ES 1:1 con los primitivos; el «verificado a mano»
de gradient-picker era verdad (15/16 alcanzan al montar); sin doble animación;
eidos-lint 0 invalid.
Los 4 de código, arreglados con diff de computed VACÍO (re-medido: command
8.091 y carousel 2.842 valores, 0 diffs):
1. command: el re-point de `--command-radius` dentro del Dialog era una
declaración muerta con comentario falso — la misma regla pone el radio a 0
y el input lee `--command-input-radius`, nunca este token (medido:
`--dialog-content-radius: 9999px` no movía NADA; `--command-radius: 77px`
movía la paleta inline y no la del dialog). Retirado; comentarios y README
reescritos con lo medido.
2. carousel: la regla vertical del indicador activo llevaba el `2.25` a pelo
mientras la horizontal leía `active-indicator-scale` — el token alcanzaba
media superficie (scale:6 → horizontal 18→48px, vertical 18→18px). Ahora
lo leen las dos.
3. carousel: el trío `item-gap*` no podía ganar NUNCA — soma estampa `gap`
INLINE desde la prop (lo necesita para el flex-basis; reglas casadas por
CDP: la de la receta y encima INLINE gap:0rem). Retirados los 3 tokens y
las 2 declaraciones muertas (la base y el gap:0 de ghost): 0 diffs, que es
la prueba. Canal de valor de soma, como --gp-current-gradient. Censo
carousel 35 knobs · 23 públicos · 77 % · 27 claves; global 43 % intacto
(5.190 · 2.102).
4. table: `striped-row-bg` no alcanza NADA en la demo publicada —
`:nth-of-type(even)` cuenta los <tr> de RowDetail y las filas de datos
quedan impares (medido tr a tr: diez inmunes). Arreglarlo mueve píxel →
PENDIENTE DE FIRMA (candidato `:nth-child(even of [data-table-row])`);
adjudicado en el ledger mientras tanto.
El instrumento cargaba 22 falsos negativos de 26 «no effect», con tres causas
medidas, y la cuarta era de la sonda:
- el paso de abrir hacía CLIC en el input, y un clic de ratón sobre un input
de texto SÍ casa `:focus-visible` — la regla de foco pintaba el borde y el
token de reposo leía muerto (era el «falso negativo sin causa» de §13);
- el override se escribía solo en `[data-{c}]`, y las partes que cuelgan de
`{c}-root` no lo recibían;
- `::before`/`::after` eran invisibles (el aro de buffering, el spinner);
- la no-determinación de feed no era una animación sin localizar: era la demo
aún CARGANDO — el auto load-more mantiene [data-busy] ~3 s y la firma
`commit-settle` anima box-shadow sobre el nodo medido. Con la espera de
asentado: 0 diffs y 0 nodos ausentes sobre 6.264 valores, dos corridas.
Con eso NACE EL GUARD R-5.4 (la deuda que §13 pedía desde gradient-picker, y
que carousel convirtió en patrón — un estilo inline de otra capa es invisible
para todo análisis estático): `npm run theming:sentinel -- <c> <url>`
(scripts/theming-sentinel.ts, promovido de `__`; blur tras abrir, override en
todos los nodos del componente, pseudos, pase de hover, settle) + ledger
scripts/theming-sentinel-exceptions.ts con UNA razón medida por token — muerto
sin adjudicar = exit 1, excepción STALE = aviso. Los ocho componentes en
verde: command 50/57 · table 41/45 · media-player 46/61 · tree-grid 45/47 ·
gradient-picker 9/25 · listbox 20/21 · carousel 26/27 · feed 43/50, con 46
excepciones adjudicadas. El guard ya me corrigió a mí: cinco entradas mías
salieron STALE (los control-height-* de media-player SÍ alcanzan) y las borró;
y dos muertos nuevos los produjeron los propios arreglos (el blur apaga
focus-input-border; el settle desmonta el spinner de feed) — adjudicados con
su porqué. Doctrina en recipe-contract §4.
También: tres cifras de especificidad de next-features §12 recontadas (la
banda de tree-grid es (0,8,0); la tinta seleccionada del arquetipo es (0,4,0);
y el `item` de table vive en la fila Y EN LA CELDA — elementsFromPoint pone el
velo en la celda), el comentario de listbox que mandaba pisar el privado
`--_listbox-max-height` apunta ahora al público, y quedan registrados en §13
los dos `header-z` con literal '2' y el spinner de media-player a 720ms
mientras el gemelo de feed lleva tokens.
Guards: censo global 43 % intacto · audit --only command+carousel PASS ·
eidos-lint 0 invalid (command 16/12, carousel 29/9, listbox 12/7) · vitest
eidos 434/435 (el rojo es el conocido skin-media-player) · rtl 0/180 · docs
0/813 · check 0 errores en tocados (72 globales de otras sesiones) · prettier
limpio en lo nuevo (los desvíos de command.css/carousel.css ya estaban en
HEAD y no se tocan).
Pendiente de firma (CONTINUE §«PENDIENTE DE FIRMA»): el striped inerte, el
arquetipo en fila+celda, y que soma lea el gap de un token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Lo que la revisión adversarial de F2-A añadió (2026-08-21):
fix(table): el striped cuenta filas de DATOS — la banda vuelve a existir
Firmado por el autor tras la revisión adversarial: el hallazgo 4 (679dfdb29)
midió que `:nth-of-type(even)` cuenta los <tr> hermanos, y con un RowDetail
intercalado tras cada fila las filas de datos quedan todas impares — la demo
publicada no bandeaba NADA (diez tr inmunes al token, medido uno a uno).
El selector pasa a `:nth-child(even of [data-table-row])`, con dos decisiones
de peso dentro:
- **conserva el (0,6,0) del viejo**: el pseudo más el argumento `of` pesan
exactamente lo que pesaba `[data-table-row]:nth-of-type()`, así que el hover
—posterior en el fichero— sigue ganando su empate y la banda NO mata el
hover. Subirlo de peso habría importado a table el defecto §12.4 de
tree-grid.
- **`:where(:not([data-selected]))` a peso CERO**: la fila seleccionada
siempre estuvo prometida al acento («Overrides hover + striped», dice la
regla de al lado a (0,3,0)) pero la banda vieja, de haber casado, se la
habría comido por especificidad. El :where excluye sin pesar.
Medido (sonda antes/después, 13.195 valores en 7 estados): **12 diffs
exactos** — 2 filas pares × 6 estados, backgroundColor 0.9911 → 0.9551 — y
nada más se movió. Interacciones verificadas en vivo: selected par conserva
el acento (0.9588 con croma) · hover sobre par pinta su tinte (0.931).
El ledger de R-5.4 pierde la excepción de `striped-row-bg` (era «genuinely
unreachable, pending signature»): el guard adjudica ahora table 42/45 con 3
excepciones, exit 0, sin STALE.
eidos-lint 0 invalid · audit --only table PASS · censo table 86 % intacto ·
vitest eidos 434/435 (el rojo conocido) · rtl 0 · docs 0 · check 0 en
tocados · prettier limpio en lo tocado (el wrap de la regla del hover ya
estaba desviado en HEAD y no se toca)
Incidencia operativa, reparada y anotada: al retirar el worktree de medición
de la revisión, `git worktree remove --force` recursó A TRAVÉS de la junction
de node_modules y borró parte del real (node_modules/.bin incluido) antes de
fallar; `npm install` lo reconstruyó y la suite volvió a 434/435. El árbol
git nunca se tocó.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- ~~**PENDIENTE DE FIRMA — el `striped` de `table` es inerte con RowDetail.**~~
**FIRMADO Y EJECUTADO 2026-08-21** : `:nth-child(even of [data-table-row])`
cuenta filas de DATOS y la paridad ya no cae en los `<tr>` de detalle (la
demo no bandeaba NADA; ahora las filas 2 y 4 tiñen — 12 diffs medidos, 2
filas × 6 estados, y nada más se movió en 13.195 valores). El selector
conserva el (0,6,0) del viejo para que el hover siga ganando su empate (no
se importa el defecto §12.4 de tree-grid) y un `:where(:not([data-selected]))`
de peso cero deja el acento seleccionado al mando — verificado los tres.
La excepción del ledger se retiró; `striped-row-bg` adjudica VIVO (42/45).
uix(theming): revisión adversarial de F2-A — 8 hallazgos de 34, y nace R-5.4
Pase escéptico del bloque (§7.7, molde Sidebar) con medición propia: worktree
con SOLO el CSS revertido a la base del bloque (DOM de demo constante — prueba
más dura que la del autor), sonda x2 por componente para fijar el suelo de
ruido. 34 hipótesis · 26 refutadas · 8 reales.
Lo que NO se pudo romper: el «default idéntico» aguanta en los ocho (0 diffs;
command 8.091 · table 13.050 · media-player 3.277 · gradient-picker 609 ·
listbox 2.813 · carousel 2.842; los 3 de tree-grid y los 5 de feed salen
idénticos corriendo DOS veces el mismo código → instrumento); 0 tokens
huérfanos de 336; el bundle ES 1:1 con los primitivos; el «verificado a mano»
de gradient-picker era verdad (15/16 alcanzan al montar); sin doble animación;
eidos-lint 0 invalid.
Los 4 de código, arreglados con diff de computed VACÍO (re-medido: command
8.091 y carousel 2.842 valores, 0 diffs):
1. command: el re-point de `--command-radius` dentro del Dialog era una
declaración muerta con comentario falso — la misma regla pone el radio a 0
y el input lee `--command-input-radius`, nunca este token (medido:
`--dialog-content-radius: 9999px` no movía NADA; `--command-radius: 77px`
movía la paleta inline y no la del dialog). Retirado; comentarios y README
reescritos con lo medido.
2. carousel: la regla vertical del indicador activo llevaba el `2.25` a pelo
mientras la horizontal leía `active-indicator-scale` — el token alcanzaba
media superficie (scale:6 → horizontal 18→48px, vertical 18→18px). Ahora
lo leen las dos.
3. carousel: el trío `item-gap*` no podía ganar NUNCA — soma estampa `gap`
INLINE desde la prop (lo necesita para el flex-basis; reglas casadas por
CDP: la de la receta y encima INLINE gap:0rem). Retirados los 3 tokens y
las 2 declaraciones muertas (la base y el gap:0 de ghost): 0 diffs, que es
la prueba. Canal de valor de soma, como --gp-current-gradient. Censo
carousel 35 knobs · 23 públicos · 77 % · 27 claves; global 43 % intacto
(5.190 · 2.102).
4. table: `striped-row-bg` no alcanza NADA en la demo publicada —
`:nth-of-type(even)` cuenta los <tr> de RowDetail y las filas de datos
quedan impares (medido tr a tr: diez inmunes). Arreglarlo mueve píxel →
PENDIENTE DE FIRMA (candidato `:nth-child(even of [data-table-row])`);
adjudicado en el ledger mientras tanto.
El instrumento cargaba 22 falsos negativos de 26 «no effect», con tres causas
medidas, y la cuarta era de la sonda:
- el paso de abrir hacía CLIC en el input, y un clic de ratón sobre un input
de texto SÍ casa `:focus-visible` — la regla de foco pintaba el borde y el
token de reposo leía muerto (era el «falso negativo sin causa» de §13);
- el override se escribía solo en `[data-{c}]`, y las partes que cuelgan de
`{c}-root` no lo recibían;
- `::before`/`::after` eran invisibles (el aro de buffering, el spinner);
- la no-determinación de feed no era una animación sin localizar: era la demo
aún CARGANDO — el auto load-more mantiene [data-busy] ~3 s y la firma
`commit-settle` anima box-shadow sobre el nodo medido. Con la espera de
asentado: 0 diffs y 0 nodos ausentes sobre 6.264 valores, dos corridas.
Con eso NACE EL GUARD R-5.4 (la deuda que §13 pedía desde gradient-picker, y
que carousel convirtió en patrón — un estilo inline de otra capa es invisible
para todo análisis estático): `npm run theming:sentinel -- <c> <url>`
(scripts/theming-sentinel.ts, promovido de `__`; blur tras abrir, override en
todos los nodos del componente, pseudos, pase de hover, settle) + ledger
scripts/theming-sentinel-exceptions.ts con UNA razón medida por token — muerto
sin adjudicar = exit 1, excepción STALE = aviso. Los ocho componentes en
verde: command 50/57 · table 41/45 · media-player 46/61 · tree-grid 45/47 ·
gradient-picker 9/25 · listbox 20/21 · carousel 26/27 · feed 43/50, con 46
excepciones adjudicadas. El guard ya me corrigió a mí: cinco entradas mías
salieron STALE (los control-height-* de media-player SÍ alcanzan) y las borró;
y dos muertos nuevos los produjeron los propios arreglos (el blur apaga
focus-input-border; el settle desmonta el spinner de feed) — adjudicados con
su porqué. Doctrina en recipe-contract §4.
También: tres cifras de especificidad de next-features §12 recontadas (la
banda de tree-grid es (0,8,0); la tinta seleccionada del arquetipo es (0,4,0);
y el `item` de table vive en la fila Y EN LA CELDA — elementsFromPoint pone el
velo en la celda), el comentario de listbox que mandaba pisar el privado
`--_listbox-max-height` apunta ahora al público, y quedan registrados en §13
los dos `header-z` con literal '2' y el spinner de media-player a 720ms
mientras el gemelo de feed lleva tokens.
Guards: censo global 43 % intacto · audit --only command+carousel PASS ·
eidos-lint 0 invalid (command 16/12, carousel 29/9, listbox 12/7) · vitest
eidos 434/435 (el rojo es el conocido skin-media-player) · rtl 0/180 · docs
0/813 · check 0 errores en tocados (72 globales de otras sesiones) · prettier
limpio en lo nuevo (los desvíos de command.css/carousel.css ya estaban en
HEAD y no se tocan).
Pendiente de firma (CONTINUE §«PENDIENTE DE FIRMA»): el striped inerte, el
arquetipo en fila+celda, y que soma lea el gap de un token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **El gap entre diapositivas de `carousel` es un canal de valor de soma.**
Soma lo estampa INLINE desde la prop `gap` (necesita el número para el
`flex-basis` ), así que ninguna declaración de receta puede ganarle — el trío
`item-gap*` se retiró con 0 diffs. Si algún día debe ser temable, es soma
quien tendría que leer un token; decisión de otra capa.
- **Dos `header-z` llevan el literal `'2'` ** (`table`, `tree-grid` ) mientras los
otros catorce `*-z` del catálogo leen la escalera `--z-index-*` . Alinearlos es
decidir qué peldaño significa «cabecera pegajosa dentro de un scroller».
- **El spinner de `media-player` conserva `720ms` a pelo** mientras el spinner
idéntico de `feed` ganó dos tokens públicos de duración
(`sentinel-spinner-duration` / `-reduced-duration` ). El mismo trato o una
anotación `functional:` , pero no las dos varas.
uix(grid-list): temable — 0 % → 85 %, y una selección que nunca pintó
38 claves nuevas en el bloque `grid-list` de `recipes/base.ts` (que ya existía
con los dos forwards de paleta: se FUSIONA, no se añade un segundo). Alcance
del censo 0 % → 85 % (22 de 26 puntuables); global 45 % → 46 %, sin contrato
49 → 48.
El eje `size` se estampa en el PROVIDER —el wrapper de eidos no tiene parte
`root`—, así que los tres nombres resueltos van SIN `parts`: molde `command`,
no `table`/`tree-grid`. Fuera los cuatro bloques `[data-size]` del CSS; los
emite el TSC. Altura de fila 1:1 con el bundle, `row-padding-inline` con ritmo
propio, y la tipografía un paso por debajo de md en adelante — verbatim, como
table y tree-grid.
LO QUE LA MEDICIÓN CAMBIÓ (la §4 generada tenía un error de doctrina y dos
defectos que sólo aparecieron midiendo; el §5 Veredicto se escribió con la
sonda delante, porque estaba vacío):
1. La selección de fila NUNCA HA PINTADO. La receta seleccionaba
`[data-grid-list-row][data-selected]`, un atributo que ni el morfo declara
ni soma estampa (estampa `data-state='selected'` + `aria-selected`): la
regla no casaba jamás y su acento `--_grid-list-palette-element` no llegó
nunca al píxel — una fila seleccionada muestra el velo NEUTRO del
arquetipo, indistinguible del hover (capturado). Retirada la declaración
muerta y su forward huérfano: 0 diffs, que es la prueba. Repararlo mueve
píxel y choca con §12.5 (el arquetipo fija también `color`) → §13, espera
firma. El acento del checkbox (`_palette-solid`) sí vive y se conserva.
2. NO se acuña `hover-row-bg` aunque la propuesta lo pedía. La fila es
`archetype: 'item'`: el velo del sistema la pinta a especificidad plena y
el plano de la receta pierde — medido, el hover pinta DOS VECES sobre el
MISMO nodo (a diferencia de table/tree-grid, donde velo y plano caen en
nodos distintos). Sería un token que no puede ganar; mismo criterio que
listbox.highlighted → §12.7. Y fila Y celda llevan ambas `item`, así que
bajo el puntero se apilan tres capas.
3. «Consume la capa compartida `menu-indicator`» es FALSO. Sale de
`SHARED_LAYERS` (`theming-census.ts:432`), pero esa capa no tiene un solo
selector de grid-list, la receta no la importa y soma no renderiza
indicator: el checkbox ESPEJA el visual pintándolo por su cuenta. Entrada
desviada del censo → §13 (listbox / menubar / navigation-menu están en la
misma entrada y tampoco aparecen en el fichero).
Nombres, sobre la propuesta: `selection-checkbox-{width,height}` son UN knob →
`selection-checkbox-size`; `on-selection-checkbox-bg` → `checked-selection-checkbox-fg`
(es la tinta del glifo, y `on-` es prefijo reservado); `border` partido en
`border-width` + color (molde `command.input-border*`), que el checkbox
comparte; `padding` uniforme se queda `padding` (precedente
`command.viewport-padding`); `max-block` entra al contrato (molde
`command.viewport-max-block`).
Artefactos y gates:
- Sonda antes/después = 0 diffs en 6.090 valores × 7 estados. Determinismo
verificado: dos corridas del mismo código, 0 diffs.
- R-5.4: 37/38 tokens mueven un computed. `invalid-border` adjudicado EN EL
LEDGER con su razón medida (la demo arranca con su control en off; activado
a mano alcanza: oklch(0.9555 0.0207 13.86) → rgb(1,2,3)).
- censo --only 85 % · component:audit PASS (R-5.3 incluida) · eidos-lint
invalid 0, class-hooks 0 · suite eidos sin rojos nuevos (el único es el
conocido skin-media-player) · rtl:check 0 · docs:check 0 · check por fichero
limpio (72 globales, ninguno mío).
- README con su «Talla y tema» y tab `Tokens` en la demo (38 claves, leídas en
vivo). El tab `Recipe` documentaba la regla retirada: corregido.
Las fichas de calendar / carousel / command / month-grid / range-calendar /
table / year-grid entran porque `--report` regenera el informe entero: sólo
actualizan NÚMEROS DE LÍNEA de commits ya hechos que no lo regeneraron. Cero
cambios de alcance. Las otras 161, que sólo cambiaban la fecha, se revirtieron.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
Lo que `grid-list` añadió (2026-08-21):
- **⚠ PENDIENTE DE FIRMA — la selección de fila de `grid-list` nunca ha
pintado.** La receta seleccionaba `[data-grid-list-row][data-selected]` , un
atributo que ni el morfo declara ni soma estampa: soma estampa
`data-state='selected'` + `aria-selected='true'` . La regla no casaba jamás,
así que su acento (`--_grid-list-palette-element`) no llegó nunca al píxel y
una fila seleccionada muestra sólo el velo NEUTRO del arquetipo —
visualmente indistinguible de una fila con el ratón encima (capturado). La
declaración muerta y su forward huérfano se retiraron en el commit de
tokenización con **0 diffs de computed** , que es la prueba de que estaban
muertos. **Repararlo mueve píxel** (re-apuntar a `[data-state='selected']`
haría aparecer un acento que el componente nunca tuvo), y choca de frente con
§12.5: el arquetipo `item` fija también `color` a (0,4,0) sobre el estado
seleccionado. Es la misma decisión del velo, no una corrección al paso.
El acento del SelectionCheckbox (`_palette-solid`) sí vive y se conservó.
- **`SHARED_LAYERS` declara a `grid-list` consumidor de `menu-indicator` , y no
lo es.** `theming-census.ts:432` lo lista, pero `lib/menu-indicator.css` no
tiene un solo selector que apunte a grid-list (sus 134 líneas targetean
`[data-context-menu-*]` / `[data-dropdown-menu-*]` ), la receta no importa la
capa y soma no renderiza indicator: el checkbox **espeja** ese visual
pintándolo por su cuenta. La entrada hace que el informe generado escriba
«Consume la capa compartida `menu-indicator` » en la §4 de la ficha, que es
falso y dirige mal a quien la lea. Revisar las seis entradas de esa lista
contra los selectores reales de cada capa (`listbox`, `menubar` y
`navigation-menu` están en la misma entrada y tampoco aparecen en el fichero).
uix(textarea): temable — 0 % → 80 %, y el contador que casi pierde su 0.85
37 claves en el bloque `textarea` de `recipes/base.ts` (fusionadas con el
forward de paleta que ya estaba). Censo 0 % → 80 %, con **cero globales y cero
literales de valor**: los 16 privados leen ya su público. Global 46 %, sin
contrato 48 → 47.
LO QUE LA MEDICIÓN CAMBIÓ. La §4 generada proponía 35 tokens; cuatro eran
colisiones y uno habría movido el default:
1. `count-font-size-{k}` × 5 BORRABA el factor 0.85. El CSS calcula
`calc(var(--_textarea-font-size) * 0.85)` y la propuesta ponía el tipo del
input crudo en las cinco tallas: el contador habría crecido un 18 %.
D-TH.5 lo prohíbe. Queda en UN token con el `calc` sobre el público del
input — la escala se escribe una vez y la talla la resuelve el input. Pide
`scope: 'host'`: emitido en `:root` su dependencia no existe, y el contrato
de scope de la receta lo caza (lo cazó).
2. `input-fg` ⚠ eran DOS knobs (texto y placeholder) → `input-fg` +
`input-placeholder-fg`. Igual `count-fg` → `count-fg` + `overflow-count-fg`.
3. `input-border` ⚠ NO eran dos knobs sino UNA variable con su swap de paleta,
y el nombre estaba mal: es el borde de FOCO → `focus-input-border`. El de
reposo es otro token, que la propuesta ni listó por viajar en el atajo.
4. `ghost-input-border` se RETIRA: la regla ghost lee ese mismo conmutador —
sería un valor con dos nombres.
Bundle, medido: la tipografía casa 1:1 en las cinco (lee `--size-{k}-font-size`);
el spacing NO — `padding-inline` casa salvo en xs (8px vs 6px) y `padding-block`
sólo en xs. Verbatim y documentado: un campo multi-línea respira más, y el
spacing queda fuera del guard del bundle. Resueltos sin `parts` (el `data-size`
va en `[data-textarea]` y el input hereda); fuera los cuatro `[data-size]`.
DOS ARREGLOS DEL INSTRUMENTO, los dos por medición y no por sospecha:
- **El guard medía con el PUNTERO encima.** `reopen()` corre antes de CADA
token y hace clic en `[data-{c}-input]` cuando el componente no tiene parte
`content`; el `blur()` de la revisión adversarial quitaba el foco pero no el
puntero, así que `:hover` casaba toda la corrida y toda regla de hover pisaba
a sus vecinas. Costaba TRES falsos negativos aquí. Con `mouse.move(0,0)`
tras cada blur, `input-border` revive solo (31 → 32).
- **`::placeholder` SÍ se lee con `getComputedStyle`.** La nota de §13 que
decía lo contrario nunca se comprobó; medido, el color centinela vuelve tal
cual. Entra en el snapshot (32 → 33) y deja STALE la entrada
`command.input-placeholder-fg`, que el guard señaló y se ha borrado.
Corridos los DIEZ componentes con ledger tras los dos arreglos: ningún otro
cambia, un único STALE, el previsto.
Y un hallazgo de píxel que NO se toca aquí (→ next-features §13): el borde de
foco sólo se ve con Tab. Las tres reglas de `border-color` del input están
ordenadas al revés de lo que significan — hover (0,4,0) > foco (0,3,0) >
invalid (0,2,0) — y hacer clic deja el puntero encima por definición, así que
gana el hover; un campo inválido pierde su borde rojo al enfocarlo o al pasar
el ratón. El anillo de foco del sistema sí se ve siempre, así que no es fallo
de accesibilidad. Arreglarlo mueve píxel.
El 20 % que falta son las dos declaraciones que consumen
`--_textarea-border-focus`, privado a propósito: es el conmutador de una
variable con dos fuentes (el público y el forward THM-2), y aplanarlo exigiría
duplicar la regla por color. Mismo techo que `listbox` paga por consumir bien
su capa. Mi propio veredicto predecía ~100 % y la medición lo corrigió a 80 %.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 1.247 valores × 8 estados (el 0.85 intacto,
que era el riesgo). Determinismo verificado: dos corridas, 0 diffs.
- R-5.4: 33/37; los 4 mudos adjudicados en el ledger con su medición (foco,
invalid y las dos del contador rebasado — estados que la demo no monta).
- censo --only 80 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0
· suite eidos sin rojos nuevos (el conocido skin-media-player) · rtl:check 0
· docs:check 0 · check por fichero limpio (72 globales, ninguno mío).
- README con su «Talla y tema» y tab `Tokens` en la demo (37 claves; el panel
muestra el contador computando `calc(calc(1rem * 1) * 0.85)`).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
Lo que `textarea` añadió (2026-08-21):
- **⚠ El borde de foco de `textarea` no se ve al hacer CLIC — sólo con Tab.**
Medido: la regla de hover `[data-textarea-input]:hover:not([data-disabled]):not([data-readonly])`
pesa ** (0,4,0)** y la de foco `[data-textarea][data-focused] [data-textarea-input]`
** (0,3,0)**, así que con foco y puntero a la vez gana el hover — y hacer clic
deja el puntero encima por definición. Verificado con las dos reglas
apuntando a colores distintos: sale el del hover. Debajo, `[data-invalid]`
pesa ** (0,2,0)**: un campo inválido pierde su borde rojo tanto al enfocarlo
como al pasar el ratón. El anillo de foco del SISTEMA (`outline`) sí se ve
siempre, así que no es un fallo de accesibilidad, pero el orden de las tres
reglas de `border-color` está invertido respecto a lo que significan.
Arreglarlo mueve píxel: no entra en el commit de tokenización.
- ~~**El guard mide con el PUNTERO encima del componente.**~~ **ARREGLADO
2026-08-21**: `reopen()` corre antes de CADA token y hace clic en
`[data-{c}-input]` cuando el componente no tiene parte `content` — el
`blur()` que añadió la revisión adversarial quita el foco pero **no el
puntero**, así que `:hover` seguía casando durante toda la corrida y toda
regla de hover pisaba a sus vecinas de reposo. Es la mitad que aquel arreglo
dejó atrás. En `textarea` costaba tres falsos negativos
(`input-border`, `focus-input-border` , `invalid-input-border` ); con
`page.mouse.move(0, 0)` tras cada blur, `input-border` revive solo. Corridos
los diez componentes con ledger: ningún otro cambia, un único STALE
(`command.input-placeholder-fg`, por el arreglo del `::placeholder` ).
uix(card-group): temable — 0 % → 90 %, y dos privados abreviados que mueren
20 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → 90 %, cero
globales y cero privados. Global 47 %, sin contrato 45 → 44.
Es un CASCO de composición y su propia cabecera ya lo decía: compone
ToggleGroup, Collapsible, Card y Button, y la receta posee «ONLY the genuinely
CardGroup-level visual». Eso acota el contrato antes de nombrar nada — el cromo
de las tarjetas es de Card, el del título de disclosure es de Button.
1. **`--_cg-pad` y `--_cg-card-radius` eran privados ABREVIADOS** (§6 r5 los
prohíbe: `cg` por `card-group`), el mismo defecto que los `--_mp-*` de
media-player. Pasar a públicos con el nombre completo es a la vez el
renombrado que la regla pedía y la tokenización. La §3 de la ficha decía
«la receta no declara privados propios»: los declaraba en su bloque raíz
(`card-group.css:15-16`), y el escáner no los veía por buscarlos con el
prefijo completo del componente.
2. **`--shape-outer-radius` NO se acuña: se CALCULA.** La receta lo compone
como `card-radius + padding` para que las tarjetas aniden concéntricamente
(§30). Un tema mueve las dos coordenadas y el radio exterior se recompone
solo; el censo ya lo clasifica como sistema.
3. **El eje `size` es sm|md|lg, no las cinco tallas**
(`Extract<Size, 'sm' | 'md' | 'lg'>`): seis coordenadas por talla, no diez.
Escribir xs/xl habría sido vocabulario muerto.
4. Los tres `padding` son shorthand con una parte fija y otra variable; lo que
varía y no es el inset del grupo es el hueco inferior →
`title-padding-block-end` y `description-padding-block-end`.
5. El chevron es geometría propia (dos bordes girados 45°): `chevron-size`
cubre las dos dimensiones y `chevron-width` el trazo. Su `currentColor`
hereda del título a propósito y no es knob.
6. Los `font-size` van al BUNDLE (`--size-{k}-font-size`, alias 1:1) y no al
primitivo crudo — misma trampa que cayó en code-block: la propuesta copia el
CSS verbatim y hereda el incumplimiento cuando el valor de partida ya
violaba `recipe-css-contract`. Aquí se aplicó de entrada.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 2.581 valores × 7 estados. Determinismo
verificado: dos corridas, 0 diffs.
- R-5.4: 15/20. Los cinco mudos son los del encabezado ESTÁTICO, que la demo no
monta (0 nodos `[data-static]` contra 1 `[data-button]`, CONTADOS); forzado
`data-static` alcanzan los cinco, y cada uno lleva su medición en el ledger.
El hueco de demo queda registrado en next-features §13, misma clase que el
`kind='month'|'year'` de date-range-picker.
- censo --only 90 % · component:audit PASS · eidos-lint invalid 0, class-hooks
0 · suite eidos sin rojos nuevos (el conocido skin-media-player) · rtl:check
0 · docs:check 0 · check por fichero limpio.
- README con su «Talla y tema» y tab `Tokens` en la demo (20 claves, en vivo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
- **Hueco de demo: `card-group` no monta su encabezado ESTÁTICO.** La página
renderiza el título sólo en su forma de disclosure (`[data-button]`), así que
la mitad `[data-static]` del cromo del título —tinta, familia, tamaño,
interlineado y el hueco inferior— no se ve ni se mide: cinco tokens
adjudicados por eso. Misma clase que `date-range-picker` con
`kind='month'|'year'` (norma N-6).
uix(picker-shell): temable — 0 % → 76 %, y el componente que el instrumento no veía
31 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → 76 %. Global
48 %, sin contrato 43 → 42.
La cola avisaba «es CAPA de los pickers: mirar antes qué posee y qué presta».
Mirarlo cambió tres cosas del encargo:
1. **NO es una capa compartida: es un COMPONENTE.** A diferencia de
list-surface o calendar-surface no hay hook de capa ni vocabulario prestado
— color-picker, date-picker y chronos importan sus `.svelte` directamente.
No toca `LAYER_VOCABULARY` y el censo ya lo puntuaba bien.
2. **Su eje de talla no es `data-size`, es `data-picker-size` sobre
`[data-popover-content]`**, un ancestro PORTALADO. El TSC no puede emitir esa
cascada (`scope: 'size:xs'` generaría `[data-picker-shell][data-size='xs']`,
que no casa nunca), así que las coordenadas por talla quedan como públicos
PLANOS y la receta las consume desde sus propios selectores. Es el único
componente del eje donde el patrón de nombres resueltos no aplica; la regla
base es el default `sm` y `md` sólo re-declara lo que cambia. La fila de
tiempo lleva un TERCER eje, `data-size` sobre su propia clase.
3. **El instrumental del eje estaba CIEGO aquí, y la sonda genérica devolvió 0
nodos** — el antipatrón de comparar cero valores y pasar. Dos causas a la
vez: `picker-shell` no tiene demo propia (404, se mide dentro de un picker
anfitrión y detrás de su popover), y sus partes se llaman
`data-picker-header` / `-body` / `-footer` en vez de
`data-picker-shell-{parte}` — genéricas A PROPÓSITO, dice la receta, «so
every picker gets the same visual contract for free» —, así que todo filtro
por prefijo del componente ve sólo la raíz.
CÓMO SE VERIFICÓ, ya que el instrumento estándar no servía:
- Sonda DIRIGIDA (abre el popover del date-picker, captura los seis nodos
reales en las cuatro picker-sizes): determinismo comprobado (576 valores, 0
diffs entre dos corridas del mismo código) y **gate antes/después = 0 diffs**.
- Los knobs que la demo no monta —el header entero y la fila de tiempo— se
midieron FORZANDO el nodo: los quince mueven su propiedad, y el font-size del
header se comprobó talla a talla (12/14/16/16px → 102px, cada uno en su
picker-size).
- El guard R-5.4 pasó de **0/31 a 6/31** al ganar un `COMPONENT_OVERRIDES`
(prefijo de atributo + selector de apertura) acotado a este componente.
Verificado que los otros CATORCE con ledger no se mueven ni una cifra.
Adjudicar 31 excepciones sin ese arreglo habría sido escribir una mentira en
el ledger; los 25 que siguen mudos llevan cada uno su razón medida.
Header y footer comparten la regla que los separa del cuerpo → UN par de knobs
(`border-width` + `border`), no dos.
Sin tab `Tokens`: este componente no tiene página donde ponerlo. Registrado en
next-features §13 junto con la pregunta de fondo — si el precio de los nombres
genéricos (un contrato visual común para todos los pickers) compensa la ceguera
del instrumental — y con el eje portalado como caso de prueba si el TSC llega a
ganar un scope configurable.
Gates: censo --only 76 % · suite eidos sin rojos nuevos (el conocido
skin-media-player) · rtl:check 0 · docs:check 0 · check por fichero limpio ·
`component:audit` no lo cubre (eidos-only, sin morfo) y `eidos-lint` tampoco.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
Lo que `picker-shell` añadió (2026-08-21):
- **⚠ `picker-shell` no tiene ruta de demo propia** (`/uix/components/picker-shell`
da 404) y sus partes se llaman `data-picker-header` / `-body` / `-footer` en
vez de `data-picker-shell-{parte}` — genéricas **a propósito** , dice su
receta, «so every picker gets the same visual contract for free». La suma de
las dos cosas deja al instrumental ciego: la sonda genérica devolvió **0
nodos**, que es el antipatrón de comparar cero valores y pasar. Se tokenizó
con sonda dirigida (abre el popover del `date-picker` , captura los seis nodos
reales en las cuatro `data-picker-size` : 576 valores, 0 diffs) y el guard
R-5.4 ganó un `COMPONENT_OVERRIDES` para poder abrirlo y filtrarlo — acotado
a este componente, con los otros catorce verificados sin cambio. Lo de fondo
sigue abierto: **una demo propia** , y decidir si el precio de los nombres
genéricos (un contrato visual común para todos los pickers) vale la ceguera
del instrumental.
- **El eje de talla de `picker-shell` es `data-picker-size` sobre un ancestro
PORTALADO**, así que el TSC no puede emitir su cascada (`scope: 'size:xs'`
generaría `[data-picker-shell][data-size='xs']` , que no casa nunca). Es el
único componente del eje cuyas coordenadas por talla quedan como públicos
planos que la receta consume desde sus propios selectores. Si algún día el
TSC gana un eje de scope configurable, éste es su caso de prueba.
uix(code-block): temable — 0 % → 81 %, y la demo que no enseña el componente
20 claves en una entrada NUEVA de `recipes/base.ts` (no tenía). Censo 0 % →
81 %. Global 46 % → 47 %, sin contrato 46 → 45.
El handoff lo llamaba «el más mecánico de los que quedan». No lo es: sus 26
knobs se parten en dos grupos con doctrinas distintas.
1. **El nodo del código NO acuña nada, y la §4 pedía que lo hiciera.**
`[data-code-block-code]` consume la capa tipográfica `--style-code-*` en sus
cinco declaraciones, y las dos que pasan por privado usan la forma canónica
de la escotilla por instancia
(`var(--_code-block-font-size, var(--style-code-font-size))`, escrita por el
wrapper desde la prop `size` y sólo si llega). Es el patrón que D-TH.2-b
fijó para heading/text/code. Acuñar `code-font-family` y compañía sería el
vocabulario paralelo que la regla 2 de capas prohíbe. Todo lo que entra al
contrato es el CASCO alrededor.
2. **Los tres `font-size` iban al primitivo crudo, y lo cazó un guard.** La
propuesta copia el valor del CSS verbatim, y ese valor ya violaba
`recipe-css-contract` («recipes consume the size bundle, not the raw
size-coordinate primitives»): `title/lang/copy-font-size` corregidos a
`var(--size-{k}-font-size)`. Los alias son 1:1 → 0 diffs tras aplicarlo.
Aviso general: cuando el CSS de partida ya incumple el contrato, la
propuesta generada hereda el incumplimiento.
3. Shorthand partidos: los dos `border` y el `border-bottom` de la cabecera
comparten anchura y color → `border-width` + `border`; el `padding` del
`<pre>` → `pre-padding`. Los `transparent` de outline/ghost y el `border: 0`
de ghost son identidad de variante.
El 19 % que falta son esas cinco de la capa, que el censo cuenta como `global`
porque `code-block` no está en `TYPOGRAPHIC_PRIMITIVES` — es un compuesto que
CONTIENE un primitivo tipográfico, no uno de ellos. Extender la clase `system`
del censo a las capas sigue pendiente de tu firma; la métrica penaliza hacer lo
correcto y no se fuerza de oficio.
DOS HALLAZGOS QUE NO SE ARREGLAN AQUÍ (→ next-features §13):
- **La demo de `code-block` no enseña el `code-block`.** `[data-uix-docs] pre`
(`web/routes/uix/uix.css:801`) pesa (0,1,1) contra los (0,1,0) de
`[data-code-block-pre]`, y `[data-uix-docs]` es el shell de TODAS las
páginas. Siete propiedades del `<pre>` las pinta el harness y no la receta:
padding (16px 20px contra 12px), border (1px contra ninguno), radius (8px
contra 0), background (gris contra transparente), font-family, font-size
(13 contra 14) y line-height (18.2 contra 20.3). El fondo, el borde y el
radio que se ven ahí son del sitio. Por eso `pre-padding` lee muerto en el
guard, adjudicado con esa medición. Acotar la regla toca el CSS del SITIO,
que sirve a ~162 páginas: decisión aparte.
- **El guard de claves duplicadas del handoff inspeccionaba el VACÍO.** El
comando documentado usa `grep -oE "^\t…"`, y `grep -E` lee `\t` como una `t`
literal: 0 coincidencias sobre 130 bloques reales, así que salía «vacío = OK»
sin mirar nada — el antipatrón que el proyecto tiene registrado, dentro del
propio handoff. Comprobado con un contador fiable: cero duplicados en el
árbol, así que no hay daño en los commits que confiaron en él. CONTINUE
queda corregido con una versión que CUENTA los bloques y avisa de que «0
bloques» significa guard roto, no fichero limpio.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 1.305 valores × 7 estados (dos veces: tras
la tokenización y tras la corrección del bundle). Determinismo verificado.
- R-5.4: 19/20; `pre-padding` adjudicado con las siete propiedades medidas.
- censo --only 81 % · component:audit PASS · eidos-lint invalid 0, class-hooks
0 · suite eidos de vuelta al único rojo conocido (skin-media-player) tras
corregir el bundle · rtl:check 0 · docs:check 0 · check por fichero limpio.
- README con su «Talla y tema» y tab `Tokens` en la demo (20 claves, en vivo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
Lo que `code-block` añadió (2026-08-21):
- **⚠ La demo de `code-block` NO ENSEÑA el `code-block` : el harness del sitio
de documentación le pinta el `<pre>` por encima.**
`[data-uix-docs] pre` (`web/routes/uix/uix.css:801`) pesa ** (0,1,1)** y gana
a `[data-code-block-pre]` ** (0,1,0)** — y `[data-uix-docs]` es el shell de
TODAS las páginas, así que la regla alcanza cualquier demo. Medidas **siete
propiedades pisadas**: `padding` (16px 20px del harness contra los 12px de la
receta), `border` (1px sólido contra ninguno), `border-radius` (8px contra
0), `background-color` (gris contra transparente), `font-family` ,
`font-size` (13px contra 14px) y `line-height` (18.2px contra 20.3px). El
fondo, el borde y el radio que se ven en esa página son del sitio, no del
componente. Es la clase «la demo puede tapar el componente» (precedente
`proof-of-human` ), pero aquí son siete propiedades y le toca justo al
componente cuya razón de ser es mostrar código. Por eso `pre-padding` lee
muerto en el guard y está adjudicado con esa medición.
Acotar la regla del harness es tocar el CSS del SITIO, que sirve a las ~162
páginas y a los bloques de código de las fichas: no entra en el commit de un
componente. Decisión aparte.
uix(code): temable — 0 % → 100 %, y el harness que también le pisa el radio
6 claves fusionadas en el bloque `code` de `recipes/base.ts` (donde ya vivía el
forward de paleta). Censo 0 % → **100 %**: no queda un knob de apariencia fuera
del contrato.
**Seis de sus trece knobs son SISTEMA y no se acuñan.** `code` es el primitivo
tipográfico canónico: sus seis ejes de tipo consumen `--style-code-*` con la
escotilla por instancia (`var(--_code-{eje}, var(--style-code-{eje}))`), la
forma que D-TH.2-b fijó, y un primitivo no re-declara el vocabulario de su
capa. Lo que entra al contrato es el CROMO de las variantes `soft` y `outline`.
- El padding es em-relativo a propósito (la píldora crece con el código que
envuelve) y lo declaran IGUAL las dos variantes con cromo: un knob por eje,
no cuatro.
- `--_code-color` sigue privado: conmutador de dos fuentes (tinta de contenido
en línea / forward de paleta cuando llega `data-color`), mismo patrón que el
borde de foco de textarea.
- El `margin: 0` / `padding: 0` / `background: transparent` / `border: 0` de la
base es RESET: la variante `plain` es texto desnudo por identidad.
EL HALLAZGO — el harness pisa también a `code` (→ next-features §13, ampliado):
Medido con CDP: `[data-uix-docs] code` declara `font-family`, `font-size`,
`padding` y `border-radius` y gana **(0,1,1)** contra la regla BASE de la receta
**(0,1,0)**. En la página de documentación el radio del componente es el del
sitio (3px), no el suyo (4px). Sus reglas de VARIANTE (0,2,0) sí ganan, así que
el efecto es selectivo y por eso sólo `radius` lee muerto. **No es un caso
aislado**: es la misma familia de reglas del harness que tapa el `<pre>` de
code-block con siete propiedades. Cualquier decisión sobre esa regla cubre los
dos componentes.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 174 valores × 7 estados. Determinismo
verificado.
- R-5.4: 0/6, los seis adjudicados con su medición — `radius` por el harness
(CDP), los otros cinco porque la demo monta sólo la variante `plain`, sin
cromo; forzando las variantes, los cinco alcanzan.
- censo --only 100 % · component:audit PASS · rtl:check 0 · docs:check 0 ·
check por fichero limpio · suite eidos sin rojos nuevos.
- README con «Talla y tema» y tab `Tokens` en la demo (6 claves).
Nota de proceso: el fichero de excepciones llegó a quedar con sintaxis TS
inválida por un apóstrofe sin escapar, y la comprobación que hice lo dio por
bueno porque el `$?` capturado era el del `grep` de la tubería, no el del
guard. Corregido, y re-verificados los ONCE componentes con ledger comprobando
el código de salida REAL: cero fallos.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
**No es un caso aislado — la misma familia de reglas pisa también a `code`**
(medido 2026-08-21 al tokenizarlo, con CDP): `[data-uix-docs] code` declara
`font-family` , `font-size` , `padding` y `border-radius` , y gana (0,1,1)
contra la regla BASE del componente (0,1,0). Sus reglas de VARIANTE (0,2,0)
sí ganan, así que el efecto es selectivo y por eso sólo su `radius` lee
muerto — 3px del sitio donde el componente pide 4px. Cualquier decisión sobre
la regla del harness cubre los dos componentes.
uix(code-block): temable — 0 % → 81 %, y la demo que no enseña el componente
20 claves en una entrada NUEVA de `recipes/base.ts` (no tenía). Censo 0 % →
81 %. Global 46 % → 47 %, sin contrato 46 → 45.
El handoff lo llamaba «el más mecánico de los que quedan». No lo es: sus 26
knobs se parten en dos grupos con doctrinas distintas.
1. **El nodo del código NO acuña nada, y la §4 pedía que lo hiciera.**
`[data-code-block-code]` consume la capa tipográfica `--style-code-*` en sus
cinco declaraciones, y las dos que pasan por privado usan la forma canónica
de la escotilla por instancia
(`var(--_code-block-font-size, var(--style-code-font-size))`, escrita por el
wrapper desde la prop `size` y sólo si llega). Es el patrón que D-TH.2-b
fijó para heading/text/code. Acuñar `code-font-family` y compañía sería el
vocabulario paralelo que la regla 2 de capas prohíbe. Todo lo que entra al
contrato es el CASCO alrededor.
2. **Los tres `font-size` iban al primitivo crudo, y lo cazó un guard.** La
propuesta copia el valor del CSS verbatim, y ese valor ya violaba
`recipe-css-contract` («recipes consume the size bundle, not the raw
size-coordinate primitives»): `title/lang/copy-font-size` corregidos a
`var(--size-{k}-font-size)`. Los alias son 1:1 → 0 diffs tras aplicarlo.
Aviso general: cuando el CSS de partida ya incumple el contrato, la
propuesta generada hereda el incumplimiento.
3. Shorthand partidos: los dos `border` y el `border-bottom` de la cabecera
comparten anchura y color → `border-width` + `border`; el `padding` del
`<pre>` → `pre-padding`. Los `transparent` de outline/ghost y el `border: 0`
de ghost son identidad de variante.
El 19 % que falta son esas cinco de la capa, que el censo cuenta como `global`
porque `code-block` no está en `TYPOGRAPHIC_PRIMITIVES` — es un compuesto que
CONTIENE un primitivo tipográfico, no uno de ellos. Extender la clase `system`
del censo a las capas sigue pendiente de tu firma; la métrica penaliza hacer lo
correcto y no se fuerza de oficio.
DOS HALLAZGOS QUE NO SE ARREGLAN AQUÍ (→ next-features §13):
- **La demo de `code-block` no enseña el `code-block`.** `[data-uix-docs] pre`
(`web/routes/uix/uix.css:801`) pesa (0,1,1) contra los (0,1,0) de
`[data-code-block-pre]`, y `[data-uix-docs]` es el shell de TODAS las
páginas. Siete propiedades del `<pre>` las pinta el harness y no la receta:
padding (16px 20px contra 12px), border (1px contra ninguno), radius (8px
contra 0), background (gris contra transparente), font-family, font-size
(13 contra 14) y line-height (18.2 contra 20.3). El fondo, el borde y el
radio que se ven ahí son del sitio. Por eso `pre-padding` lee muerto en el
guard, adjudicado con esa medición. Acotar la regla toca el CSS del SITIO,
que sirve a ~162 páginas: decisión aparte.
- **El guard de claves duplicadas del handoff inspeccionaba el VACÍO.** El
comando documentado usa `grep -oE "^\t…"`, y `grep -E` lee `\t` como una `t`
literal: 0 coincidencias sobre 130 bloques reales, así que salía «vacío = OK»
sin mirar nada — el antipatrón que el proyecto tiene registrado, dentro del
propio handoff. Comprobado con un contador fiable: cero duplicados en el
árbol, así que no hay daño en los commits que confiaron en él. CONTINUE
queda corregido con una versión que CUENTA los bloques y avisa de que «0
bloques» significa guard roto, no fichero limpio.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 1.305 valores × 7 estados (dos veces: tras
la tokenización y tras la corrección del bundle). Determinismo verificado.
- R-5.4: 19/20; `pre-padding` adjudicado con las siete propiedades medidas.
- censo --only 81 % · component:audit PASS · eidos-lint invalid 0, class-hooks
0 · suite eidos de vuelta al único rojo conocido (skin-media-player) tras
corregir el bundle · rtl:check 0 · docs:check 0 · check por fichero limpio.
- README con su «Talla y tema» y tab `Tokens` en la demo (20 claves, en vivo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
- **⚠ El guard de claves duplicadas del handoff INSPECCIONA EL VACÍO.** El
comando documentado en `CONTINUE-theming.md` («Trampas que costaron un commit
cada una») es
`grep -oE "^ '?[a-z0-9-]+'?: \{" src/uix/eidos/lib/recipes/base.ts | …` ,
y ** `grep -E` no interpreta ` ` como tabulador** — lo lee como una `t`
literal, así que el patrón no casa NADA: 0 coincidencias sobre 130 bloques
reales. El guard salía siempre «vacío = OK» sin haber mirado nada, que es la
definición del antipatrón que el propio proyecto tiene registrado. Con un tab
real en el patrón encuentra los 130 (comprobado: cero duplicados en el árbol,
así que no hay daño que reparar). El handoff queda corregido con la versión
que sí mira.
docs(next-features): el registro de incidencias del eje theme-reach
Encargo del autor: anotar todas las incidencias de esta clase para resolverlas
después. Van al registro canónico (`next-features.md`), no a un sitio nuevo, y
con su MEDICIÓN — no como impresión.
**§12 — El contrato de cascada del velo de estado.** Seis incidencias que
resultaron ser la misma: nadie fijó nunca cómo compone el velo de
`archetypes.css` con las reglas de una receta. Y la raíz explica por qué se
manifiestan de formas tan distintas: el velo se pinta con DOS pesos según el
arquetipo — `:where()` (0,0,0) para `trigger`, especificidad plena (0,5,0) para
`item`/`option`.
131 declaraciones-atajo en 45 componentes cancelan el velo (66 BASE ·
32 ACENTO · 23 HOVER · 10 OTRA; cero valores con gradiente, así que el paso a
longhand no tiene riesgo técnico; 32 nodos velados que hoy no reaccionan al
ratón pasarían a hacerlo) · la prop `hoverable` de table no suprime nada · en
tree-grid el empate (0,5,0) lo decide el ORDEN DE CARGA y varía entre
recargas · la banda de tree-grid mata el hover en las filas pares · un token
de tinta no puede ganar al arquetipo (`selected-row-fg` se retiró por eso) ·
el thumb de scroll-area no tiene velo al que migrar.
Esto explica de paso la adopción 21/135 que midió la auditoría del 2026-07-01:
el velo estaba escrito y estructuralmente derrotado.
**§13 — Huecos de instrumento y de demo.** Lo que hizo que una medición
mintiera o no existiera, que importa porque el eje entero se apoya en ellas:
falsos negativos del centinela sobre propiedades transicionadas, pseudo-elementos
y componentes compuestos que no ve, la sonda que no pasa el ratón por un `<tr>`,
y la lección general —**una sonda sobre una parte que la demo no monta compara
CERO valores y pasa**—, con la lista de partes condicionales afectadas.
El handoff gana la regla para las sesiones del eje: si una incidencia mueve
píxel o toca morfo, se mide, se anota ahí y se sigue; no se arregla dentro del
commit del componente.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**Deps**: ninguna. Son mejoras del instrumental del eje, ejecutables cuando
estorben.