Zudo Token Panel
GitHub repository

Type to search...

to open search from anywhere

Apply pipeline

How the tier resolver turns TabOverrides into CSS-var writes, the v2 export schema, applyEndpoint + applyRouting, request/response envelopes, and atomicity guarantees.

The apply pipeline turns the panel's in-memory override state into CSS writes and (when the host wires an endpoint) into source-file rewrites on disk. There are two distinct paths:

  1. :root inline style — applied client-side, immediately, on every user tweak.

  2. Apply-to-disk — triggered by the Apply button; POSTs a diff to the host's dev endpoint which routes it to the bin server.

State → tier resolver → CSS emission

TabOverrides shape

The panel's persisted state for each tab is a two-level nested map:

type TabOverrides = Readonly<Record<string, Readonly<Record<string, string>>>>;
// tierId → itemId → overrideValue

Example:

const overrides: TabOverrides = {
  raw:      { 'ease-in': 'cubic-bezier(0.42, 0, 1, 1)' },
  semantic: { 'tab-open': 'ease-in' }, // reference tier — value is a raw-tier item id
};

resolveTierItemValue

The core resolver (resolveTierItemValue) determines the effective CSS value for a single item:

  • Literal tier (no referencesTier): returns the override string, or pill.customDefault when a pill is present and no override is active, or item.default as the final fallback.

  • Reference tier (referencesTier is set): the override is interpreted as the id of an item in the named tier. Returns { kind: 'ref', targetCssVar } pointing at that item's cssVar.

emitTierItemCssValue

Converts a ResolvedTierItem to the final string written to the CSS custom property:

  • Literal → value string as-is (e.g. 1.25rem).

  • Ref → var(--targetCssVar) (e.g. var(--myapp-easing-ease-in)).

Cross-tier reference example

Given a two-tier easing tab (raw + semantic):

semantic.tab-open override = 'ease-in'
  → looks up 'ease-in' in raw tier
  → raw tier item ease-in has cssVar '--myapp-easing-ease-in'
  → emits --myapp-transition-tab-open: var(--myapp-easing-ease-in)

Only the raw tier item's cssVar is written to :root directly. The semantic tier item always emits var(...) — it never writes a raw CSS value.

:root inline style write

The panel writes overrides to document.documentElement.style.setProperty(cssVar, value) on every user change. When no override is active for an item, the inline property is removed (so the stylesheet default wins).

The Color tab's palette, base-role, and semantic slots are written by the same mechanism — palette items directly, semantic items via var(--palette-cssVar).

Export schema (v2)

The panel exports and imports overrides as a JSON envelope with a $schema field for version identification:

{
  "$schema": "zudo-design-tokens/v2",
  "--myapp-spacing-md": "1.5rem",
  "--myapp-easing-ease-in": "cubic-bezier(0.42, 0, 1, 1)"
}

The top-level keys (after $schema) are the CSS custom property names of every active override. The values are CSS strings. On import, the panel validates the $schema field against the canonical SCHEMA_V1 / SCHEMA_V2 / SCHEMA_V3 constants — NOT against PanelConfig.schemaId, which is a display-only label the serde never reads (see configurePanel reference). A $schema value that matches none of the three canonical constants is rejected with a schema-mismatch error.

Flat cssVar-keyed shape

The v2 export format is a flat map of cssVar → CSS string. Reference-tier items are expanded to their emitted var(...) value at export time — the exported file contains only the CSS values the user would see in :root, not the intermediate item ids.

Apply-to-disk

applyEndpoint

When PanelConfig.applyEndpoint is set, the Apply button is enabled. Clicking it POSTs the active override diff to this URL.

When applyEndpoint is undefined, the Apply button stays disabled with a tooltip — hosts that use export/import only can omit this field.

applyRouting

applyRouting: {
  'myapp-spacing': 'src/styles/spacing.css',
  'myapp-color':   'src/styles/color.css',
}

A map of CSS-var prefix family (without leading -- and trailing -) to the repo-relative source file the bin server rewrites. Each token in the POST diff is routed to a file based on its prefix. Tokens with prefixes not in the map are rejected by the bin with "Unsupported cssVar prefix".

Apply is gated on applyEndpoint AND a non-empty applyRouting map. When either is missing, the Apply modal still mounts for diff preview but the action button remains disabled.

Which CSS blocks get rewritten

Each routed file is rewritten by scanning exactly two locations: the FIRST top-level :root { ... } block and the FIRST top-level @theme { ... } block (bare @theme, or with one modifier such as @theme inline — the shape Tailwind v4 prescribes when theme values reference other variables). Later blocks of either kind, and anything nested under @media / @layer / @supports, are not scanned.

Per override var, :root is tried first and @theme is the fallback — a var declared in BOTH blocks is rewritten only in :root. This makes a Tailwind v4 token file reachable out of the box:

:root {
  --palette-cool-700: oklch(0.21 0.03 264);
}

@theme {
  --spacing-md: 0.75rem;
  --color-ink: light-dark(var(--palette-cool-700), var(--palette-cool-50));
}

All three vars above — --palette-cool-700 (in :root), --spacing-md, and --color-ink (both in @theme) — apply cleanly in one request. A file that is 100% @theme (no :root block at all) also applies without error; only a file with neither block returns a 409 (see below).

Note

See Apply pipeline setup for the routing-JSON recipe that wires a Tailwind v4 host's @theme file into applyRouting.

Per-tier independence

The bin server processes each tier's tokens independently against the applyRouting map. Reference-tier and semantic reference overrides are included in the diff payload after resolution: they emit var(--target-cssvar) (for example, semantic.tab-open = 'ease-in' emits --myapp-transition-tab-open: var(--myapp-easing-ease-in)). The payload carries the resolved CSS value, not the raw item id.

Request & response envelopes

Request

POST <applyEndpoint>
Content-Type: application/json

{
  "tokens": {
    "--myapp-spacing-md": "1.5rem",
    "--myapp-color-bg": "#0f172a"
  }
}

tokens is a flat object: CSS custom property name (must start with --) → CSS string value.

Response 200 (success)

{
  "ok": true,
  "updated": [
    {
      "file": "src/styles/spacing.css",
      "changed": ["--myapp-spacing-md"],
      "unchanged": [],
      "unknown": ["--myapp-spacing-lg"],
      "unknownOutsideBlock": ["--myapp-spacing-lg"]
    }
  ],
  "unknownCssVars": ["--myapp-spacing-lg"],
  "unchangedCssVars": [],
  "unknownOutsideBlockCssVars": ["--myapp-spacing-lg"]
}
FieldMeaning
ok: trueMarks success.
updated[]Per-file results. changed[] = rewritten; unchanged[] = found in a scanned block but its value already matched; unknown[] = requested but not found in EITHER the file's scanned :root block or its scanned @theme block.
updated[].unknownOutsideBlockSubset of that file's unknown[]: the var IS declared somewhere in the file (found by a whole-file scan), just outside both scanned blocks — e.g. nested under @media/@layer/@supports, inside a grouped selector (:root, html { ... }), or in a SECOND top-level :root/@theme block. Distinguishes "wrong location" from "typo'd name" (the remaining entries in unknown[] not listed here).
unknownCssVarsunknown[] flattened across all files, for UI feedback.
unchangedCssVarsunchanged[] flattened across all files, for UI feedback.
unknownOutsideBlockCssVarsunknownOutsideBlock[] flattened across all files, for UI feedback.

Response 400 (bad request)

{ "ok": false, "error": "<message>", "rejected": ["--invalid-token"] }

Returned for malformed JSON, missing / empty tokens, invalid token names, or prefixes not in the routing map. rejected is optional.

Response 403 (Forbidden)

{ "ok": false, "error": "Origin not allowed" }

The bin rejects cross-origin requests.

Response 409 (Conflict)

{ "ok": false, "error": "No top-level :root { ... } or @theme { ... } block in <file>" }

The target CSS file has neither a top-level :root block nor a top-level @theme block — see Which CSS blocks get rewritten above. A file with only one of the two (e.g. @theme-only, no :root at all) does NOT 409; it applies normally against whichever block exists.

Response 500 (Internal server error)

{
  "ok": false,
  "error": "<message>",
  "failedFile": "<relativePath>",
  "restoreFailures": ["<file1>"]
}

Returned when a file write fails. restoreFailures is populated when the rollback also fails.

Atomic write contract

The bin keeps each file's original content in memory. If any write fails, every file written so far is restored from the in-memory original. Three terminal states are possible:

  1. Full success. All routed files updated. Response 200.

  2. Clean rollback. A write fails partway through; all previously-written files are restored. Response 500 with failedFile.

  3. Inconsistent disk state. A write fails AND the rollback also fails. Response 500 with failedFile AND restoreFailures[].

Inspect on `restoreFailures`

A non-empty restoreFailures[] array means at least one file was rewritten and could not be restored. Inspect the listed files manually before retrying.

Validation rules

Token name rules

  • Must start with --.

  • No spaces, slashes, or special characters.

  • Must match a prefix family in applyRouting.

Path safety

  • Each routing target is resolved to an absolute path.

  • The resolved path must sit within the bin's writeRoot.

  • Path-escape attempts (../../etc/passwd) are rejected.

Cross-references

Revision History

CreatedUpdated