Apply pipeline
How the tier resolver turns TabOverrides into CSS-var writes, dry-run previews, digest-guarded source rewrites, applyEndpoint + applyRouting, 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:
Inline style — applied client-side, immediately, on every user tweak, to the configured
applySinkor (by default)document.documentElement(:root).Apply-to-disk — triggered by the Apply button; POSTs a diff to the host's dev endpoint which routes it to the bin server.
The Apply modal uses a two-step protocol. It first sends a dryRun: true preview and displays the proposed file hunks. When the user confirms, it sends the same token map with the returned file digests in expectDigests. The server reads and computes every target before writing any file, so a stale preview returns a 409 and never partially applies the batch.
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 → overrideValueExample:
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, orpill.customDefaultwhen a pill is present and no override is active, oritem.defaultas the final fallback.Reference tier (
referencesTieris set): the override is interpreted as the id of an item in the named tier. Returns{ kind: 'ref', targetCssVar }pointing at that item'scssVar.
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)The base tier's cssVar receives its literal value at the active write target. A reference-tier item also writes its own cssVar at that target, but its value is always var(...) — it never writes the referenced raw CSS value directly.
Inline style write
The panel writes overrides to the configured applySink on every user change, or to document.documentElement.style.setProperty(cssVar, value) when no sink is configured. When no override is active for an item, the active target's property is removed (so the stylesheet default wins).
The live Color-tab apply path writes palette items directly, declared base roles from their selected palette slots, and semantic items via var(--palette-cssVar) (or their literal/reference form). The disk apply payload intentionally contains palette and semantic CSS variables only; base roles are runtime wiring and are not emitted by buildApplyOverrides.
Export schema (v2 and v3)
The panel exports and imports overrides as a JSON envelope with a $schema field for version identification:
{
"$schema": "zudo-design-tokens/v2",
"exportedAt": "2026-01-01T00:00:00.000Z",
"tabs": {
"spacing": {
"raw": {
"--myapp-spacing-md": "1.5rem"
}
}
}
}Overrides are grouped under tabs, then by tier (raw, palette, or semantic), with CSS custom properties as leaf keys. Generic reference-tier overrides retain their selected item id in this portable state format; the Apply emitter resolves that id to var(--targetCssVar) when producing CSS. Semantic color object mappings require v3. On import, the panel validates $schema 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 value matching none of the canonical constants is rejected with a schema-mismatch error.
Diff-only export
By default, export contains only changed flat-tab tokens and primary-color tokens. The current schema does not export the secondary color cluster.
Apply-to-disk
applyEndpoint
When PanelConfig.applyEndpoint is set together with a non-empty applyRouting map, the Apply button is eligible. Clicking it POSTs the active override diff to this URL.
The UI, diff-only export, and Apply use one definition of changed: the token's emitted CSS value differs from its baseline's emitted value. Empty flat overrides and values equal to the manifest default are omitted. Semantic role aliases compare by the palette slot they currently resolve to; literal and reference mappings compare structurally. A persisted default-equal override can resurface if a future manifest changes that default. To write today's default into a hand-edited file, edit that file directly or choose a different value first. Apply also diffs the secondary color cluster against its own configured defaults, while export remains scoped to the primary cluster.
When applyEndpoint is undefined (or applyRouting is empty), the Apply button stays disabled with a tooltip — hosts that use export/import only can omit these fields.
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"
},
"dryRun": true
} The confirming write sends the same tokens map without dryRun and includes the digest returned for each previewed file:
{
"tokens": {
"--myapp-spacing-md": "1.5rem"
},
"expectDigests": {
"src/styles/spacing.css": "<64 lowercase SHA-256 hex characters>"
}
}tokens is a non-empty flat object: CSS custom property name (must start with --) → CSS string value. dryRun, when supplied, must be true. expectDigests is optional and maps response file paths to 64-character SHA-256 hex digests from a previous preview. It is checked only for a real write.
Response 200 — dry-run preview
{
"ok": true,
"dryRun": true,
"files": [
{
"file": "src/styles/spacing.css",
"blockKind": "root",
"digest": "<64 lowercase SHA-256 hex characters>",
"changed": ["--myapp-spacing-md"],
"unchanged": [],
"unknown": [],
"unknownOutsideBlock": [],
"hunks": [
{
"cssVar": "--myapp-spacing-md",
"line": 12,
"before": " --myapp-spacing-md: 1rem;",
"after": " --myapp-spacing-md: 1.5rem;",
"context": {
"before": [" --myapp-spacing-sm: 0.5rem;"],
"after": [" --myapp-spacing-lg: 3rem;"]
}
}
]
}
],
"rejected": [],
"rejectedReasons": []
}files[] has one entry per physical target file. blockKind is root or theme, with :root winning when both contain requested declarations. The digest is of the exact bytes read. Hunk line numbers are one-based; each changed CSS variable gets one hunk, while unchanged and unknown variables get none. Dry runs may include unrouted variables: they are reported in rejected/rejectedReasons while routed files still preview successfully. A dry run with no routed variables returns an empty files array successfully.
Response 200 — write
{
"ok": true,
"updated": [
{
"file": "src/styles/spacing.css",
"changed": ["--myapp-spacing-md"],
"unchanged": [],
"unknown": [],
"unknownOutsideBlock": []
}
],
"unknownCssVars": [],
"unchangedCssVars": [],
"unknownOutsideBlockCssVars": []
}changed declarations are rewritten. unchanged declarations were found in a scanned block with the requested value already present. unknown were not found in either scanned block; unknownOutsideBlock is the subset declared elsewhere in the file (for example in a nested rule, grouped selector, or a second top-level block). The three top-level arrays flatten those per-file diagnostics for the panel.
Response 400 (bad request)
{ "ok": false, "error": "<message>", "rejected": ["--invalid-token"] }Returned for malformed JSON, a non-object body, missing/empty tokens, a non-true dryRun, invalid token names, invalid digest objects, or an unsupported prefix on a real write. Path-escape attempts and routing paths outside writeRoot are also rejected.
Response 403 (Forbidden)
{ "ok": false, "error": "Origin not allowed" }The CLI wrapper rejects origins outside its explicit allow-list.
Response 409 (Conflict)
The target file has neither a top-level :root nor a top-level @theme block:
{ "ok": false, "error": "No top-level :root { ... } or @theme { ... } block in <file>" }A file containing only @theme (or only :root) is valid. A real write whose current bytes differ from a supplied preview digest returns a stale-file conflict before any file is written:
{ "ok": false, "reason": "stale-file", "files": ["src/styles/spacing.css"] }The client refreshes the preview and asks the user to review the new hunks. Omitting expectDigests preserves the legacy write behavior.
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, meaning the listed files require manual inspection.
Apply modal behavior
The panel debounces preview requests. It waits for the latest dry-run response before enabling confirmation, then sends the current tokens plus the preview's expectDigests. If a 409 stale-file response arrives, it refreshes the preview instead of treating the write as successful. On a successful write, only CSS variables reported in changed are reconciled in the in-memory state; unrouted tokens and variables in other files remain available for a later apply. The ${storagePrefix}-last-applied key holds the flat comparison baseline used by the changed-state indicator. After a successful write the current implementation resets that baseline to {} while reconciling only the confirmed changed variables, so retained or unrouted overrides remain marked dirty. Base-role variables are excluded from disk Apply. When a confirmed semantic write changed only because an unchanged bg or fg alias resolved through a moved role index, reconciliation also resets that role dependency. Any unwritten semantic alias using the same role is first converted to its resolved numeric palette index, preserving its emitted value and dirty state for a later Apply.
When no applyEndpoint or no non-empty applyRouting map is configured, the modal can still show the local diff but the disk-apply action stays disabled.
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:
Full success. All routed files updated. Response 200.
Clean rollback. A write fails partway through; all previously-written files are restored. Response 500 with
failedFile.Inconsistent disk state. A write fails AND the rollback also fails. Response 500 with
failedFileANDrestoreFailures[].
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 (
.) are rejected.. / . . / etc/ passwd
Cross-references
PanelConfig.applyEndpointandapplyRouting— endpoint and routing slots.Token tiers — defines the
TabConfig/TierConfig/TierItemshapes whosecssVarnames appear in the diff.Color cluster — defines the Color-tab CSS-var names that can appear in the diff.