You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/canon/recipe-contract.md

12 KiB

title type audience authority status enforcement source related
Recipe Contract — the transversal contract of every eidos recipe canon human + agent canonical — which theming systems every recipe MUST consume, and under which names current scripts/component-audit.ts (rules R-4.x, all `error`) migrated from src/uix/eidos/RECIPE_CONTRACT.md (2026-07-02, docs-book F7.3)
tsc theming guide
docs/canon/tsc.md (where each token is emitted) docs/theming/reference.md (the system; §5 sizes · §6 naming) 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) —

Inherited naming rules (THEMING §6): public --{c}-{slot}, private --_{c}-{slot}, no color- segment, no kebab abbreviation, no --eidos-/-soma-/-air-.

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.
  • 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).

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
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

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 (same as the typographic guard) until they leave their parallel track.

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 floats → a --z-index-overlay-* rung.
  10. Every deviation carries its annotation (§3).
  11. node --import tsx/esm scripts/component-audit.ts --only {kebab} with no new R-4.x.

Powered by TurnKey Linux.