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

147 lines
9.2 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

---
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`** (physical axes — they break RTL) |
| 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-`.
## 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 (`archetypes.css`) | 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 */` |
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 |
**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).
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), 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.