Skip to content

v0.91.0: the design language, enforced — grammar tokens, cairn-audit, and the admin-screens skill

Choose a tag to compare

@glw907 glw907 released this 29 Jul 22:26

The design-infrastructure initiative's window: three passes held under one cut. The admin's design language becomes named structure (the grammar tokens and role utilities), mechanically checkable (the cairn-audit bin, nine static and eleven rendered rules, the norms manifest), and loadable by an agent (the packaged cairn-admin-screens skill, installed by cairn-doctor --fix). Two Consumers must: lines below; the type-grammar adoption recipe is in Upgrade cairn.

Added

  • The admin's structural type and spacing scale is now named: ten CSS custom properties
    (--cairn-type-title/subtitle/body/meta/label/chip, --cairn-gap-label/control/group/section)
    declared once in cairn-admin.css, outside the light and dark theme blocks, so a heading's size
    or a control-to-control gap holds across both themes. Ten role utilities
    (type-title/type-subtitle/type-body/type-meta/type-label/type-chip,
    gap-control/gap-label/gap-group/gap-section) are the supported way to reach a token from
    markup; each sets one property and nothing else. All ten ship in the compiled admin stylesheet
    whether or not cairn's own screens use them, so a role is available to a custom admin route on
    the strength of the documentation alone. See Admin grammar
    tokens
    .

  • src/lib/admin-toolkit and the built-in engine admin screens migrate their on-scale type and
    spacing literals to the new role utilities and tokens, pixel-identically: the committed
    admin-visual snapshots do not move. The toolkit's scoped styles carry the measured literal as a
    var() fallback, since those components can mount outside the admin theme root; engine screens
    reference the token bare.

  • The palette/grammar boundary is now a documented contract: a site re-tunes the palette tokens
    (--color-*) to its own brand and never redeclares a grammar token, since grammar names
    structure and palette is the brand layer.

  • The type scale closes at seven roles, each carrying a ruled line-height, and the grammar
    inventory grows from ten custom properties to eighteen. Every type role gains a paired
    --cairn-type-<role>--leading token, and a seventh role, --cairn-type-heading with its
    type-heading utility (18px, bold, the display face), unifies the two heading recipes the admin
    had been running side by side. Each type-* utility now declares
    line-height: var(--tw-leading, var(--cairn-type-<role>--leading)), so a leading-* utility
    still composes over a role rather than being overridden by it. cairn's own admin screens migrate
    onto the closed scale: 129 named type steps, 120 twelve-pixel sites resolved onto type-meta or
    type-label by the relationship each site expresses, both heading families onto type-heading,
    and the stray 9.6px and 11.2px slips onto type-chip and type-label. Five sites keep a value
    off the scale and each carries a counted suppression directive naming why: the three wordmark
    sites (the K4 keming fix) and the editor canvas and its document title, which set the editor's
    own type scale rather than the admin chrome's. The losing heading family and part of the
    twelve-pixel set change size by ruling, so the admin-visual baselines were regenerated once on
    CI and read by eye. See Admin grammar tokens.

  • Two container role utilities, card-shell and card-shadow, are the supported replacement for
    hand-copying the admin's card-shell class string
    (rounded-box border border-[var(--cairn-card-border)] bg-base-100, optionally with
    shadow-[var(--cairn-shadow)]). card-shell carries the shell's radius, hairline border, and
    fill; card-shadow carries its elevation, kept separate because a surface already nested inside
    a shadowed container takes card-shell alone. Both ship in the compiled admin stylesheet
    regardless of cairn's own usage, the same safelist discipline as the eleven grammar role utilities.
    cairn's own admin components migrate every verbatim shell site onto the new utilities,
    pixel-identically. See Admin grammar tokens.

  • The package ships a new bin, cairn-audit, the design-language audit. An install puts it on the
    project's path, and npx cairn-audit runs the static rules over the admin surfaces. The
    substrate is svelte/compiler for markup and the built cairn-admin.css for resolution, never a
    regex over source text: a class token is matched exactly, so text-base (the size utility) and
    text-base-content (the daisyUI color utility) can never read as the same class, and a class
    written as an array, an object, a template literal, or a class: directive is seen the same as
    one written in a plain attribute. Configuration is one optional cairn-audit.config.json whose
    every key defaults, so a site that has written no config still gets a meaningful run; a config
    that names a scan path the tree does not have fails the run instead of quietly auditing less.
    See The cairn-audit CLI.

  • Nine static rules ship, all error tier. no-uncompiled-class: every markup class token compiles
    into the built sheet or is a name the component's own <style> block defines. type-scale:
    every font size a text-sizing token resolves to comes from a --cairn-type-* role.
    gap-scale: an arbitrary margin, padding, or gap literal resolves to a --cairn-gap-* role or
    lands on Tailwind's own spacing grid. stock-default-hazards: four stock daisyUI patterns
    cairn's own recipes replace (badge-ghost, the focus-driven bare .dropdown, native disabled
    on a guarded button, a flat base-300 card border), each finding citing the refuted alternative
    on record. token-colors: no raw hex, rgb(), named-color, or pure-achromatic literal outside a
    declared palette site. grammar-boundary: consumer CSS never redeclares a grammar token.
    focus-parity: every hand-authored :hover selector has a matching :focus-visible or
    :focus-within. motion-band: every declared duration lands in the admin's 150 to 250ms band,
    and transition: all never ships. reduced-motion: every motion-bearing selector is named again
    inside a prefers-reduced-motion: reduce guard.

  • Suppression is a co-located, reasoned, counted comment:
    cairn-audit-disable-next-line <rule-id> -- <reason>, in HTML, //, or /* */ form. The reason
    is required, a directive that silences nothing is itself a finding, and neither of those errors
    can be suppressed in turn, so a build that passes by suppression reads as one. "Next line"
    resolves to the next AST node rather than the next physical line, which is what makes a directive
    above a multi-line element attach to that element's whole source range. This replaces the
    file-plus-token JSON allowlists the repo's own gates carried, whose entries orphaned silently on
    a rename and exempted a whole file at a time.

  • Two repo gates graduate onto the packaged engine and keep their names and their places in CI.
    npm run check:invisible-craft now runs motion-band, gap-scale, and token-colors, and
    npm run check:admin-css-classes runs no-uncompiled-class; both are thin wrappers, and the
    hand-rolled comment strippers, brace-matched attribute scanners, and duration and color regexes
    they carried are gone. Their two JSON side files go with them: every entry in
    admin-css-classes-allowlist.json proved already dead, and each entry in
    invisible-craft-budget.json resolved to a regex false positive the new rules recognize as
    compliant, a site outside the audited scope, a declared palette file, or a co-located suppression
    directive at the site itself.

  • The package now ships a norms manifest: the admin's measured design norms as data, derived by
    rendering the admin screens in both themes and reading the computed styles of twelve semantic
    roles. npx cairn-audit norms <selector-or-role> answers from it, so a developer or an agent
    building a new admin surface reads a measured control height, padding ratio, border treatment,
    radius, or type recipe instead of inferring one from a screenshot. Each entry states the band, how
    many distinct element sites it rests on, whether a ratified decision settles it, and every caveat:
    an entry an open design question governs is flagged and never reads as settled, a band resting on
    one site says so, and a palette-dependent property is stored as a relationship
    (var(--cairn-card-border), a color-mix formula) rather than a resolved Warm Stone value, so a
    re-tuned palette invalidates nothing. See The cairn-audit CLI.

  • cairn-audit's rendered mode has a working harness. It renders every configured admin page in
    both themes, always, since a rendered rule can pass one theme and fail the other; it never starts
    a server (BASE_URL must already answer, or the run fails naming the URL it tried), and it imports
    Playwright dynamically from the consumer's own install, printing a one-line
    npm i -D playwright && npx playwright install chromium instruction when it is absent. A rule
    declares which interaction states it reads from (a rest render, an open menu, a keyboard
    focus-visible pass), so a rule that never needs a menu-open pass never pays for one. Exemptions
    live in a page+selector+reason JSON allowlist (rendered.allowlist in
    cairn-audit.config.json), the same reason-required discipline the static suppression comments
    carry; an allowlist entry whose selector matches nothing the run actually visited is reported as
    a stale entry rather than silently doing nothing, the same fail-loud discipline every other part
    of the engine holds. See The cairn-audit CLI.

  • npx cairn-audit --rendered runs, and the six error-tier rendered rules spec 6.3 defines are
    registered: one-filled-action (at most one accent-filled control per surface),
    focus-renders (every tab stop renders a focus indicator), interactive-contrast (interactive
    text against its own composited background at a ratio of at least 1.5), touch-targets (a tap
    target's activation region at a 390px viewport), viewport-overflow
    (nothing wider than the viewport at 390 and at 320), and chip-ground-collision (a chip's fill
    distinguishable from the background behind it). A rendered rule that compares two colors resolves
    them by painting each on a canvas in the page and reading the sRGB bytes back rather than parsing
    color syntax, so a themed admin in any color space (cairn's own palette is oklch end to end)
    measures correctly. A rule that cannot make its measurement, a gradient leaves no single ground
    to compare against, reports an advisory finding naming what it could not read rather than
    skipping silently. See The cairn-audit CLI.

  • The five advisory rendered rules spec 6.3 defines are registered, completing the rendered rule
    set at eleven: border-contrast (a border reads at 3:1 against at least one of the two surfaces
    it separates), weight-budget (at most two distinct font-weights per content region),
    norms-bands (a component's geometry against the bands the norms manifest observed),
    screen-anatomy (one PageHeader h1, content in the card region, no accent-filled action buried
    outside both), and relational-spacing (the --cairn-gap-* scale matches the relationship the
    markup renders). Advisory means advisory: none of the five can change the process exit code,
    through its own findings, through a selector the browser cannot parse, through a stale
    suppression, or through a throw inside a rule.

  • Rendered rules share one set of in-page measurement helpers rather than each carrying a copy, so
    "is this visible" and "what selector names this element" mean one thing across the rule set. An
    element the visually-hidden recipe clips is no longer counted as rendered, an element under an
    ancestor opacity: 0 is no longer measured, and every reported selector is escaped, so a
    Tailwind class such as lg:ml-56 stays a valid CSS selector the allowlist can match on.

  • A rendered allowlist entry may name the rule it exempts ("rule": "border-contrast"). A stale
    entry is then reported at that rule's own tier, so suppressing an advisory finding cannot gate
    the build when the selector later churns. An entry whose selector the browser refuses to parse
    reports separately and always advisory, since unreadable is a different claim from stale.

  • A rendered rule that throws reports a finding at its own tier instead of aborting the run. A rule
    that reads a file inside its check (norms-bands reads the shipped manifest) could otherwise
    take an entire audit to exit code 2 on a substrate condition in a consumer install.

  • Four design rulings settle what three rendered rules enforce, each recorded in the rule's own
    source and on the reference page so the reasoning does not have to be re-derived.
    touch-targets enforces a 24x24 floor, the number WCAG 2.2 level AA's success criterion 2.5.8
    sets, rather than AAA's 44x44, since AA is the conformance level cairn can honestly claim. The
    rule is a strict superset of that criterion rather than an implementation of it, so a finding is a
    house-bar failure and not on its own an AA failure; four of the criterion's five exceptions are
    not evaluated, and each finding says so. It measures the activation region rather than the painted
    box: the control's own box, a qualifying ::before inset expansion, and every label the platform
    reports as activating the control. The --cairn-card-border hairline is a ratified exception,
    exempt at the rule rather than blanket disabled, so border-contrast still reports every other
    boundary. chip-ground-collision's floor of 1.5 is ratified, and it and interactive-contrast
    share both the number and the reason: each tests that a control or a chip is not accidentally
    camouflaged against its own ground, which is a different claim from legibility. Legibility is WCAG
    1.4.3 Contrast (Minimum), at 4.5:1, and no rule in this engine measures it; the reference page now
    states the engine's whole WCAG coverage boundary rather than leaving a reader to infer one.
    weight-budget's content region excludes chrome, and chrome is defined by HTML tag and the
    equivalent ARIA role rather than by class, so a rewritten component stays covered.

  • A rendered rule can declare its own exemption, for a ratified exception no page+selector entry
    names and no source-positioned comment can reach: a design token every recipe shares, on every
    page. An exemption suppresses without silencing. The rule still constructs the finding, the
    finding still carries its own measurement, and it reaches the report's suppressed list with the
    ruling printed beside it. Only an advisory rule can exempt itself; on an error-tier finding the
    run refuses the reason, prints the refusal, and the exit code stands, because a gate any rule
    could quiet in one line is worth no more than the runs it passes. The allowlist gains the
    matching honesty in the other direction: an entry whose selector still matches an element while
    suppressing nothing reports as a dead entry, at the tier of the rule it names.

  • cairn-audit --rendered reads an optional CAIRN_AUDIT_COOKIES environment variable, Cookie-
    header syntax (name=value entries separated by a semicolon), and adds every parsed cookie to
    each browser context alongside the theme cookie. This is how a rendered run reaches a consumer's
    authenticated admin, a session cookie against a local wrangler dev, since the run-specific
    credential belongs in the environment rather than the config file, the same reasoning BASE_URL
    follows. A malformed entry throws rather than the parser silently skipping it, and an entry named
    cairn-admin-theme throws too, since the run owns that cookie itself. See The cairn-audit
    CLI
    .

  • StatusChip (@glw907/cairn-cms/admin-toolkit) gains a register: 'bounded' | 'quiet' prop
    (design infrastructure Pass 3): 'bounded', the default, is today's discrete-object reading, its
    border demoted from badge-outline's full-strength currentColor (which read as a clickable
    button) to a hairline that clears the audit's own 3:1 border-contrast floor in both themes;
    'quiet' is a new borderless, token-tinted recipe for a settled state (a Published pill, say)
    that should recede rather than compete. Both recipes are measured, not invented
    (docs/internal/probes/2026-07-28-chip-registers/). badge-ghost retires from cairn's own tree:
    EditPage's Published pill, CairnAdminShell's CMS pill, and every bare badge-outline site move
    onto the two registers, and stock-default-hazards' guidance for badge-ghost now points at
    them, naming 'quiet' as the sanctioned put-away recipe. Consumers must: replace any own
    badge-ghost usage (stock-default-hazards now errors on it) with StatusChip register="quiet" or the equivalent .cairn-chip-quiet recipe in cairn-admin.css.

  • The package now ships a Claude Code skill, skills/cairn-admin-screens/ (design infrastructure
    Pass 3), carrying the admin design standard as loadable reference: an always-loaded core (screen
    anatomy, the register rules, the done-gate), two annotated exemplar screens (a list and a
    detail/slide-over), a form-anatomy contract, an extension grammar for deriving a component the
    toolkit lacks, a calibrated grader prompt, and a craft chapter translating an invisible-polish
    catalogue into rule-backed recipes. cairn-doctor gains a skill.admin-screens check: missing or
    stale (by a content hash of both trees) reports advisory and never fails the run, and
    cairn-doctor --fix installs or refreshes the packaged skill into a consumer's own
    .claude/skills/cairn-admin-screens/ before the checks run. See the cairn-audit
    CLI
    and cairn-doctor.

No consumer action is required for the entries above beyond the badge-ghost migration named
above. No exported type, prop, or route contract changed otherwise.

Fixed

  • The Media Library no longer 500s when a stored asset's alt text is null or missing. A
    committed or branch media.json row is trusted wholesale on read, so a hand-edited or
    older-schema manifest could cross the trust boundary with a non-string alt; mediaLibraryEntry,
    the one place MediaLibraryEntry is constructed, now normalizes it to '', the library's
    existing needs-alt signal.
  • PageHeader (@glw907/cairn-cms/admin-toolkit) no longer leaks its default <h1>/<p> margins
    past its own gap-0.5 heading-stack intent. OfficeList, the component PageHeader's own doc
    calls itself "the shape, generalized", carried this UA-margin fix already; PageHeader had not
    received it at graduation, so its title-to-meta gap rendered at roughly 58px instead of the
    intended 4px. The meta prop also now renders at the meta type role (13px, matching its own
    prop name) rather than the body role (14px), so it reads at the same size as a screen's own
    ListToolbar count line when both appear together. Consumers must: expect the header stack
    on any screen mounting PageHeader to render visibly tighter (a shorter title-to-meta gap) and
    its meta line one step smaller; no prop or type changed.
  • cairn-audit --rendered's chip-ground-collision demotes from error to advisory (design
    infrastructure Pass 3, corpus C, 2026-07-28): the formula has no chroma term and cannot see hue,
    which produced 24 false errors of 40 on the first consumer admin it measured, so as coded it
    could not serve as a consumer gate. The formula itself is unchanged; a chroma-aware repair is
    filed in ROADMAP and re-promotes the rule on re-measured evidence. Consumers must: expect a
    chip-ground-collision finding to no longer fail cairn-audit --rendered's exit code; it still
    reports.
  • Cairn's own admin's cairn-audit --rendered error tier is clean (design infrastructure Pass 3):
    ConceptList's Title/Date sort buttons gain an outward ::before hit-area expansion (a
    touch-targets fix) instead of a font-size bump, and its row-title link grows through real
    padding on its own box, since truncate's overflow: hidden would clip a ::before reaching
    past it; ListToolbar's segmented filter group can now shrink below its own preferred width
    (flex: 0 1 auto), so it wraps onto a second line at 320px instead of overflowing the viewport
    (a viewport-overflow fix). Internal admin CSS and markup only; no consumer action.

Documentation

  • A new explanation page, Why the design language is
    enforced
    , states the sixth front-door principle: cairn's
    admin design language is enforced, not merely documented, and the payoff is that a developer
    spends less effort building an admin interface. The README's principle ledger grows from five
    entries to six to match. Upgrade cairn gains the grammar-release
    adoption recipe: how to read a type-scale finding, match it to a named role in Admin grammar
    tokens
    , and rename the class, plus the safelist
    reachability note that makes the rename safe.