|
|
---
|
|
|
title: Recipe Contract — the transversal contract of every eidos recipe
|
|
|
type: canon
|
|
|
audience: human + agent
|
|
|
authority: canonical — which theming systems every recipe MUST consume, and under which names
|
|
|
status: current
|
|
|
enforcement: scripts/component-audit.ts (rules R-4.x, all `error`)
|
|
|
source: migrated from src/uix/eidos/RECIPE_CONTRACT.md (2026-07-02, docs-book F7.3)
|
|
|
related:
|
|
|
tsc: docs/canon/tsc.md (where each token is emitted)
|
|
|
theming: docs/theming/reference.md (the system; §5 sizes · §6 naming)
|
|
|
guide: docs/theming/guide.md (how to add a component, step by step)
|
|
|
---
|
|
|
|
|
|
# Recipe Contract
|
|
|
|
|
|
> The TSC decides **where** each token is emitted. This contract decides
|
|
|
> **what** every recipe consumes: the theming's transversal systems
|
|
|
> (state-layer, focus, elevation, typography, opacity, motion, spacing,
|
|
|
> touch, lists, z) and the vocabulary of its dimensional tokens. A recipe
|
|
|
> deviating without an annotated exception is out of contract — the
|
|
|
> `component-audit` (R-4.x) flags it.
|
|
|
>
|
|
|
> **Why it exists**: the 2026-07-01 audit (`fable-eidos-audit.md`) measured
|
|
|
> that canons defended by a mechanical guard drift ~0% while canons defended
|
|
|
> by prose drift 30–85% (state-layer 21/135, partial depth-channel adoption,
|
|
|
> 49 keyframes outside the channel). This document is the canon; the R-4.x
|
|
|
> rules are its executable form.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 1. A recipe's skeleton — dimensional tokens
|
|
|
|
|
|
Every recipe declares its knobs in `lib/recipes/base.ts` with this
|
|
|
vocabulary. **The names are canon**, just like the color slots are in
|
|
|
THEMING §6:
|
|
|
|
|
|
| Concept | Canonical name | Forbidden |
|
|
|
| ----------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
|
| Control height per size | `control-height-{size}` (or `{part}-height-{size}` when the part is not the control) | inventing a third name per component |
|
|
|
| Padding per axis | `padding-inline[-{size}]` / `padding-block[-{size}]` | **`padding-x` / `padding-y`** AND the abbreviated segments **`px` / `py`** (physical axes) |
|
|
|
| Margin per axis | `margin-inline[-{size}]` / `margin-block[-{size}]` | `margin-x/-y`, `mx` / `my` |
|
|
|
| Internal separation | `gap[-{size}]` | — |
|
|
|
| Radius | `radius[-{size}]` → `var(--radius-*)` | px that equal a step of the scale |
|
|
|
| Control font | `font-size-{size}` = `var(--font-size-{size})` (**1:1**, THEMING §5) | literal px/rem (guarded in `recipe-css-contract`) |
|
|
|
| Icon | `icon-size-{size}` = `var(--icon-size-{size})` | hand px; size from `--control-height-*` unless the element IS a control |
|
|
|
| Field label | one typographic step below the input (THEMING §5) | — |
|
|
|
| Ink (foreground) | `[{part}-]fg` | **`color` as a slot** — `trigger-color`, `-color-hover` (D-TH.6, 2026-08-20) |
|
|
|
| Interactive state | `{modifier}-{part-}{slot}` — the modifier IN FRONT (`hover-bg`, `disabled-trigger-fg`) | the modifier behind (`bg-hover`, `trigger-color-active`) |
|
|
|
|
|
|
**Where the modifier goes — one sentence: _in front what is interactive,
|
|
|
behind what is dimensional or contextual_** (D-TH.6, signed 2026-08-20).
|
|
|
`hover-bg` and `disabled-trigger-fg`, but `control-height-md`,
|
|
|
`gap-vertical`, `indicator-bg-below` and `width-icon`: a size, an
|
|
|
orientation, a semantic range or a collapsed mode are not states of the
|
|
|
control, and promoting them rewrites what the name MEANS (`--sidebar-width-icon`,
|
|
|
the width of the icon-collapsed rail, is not `--sidebar-icon-width`, the width
|
|
|
of an icon). The interactive vocabulary is closed: `hover · active · selected ·
|
|
|
disabled · checked · open · focus · invalid · current`.
|
|
|
|
|
|
Two families keep a grammar of their own and R-5.3 knows it:
|
|
|
|
|
|
- **role / palette** — `{part-}?{role|palette}-{slot}` where the slot comes from
|
|
|
`COLOR_ROLE_SLOTS`, which carries the modifier BEHIND by construction
|
|
|
(`primary-solid-hover`, `palette-hover`). 447 keys; they are canon, not drift.
|
|
|
- **system** — `--state-*`, `--opacity-*`, `--focus-ring-*` are consumed, never
|
|
|
minted by a recipe (§2), so this grammar does not reach them. A recipe key
|
|
|
that MIRRORS the system family keeps its spelling: `trigger-focus-ring-color`
|
|
|
is the signed exception, along with colour as a NOUN (`orb-color-*`) and
|
|
|
`stop-color-*`, where `stop-color` is a part name.
|
|
|
|
|
|
Inherited naming rules (THEMING §6): public `--{c}-{slot}`, private
|
|
|
`--_{c}-{slot}`, no `color-` segment, no kebab abbreviation, no
|
|
|
`--eidos-/-soma-/-air-`.
|
|
|
|
|
|
> **Normalización ejecutada (2026-08-20, D-TH.6 — «un idioma, todo el
|
|
|
> ecosistema», segunda parte).** El catálogo hablaba dos idiomas para la tinta:
|
|
|
> 196 claves `-color` contra 55 `fg`, y el modificador caía a los dos lados
|
|
|
> (`hover-bg` ×13 contra `bg-hover` ×22). El origen era una CONTRADICCIÓN entre
|
|
|
> dos doctrinas firmadas: THEMING §6.7 r7 dice `fg`; el principio de plataforma
|
|
|
> de la normalización px/py («el token se llama como la propiedad») dice
|
|
|
> `color`. El autor adjudicó `fg` acotando el principio: gobierna los ejes
|
|
|
> DIMENSIONALES, no la pareja `bg`/`fg` — si la gobernara, `bg` tendría que
|
|
|
> llamarse `background-color`. Codemod value-preserving: **269 claves + 793
|
|
|
> referencias en 101 ficheros**, diff de `generated/` = renombres 1:1 con cero
|
|
|
> cambios de valor, censo de alcance idéntico (162 · 5.203 · 1.841 · 37 %),
|
|
|
> computed idéntico en navegador (6.467 valores, 23 estados, 3 componentes).
|
|
|
> **No entraron**: las 447 role-slot (canónicas), las 13 exentas firmadas, y
|
|
|
> **47 hovers neutros que MIGRAN a la capa de estado** (§38 + R-4.3) en vez de
|
|
|
> renombrarse — renombrar lo condenado es churn. El muro de tipos de
|
|
|
> `defineRecipes` cierra la gramática cuando esa migración termine.
|
|
|
|
|
|
**Value-reference rules (2026-07-07, theming audit B — both guarded in
|
|
|
`recipe-css-contract.test.ts`):**
|
|
|
|
|
|
- A recipe token VALUE may reference only vocabulary that **exists** in the
|
|
|
emitted contract — a no-fallback `var(--x)` pointing at a name the
|
|
|
generators never emit is a _phantom_ (it freezes the token
|
|
|
guaranteed-invalid; `--color-content-tertiary` shipped a month that way).
|
|
|
`var(--x, fallback)` is runtime-optional by construction — **but only for
|
|
|
FOREIGN and system vocabulary**. Over the component's OWN `--{c}-*` names the
|
|
|
fallback stopped excusing anything on 2026-08-26: the namespace is closed over
|
|
|
the contract (§4, «la ley del espacio cerrado»).
|
|
|
- A value referencing a component **private** (`--_{c}-*`, declared in the
|
|
|
component's CSS) must declare a **scope that covers it** (`host` / leaf) —
|
|
|
never `root`: a `:root`-emitted token computes where the private does not
|
|
|
exist and freezes invalid down the whole tree (the calendar holiday/event
|
|
|
marks and the field segment heights shipped broken exactly this way).
|
|
|
|
|
|
> **Normalización ejecutada (2026-07-06, decisión usuario: "un idioma, todo
|
|
|
> el ecosistema").** El catálogo hablaba dos idiomas: 32 claves canónicas
|
|
|
> conviviendo con **198 claves abreviadas** `p[xy]` (+3 `my`) que evadían el
|
|
|
> regex de R-4.4 — hábito Tailwind/Chakra sin decisión que lo respaldara. El
|
|
|
> nombre canónico es el de la PLATAFORMA (CSS Logical Properties): el token
|
|
|
> se llama como la propiedad que alimenta (nombre = contrato, sin capa de
|
|
|
> traducción — a diferencia de Panda, donde `px` es azúcar que un compilador
|
|
|
> resuelve), es direccional-agnóstico, y en un contrato de tokens `px` además
|
|
|
> es un pun con la unidad. Codemod atómico value-preserving: 198+3 claves +
|
|
|
> ~570 nombres en **57 ficheros** (tracks excluidos words/palabras/chronos
|
|
|
> INCLUIDOS por decisión explícita) + los **20 shorthands físicos**
|
|
|
> (`padding: var(--py) var(--px)`) reescritos a longhands lógicos (el único
|
|
|
> subconjunto con semántica física real — modos de escritura verticales).
|
|
|
> Triple muro: R-4.4 con el regex ampliado a segmentos abreviados (error,
|
|
|
> sin allowlist — la deuda murió en el mismo pass) · **muro de tipos**
|
|
|
> `defineRecipes` (`lib/recipes/define.ts`): una clave de eje físico no
|
|
|
> compila · la suite/regen verifican (diff de generated = 230 renombres 1:1;
|
|
|
> computed padding idéntico en navegador).
|
|
|
|
|
|
**Typographic primitives are measured against the STYLE LAYER, not against
|
|
|
their own tokens** (D-TH.2, signed 2026-08-20). `heading`, `text`, `s-text`,
|
|
|
`code`, `display` and `label` resolve every typographic axis through
|
|
|
`--style-{name}-*`, selected by `data-style` or bound wholesale to one style:
|
|
|
that layer IS their theming surface, and it is already public and live. Minting
|
|
|
`--heading-*` to mirror it would be one alias per axis × level — the class of
|
|
|
alias the changelog §39 purge killed. The census therefore counts `--style-*`
|
|
|
as a transversal `system` for those six (their `--_{c}-*` privates are the
|
|
|
per-INSTANCE escape hatch the wrapper writes from a prop, not a theming knob).
|
|
|
A component that merely READS a named style for one axis — a menu label
|
|
|
reading `--style-label-font-family` — is not a primitive and is not in the set.
|
|
|
|
|
|
## 2. Mandatory transversal systems
|
|
|
|
|
|
The table IS the contract: **concept → canonical mechanism → the rule that
|
|
|
guards it**. A recipe reimplements none of these concepts on its own.
|
|
|
|
|
|
| Concept | Canonical mechanism | How it is consumed | Rule |
|
|
|
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
|
|
|
| **Neutral hover / press / selected** | the state-layer (rules in `archetypes.css`; magnitudes are config data `primitives.state` — changelog §40) | transparent background → `background: var(--state-hover)`; filled background → the overlay `background-image: linear-gradient(var(--state-hover), var(--state-hover))` | R-4.3 |
|
|
|
| **Valenced hover** (solid / soft per color) | the recipe's palette swap | `background: var(--{c}-solid-hover)` etc. — tokens, never hand-rolled `color-mix(currentColor …)` | R-4.3 |
|
|
|
| **Elevation** | the scale + the depth channel | `box-shadow: var(--shadow-*)` or compose `var(--depth-{plane}-shadow), var(--depth-{plane}-halo)`; never literal shadows | R-4.1 |
|
|
|
| **Crisp inner ring** | the inset-ring (THEMING §29) | `box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, …)` — the expression lives at the point of use | R-4.1 (passes: it carries `var()`) |
|
|
|
| **Focus** | per archetype (THEMING §32) | fields → the two-ring `--focus-ring-*` (box-shadow); surfaces/controls → their own `outline` (HCM-safe); declaring nothing falls back to `archetypes.css` | R-1.5 |
|
|
|
| **Disabled** | the opacity token | `opacity: var(--opacity-disabled)` + `cursor: not-allowed` — never hand `0.4`/`0.5`/`0.6` | R-4.2 |
|
|
|
| **Typography** | 1:1 with the scale | see §1; canonical exceptions: avatar/marker (glyph ∝ diameter), accordion (prose scale) | R-2.7 + guard |
|
|
|
| **Motion — state transitions** | tokens | `transition: X var(--duration-*) var(--ease-*)` | THEMING §35 block C |
|
|
|
| **Motion — event signatures** | `EidosConfig.motion.{keyframes,signatures}` (generated, themeable) | NO local `@keyframes` for perceptual reactions; a local `@keyframes` only when **functional** (spin/shimmer/continuous period) and annotated | R-4.5 |
|
|
|
| **Spacing** | `var(--space-*)` | density × scaling arrive composed from the foundation — **never** multiply by `--density-*`/`--scaling` in the recipe | R-2.3 |
|
|
|
| **Touch target** | archetype (`trigger`/`close`/`action`) + list-surface | free via `@media (pointer: coarse)` in `archetypes.css` — don't declare your own 44px minimums | — |
|
|
|
| **List/menu rows** | the list-surface (`--list-item-*`) | list surfaces bridge `--list-item-height`/`-py`, not their own row heights | — |
|
|
|
| **Floating z-index** | the `--z-index-overlay-*` band | each PORTALED overlay consumes its rung; raw integers `0..5` only for intra-component order | guard in contracts.test |
|
|
|
| **Viewport-fixed chrome** (not portaled) | the `--z-index-*` ladder, `affix` rung | a consent strip / FAB / floating bar is page chrome, not an overlay: it rides `affix` (150) — above `sticky`, below every menu. Two elements in the SAME band tie, and DOM order breaks the tie (measured: a notice correctly first in the source lost to a sticky header, 30.7px of overlap) | THEME-SYS-1 + `layer:check` |
|
|
|
| **Color** | role slots / recipe tokens | `var(--color-{role}-{slot})` or `var(--{c}-*)`; never direct `--scale-*`/`--primitive-*` nor hex/rgb | R-2.1/2.6, R-4.6 |
|
|
|
|
|
|
## 3. Exceptions — how they are declared
|
|
|
|
|
|
A deviation is valid **only** when annotated on the same line. The audit
|
|
|
honors annotations; an unannotated deviation is drift.
|
|
|
|
|
|
| Annotation | When | Example |
|
|
|
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
|
| `/* literal: <reason> */` | a physically-fixed value or a justified optical tuning | `font-size: 13px; /* literal: tight icon affordance */` |
|
|
|
| `/* functional: <reason> */` | a local `@keyframes` that is NOT a perceptual signature (continuous period, machinery) | `/* functional: continuous spin period, not an event signature */` `@keyframes spin { … }` |
|
|
|
| physically-fixed | colors that must not follow the theme (the QR's white, the natural clock's skies) — hex allowed WITH a comment | `--_qr-bg: #ffffff; /* literal: QR quiet zone must be true white */` |
|
|
|
| `/* important: <reason> */` | an `!important` that must win EVERY cascade fight — the three legitimate shapes: bypassing a soma inline style, cross-recipe fusion (the group's radius flattening), reduced-motion kills | `cursor: nwse-resize !important; /* important: wins over the header's inherited grab cursor */` |
|
|
|
|
|
|
What does **not** pass as an exception: "it's faster this way", "the token
|
|
|
didn't exist" (create it), "it's just a hover" (that is exactly the
|
|
|
contract's case).
|
|
|
|
|
|
## 4. Enforcement
|
|
|
|
|
|
| Rule | Guards | Severity |
|
|
|
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
|
|
|
| R-4.1 | `box-shadow` without `var(` (elevation/ring outside tokens) | error |
|
|
|
| R-4.2 | literal `opacity: 0<N<1` outside `@keyframes` | error |
|
|
|
| R-4.3 | `background*` in `:hover` rules without a token (`var(`) or with manual `color-mix(… currentColor …)` | error |
|
|
|
| R-4.4 | recipe tokens with physical axes `padding-x/-y`, `margin-x/-y` | error |
|
|
|
| R-4.5 | a local `@keyframes` without a `/* functional: … */` annotation | error |
|
|
|
| R-4.6 | direct `var(--scale-*)` / `var(--primitive-*)` in component CSS | error |
|
|
|
| R-4.7 | `!important` without a same-line `/* important: <reason> */` annotation (THM-5, 2026-07-11) | error |
|
|
|
| R-5.1 | an appearance declaration that reaches no theme and carries no act — neither a public token, nor a `/* literal: … */` annotation, nor an entry in the debt ledger (2026-08-25) | error |
|
|
|
| R-5.2 | a recipe with themeable knobs, no entry in `lib/recipes/base.ts`, and unregistered debt (2026-08-25) | error |
|
|
|
| R-5.3 | recipe token keys outside the naming grammar of §1 — `color` as a slot, or an interactive modifier behind (D-TH.6, 2026-08-20) | error |
|
|
|
| R-5.4 | a public token that moves no computed value in the live demo and carries no written adjudication (`npm run theming:sentinel -- <c> <url>`, 2026-08-21) | error |
|
|
|
| R-5.5 | a per-instance custom-property WRITE outside `--_{c}-*`, in the nine roots the guard sweeps — a contracted name (a token that lies) or an ownerless public form. `src/uix/eidos` is OUT by declaration, §18 (`src/uix/value-channels.test.ts`, 2026-08-27) | error |
|
|
|
|
|
|
**R-5.1 measures against a LEDGER OF DEBT, not against a ceiling** (firma
|
|
|
2026-08-25). The two rules spent months documented and unimplemented
|
|
|
(`PLAN-theming.md` §3, `next-features.md` §13) for one reason: with 1088
|
|
|
declarations out of reach, the rule could only ship as an eternal `warn` or as
|
|
|
a false claim that 74 recipes are broken. The third way is the one R-5.4
|
|
|
already walks — **register the debt in writing**.
|
|
|
[`scripts/theming-census-debt.ts`](../../scripts/theming-census-debt.ts)
|
|
|
carries all 1154 keys, one line each, in the form `{class} · {file} ·
|
|
|
{selector} · {property}` (line-independent, so moving a rule fabricates no
|
|
|
regression; compared as a MULTISET, so a duplicated declaration cannot slip
|
|
|
in). It is **not** an exceptions file: an exception asserts «this is fine» and
|
|
|
every key there asserts the opposite.
|
|
|
|
|
|
So R-5.1 is `error` against the ledger. New debt outside it fails, and an
|
|
|
entry whose knob is no longer debt — tokenized, annotated, dead — is reported
|
|
|
**STALE** and fails until the line is deleted; the debt only shrinks, key by
|
|
|
key. The old coarse ceilings of `theming-reach-floor.test.ts` (`maxLiteral`
|
|
|
564 / `maxGlobal` 1156) are RETIRED by it: a total only sees a net move, so
|
|
|
five regressions under five fixes read green.
|
|
|
|
|
|
**R-5.2 is the contract half**, and its **23** contract-less recipes were
|
|
|
adjudicated one by one rather than waived (recounted by the adversarial pass
|
|
|
of the same day — an earlier draft said 18 by leaving the structural five out
|
|
|
of its own total): eight paint nothing; the typographic primitives
|
|
|
(`heading`, `text`, `display`) are all-`system` and their theming surface IS
|
|
|
the layer they consume (D-TH.2-b); five are `structural`; the three
|
|
|
calendar-layer consumers keep bridge privates and their raw knobs in the
|
|
|
ledger; `palabras` is a WIP track outside the audited catalogue; and the real
|
|
|
debt (`field-langs`, 31 raw globals with no contract) is covered BY the
|
|
|
ledger — the entry IS its written registry.
|
|
|
|
|
|
**The hole that rule left is CLOSED** (firma «la ley del espacio cerrado»,
|
|
|
2026-08-26). What R-5.2 did not adjudicate, and said so in its own PASS note,
|
|
|
was the `var(--{c}-x, fallback)` reference that no `base.ts` entry declares:
|
|
|
`recipe-css-contract.test.ts` sanctioned the idiom explicitly — its
|
|
|
phantom-public guard bit only the NO-fallback `var()` — and one guard must not
|
|
|
contradict another. The contradiction was resolved by moving the OTHER guard:
|
|
|
**the public namespace is CLOSED over the contract — a `--{c}-*` exists if and
|
|
|
only if `base.ts` declares it. There is no third state.** Three corollaries:
|
|
|
|
|
|
1. **The fallback is a safety device for the VALUE, not an escape from the
|
|
|
contract.** It keeps a page from breaking; it never excuses the name.
|
|
|
2. **Once the key is declared, the inline fallback on the component's OWN
|
|
|
public name dies** — the default lives in ONE place. Two sites are free to
|
|
|
disagree while the default sits at the point of use: `--mockup-shadow`
|
|
|
carried `var(--shadow-4)` on the frame and `var(--shadow-5)` on the bezel
|
|
|
under a single name, so a theme setting it could not know which of the two it
|
|
|
was moving. It was split into `--mockup-phone-shadow` in the same pass — a
|
|
|
surface that is different says so in its name.
|
|
|
3. **The guard hardens to the «if and only if»**: an own `--{c}-*` outside
|
|
|
contract is red, with or without a fallback. FOREIGN vocabulary (a borrowed
|
|
|
`--field-control-height-md` in `time-range-picker` — «un token PRESTADO trae
|
|
|
su semántica») and system names keep their rules: the guard filters by the
|
|
|
component's own prefix and never sees them.
|
|
|
|
|
|
Eleven keys were declared — `mockup` ×9, `text-scramble` ×2 — each taking its
|
|
|
retired fallback as its default, verbatim, at `root` scope. (The earlier note
|
|
|
here said «`mockup` ×11»: that 11 is the census's count of DECLARATIONS reaching
|
|
|
a public name, a different unit from the guard's, which counts 9 `var()` sites
|
|
|
over 8 names — the extra site being the double `shadow`.) Neutrality was proved
|
|
|
twice: the diff of `generated/` is those eleven lines and nothing else, and an
|
|
|
A/B in the browser — every new token forced back to `initial` with the old
|
|
|
declarations restored inline — measured identical computed values on all three
|
|
|
mockups of `/blocks/feature-split`, both chromes, and on `text-scramble`.
|
|
|
|
|
|
What the law did **not** close from itself was the VALUE CHANNEL: the 14 public
|
|
|
names soma or a wrapper writes per instance, inline. Their destination was
|
|
|
already signed — a value channel is `--_{c}-*`, the 318's class `channel` — but
|
|
|
the rename is a cross-layer codemod through soma providers, morfos and READMEs,
|
|
|
so it rode its own axis. `recipe-css-contract.test.ts` carries them meanwhile in
|
|
|
a NAMED, DATED registry (`PENDING_PRIVATE_RENAME`) that may only SHRINK: an
|
|
|
entry whose name stops being consumed, or that gains a contract entry, is
|
|
|
reported STALE and stays red until the line is deleted. The registry COUNTS
|
|
|
names; it does not bless them.
|
|
|
|
|
|
**The codemod landed on 2026-08-26 and retired THIRTEEN of the fifteen entries**
|
|
|
— `--background-{pointer-x,pointer-y,pointer-px,pointer-py,progress}` ·
|
|
|
`--color-swatch-fill` · `--emoji-picker-columns` ·
|
|
|
`--knob-{angle,progress,start-angle,sweep}` · `--menubar-panel-anchor-width` ·
|
|
|
`--text-gradient-duration`. The flip was atomic per name (writer and every
|
|
|
reader in one edit, found by a `git ls-files` sweep of the whole tracked tree)
|
|
|
and value-preserving: the census moved by exactly ONE knob
|
|
|
(`color-swatch.background`, the only one of the thirteen sitting in an
|
|
|
appearance property whose declaration did not also read a public sibling), so
|
|
|
`public` 3074 → 3073, `channel` 36 → 37, reach 73 % unchanged. The measured
|
|
|
signature that retired `--color-swatch-fill` from the contract on 2026-08-23
|
|
|
stands untouched: it was, and remains, a value channel — only the spelling
|
|
|
changed.
|
|
|
|
|
|
TWO entries survive, each for its own reason. `--motion-stagger-each-default`
|
|
|
rides the registry because it is not a channel at all: nobody writes it at
|
|
|
runtime, its default belongs to `EidosConfig.motion`, and declaring it would
|
|
|
contradict `motion`'s signed `structural` row. And
|
|
|
`--navigation-menu-indicator-h` is where the codemod **STOPPED**: the provider
|
|
|
writes FOUR sibling measurements in one object literal
|
|
|
(`--navigation-menu-indicator-{x,y,w,h}`), the soma README teaches all four
|
|
|
under a `## CSS Variables` heading as consumer-controlled shape with two worked
|
|
|
examples reading `-h`, and `-w` is a DECLARED contract key — which is the only
|
|
|
reason `-h` is the single member of the family this guard can see. Privatising
|
|
|
one of four splits a documented public API and leaves a declared sibling
|
|
|
contradicting it. Whether the family goes private, goes public, or stays split
|
|
|
is a SIGNATURE, not a codemod.
|
|
|
|
|
|
> **Annotation 2026-08-26 (later the same day) — the signature landed, and the
|
|
|
> registry is down to ONE.** The paragraph above records where the codemod stood
|
|
|
> that morning; it stays because its measurement was right for its day. What was
|
|
|
> signed is option **(a)**: the FOUR siblings are private
|
|
|
> (`--_navigation-menu-indicator-{x,y,w,h}`, writer and every reader flipped
|
|
|
> atomically), `indicator-w` leaves the contract (`navigation-menu` 44 → 43
|
|
|
> rows), and the soma README's `## CSS Variables` section stops teaching the
|
|
|
> measurements as API — it teaches the PART and its three shape knobs. Option
|
|
|
> (b), declaring `-x/-y/-h` to match the README, was REJECTED: it would bless
|
|
|
> four per-instance measurements as theming surface, the exact thing the
|
|
|
> `channel` class exists to deny. A new species `output` — a third class between
|
|
|
> public and private — was proposed and rejected too, for opening exceptions on
|
|
|
> a cheap class with a prose border. `-w` was a token that LIES, measured: soma
|
|
|
> writes it inline on the element, so with the underline drawn neither a write on
|
|
|
> the component root (what the sentinel does) nor one on `:root` reaches it; it
|
|
|
> scored `reach` only in the window where the indicator is mounted with no rect
|
|
|
> yet and the underline is 0px wide and invisible. Measured: `public`
|
|
|
> 3073 → 3072, `channel` 37 → 38, reach **73 %** and `atHundred` **65**
|
|
|
> untouched, ledger **1154 · 0 new · 0 stale**; the `navigation-menu` sentinel
|
|
|
> 43/44 → **42/43**, same single adjudication. **ONE** entry survives, motion's.
|
|
|
|
|
|
> **Annotation 2026-08-26 (the same day, the third and last) — the registry is
|
|
|
> at ZERO, and DISMANTLED.** The fifteenth and final entry,
|
|
|
> `--motion-stagger-each-default`, went out by MECHANICS — not by codemod, not
|
|
|
> by signature. It was never a value channel: nobody wrote it at runtime, and
|
|
|
> the `var(--motion-stagger-each-default, 70ms)` of `motion/motion.css` resolved
|
|
|
> to its fallback ALWAYS, because no sheet in the tree ever declared the name.
|
|
|
> The wiring its acta was waiting for landed:
|
|
|
> `primitives.motion.staggerViewport` (default `'70ms'`, embarked in `static.ts`
|
|
|
> beside `stagger: '20ms'`) emits `--motion-stagger-viewport` on `:root`, and
|
|
|
> the viewport rule `[data-stagger] > [data-animation-trigger='viewport']`
|
|
|
> EMIGRATED from `motion.css` to `renderMotionBlocks` — the foundation that
|
|
|
> already emits the 24 `[data-stagger]` index rows of that same selector family.
|
|
|
> So `motion.css` stopped consuming `--motion-*` at all, the guard had nothing
|
|
|
> left to demand declared, and the entry fell STALE on its own. **The dead name
|
|
|
> `--motion-stagger-each-default` no longer exists as a live CSS consumption**
|
|
|
> (the name survives only as dated history — docs and this file's acta). Zero guards
|
|
|
> touched, zero exceptions opened, zero pixels moved (70ms verbatim, measured in
|
|
|
> real Chrome: `0s / 0.07s / 0.14s / 0.21s` on `/blocks/stats-band/preview`,
|
|
|
> identical to the baseline of `AUDIT-blocks-ledger.md`; the old name written at
|
|
|
> `333ms` moves nothing, the new token written at `333ms` rewrites the whole
|
|
|
> cascade). And `motion`'s signed `structural` row came out MORE true, not less:
|
|
|
> still no key in `base.ts`, still one knob, the `opacity` of the
|
|
|
> `[data-animation-pending]` gate.
|
|
|
>
|
|
|
> **With it, `PENDING_PRIVATE_RENAME` is DISMANTLED, not kept empty** — the
|
|
|
> object, its `PENDING_DESTINATION`, the branch that consulted it, the `stale`
|
|
|
> field of `auditClosedNamespace` and the `it` that read it, all deleted; a
|
|
|
> short acta comment stands in their place and points here. The precedent is the
|
|
|
> house's own (`docs/decisions/book-deviations.md`: `targetOverride` was DELETED
|
|
|
> once the F3 census reached zero — «el estado ilegal del eje quedó
|
|
|
> inexpresable»), and the reason is the law this very guard file writes about
|
|
|
> itself at its `knob axes` check: an empty registry's `it` asserts exactly
|
|
|
> nothing, and a guard that inspects nothing is a false green. The honest
|
|
|
> counter-argument — a sixteenth entry now has nowhere to go — is precisely the
|
|
|
> point: with no registry, an own `--{c}-*` outside the contract is red, and
|
|
|
> that is all. There is no third state.
|
|
|
|
|
|
Mutation-proved three ways the day it landed: an invented
|
|
|
`var(--mockup-inventado, red)` — own prefix, WITH a fallback — turns the guard
|
|
|
red naming the key; the same injection with a FOREIGN borrowed name and a
|
|
|
fallback stays green; and a registry entry that stops describing a consumption
|
|
|
is named STALE and reddens until it is deleted.
|
|
|
|
|
|
**And its SIBLING closes the other side — the one that WRITES** (firma «el
|
|
|
guard vive donde NACE el valor», 2026-08-27, R-5.5; its SCOPE written the same
|
|
|
day, over the objection below). The law above audits who READS a name; this one
|
|
|
audits who writes it:
|
|
|
|
|
|
> **Where the framework writes a custom property PER INSTANCE, the name lives
|
|
|
> in `--_{c}-*` and `base.ts` does not declare it. A name the contract DOES
|
|
|
> declare — public `--{c}-x` or private `--_{c}-x`, one single species — is a
|
|
|
> TOKEN THAT LIES: a theme is entitled to it and can never win it.**
|
|
|
|
|
|
**THE SCOPE, in the law itself, because a rule that says «every» while its
|
|
|
guard reads two directories is prose.** Its subject is _the framework's writing
|
|
|
side_, and that is nine roots: `src/uix/soma` · `src/arts` · `src/uix/blocks` ·
|
|
|
`src/packs` · `src/libs` · `src/svrs` · `src/uix/sema` · `src/uix/morfo` ·
|
|
|
`src/uix/active-uix`. The first two write; the other seven wrote **nothing** on
|
|
|
2026-08-27 and are swept anyway, so a first write cannot be born unseen — held
|
|
|
to the same two reds as everyone else and **never to zero**, because a block
|
|
|
that stamps its own `--_{c}-*` geometry tomorrow is legal, and reddening it
|
|
|
would be enforcing a census instead of a law.
|
|
|
|
|
|
**Two roots are OUT by declaration, and neither one is empty:**
|
|
|
|
|
|
- **`src/uix/eidos`** — **11 writes of CONTRACTED names and 18 ownerless public
|
|
|
forms live there today** (measured by pointing the same scanner at it). They
|
|
|
are two species and not one, which is exactly why the answer is not to add
|
|
|
the root: **13 sites** in the `lib/build-*` and `lib/render-css` generators
|
|
|
are **the foundation emitting its own vocabulary**, which is what a
|
|
|
foundation is for, while **16 in `components/*`** are **the ergonomic props a
|
|
|
wrapper offers its consumer**. Telling those apart takes a signature
|
|
|
— one of the eleven, `--dialog-overlay-opacity`, is the exact twin of the
|
|
|
drawer veil this firma just fixed, still alive because `dialog` has no
|
|
|
progress channel. **The expediente is [`next-features.md`](../next-features.md)
|
|
|
§18, and it is OPEN.**
|
|
|
- **`web/routes`** — FROZEN (`05dcb4db7`) and rebuilt whole; the 204 writes
|
|
|
there are a tree that is leaving. Judging them would need a clause this law
|
|
|
does not have, about WHO: a demo writing `--button-palette-solid` inline is a
|
|
|
CONSUMER using a token for what it is, not a framework publishing a name.
|
|
|
|
|
|
A write inside those roots has **four** outcomes — two green, two red — and
|
|
|
they are listed in the order the guard asks them, because the order is part of
|
|
|
the law:
|
|
|
|
|
|
1. **A name `base.ts` declares — RED.** The worst species, and the reason this
|
|
|
law is not a style rule. An inline per-instance write beats every normal
|
|
|
rule, so the theme entitled to that name moves NOTHING while the contract
|
|
|
keeps promising that it does. It is invisible from the reading side by
|
|
|
construction: the recipe reads a declared, contracted, perfectly legal name,
|
|
|
and the census scores it `public` and REACHED. R-5.4 is the rule that exists
|
|
|
to kill this species and it cannot see it either — R-5.4 writes on `:root`
|
|
|
or on the component root, and the inline write outranks both.
|
|
|
**This is asked BEFORE the underscore, and it was not always**: while the
|
|
|
prefix answered first, the **124** `_`-prefixed keys `base.ts` declares could
|
|
|
be written inline for free. A private recipe token is a theme's token too
|
|
|
(`render-css.ts:1900` emits a `_x` key as `--_{c}-x` out of the same
|
|
|
`EidosConfig.recipes` a theme supplies), so the species is «the theme is
|
|
|
entitled to this name and the write outranks it» and the underscore has
|
|
|
nothing to say about it. Measured before flipping the order: of 64 private
|
|
|
writes in soma + arts and 46 in eidos, **zero** name a contracted key — the
|
|
|
hole closes and nothing reddens.
|
|
|
2. **`--_{c}-*` that the contract does not declare — GREEN.** The destination.
|
|
|
The layer holds the pen; it does not own the surface. A value channel is not
|
|
|
theming surface and says so in its name.
|
|
|
3. **A name of the SYSTEM vocabulary — GREEN.** An art writing `--color-*` /
|
|
|
`--space-*` / `--motion-*` speaks the foundation's language; it is not
|
|
|
inventing a surface.
|
|
|
4. **Any other `--…` — RED.** A public FORM with no owner: published as API by
|
|
|
its spelling alone, reachable by nobody. The `--{c}-indicator-*` species of
|
|
|
§14, one floor down.
|
|
|
|
|
|
**WHAT THE GUARD DOES NOT CHECK, said here so no reader infers it from a
|
|
|
green.** Two gaps, both deliberate, and each one a place where the sentence and
|
|
|
the instrument do not coincide:
|
|
|
|
|
|
- **«PER INSTANCE» is not decidable statically, so the guard does not decide
|
|
|
it.** Its antecedent is WIDER than the doctrine's: it reddens any write of a
|
|
|
contracted or ownerless name in those roots, per-instance or not. That is why
|
|
|
the registry exists at all rather than being a leak in it — `--scrollbar-width`
|
|
|
is a single write on the one `<body>`, so the doctrine does not reach it while
|
|
|
the guard does, and the entry is what carries the difference in writing. A
|
|
|
rule whose antecedent the machine cannot evaluate is enforced by the nearest
|
|
|
one it can, and the gap is booked, not hidden.
|
|
|
- **The `{c}` of `--_{c}-*` is NOT enforced — only the underscore is.**
|
|
|
`--_totally-invented-thing` passes. The component segment is naming doctrine
|
|
|
(THEMING §6: no abbreviation, the component's own namespace) and this guard is
|
|
|
not its enforcer; the tree carries legitimate exceptions to the letter anyway,
|
|
|
because a shared LAYER writes in the HOST's namespace (`--_{host}-floating-*`,
|
|
|
§15) and some privates are layer-scoped, not component-scoped
|
|
|
(`--_viewport-placement-offset`). Closing this one means a rename axis, not a
|
|
|
line of guard.
|
|
|
|
|
|
**The system vocabulary is a DERIVED list, never a hand-written one**: the
|
|
|
guard subtracts the recipe contract from everything the foundation emits
|
|
|
(`renderGeneratedBaseEidosCss`), and what remains is what an art may speak. A
|
|
|
hand-kept allowlist would need editing every time the foundation grows, and
|
|
|
would drift into the exception file this axis has spent five signatures
|
|
|
avoiding. What that subtraction does is **carve outcome 3 out of outcome 4** —
|
|
|
it is not what separates the two reds, which is the contract set alone. Said
|
|
|
plainly because the earlier wording had it backwards, and «anything else is
|
|
|
red» is false while outcome 3 exists.
|
|
|
|
|
|
The exemption registry is the debt ledger's shape, not the exception file's:
|
|
|
it may only SHRINK — an entry whose name stops being an ownerless write is
|
|
|
STALE and stays red until deleted. It COUNTS a name; it does not bless it.
|
|
|
**And it answers the house's own objection, which is 24 hours older than it**:
|
|
|
`PENDING_PRIVATE_RENAME` was DISMANTLED on 2026-08-26 over this same species,
|
|
|
«there is no third state». What keeps this one alive is not permissiveness but
|
|
|
FORM — that registry held names awaiting a rename already decided, which is a
|
|
|
queue, and a queue with no deadline is a cupboard; these hold names the law
|
|
|
does not reach or cannot judge alone, which is the ledger's own case. So an
|
|
|
ENTRY is four fields and the guard asserts all four are filled:
|
|
|
|
|
|
| field | what it stops |
|
|
|
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
| `since` | an undated registry cannot age |
|
|
|
| `reason` | why the antecedent misses it — MEASURED, not asserted |
|
|
|
| `destination` | where it goes, named. Not «to be decided» |
|
|
|
| `heldBecause` | **why it did not move THAT day.** The load-bearing one: an entry that cannot say what blocks it has nothing blocking it, and belongs in the commit instead of in this list |
|
|
|
|
|
|
Two adjudications opened it and both are outside the law's antecedent rather
|
|
|
than deviations from it: `--scrollbar-width` (`arts/adom/body-scroll-lock` —
|
|
|
one write on the single `<body>`, a fact of the viewport, not of an instance)
|
|
|
and the seven `--palabras-scheme-*` of the WIP track, registered rather than
|
|
|
waived because the debt ledger already settled that question the other way
|
|
|
(«THE WIP TRACKS ARE IN … debt is real»).
|
|
|
|
|
|
**Why the guard is a vitest file and not a script**: `npm run gate` ends in
|
|
|
`npm run test`, so the suite is what turns a doctrine into a law. And why it
|
|
|
sits at `src/uix/` root rather than under `eidos/`: its SUBJECT is the writing
|
|
|
side and its JUDGE is the eidos contract, so it belongs to no single layer.
|
|
|
|
|
|
The measurement that forced it: the census, the debt ledger, R-5.1 and the
|
|
|
closed-namespace law all read `src/uix/eidos/components` and nothing else
|
|
|
(`scripts/theming-census.ts`, `const ROOT`). **The 73 % was measured over the
|
|
|
side that READS, and only that side** — deliberately not called «half», because
|
|
|
no recomputed denominator exists: a write in soma is not a recipe declaration,
|
|
|
so the two do not share a fraction. What is true is narrower and enough: the
|
|
|
number says nothing about the writers, and until this rule there was no
|
|
|
instrument that did. The commit that named the general law is
|
|
|
`05dcb4db7` («LEY — web/routes congelado, y la consecuencia: el guard vive
|
|
|
donde NACE el valor»), signed for the P1 axis the same week with the same
|
|
|
argument: an instrument aimed at the observed artefact measures a tree that is
|
|
|
leaving, not the contract. This is that sentence translated to theming.
|
|
|
|
|
|
Mutation-proved six ways, each with the tree restored byte-identical: a new
|
|
|
unprefixed name in a provider → red, NAMING it; a write of a key that IS in the
|
|
|
contract → red, «a token that LIES»; a correct `--_` channel → green; a SYSTEM
|
|
|
name written by an art → green (the only branch with no live case in the tree,
|
|
|
so it had to be proved by injection); the canonical dynamic form
|
|
|
``[`--${component}-x`]`` → red as `--{}-x`; and a registry entry that stops
|
|
|
describing a write → STALE. The scope pass added three more the same day: a
|
|
|
CONTRACTED private key (`--_avatar-badge-bg`) written inline → red as a token
|
|
|
that lies, which was **green** before the order flipped; and, in `blocks` — a
|
|
|
root nobody swept that morning — `--_hero-private-leak` → **green** while
|
|
|
`--hero-public-leak` → red, which is the difference between the law and the
|
|
|
census of zero it used to assert there.
|
|
|
|
|
|
**Sweeping by CAPTURE and not by pattern is what made it correct.** The firma
|
|
|
named four write forms; the scanner resolves **six**, and one of the two missing
|
|
|
was
|
|
|
the CANONICAL one — the computed key ``[`--_${component}-floating-anchor-width`]``
|
|
|
that §14 and §15 signed. A guard blind to that form is blind to the destination
|
|
|
it enforces, and it would have been green over a public `--{c}-*` written that
|
|
|
way. The other was `out['--x'] = v`, the member assignment `palabras` uses,
|
|
|
which is what put those seven names on the record at all. Two shapes stay
|
|
|
unresolvable and are written down rather than implied by a green: a name
|
|
|
ASSEMBLED across statements (`soma/layers/measured-indicator`) and a name
|
|
|
reaching `setProperty` through a variable.
|
|
|
|
|
|
The exception valve of §3 is untouched: the `/* literal: <reason> */`
|
|
|
annotation on the declaration, and the component-level `R-5.1 exception:` /
|
|
|
`R-5.2 exception:` of a README (23 components carry one today). Precedence,
|
|
|
because it is not obvious: a README valve softens the component's audit ROW,
|
|
|
while the catalogue floor (`theming-reach-floor.test.ts`) reads ONLY the
|
|
|
ledger — so a valve never blinds the ratchet. A STALE entry is excused by no
|
|
|
valve: it is not a deviation to sign, it is bookkeeping that claims a knob is
|
|
|
broken after it was fixed.
|
|
|
|
|
|
Mutation-proved the day it landed, both directions: injecting
|
|
|
`border-radius: 7px` into `mark` names the key in `newDebt`, reddens the floor
|
|
|
and fails `mark`'s R-5.1 row; tokenizing a registered knob of `aura` reports
|
|
|
that entry STALE and stays red until the line is retired (that day, 1088 → 1087).
|
|
|
|
|
|
**The third unreached class closed one signature later** (firma
|
|
|
«clasificación de los 318», 2026-08-26). A `private` that derives from no
|
|
|
public is debt by the letter of R-5.1 (`PLAN-theming.md` §3), and until that
|
|
|
day it was OUT of the per-key ratchet — 318 knobs in a bulk, named rather than
|
|
|
hidden, riding only the coarse `reachPct` floor. They are adjudicated key by
|
|
|
key now, and a non-derived private has exactly **five exits**, two decided by
|
|
|
measurement and three by an act:
|
|
|
|
|
|
1. **`bridge`** (132) — the knob reads the private the THM-2 palette forward
|
|
|
WRITES. Decided against the generator's real output (the
|
|
|
`--_{c}-palette-{slot}` line of `renderRecipePaletteForward`), never
|
|
|
against the shape of the name: a `--_c-palette-shadow` nobody forwards is
|
|
|
no bridge. It is reachable twice over — the `--palette-*` layer and the
|
|
|
public tones — and a public on top would let a theme PIN it and kill the
|
|
|
instance's `color=`.
|
|
|
2. **`channel`** (36) — the private NOBODY declares, neither the recipe nor the
|
|
|
generator, because soma or the wrapper writes it per instance. A theme must
|
|
|
not reach it.
|
|
|
3. **The `private:` annotation** (84) — §3's valve, one class over: the same
|
|
|
written act as `literal:`, on the declaration, for a shape this doctrine
|
|
|
already blesses ONE LEVEL BELOW the knob (a variant / role CONMUTADOR whose
|
|
|
branches are the palette bridge; a value channel read through an alias). It
|
|
|
classes `exception` — the canon's existing signed-deviation class, not a
|
|
|
new tier.
|
|
|
4. **An entry in the debt ledger** (66) — `private` is a `DebtClass` beside
|
|
|
`global` and `literal`, so an unsigned non-derived private is `newDebt`,
|
|
|
NAMED, and its entry reports STALE the day it gains its annotation or starts
|
|
|
deriving from a public.
|
|
|
5. **Deriving from a public**, the exit that empties the other four.
|
|
|
|
|
|
The arithmetic closed with nothing left over: 132 + 36 + 84 + 66 = 318, and
|
|
|
the ledger went 1088 → **1154** keys. The ratio moved 69 → **73 %**
|
|
|
(3074 of 4228) without a single recipe changing a value: the numerator stood
|
|
|
still and the denominator fell by exactly the 252 that stopped counting as
|
|
|
unreached
|
|
|
(132 + 36 + 84), the way `system` and `structural` already left it — the 66
|
|
|
ledger'd privates stay in. The floor (`theming-reach-floor.test.ts`) rose to
|
|
|
73 % / 65 components at 100 % in the commit that earned it.
|
|
|
|
|
|
**R-5.3 is a grammar guard, not a list guard** — unlike
|
|
|
`eidos-event-vocabulary`, which checks membership of a closed vocabulary, this
|
|
|
one checks a SHAPE. It consumes the classifier of
|
|
|
`theming-census --names`, the same source the codemod ran on: two
|
|
|
implementations of one grammar are two grammars that end up disagreeing. It
|
|
|
went to `error` directly, with no `warn` ramp, because the codemod emptied its
|
|
|
debt in the same pass (the R-4.4 precedent). Keys queued for the state-layer
|
|
|
migration are reported apart — they are not naming debt, they are knobs about
|
|
|
to disappear. Muta-prueba de tres caras (it is in `scripts/__names-mutatest.ts`
|
|
|
and it earned its keep: it caught the classifier promoting ANY morfo-declared
|
|
|
value to the front, which turned `--sidebar-width-icon` into
|
|
|
`--sidebar-icon-width`): `-bg-hover` with a valenced value → red ·
|
|
|
`trigger-color` → red · `primary-solid-hover` → **green**.
|
|
|
|
|
|
**R-5.4 is a REACH guard, and it needs a browser** — like `morfo-check`, it
|
|
|
runs against a live dev server (`scripts/theming-sentinel.ts`), because what
|
|
|
it checks no static analysis can see: for every public token it writes an
|
|
|
unmistakable value and demands that some node's computed style follow it. It
|
|
|
was born from two findings of the F2-A adversarial review (2026-08-21) that
|
|
|
only this instrument caught — `gradient-picker` re-declared its trigger's
|
|
|
entire chrome under a `popover.css` rule that always wins (36/45 tokens dead),
|
|
|
and `carousel` minted an `item-gap` trio against a soma INLINE style that no
|
|
|
cascade can beat. A silent token is either retired or ADJUDICATED: the ledger
|
|
|
(`scripts/theming-sentinel-exceptions.ts`) carries one written reason per
|
|
|
token — part not mounted by the demo, state that needs forcing, pseudo-element
|
|
|
or composed-component limit — and the guard fails on any dead token without an
|
|
|
entry. A ledger entry whose token moves again is reported as STALE (prune it;
|
|
|
same debt as a stale allowlist row — warning, not failure, because the
|
|
|
instrument keeps a residual nondeterminism the probe's history documents).
|
|
|
|
|
|
**The guard grew six capabilities during F2-B (2026-08-22), each born from a
|
|
|
measurement and each re-verified against every component that already had
|
|
|
ledger entries — zero regressions after any of them.** They are worth knowing
|
|
|
because each names a way the instrument used to LIE:
|
|
|
|
|
|
- **The pointer is parked after the blur.** `reopen()` runs before EVERY token
|
|
|
and clicks the component open, so `blur()` alone left the cursor on the node:
|
|
|
`:hover` matched for the whole run and every hover rule out-ranked its
|
|
|
resting neighbours. Cost three false negatives on `textarea` alone.
|
|
|
- **`::placeholder` IS readable** through `getComputedStyle`. The note claiming
|
|
|
otherwise was never checked; it had already cost `command.input-placeholder-fg`
|
|
|
a hand-verified ledger entry, now pruned as STALE.
|
|
|
- **`COMPONENT_OVERRIDES`** — per-component attribute prefix and opening
|
|
|
selector, for components whose DOM does not follow `data-{component}-{part}`.
|
|
|
`picker-shell` names its parts generically ON PURPOSE (`data-picker-header`)
|
|
|
so every picker shares one visual contract, and that convention blinded every
|
|
|
filter: the guard reported 0/31.
|
|
|
- **`openBy: 'hover'`** for hover cards, whose trigger is an `<a>`: clicking it
|
|
|
navigates instead of revealing the portaled panel.
|
|
|
- **`text-decoration-thickness` / `-underline-offset`** joined the property
|
|
|
list, which had `text-decoration-color` but not its siblings.
|
|
|
|
|
|
The lesson that generalizes: **a probe or guard that inspects few nodes passes
|
|
|
in false**. Count what the instrument actually sees before trusting a green
|
|
|
gate — `picker-shell` measured ZERO nodes and reported success.
|
|
|
|
|
|
**All R-4.x are `error`** (graduated 2026-07-02: the five mechanical ones
|
|
|
after the backfill; R-4.5 after the motion migration emptied its 15
|
|
|
components). The doctrine the migration left — **how a recipe consumes the
|
|
|
motion channel**:
|
|
|
|
|
|
1. **Preset stamp** (`data-animation-style` via the `motion` prop) when the
|
|
|
element has no semantic `data-state` of its own and no conditional
|
|
|
trigger: overlays (popover, menus, select, combobox, tooltip via the
|
|
|
`delayed-open` alias), content mounts (`motionAttrs`).
|
|
|
2. **Event signature** (`EidosConfig.motion.signatures`) when the animation
|
|
|
reacts to sema's `data-event-*` and is generic per verb/family
|
|
|
(expand/collapse, present/dismiss, announce per intent, the `shift`
|
|
|
crossing per `data-event-direction`).
|
|
|
3. **Materials pattern** (the rule lives in the recipe but consumes ONLY
|
|
|
registered keyframes + `--motion-*` hooks) when the trigger is
|
|
|
irreducible to the generic: a semantic `data-state` of its own (card),
|
|
|
container-flag gating (timeline `data-live`), per-component axes (tabs
|
|
|
`data-motion` × orientation), soma's directional machinery
|
|
|
(navigation-menu from/to), or the per-component materialization of a
|
|
|
signal (metrics `value-flash` per intent). The reason is documented in
|
|
|
the rule.
|
|
|
|
|
|
Sibling guards already active: R-2.1/2.5/2.6 (color), R-2.7 (literal
|
|
|
typography), `recipe-css-contract.test.ts` (consumed tokens + TSC +
|
|
|
non-literal font/icon-size + variants vs type unions + **no phantom public
|
|
|
refs** — every no-fallback `var()` in a recipe value must exist in the
|
|
|
derived contract + **privates need a covering scope** — `--_*` refs never
|
|
|
from `root`, both 2026-07-07), the raw-z guard in `contracts.test.ts`.
|
|
|
|
|
|
The WIP tracks `words` / `palabras` / `chronos` are excluded from R-4.x and
|
|
|
R-5.1/5.2 (same as the typographic guard) until they leave their parallel
|
|
|
track. Their DEBT is registered anyway — 583 of the 1154 keys — because the
|
|
|
catalogue floor asks a catalogue-wide question and leaving a third of the debt
|
|
|
unfrozen would make F3's gate a claim about two thirds of the tree. The
|
|
|
ledger's header carries the reasoning, and one measured caveat about
|
|
|
`palabras`.
|
|
|
|
|
|
## 5. Authoring checklist (before calling a recipe done)
|
|
|
|
|
|
1. Dimensional tokens with §1's names (logical axes, canonical heights).
|
|
|
2. Neutral hover = state-layer; valenced hover = palette tokens.
|
|
|
3. Zero literal `box-shadow` — everything through
|
|
|
`--shadow-*`/`--depth-*`/inset-ring.
|
|
|
4. Focus per archetype; not reinvented.
|
|
|
5. `opacity: var(--opacity-disabled)` on disabled.
|
|
|
6. `font-size`/`icon-size` 1:1 with the scale (or a documented canonical
|
|
|
exception).
|
|
|
7. Transitions with `--duration-*`/`--ease-*`; no perceptual-signature
|
|
|
`@keyframes`.
|
|
|
8. Spacing with `--space-*`, no hand-multiplied density.
|
|
|
9. If it's a list/menu → list-surface; if it PORTALS → a `--z-index-overlay-*`
|
|
|
rung; if it is viewport-fixed page chrome → the `affix` rung, via the
|
|
|
shared layer rather than a private copy of the geometry.
|
|
|
10. Every deviation carries its annotation (§3).
|
|
|
11. `node --import tsx/esm scripts/component-audit.ts --only {kebab}` with no
|
|
|
new R-4.x.
|