# Zudo Token Panel > --- # Reference > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/reference This section is the per-page reference for the `@takazudo/zudo-design-token-panel` portable contract. Each page pins one slice of the public surface — the configure-once init, the token manifest, the color cluster, the apply pipeline, and the panel CSS tokens. The contract itself is the single source of truth (`packages/zudo-design-token-panel/PORTABLE-CONTRACT.md`). These pages distill it into a navigable browse-and-link form. ## Pages - [`configurePanel`](/pj/zudo-design-token-panel/docs/reference/configure-panel/) — `configurePanel({...})`, the `PanelConfig` shape, storage-key derivation, and the lifecycle helpers (`showDesignTokenPanel`, `hideDesignTokenPanel`, `toggleDesignPanel`, `reapplyPersistedOverrides`, `setPanelColorPresets`). - [Token manifest](/pj/zudo-design-token-panel/docs/reference/token-manifest/) — `TokenManifest`, `TokenDef`, group ordering, and section-title overrides. - [Color cluster](/pj/zudo-design-token-panel/docs/reference/color-cluster/) — `ColorClusterConfig`, `ColorScheme`, `ColorRef`, the multi-cluster contract, and host-supplied scheme presets. - [Apply pipeline](/pj/zudo-design-token-panel/docs/reference/apply-pipeline/) — `applyEndpoint`, `applyRouting`, request/response envelopes, atomicity, and validation rules. - [Panel CSS tokens](/pj/zudo-design-token-panel/docs/reference/panel-css-tokens/) — `--tokentweak-*` private vars, the `var(--host, fallback)` indirection ladder, and the paired stylesheet/host-adapter import obligations. ## Intentionally undocumented exports The package root re-exports one symbol that is deliberately omitted from this reference: - `__panelConfigForTest()` — internal/test-only accessor used by the Astro host adapter to verify both surfaces observe the same panel-config singleton. The leading `__` prefix marks it as internal; it is not part of the portable contract and may be removed without notice. --- # v0.1.0 > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/changelog/v0.1.0 Released 2026-04-27. Initial OSS port of the design-token panel + bin server from `zmodular` into a standalone monorepo. Tracked under [super-epic issue #2](https://github.com/Takazudo/zudo-design-token-panel/issues/2). ## Added - **Repo bootstrap** — pnpm workspace skeleton (root `package.json`, `pnpm-workspace.yaml`, `.gitignore`, MIT `LICENSE`), top-level `README.md`, the root `CHANGELOG.md`, `.editorconfig`, `.prettierrc`, and `.npmrc` aligned with myoss conventions. Generated project logo via `kumiko-gen`. Added GitHub Actions workflows and a `b4push` validation script. ([#11](https://github.com/Takazudo/zudo-design-token-panel/pull/11)) - **Doc site scaffold** — `doc/` site generated via `create-zudo-doc` with the `zudo-token-panel` preset (light/dark color schemes; default feature set: search, sidebarFilter, claudeResources, sidebarResizer, sidebarToggle, versioning, docHistory, llmsTxt, changelog). Top page rebuilt with a hero + feature cards layout. ([#11](https://github.com/Takazudo/zudo-design-token-panel/pull/11)) - **`@takazudo/zudo-design-token-panel` package** — ported the Preact-based live design-token tweak panel into `packages/zudo-design-token-panel/`, including the host-config-driven runtime (`TokenManifest`, `ColorClusterConfig`), tests, and the portable contract / README. Renamed the secondary cluster identifier from a reference-project name to the generic `secondary`, and scrubbed reference-project name leaks from source comments. ([#12](https://github.com/Takazudo/zudo-design-token-panel/pull/12)) - **`design-token-panel-server` bin** — ported the Node bin entry and server-side apply pipeline modules into the panel package, wired the `bin` and `server` entries into the package build, and rewrote `README.md` §3 for the OSS-standalone bin surface. ([#13](https://github.com/Takazudo/zudo-design-token-panel/pull/13)) ## Notes The `examples/*` workspaces (Astro, Vite + React, Next.js reference integrations) are listed in `pnpm-workspace.yaml` but the actual example apps are still being ported. Links from the doc site and top page may 404 until the examples land. --- # Welcome > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/getting-started # Welcome to Zudo Token Panel This documentation site was created with [zudo-doc](https://github.com/zudolab/zudo-doc). ## Getting Started Edit the files in `src/content/docs/` to add your documentation. --- # Custom color cluster > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/recipes/custom-color-cluster The package ships **zero baked-in color data**. The host owns the palette template, the base roles, the semantic table, and the scheme registry. This recipe walks through wiring a fresh `ColorClusterConfig` for a new project. For the authoritative shape and apply-time semantics, see [Color cluster reference](/pj/zudo-design-token-panel/docs/reference/color-cluster). Below is the minimum viable cluster that compiles against the public exports. ## 1. Define the cluster Pick CSS variable names that match your stylesheet. The `{n}` placeholder in `paletteCssVarTemplate` is replaced with the palette index at apply time. ```ts // src/lib/my-color-cluster.ts ColorClusterConfig, ColorScheme, } from '@takazudo/zudo-design-token-panel'; const defaultDark: ColorScheme = { background: 0, foreground: 7, cursor: 7, selectionBg: 8, selectionFg: 0, palette: [ '#1e1e2e', // p0 — base background '#f38ba8', // p1 — red '#a6e3a1', // p2 — green '#f9e2af', // p3 — yellow '#89b4fa', // p4 — blue '#cba6f7', // p5 — magenta '#94e2d5', // p6 — cyan '#cdd6f4', // p7 — base foreground '#45475a', // p8 — bright black '#f38ba8', '#a6e3a1', '#f9e2af', '#89b4fa', '#cba6f7', '#94e2d5', '#bac2de', ], shikiTheme: 'github-dark', // Overrides for individual semantics. Anything omitted falls back to // `cluster.semanticDefaults`. semantic: { primary: 4, accent: 5, danger: 1, success: 2, warning: 3, muted: 8, }, }; id: 'myapp', label: 'MyApp Brand', paletteSize: 16, paletteCssVarTemplate: '--myapp-p{n}', baseRoles: { background: '--myapp-color-bg', foreground: '--myapp-color-fg', cursor: '--myapp-color-cursor', selectionBg: '--myapp-color-selection-bg', selectionFg: '--myapp-color-selection-fg', }, baseDefaults: { background: 0, foreground: 7, cursor: 7, selectionBg: 8, selectionFg: 0, }, semanticDefaults: { primary: 4, accent: 5, danger: 1, success: 2, warning: 3, muted: 8, }, semanticCssNames: { primary: '--myapp-color-primary', accent: '--myapp-color-accent', danger: '--myapp-color-danger', success: '--myapp-color-success', warning: '--myapp-color-warning', muted: '--myapp-color-muted', }, defaultShikiTheme: 'github-dark', colorSchemes: { 'Default Dark': defaultDark, }, panelSettings: { colorScheme: 'Default Dark', // Single-mode site. See secondary-cluster-or-disable for the light/dark // pairing pattern. colorMode: false, }, }; ``` **`paletteSize` must match every `palette` array.** The validator (called automatically by the Astro adapter) throws when a `ColorScheme.palette` does not match `paletteSize`. Keep them in lockstep when you add or rename slots. ## 2. Plug it into `configurePanel` Pass the cluster to `configurePanel` (the configure-once entry point) along with the rest of `PanelConfig`. The token manifest can stay minimal while you focus on color — the four arrays are required by the type but may be empty. ```ts // src/lib/my-panel-config.ts storagePrefix: 'myapp-design-token-panel', consoleNamespace: 'myapp', modalClassPrefix: 'myapp-design-token-panel-modal', schemaId: 'myapp-design-tokens/v1', exportFilenameBase: 'myapp-design-tokens', tokens: { spacing: [], typography: [], size: [], color: [], }, colorCluster: myColorCluster, }; ``` See [`configurePanel` reference](/pj/zudo-design-token-panel/docs/reference/configure-panel) for the full list of `PanelConfig` fields. ## 3. Match your stylesheet The panel writes to `:root` using the names you declared. Your stylesheet needs to consume them — usually via fallback defaults so the page paints even before any user override: ```css /* src/styles/tokens.css */ :root { --myapp-p0: #1e1e2e; --myapp-p1: #f38ba8; /* …rest of the 16-tuple… */ --myapp-color-bg: var(--myapp-p0); --myapp-color-fg: var(--myapp-p7); --myapp-color-primary: var(--myapp-p4); --myapp-color-accent: var(--myapp-p5); --myapp-color-danger: var(--myapp-p1); --myapp-color-success: var(--myapp-p2); --myapp-color-warning: var(--myapp-p3); --myapp-color-muted: var(--myapp-p8); } ``` The panel never **reads** these declarations — it only writes through `document.documentElement.style.setProperty(...)`. Treat your stylesheet as the source of the defaults; treat the panel as a layer of inline overrides on top. ## 4. Add a second scheme (optional) Schemes appear in the Color tab "Scheme..." dropdown. Add as many as you like; their order is the insertion order of `colorSchemes`. ```ts const defaultLight: ColorScheme = { background: 0, foreground: 7, cursor: 7, selectionBg: 8, selectionFg: 0, palette: [ '#fafafa', '#d20f39', '#40a02b', '#df8e1d', '#1e66f5', '#8839ef', '#179299', '#4c4f69', '#9ca0b0', '#d20f39', '#40a02b', '#df8e1d', '#1e66f5', '#8839ef', '#179299', '#5c5f77', ], shikiTheme: 'github-light', }; // then, inside myColorCluster: colorSchemes: { 'Default Dark': defaultDark, 'Default Light': defaultLight, }, ``` If you also want true light/dark switching driven by the page's `data-theme` attribute, see the [secondary cluster recipe](/pj/zudo-design-token-panel/docs/recipes/secondary-cluster-or-disable) and the [Color cluster reference](/pj/zudo-design-token-panel/docs/reference/color-cluster) section on `panelSettings.colorMode`. ## Related - [Color cluster reference](/pj/zudo-design-token-panel/docs/reference/color-cluster) — full type contract, JSON-serializability rule, and apply-time write order. - [Panel CSS tokens](/pj/zudo-design-token-panel/docs/reference/panel-css-tokens) — overriding the panel's chrome colors (separate from your app's color cluster). - [Lazy color presets](/pj/zudo-design-token-panel/docs/recipes/lazy-color-presets) — keep a large preset library out of the SSR config blob. --- # configurePanel > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/reference/configure-panel The package's entire portable contract enters through a single, idempotent setup function. Hosts call `configurePanel({...})` exactly once per page lifecycle, before the panel adapter is dynamically imported (typically from a small Astro host script that gates the adapter behind a visibility / persistence probe). This page pins the public shape of `PanelConfig`, the `configurePanel` call itself, and the runtime lifecycle helpers (`showDesignTokenPanel`, `hideDesignTokenPanel`, `toggleDesignPanel`, `reapplyPersistedOverrides`). ## `configurePanel(config)` ```ts configurePanel({ storagePrefix: 'myapp-design-token-panel', consoleNamespace: 'myapp', modalClassPrefix: 'myapp-design-token-panel-modal', schemaId: 'https://example.com/schemas/design-tokens.json', exportFilenameBase: 'myapp-design-tokens', tokens: { spacing: [], typography: [], size: [], color: [] }, colorCluster: { /* see Color cluster reference */ }, }); ``` ### Required behaviours - **One-shot.** Calling `configurePanel` more than once with different values is an error. The panel may either throw or warn-and-ignore, but it MUST NOT silently overwrite a previously-configured cluster mid-session. - **Synchronous.** No I/O, no awaits. The call must be cheap enough to run inline at module-init from the Astro frontmatter side. - **Pure data only.** Every field on `PanelConfig` (and every nested field inside `tokens` / `colorCluster`) MUST be JSON-serializable. This is the hard precondition for the Astro frontmatter → island prop handoff: Astro stringifies props, so functions / class instances do not survive. - **No default `PanelConfig` baked into the package.** Hosts MUST configure the panel explicitly. The package ships zero baked-in identifiers — every storage prefix, namespace, palette template, and manifest entry comes from the host. A package import without an explicit `configurePanel(...)` call surfaces a clear runtime error that names the missing field. The package does not provide fallback identifiers. ## `PanelConfig` interface ```ts /** Base for every derived storage key. */ storagePrefix: string; /** Console API namespace — installed as `window[consoleNamespace].showDesignPanel`, etc. */ consoleNamespace: string; /** BEM-style prefix used by every modal in the panel (export / import / apply). */ modalClassPrefix: string; /** `$schema` value emitted into export JSON and required on import. */ schemaId: string; /** Default filename base — exports save as `${exportFilenameBase}.json`. */ exportFilenameBase: string; /** Editable design tokens grouped per-tab. */ tokens: TokenManifest; /** Palette + base roles + semantic table. */ colorCluster: ColorClusterConfig; /** * Optional secondary color cluster. Host-driven: * - `undefined` (field omitted) — secondary section hidden. * - `null` — explicit opt-out: secondary section hidden + apply/clear skipped. * - `ColorClusterConfig` — host-supplied secondary cluster. */ secondaryColorCluster?: ColorClusterConfig | null; /** * Optional host-supplied color-scheme presets. Surfaces additional named * `ColorScheme` entries in the Color tab "Scheme..." dropdown. * Defaults to `{}`. */ colorPresets?: Record; /** * Optional dev-API endpoint URL. When set, the Apply button POSTs its diff payload to it. * When `undefined`, the Apply button stays disabled with a tooltip. */ applyEndpoint?: string; /** * Optional CSS-var prefix → repo-relative source-file routing map. * Apply is gated on `applyEndpoint` AND a non-empty routing map. */ applyRouting?: Record; } ``` ### Field reference | Field | Type | Required | Notes | | --- | --- | --- | --- | | `storagePrefix` | `string` | yes | Drives every persisted localStorage key. The only knob that controls panel storage. | | `consoleNamespace` | `string` | yes | Globals are installed under `window[consoleNamespace]`. | | `modalClassPrefix` | `string` | yes | BEM root for every modal the panel owns. See [Panel CSS tokens](/pj/zudo-design-token-panel/docs/reference/panel-css-tokens/) for how the bundled CSS keys on `data-design-token-panel-modal` instead of this prefix. | | `schemaId` | `string` | yes | Emitted as `$schema` on exports; required on import. | | `exportFilenameBase` | `string` | yes | Saves as `${exportFilenameBase}.json`. | | `tokens` | [`TokenManifest`](/pj/zudo-design-token-panel/docs/reference/token-manifest/) | yes | Editable tokens grouped per-tab. | | `colorCluster` | [`ColorClusterConfig`](/pj/zudo-design-token-panel/docs/reference/color-cluster/) | yes | Primary palette + base roles + semantic table. | | `secondaryColorCluster` | `ColorClusterConfig \| null` | no | Three-state field — see [Color cluster](/pj/zudo-design-token-panel/docs/reference/color-cluster/#multi-cluster-support). | | `colorPresets` | `Record` | no | Host-supplied scheme presets — see [Color cluster](/pj/zudo-design-token-panel/docs/reference/color-cluster/#host-supplied-scheme-presets). | | `applyEndpoint` | `string` | no | Apply POST target — see [Apply pipeline](/pj/zudo-design-token-panel/docs/reference/apply-pipeline/). | | `applyRouting` | `Record` | no | Apply routing map — see [Apply pipeline](/pj/zudo-design-token-panel/docs/reference/apply-pipeline/). | Every field on `PanelConfig` (including nested `tokens` and `colorCluster`) must round-trip through `JSON.stringify` without loss. The Astro frontmatter → island prop handoff stringifies the config, so function fields, class instances, `Symbol` keys, and `undefined` (where `null` is meant) silently disappear under that round-trip and surface later as cryptic runtime errors. The palette CSS-var name is therefore expressed as a string template, not a function — see [Color cluster](/pj/zudo-design-token-panel/docs/reference/color-cluster/). ## Storage-key derivation `storagePrefix` is the only knob that controls every persisted key. The panel derives the keys at runtime from this single base. | Logical key | Derivation | Owner | Purpose | | --- | --- | --- | --- | | `state-v2` | `${storagePrefix}-state-v2` | tweak-state | Unified envelope: color + spacing + typography + size + panelPosition + optional secondary cluster slice. | | `state-v1` | `${storagePrefix}-state` | tweak-state (legacy) | Pre-v2 flat-state format (Color-only). Migrated into `state-v2` on first load, then deleted. | | `open` | `${storagePrefix}-open` | panel | Mirror of the panel's `open` boolean state. | | `position` | `${storagePrefix}-position` | panel | Drag position (`{ top, right }`) so the panel reappears where the user left it. | | `visible` | `${storagePrefix}:visible` | adapter | Adapter-level visibility-intent flag, owned by the lazy-load gate. | With `storagePrefix: "myapp-design-token-panel"`, the derivation produces: ``` myapp-design-token-panel-state-v2 myapp-design-token-panel-state myapp-design-token-panel-open myapp-design-token-panel-position myapp-design-token-panel:visible ``` The `visible` key uses a `:` separator; every other derived key uses `-`. This is a historical artifact preserved for storage-key continuity — a key rename would silently lose users' visibility intent on first load. The derivation MUST emit the colon literally. ### v1 → v2 in-place migration `loadPersistedState` performs an in-place v1 → v2 migration per `storagePrefix`. A user who last opened the panel before v2 landed gets their old color tweaks lifted into the new envelope on first load: - v1 read key: `${storagePrefix}-state` - v2 write key: `${storagePrefix}-state-v2` After the rewrite, the v1 key is deleted. A hard-coded typography-id rename map (`text-caption` → `text-xs`, plus a small set of dropped legacy ids) also lives inside `loadPersistedState`. It applies regardless of `storagePrefix` and is a no-op for hosts whose token manifest does not use those legacy ids. ## Lifecycle helpers The package exposes four runtime helpers from its root entry. They are normally invoked via the console namespace (the host adapter installs `window[consoleNamespace].showDesignPanel` etc.), but a Vite-only host can import them directly. ```ts showDesignTokenPanel, hideDesignTokenPanel, toggleDesignPanel, reapplyPersistedOverrides, } from '@takazudo/zudo-design-token-panel'; ``` ### `showDesignTokenPanel(): void` Opens the panel. Idempotent — calling it while the panel is already open is a no-op. Safe to call before mount: the helper seeds the panel's open-state key synchronously, mounts the Preact shell into a body-appended `` whose id is derived from `storagePrefix`, and writes `${storagePrefix}:visible = "1"` so the next reload restores the open state. ### `hideDesignTokenPanel(): void` Closes the panel. Symmetric to `showDesignTokenPanel`. The Preact shell stays mounted in the DOM (CSS hides it); only the open flag flips. Persists `${storagePrefix}:visible = "0"`. ### `toggleDesignPanel(): void` Flips the panel between open and closed. Reads the panel's current open state, seeds the inverse before a fresh mount if needed, and dispatches the toggle event for steady-state flips. The exported function name is `toggleDesignPanel` (not `toggleDesignTokenPanel`); the console-API helper is also `toggleDesignPanel`. ### `reapplyPersistedOverrides(): void` Applies persisted token overrides directly to `:root` BEFORE any Preact render. Called at adapter module init (and again on every `astro:page-load`) so the bundle's arrival is enough to kill the FOUT on hard navigation. The Preact shell still mounts separately when visibility intent requires it. This is a no-op when nothing is persisted, and it swallows errors — missing storage or corrupt state never blocks the UI thread; the stylesheet defaults paint instead. The host adapter installs the lazy-import wrappers under `window[consoleNamespace]`: ```ts window[consoleNamespace].showDesignPanel = () => Promise; window[consoleNamespace].hideDesignPanel = () => Promise; window[consoleNamespace].toggleDesignPanel = () => Promise; ``` Each helper lazy-imports the adapter module and forwards to its corresponding non-async public function. The host script preserves co-existing namespace fields, so installation MUST merge into the existing namespace, not overwrite it. ## `setPanelColorPresets(presets)` ```ts setPanelColorPresets({ Dracula: { /* ColorScheme */ }, Solarized: { /* ColorScheme */ }, }); ``` Lazy preset attachment. Hosts that don't want to ship the preset library inline in the SSR config blob can call this AFTER `configurePanel` to attach the preset map from a deferred dynamic import. Same precedence rules as `PanelConfig.colorPresets` — see the [Color cluster reference](/pj/zudo-design-token-panel/docs/reference/color-cluster/#host-supplied-scheme-presets) for the full merge contract. The trailing call wins on conflict (no throw, unlike `configurePanel`); a host that pre-calls `setPanelColorPresets` before `configurePanel` is serviced via a holding slot inside the panel-config module. --- # Architecture > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/architecture The design-token panel is split into three layers that talk to each other through narrow, JSON-serializable contracts. Most of the package's portability story falls out of that split — the panel UI never knows what host it runs in, the host-adapter never knows what tokens you ship, and the apply-pipeline never runs in the browser. ## Overview There are three concerns and the package keeps them in three separate layers: 1. **Panel UI** — the Preact island that renders the side panel, owns tab/control state, and writes overrides to `:root` via `setProperty`. Lives entirely in the browser. 2. **Host-adapter** — the thin shim a host imports as a side effect. It reads the inline JSON config, calls `configurePanel(...)`, installs `window..*`, and gates the lazy-load of the panel module. 3. **Apply-pipeline** — the optional path from the panel's "Apply" button back to disk. It's the in-package payload builder plus the standalone Node bin server (`design-token-panel-server`) that owns the file rewrites. ``` ┌─ Your dev server (Astro / Vite / any host) ──────────────┐ │ │ │ ┌─────────────┐ reads ┌────────────────┐ │ │ │ Layout │ ──JSON────▶ │ Host adapter │ │ │ │ (config) │ │ (lazy-gate) │ │ │ └─────────────┘ └────────┬───────┘ │ │ │ dynamic import │ │ ▼ │ │ ┌────────────────┐ │ │ │ Panel UI │ writes │ │ │ (Preact) │ ──────▶ :root │ └────────┬───────┘ │ │ │ POST /apply │ └────────────────────────────────────────┼─────────────────┘ │ (HTTP, loopback) ▼ ┌──────────────────────────┐ │ Bin server │ │ design-token-panel- │ │ server │ │ • CORS allow-list │ │ • write-root sandbox │ │ • atomic file rewrites │ └──────────────────────────┘ ``` The boundaries between these three layers are stable — every project-specific identifier (storage prefix, console namespace, palette CSS-var template, semantic names, routing) crosses a layer boundary as plain JSON. That's the lever that lets the same package ship into Astro, Vite, Next, or a Rust SSG without code changes. ## Layer responsibilities ### Panel UI The Preact tree under [`src/panel.tsx`](https://github.com/Takazudo/zudo-design-token-panel/blob/main/packages/zudo-design-token-panel/src/panel.tsx) plus the per-tab components under `src/tabs/`. It: - Renders one row per editable token, driven by the host-supplied `TokenManifest`. - Renders the color tab (palette + base roles + semantic table) from the host-supplied `ColorClusterConfig`. - Owns the in-memory `TweakState` and persists it to `localStorage` under keys derived from `storagePrefix`. - Writes overrides to `document.documentElement.style.setProperty(...)` against the consumer's CSS-var names. The panel UI never reads consumer CSS variables — it only writes to them. Its own chrome is scoped under the panel-private `--tokentweak-*` namespace. The full surface for `TokenManifest` and `ColorClusterConfig` lives at [/docs/reference/configure-panel](/pj/zudo-design-token-panel/docs/reference/configure-panel). The panel-private chrome variables are documented at [/docs/reference/panel-css-tokens](/pj/zudo-design-token-panel/docs/reference/panel-css-tokens). ### Host-adapter A small bundle at [`src/astro/host-adapter.ts`](https://github.com/Takazudo/zudo-design-token-panel/blob/main/packages/zudo-design-token-panel/src/astro/host-adapter.ts) that the consumer's layout imports as a side effect: ```ts void import('@takazudo/zudo-design-token-panel/astro/host-adapter'); ``` It owns the boundary between the host page and the panel module: - **Reads the inline config**: `` emits a `` payload; the adapter `JSON.parse`s it and calls `configurePanel(...)` synchronously at module init. - **Installs the console API** (`showDesignPanel` / `hideDesignPanel` / `toggleDesignPanel`) on `window[consoleNamespace]` eagerly — even before the panel module has loaded. - **Gates the lazy import**: probes `localStorage` for either the `${storagePrefix}-state-v2` payload or the `${storagePrefix}:visible` flag and dynamically imports the panel module only when one is set. - **Hooks Astro view-transitions** (`astro:before-swap` / `astro:page-load`) so soft navigation re-applies persisted overrides without a flash and without double-mounting. The adapter is the only layer that knows about Astro at all. The panel UI itself is framework-agnostic. ### Apply-pipeline Two pieces stitched through HTTP: - **In-package payload builder** under [`src/apply/`](https://github.com/Takazudo/zudo-design-token-panel/tree/main/packages/zudo-design-token-panel/src/apply) — turns the current `TweakState` into a routed token-overrides payload using the host-supplied `applyRouting` map. - **Bin server** under [`src/bin/`](https://github.com/Takazudo/zudo-design-token-panel/tree/main/packages/zudo-design-token-panel/src/bin) and [`src/server/`](https://github.com/Takazudo/zudo-design-token-panel/tree/main/packages/zudo-design-token-panel/src/server) — a standalone Node loopback HTTP server (`design-token-panel-server`) that receives the POST, validates the request against the CORS allow-list, validates each routing entry against the `--write-root` sandbox, and rewrites the target CSS files atomically. The apply-pipeline is **optional**. Hosts that only need export/import omit `applyEndpoint` and `applyRouting` from `PanelConfig`; the Apply button stays disabled with a tooltip. Full CLI flag surface, security model, and routing JSON shape are documented at [/docs/reference/apply-pipeline](/pj/zudo-design-token-panel/docs/reference/apply-pipeline). ## Data flow Three flows cover the moments the layers actually interact: a tweak (UI-only), an apply (UI → bin → disk), and a view-transition reapply (host re-render). ### On tweak — user moves a slider ```mermaid sequenceDiagram participant U as User participant P as Panel UI (Preact) participant S as Tweak State (memory + localStorage) participant R as Document root U->>P: Drag slider P->>S: Update in-memory override S->>S: Debounced write to state-v2 key P->>R: setProperty(cssVar, value) R-->>U: Page restyles instantly Note over U,R: Entirely client-side. No network round-trip. ``` A tweak never leaves the browser. The bin server does not need to be running for the panel to be useful — export/import to JSON works without it, and `:root` is updated synchronously on every change. ### On apply — commit overrides to disk ```mermaid sequenceDiagram participant U as User participant P as Panel UI participant A as Apply Pipeline participant B as Bin Server (loopback) participant D as Disk CSS Files U->>P: Click Apply P->>A: Build token-overrides payload A-->>P: JSON diff P->>B: POST applyEndpoint with diff B->>B: Validate origin against CORS allow-list B->>B: Validate routing within write-root sandbox B->>D: Atomic rewrite (temp + rename, per-file mutex) D-->>B: Write OK B-->>P: 200 OK with summary P-->>U: Apply modal shows success Note over B,D: On any failure, in-memory snapshot restores all files ``` The browser side ends at the POST. Everything past that — origin checks, sandbox checks, atomic write, mutex serialisation, rollback-on-failure — happens in the bin process and is identical regardless of the host framework that spawned the bin. ### On view-transition reapply — host re-renders ```mermaid sequenceDiagram participant R as Astro Router participant H as Host Adapter participant M as Panel Module (lazy) participant D as Document root R->>H: astro:before-swap H->>M: render(null, root) — unmount H->>H: Snapshot visibility intent R->>R: Swap body R->>H: astro:page-load H->>H: Probe state-v2 and visible flag in localStorage H->>D: Re-apply persisted overrides synchronously Note over H,D: Runs before first paint — no FOUT alt visibility flag set H->>M: Dynamic import and remount shell M->>M: Resume from persisted state end ``` Soft navigation through Astro's `` is the only reason the host-adapter has lifecycle hooks. On non-Astro hosts the same module loads, the same console API is installed, and the same lazy-load gate runs — the `astro:*` listeners simply never fire. ## Why this separation? The three-layer split is what keeps the package portable. Each layer crosses its boundary through a specific contract, and every contract is JSON-serializable on purpose. - **Panel UI ↔ host**: a single `PanelConfig` object. Storage prefix, console namespace, modal class prefix, schema id, the editable token list, the entire color cluster (palette + semantics + scheme registry), and the optional preset library — all host-supplied, all plain JSON. The panel UI has no compile-time knowledge of any of them. - **Host-adapter ↔ panel UI**: the inline `` payload plus the `configurePanel(...)` call. Functions, class instances, and `undefined` would silently disappear across this boundary, so the contract forbids them. - **Panel UI ↔ bin**: a single POST with `application/json`. The bin doesn't trust the request — it re-validates origin and routing every time. The browser can't reach disk without going through the sandbox. That last point is worth its own note: The host-adapter contract is a **paired-unit contract** — `` AND a sibling `void import('...host-adapter')` block must appear together in your layout. Omitting the script tag emits the JSON config but never executes the adapter that reads it; the console API throws `ReferenceError`. See the [package README §4.1.2](https://github.com/Takazudo/zudo-design-token-panel/blob/main/packages/zudo-design-token-panel/README.md#412-drop-the-host-into-your-layout) and [§12.1](https://github.com/Takazudo/zudo-design-token-panel/blob/main/packages/zudo-design-token-panel/README.md#121-host-adapter-side-effect-import-paired-unit-contract) for the full rationale and bundler detail. The benefit of this design is portability. The benefit you actually feel is that you can drop the panel into a brand-new host and only think about your `PanelConfig`, not about layer wiring. --- # /CLAUDE.md > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/claude-md/root **Path:** `CLAUDE.md` # Zudo Token Panel Documentation site built with [zudo-doc](https://github.com/zudolab/zudo-doc) — an Astro-based documentation framework with MDX, Tailwind CSS v4, and Preact islands. ## Tech Stack - **Astro** — static site generator with Content Collections - **MDX** — content format via `@astrojs/mdx` - **Tailwind CSS v4** — via `@tailwindcss/vite` (not `@astrojs/tailwind`) - **Preact** — for interactive islands only (with compat mode for React API) - **Shiki** — built-in code highlighting ## Commands - `pnpm dev` — Astro dev server (port 4321) - `pnpm build` — static HTML export to `dist/` - `pnpm check` — Astro type checking ## Key Directories ``` src/ ├── components/ # Astro + Preact components │ └── admonitions/ # Note, Tip, Info, Warning, Danger ├── config/ # Settings, color schemes ├── content/ │ └── docs/ # MDX content │ └── docs-ja/ # Japanese MDX content (mirrors docs/) ├── layouts/ # Astro layouts ├── pages/ # File-based routing └── styles/ └── global.css # Design tokens & Tailwind config ``` ## Content Conventions ### Frontmatter - Required: `title` (string) - Optional: `description`, `sidebar_position` (number), `category` - Sidebar order is driven by `sidebar_position` ### Admonitions Available in all MDX files without imports: ``, ``, ``, ``, `` Each accepts an optional `title` prop. ### Headings Do NOT use h1 (`#`) in doc content — the page title from frontmatter is rendered as h1. Start content headings from h2 (`##`). ## Components - Default to **Astro components** (`.astro`) — zero JS, server-rendered - Use **Preact islands** (`client:load`) only when client-side interactivity is needed ## i18n - English (default): `/docs/...` — content in `src/content/docs/` - Japanese: `/ja/docs/...` — content in `src/content/docs-ja/` - Japanese docs should mirror the English directory structure ## Enabled Features - **search** — Full-text search via Pagefind - **sidebarFilter** — Real-time sidebar filtering - **claudeResources** — Auto-generated docs for Claude Code resources - **sidebarResizer** — Draggable sidebar width - **sidebarToggle** — Show/hide desktop sidebar - **versioning** — Multi-version documentation support - **docHistory** — Document edit history - **llmsTxt** — Generates llms.txt for LLM consumption - **changelog** — Changelog page at `/docs/changelog` --- # Install > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/getting-started/install `@takazudo/zudo-design-token-panel` is a regular npm package. It works in any project that can render a Preact island — Astro, Vite + React, Next.js, or a plain Vite SPA — because the public surface is just a `configurePanel({...})` call plus a Preact-rendered shell. This page covers installing the package itself. For wiring it into your layout, see [Quickstart](./quickstart). ## Install the package Pick the invocation that matches your project's package manager. Each command installs the panel package and its single peer dependency, [Preact](https://preactjs.com/). ### pnpm ```sh pnpm add @takazudo/zudo-design-token-panel preact ``` ### npm ```sh npm install @takazudo/zudo-design-token-panel preact ``` ### yarn ```sh yarn add @takazudo/zudo-design-token-panel preact ``` ### bun ```sh bun add @takazudo/zudo-design-token-panel preact ``` The panel UI is rendered with Preact and declares it as a `peerDependency` at `^10.29.1`. You bring your own copy so the panel and any other Preact islands in your app share one runtime — installing it twice would split the runtime and break event delegation across roots. ## Verify the install Once the install completes, the package exposes three things you can sanity-check from your project root: - The library entry — `@takazudo/zudo-design-token-panel` (calls `configurePanel`, renders the panel). - The Astro entry — `@takazudo/zudo-design-token-panel/astro` (the `` component plus the host adapter). - The CLI bin — `design-token-panel-server`, used by the apply pipeline (see below). You can confirm the bin is on the path inside your project with: ```sh pnpm exec design-token-panel-server --help # or npx design-token-panel-server --help ``` If the help text prints, the package is installed correctly. ## The apply-pipeline bin The package ships an executable named `design-token-panel-server`. It runs alongside your dev server, listens on a loopback port, and rewrites your token CSS files when the user clicks **Apply** in the panel. The bin is optional — the panel is fully functional in export/import mode without it. Wire it in once you want users (or you yourself in dev) to push panel tweaks back to disk. The full CLI reference lives on its own page once the reference section is in place. The short version: ```sh npx design-token-panel-server \ --routing ./panel-routing.json \ --write-root ./tokens \ --allow-origin http://localhost:5173 ``` ## Peer-dependency compatibility The package's `package.json` declares the following peer: | Peer | Range | Why | | -------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------ | | `preact` | `^10.29.1` | The panel UI is Preact-rendered. Sharing one Preact runtime with the rest of your app is mandatory — see the note above. | The panel's CSS is self-contained. It ships its own bundled stylesheet under the panel-private `--tokentweak-*` namespace. **Tailwind is not required in your project** to consume the panel. After installing, you must add `import '@takazudo/zudo-design-token-panel/styles';` to your static module graph (typically next to where you mount the host component). Vite's library mode strips the package-internal CSS imports from the emitted JS, so the consumer side has to pull the bundled stylesheet in explicitly. If you forget, the panel JS still runs but every chrome rule is missing — the panel paints invisibly against the host page background. ## Next step You're installed. Head to [Quickstart](./quickstart) for the smallest end-to-end wiring, or to [Three Frameworks](./three-frameworks) for a side-by-side comparison of Astro / Vite + React / Next.js setups. --- # Custom token manifest > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/recipes/custom-token-manifest The panel's spacing / typography / size tabs are driven by a host-supplied `TokenManifest`. The shape is intentionally open — `TokenGroup` is just `string`, so you can coin your own group ids without forking the package. This recipe walks through the moves you actually do day-to-day. For the full type contract, see [Token manifest reference](/pj/zudo-design-token-panel/docs/reference/token-manifest). ## Minimal manifest Every `TokenManifest` must have all four arrays present (any can be empty). A token with `control: 'slider'` (the default when omitted) needs `min`, `max`, `step`, and `unit`. ```ts // src/lib/my-tokens.ts const SPACING_TOKENS: readonly TokenDef[] = [ { id: 'sp-md', cssVar: '--myapp-spacing-md', label: 'Medium', group: 'core', default: '1rem', min: 0, max: 4, step: 0.125, unit: 'rem', }, { id: 'sp-lg', cssVar: '--myapp-spacing-lg', label: 'Large', group: 'core', default: '1.5rem', min: 0, max: 6, step: 0.125, unit: 'rem', }, ]; spacing: SPACING_TOKENS, typography: [], size: [], color: [], }; ``` ## Coining your own group ids `TokenGroup` is `string`, not a closed union. You can add custom group ids freely — but you almost always want to also populate `groupTitles` so the section header renders something nicer than the raw id. ```ts const SPACING_TOKENS: readonly TokenDef[] = [ // ...existing core tokens... { id: 'sp-card', cssVar: '--myapp-spacing-card', label: 'Card padding', group: 'cards', // ← brand-new group id default: '1.25rem', min: 0, max: 4, step: 0.0625, unit: 'rem', }, ]; spacing: SPACING_TOKENS, typography: [], size: [], color: [], // Render order — anything not listed falls back to the package default. spacingGroupOrder: ['core', 'cards'], // Human-readable section headers. groupTitles: { core: 'Core spacing', cards: 'Card-specific spacing', }, }; ``` **`TokenGroup` opens the union deliberately.** Section headers fall back to the raw group id if you forget `groupTitles`, so a typo will show up visually rather than as a TypeScript error. ## Different control types Beyond the default slider, two more controls are supported. ```ts const TYPOGRAPHY_TOKENS: readonly TokenDef[] = [ // 1. Select — fixed list of allowed values. { id: 'font-family-body', cssVar: '--myapp-font-family-body', label: 'Body font', group: 'family', default: 'system-ui', min: 0, max: 0, step: 0, unit: '', control: 'select', options: ['system-ui', 'Inter, sans-serif', 'Georgia, serif'], }, // 2. Text — free-form CSS string. { id: 'line-height-relaxed', cssVar: '--myapp-line-height-relaxed', label: 'Relaxed line height', group: 'rhythm', default: '1.6', min: 0, max: 0, step: 0, unit: '', control: 'text', }, // 3. Slider with pill toggle — exposes a "max" sentinel value. { id: 'radius-button', cssVar: '--myapp-radius-button', label: 'Button radius', group: 'shape', default: '0.375rem', min: 0, max: 1, step: 0.0625, unit: 'rem', pill: { value: '9999px', customDefault: '0.375rem' }, }, ]; ``` For non-slider controls (`select`, `text`), set `min`, `max`, `step` to sensible filler values (e.g. `0`) — they are required by the `TokenDef` shape but ignored by the renderer. ## Hiding rows behind "Advanced" Set `advanced: true` on rows that should live inside the per-tab disclosure rather than the always-visible list: ```ts { id: 'sp-rare', cssVar: '--myapp-spacing-rare', label: 'Rarely-used spacing', group: 'core', default: '0.0625rem', min: 0, max: 1, step: 0.0625, unit: 'rem', advanced: true, }, ``` ## Read-only display rows Use `readonly: true` for tokens you want to display in the panel without allowing edits. The apply pipeline skips read-only tokens in both directions. ```ts { id: 'computed-content-width', cssVar: '--myapp-computed-content-width', label: 'Content width (computed)', group: 'layout', default: 'min(100%, 64rem)', min: 0, max: 0, step: 0, unit: '', control: 'text', readonly: true, }, ``` ## Color tab: empty array vs cluster-driven For color, the package expects you to drive the Color tab through a `ColorClusterConfig`, not through per-token rows in `tokens.color`. Cluster- driven hosts ship `color: []`; the field stays required so a cluster-less host could in principle provide rows here. ```ts spacing: SPACING_TOKENS, typography: TYPOGRAPHY_TOKENS, size: SIZE_TOKENS, color: [], // ← cluster-driven; see custom-color-cluster recipe. }; ``` ## Related - [Token manifest reference](/pj/zudo-design-token-panel/docs/reference/token-manifest) — full `TokenDef` and `TokenManifest` interfaces, helpers, and apply semantics. - [Custom color cluster](/pj/zudo-design-token-panel/docs/recipes/custom-color-cluster) — the cluster-driven Color tab. - [`configurePanel` reference](/pj/zudo-design-token-panel/docs/reference/configure-panel) — where the manifest plugs in. --- # Token manifest > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/reference/token-manifest The panel does not know which tokens a host site exposes. The host supplies its own token manifest, and the panel iterates it to render rows and apply overrides to `:root`. This page pins the public shape of [`TokenManifest`](#tokenmanifest), [`TokenDef`](#tokendef), the per-tab group ordering hooks, and the `groupTitles` table. ## `TokenDef` The atomic unit of the manifest. One `TokenDef` describes one editable design-token row inside the panel. ```ts /** Stable id used as the Record key in persisted state (e.g. `hsp-2xs`). */ id: string; /** CSS custom property written to `:root` (e.g. `--myapp-spacing-hgap-2xs`). */ cssVar: string; /** Display label shown in the panel row. */ label: string; /** Manifest group — tab components use this for section headers. */ group: TokenGroup; /** Default value as a CSS string (`0.125rem`, `12px`, etc.). */ default: string; /** Slider min, in `unit`. Unused when `readonly` or non-slider. */ min: number; /** Slider max, in `unit`. */ max: number; /** Slider step, in `unit`. */ step: number; /** Unit suffix (`rem`, `px`, …). May be empty for unitless / read-only tokens. */ unit: string; /** Read-only tokens are displayed but not editable. */ readonly?: true; /** Which control renders this token. Defaults to `"slider"` when absent. */ control?: TokenControl; /** Select options — only used when `control === "select"`. */ options?: readonly string[]; /** Hide behind the per-tab Advanced `` disclosure. */ advanced?: true; /** Opt-in pill toggle (e.g. for `--radius-full` 9999px sentinel). */ pill?: { value: string; customDefault: string }; } ``` ### Field reference | Field | Required | Notes | | --- | --- | --- | | `id` | yes | Stable id used as the Record key in persisted state. Renaming an id breaks user state continuity. | | `cssVar` | yes | The CSS custom property the panel writes to `:root` on apply. Host-controlled — pick names like `--myapp-spacing-md`. | | `label` | yes | Human-visible label rendered in the panel row. | | `group` | yes | Group id (a free-form string — see below). Determines the section header the row appears under. | | `default` | yes | CSS-string default. Read by the panel's reset behaviour. | | `min` / `max` / `step` | yes | Slider bounds, in `unit`. Unused when `readonly` or non-slider. | | `unit` | yes | Suffix for slider values (`rem`, `px`, …). May be empty for unitless / read-only tokens. | | `readonly` | no | When `true`, the row is displayed but not editable. The panel skips both directions in `applyTokenOverrides`. | | `control` | no | `'slider'` (default) / `'select'` / `'text'`. Picks the row's edit widget. | | `options` | no | Required when `control === 'select'`; ignored otherwise. | | `advanced` | no | When `true`, the row is hidden behind the per-tab Advanced `` disclosure. | | `pill` | no | Opt-in pill toggle. Used for sentinel-value tokens like `--radius-full` (`9999px`) where a slider does not make sense. | ### `TokenGroup` is open `TokenGroup` is an alias for `string` — not a closed union — so consumers can coin their own group ids without forking the package types. ```ts ``` The package ships default orderings (`GROUP_ORDER`, `FONT_GROUP_ORDER`, `SIZE_GROUP_ORDER`) and titles (`GROUP_TITLES`) covering the historical group ids. Hosts coining unknown group ids SHOULD populate `groupTitles` so the section headers carry human-readable labels — the tabs fall back to printing the raw group id otherwise. ### `TokenControl` ```ts ``` | Value | Renders | Required companion field | | --- | --- | --- | | `'slider'` (default) | Range input with min / max / step in `unit`. | none | | `'select'` | Dropdown of strings. | `options: readonly string[]` | | `'text'` | Free-form text input. | none | ## `TokenManifest` The container shape carried on `PanelConfig.tokens`. Holds the four arrays the panel consumes plus optional per-tab ordering / titling hooks. ```ts spacing: readonly TokenDef[]; typography: readonly TokenDef[]; size: readonly TokenDef[]; color: readonly TokenDef[]; /** Optional spacing-tab group order. Falls back to the package-bundled `GROUP_ORDER`. */ spacingGroupOrder?: readonly string[]; /** Optional font-tab primary group order. Falls back to `FONT_GROUP_ORDER`. */ fontGroupOrder?: readonly string[]; /** Optional size-tab group order. Falls back to `SIZE_GROUP_ORDER`. */ sizeGroupOrder?: readonly string[]; /** Optional human-readable section titles keyed by group id. Falls back to `GROUP_TITLES`. */ groupTitles?: Readonly>; } ``` ### Required arrays The host project provides the four manifest arrays: | Field on `PanelConfig.tokens` | Notes | | --- | --- | | `spacing` | Spacing-tab rows. | | `typography` | Font-tab rows. (The persist envelope's slice is `typography`; the upstream array constant is often named `FONT_TOKENS`. The panel reads it through `panelConfig.tokens.typography`, so consumers can use either name in their own source — the field on `PanelConfig` is what the contract pins.) | | `size` | Size-tab rows. | | `color` | Color-tab non-cluster rows. Cluster-driven hosts ship an empty array (color is driven by the cluster, not by per-token rows); the field is still required by the manifest shape so a cluster-less host can provide rows here. | The panel package itself ships ZERO baked-in manifest data — the host is the source of truth. The package's role is consuming whatever the host hands in. ### Optional ordering / titling hooks The four optional fields let a host customise how groups within a tab are ordered and titled. Manifests that omit a field inherit the package-bundled default for that tab. | Optional field | Falls back to | Purpose | | --- | --- | --- | | `spacingGroupOrder` | `GROUP_ORDER` | Order in which spacing groups appear inside the Spacing tab. | | `fontGroupOrder` | `FONT_GROUP_ORDER` | Order in which primary font groups appear inside the Font tab. | | `sizeGroupOrder` | `SIZE_GROUP_ORDER` | Order in which size groups appear inside the Size tab. | | `groupTitles` | `GROUP_TITLES` | Map of group id → human-visible section header. Missing entries fall back to the raw group id. | Example with custom group order: ```ts spacing: SPACING_TOKENS, typography: FONT_TOKENS, size: SIZE_TOKENS, color: [], spacingGroupOrder: ['hgap', 'vgap', 'pad', 'inset'], groupTitles: { hgap: 'Horizontal gap', vgap: 'Vertical gap', pad: 'Padding', inset: 'Inset', }, }; ``` ## Apply behaviour The panel's internal `applyTokenOverrides(tokens, overrides)` walks each `TokenDef`: - If `readonly`, skip both directions (display-only). - If the override map has a non-empty string for `id`, write `document.documentElement.style.setProperty(t.cssVar, value)`. - Otherwise, remove the inline property (so the stylesheet default wins). The contract requires this read/write target to be `:root`. No shadow DOM, no scoped overrides — this is intentional; the panel ships a global tweak. The panel never reads consumer CSS variables (it carries its own defaults via `TokenDef.default`). It only writes the consumer-supplied `cssVar` strings, one per overridden token, plus the cluster's palette / base / semantic vars on apply. See [Color cluster](/pj/zudo-design-token-panel/docs/reference/color-cluster/) for the cluster-side write contract. ## Cross-references - [`PanelConfig.tokens`](/pj/zudo-design-token-panel/docs/reference/configure-panel/#panelconfig-interface) — where the manifest is plugged in. - [`ColorClusterConfig`](/pj/zudo-design-token-panel/docs/reference/color-cluster/) — drives the Color tab's cluster section, complementing or replacing the manifest's `color` array. - [Apply pipeline](/pj/zudo-design-token-panel/docs/reference/apply-pipeline/) — how the panel's per-token writes are forwarded to the bin server when `applyEndpoint` is configured. --- # Quickstart > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/getting-started/quickstart This page is the smallest end-to-end wiring of `@takazudo/zudo-design-token-panel` — just enough code to call `configurePanel({...})`, declare a five-token manifest, and pop the panel from your browser console. After this, the [Configure Panel reference](/pj/zudo-design-token-panel/docs/reference/configure-panel) covers every field on `PanelConfig` in depth. ## What you'll wire up The public surface of the package is a single configure-once init: ```ts configurePanel({ storagePrefix: 'myapp-design-token-panel', consoleNamespace: 'myapp', modalClassPrefix: 'myapp-design-token-panel-modal', schemaId: 'myapp-design-tokens/v1', exportFilenameBase: 'myapp-design-tokens', tokens: { spacing: [], typography: [], size: [], color: [] }, colorCluster: myColorCluster, }); ``` Once configured, the package installs three async helpers on `window.` — `showDesignPanel()`, `hideDesignPanel()`, `toggleDesignPanel()`. Calling any of them lazy-imports the panel module and mounts the UI. When you drop `` into an Astro layout, the host adapter reads the inline JSON config and calls `configurePanel(...)` on your behalf. You don't need to call it manually. For Vite + React or Next.js, you call it yourself before importing the panel module. ## Step 1 — Define a minimal `PanelConfig` Create a file alongside your source to hold the config. Five identifiers (storage prefix, console namespace, modal class prefix, schema id, export filename) plus your token manifest and color cluster are all the package needs. ```ts // src/lib/my-panel-config.ts storagePrefix: 'myapp-design-token-panel', consoleNamespace: 'myapp', modalClassPrefix: 'myapp-design-token-panel-modal', schemaId: 'myapp-design-tokens/v1', exportFilenameBase: 'myapp-design-tokens', tokens: myTokens, colorCluster: myColorCluster, }; ``` Pick identifiers that are unambiguous to your app — typically your app slug. The `storagePrefix` value is the only knob that controls every persisted `localStorage` key, so renaming it later loses users' saved tweaks. ## Step 2 — A five-line cssVar manifest The token manifest is a host-supplied list of editable CSS custom properties grouped per tab (spacing / typography / size / color). The four arrays are required even if some are empty. Here is the smallest manifest that renders something in each non-color tab: ```ts // src/lib/my-tokens.ts spacing: [ { id: 'sp-md', cssVar: '--myapp-spacing-md', label: 'Spacing M', group: 'hsp', default: '1rem', min: 0, max: 4, step: 0.0625, unit: 'rem' }, ], typography: [ { id: 'text-base', cssVar: '--myapp-text-base', label: 'Body Text', group: 'font-size', default: '1rem', min: 0.75, max: 1.5, step: 0.0625, unit: 'rem' }, ], size: [ { id: 'sz-radius', cssVar: '--myapp-radius', label: 'Radius', group: 'radius', default: '0.5rem', min: 0, max: 2, step: 0.0625, unit: 'rem' }, ], color: [], }; ``` That's five tokens (one per array, color-cluster-driven sites ship `color: []`). The `cssVar` field is what the panel writes to `:root` via `setProperty()`, so make sure your stylesheet reads from the same variable names. `colorCluster` is a separate, larger config that drives the color tab — palette, base roles, semantic table, scheme registry. The smallest legal cluster declares a palette size, a `paletteCssVarTemplate` like `--myapp-palette-{n}`, and a single bundled `Default` scheme. See the [Configure Panel reference](/pj/zudo-design-token-panel/docs/reference/configure-panel) for the full shape and a worked minimal example. ## Step 3 — Call `configurePanel(...)` and toggle the panel For a non-Astro host (Vite + React, Next.js, custom Vite SPA), call `configurePanel(...)` once at app boot — typically from your entry module — then open the devtools console: ```ts // src/main.ts (Vite + React) or app/layout.tsx (Next.js, with 'use client') configurePanel(myPanelConfig); ``` Then, in the browser devtools console: ```js window.myapp.toggleDesignPanel(); ``` The first call lazy-imports the panel module and mounts it. Subsequent calls share the memoised module promise. All three console helpers are `async`. Their signatures: `showDesignPanel(): Promise`, `hideDesignPanel(): Promise`, `toggleDesignPanel(): Promise`. They merge into any pre-existing `window.` object so a host can share the namespace between multiple dev tools. For Astro, you don't call `configurePanel(...)` directly — you drop the host component into your layout and the host adapter wires everything up: ```astro --- // src/layouts/Layout.astro --- void import('@takazudo/zudo-design-token-panel/astro/host-adapter'); ``` The `` component AND the host-adapter `` block are a paired unit — both lines are required, always together. Skipping the script leaves the JSON config payload on the page with no JS to read it. ## Verify the panel mounts Open the page in a browser, drop into devtools, and run: ```js window.myapp.toggleDesignPanel(); ``` You should see a Preact-rendered side panel mount. Drag a slider; the matching `--myapp-*` CSS variable updates on `:root` immediately. Reload the page — the override re-applies before first paint, no FOUT. If the panel JS runs but the chrome looks unstyled (transparent background, default page font), you skipped the `import '@takazudo/zudo-design-token-panel/styles';` line. Vite library mode strips the package-internal CSS imports from emitted JS, so the consumer side must pull the bundled stylesheet in explicitly. ## Next step This page got you to "the panel renders, my CSS variables update." For the full `PanelConfig` shape, color-cluster anatomy, and apply-pipeline wiring, continue to the [Configure Panel reference](/pj/zudo-design-token-panel/docs/reference/configure-panel). For framework-specific snippets, see [Three Frameworks](./three-frameworks). --- # Secondary cluster: configure or disable > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/recipes/secondary-cluster-or-disable `PanelConfig.secondaryColorCluster` is a three-state field. The state you pick controls both what the Color tab renders and which apply / clear / load code paths run. This recipe covers the three cases with concrete snippets. For the underlying contract, see [Color cluster reference](/pj/zudo-design-token-panel/docs/reference/color-cluster) (multi-cluster section) and the `resolveSecondaryColorCluster` helper. ## The three states | Value | Render | Apply / clear / load | |---|---|---| | `undefined` (field omitted) | Secondary section hidden. | No write. The persist envelope's secondary slice stays inert. | | `null` | Secondary section hidden — **same as `undefined` visually**. | Hard opt-out: every secondary code path skips. The persist envelope's secondary slice is **not** hydrated even if it exists in `localStorage`. | | `ColorClusterConfig` | Secondary palette + semantic table render below the primary. | Same write semantics as the primary, scoped to the secondary palette + names. | **Why `null` is not the same as `undefined`.** Both hide the section, but only `null` skips the secondary load path. A host that previously shipped a secondary cluster and now wants to permanently retire it should set `secondaryColorCluster: null` so old saved states do not silently re-apply on next page load. ## Case 1 — secondary hidden (default) Just omit the field. This is the recommended starting point for every new host. ```ts // src/lib/my-panel-config.ts storagePrefix: 'myapp-design-token-panel', consoleNamespace: 'myapp', modalClassPrefix: 'myapp-design-token-panel-modal', schemaId: 'myapp-design-tokens/v1', exportFilenameBase: 'myapp-design-tokens', tokens: { spacing: [], typography: [], size: [], color: [], }, colorCluster: primaryCluster, // secondaryColorCluster intentionally omitted. }; ``` ## Case 2 — explicit opt-out Use `null` after a previous deploy carried a secondary cluster, and you want to make sure no user's saved state will resurrect it on next load. ```ts // ...common fields... colorCluster: primaryCluster, secondaryColorCluster: null, }; ``` The persisted envelope under `${storagePrefix}-state-v2` may still contain a secondary slice from earlier versions. With `secondaryColorCluster: null` the package skips that slice on load, so users do not see stale secondary overrides. The slice itself is preserved in storage for round-trip safety — flipping back to a real `ColorClusterConfig` later will pick it up again unless you also bump `storagePrefix` or `schemaId`. ## Case 3 — host-supplied secondary cluster The secondary cluster has the same shape as the primary ([`ColorClusterConfig`](/pj/zudo-design-token-panel/docs/reference/color-cluster)). Use a different `paletteCssVarTemplate` and different `baseRoles` / `semanticCssNames` so the writes do not collide with the primary cluster. ```ts ColorClusterConfig, ColorScheme, PanelConfig, } from '@takazudo/zudo-design-token-panel'; const accentDark: ColorScheme = { background: 0, foreground: 7, cursor: 7, selectionBg: 8, selectionFg: 0, palette: [ '#202124', '#ff7597', '#7cd992', '#f5d97a', '#7ab0f5', '#c79df0', '#7fdacf', '#dde2ec', '#3a3d44', '#ff7597', '#7cd992', '#f5d97a', '#7ab0f5', '#c79df0', '#7fdacf', '#bfc6d4', ], shikiTheme: 'github-dark', }; const secondaryCluster: ColorClusterConfig = { id: 'myapp-accent', label: 'Accent surface', paletteSize: 16, paletteCssVarTemplate: '--myapp-accent-p{n}', baseRoles: { background: '--myapp-accent-bg', foreground: '--myapp-accent-fg', }, baseDefaults: { background: 0, foreground: 7, }, semanticDefaults: { surface: 0, onSurface: 7, }, semanticCssNames: { surface: '--myapp-accent-surface', onSurface: '--myapp-accent-on-surface', }, defaultShikiTheme: 'github-dark', colorSchemes: { 'Accent Dark': accentDark, }, panelSettings: { colorScheme: 'Accent Dark', colorMode: false, }, }; // ...common fields... colorCluster: primaryCluster, secondaryColorCluster: secondaryCluster, }; ``` ## Case 4 — light/dark pairing on the secondary Same shape, but with the `panelSettings.colorMode` light/dark pairing enabled. The panel watches `document.documentElement[data-theme]` and swaps schemes accordingly on init. ```ts const secondaryCluster: ColorClusterConfig = { // ...same fields as Case 3... colorSchemes: { 'Accent Light': accentLight, 'Accent Dark': accentDark, }, panelSettings: { colorScheme: 'Accent Dark', colorMode: { defaultMode: 'dark', lightScheme: 'Accent Light', darkScheme: 'Accent Dark', }, }, }; ``` `colorMode` is independent on the primary and the secondary. You can set it on one and leave the other as `false`; the panel resolves them separately through `resolveSecondaryColorCluster`. ## Related - [Color cluster reference](/pj/zudo-design-token-panel/docs/reference/color-cluster) — primary cluster shape and apply-time write order; the secondary follows the same rules. - [Custom color cluster](/pj/zudo-design-token-panel/docs/recipes/custom-color-cluster) — wiring the primary cluster from scratch. - [`configurePanel` reference](/pj/zudo-design-token-panel/docs/reference/configure-panel) — `PanelConfig.secondaryColorCluster` field documentation. --- # Color cluster > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/reference/color-cluster The Color tab — palette + base roles + semantic table + scheme list — is parameterised through a `ColorClusterConfig` so a portable host can ship a different palette size, a different CSS-var family, or a different semantic vocabulary without touching the panel internals. This page pins [`ColorClusterConfig`](#colorclusterconfig), [`ColorScheme`](#colorscheme), [`ColorRef`](#colorref), the multi-cluster (primary + optional secondary) resolution, and the [host-supplied scheme presets](#host-supplied-scheme-presets) merge contract. ## `ColorClusterConfig` ```ts /** Stable id — used for debugging / logging only. */ id: string; /** * Optional human-visible label rendered in the Color tab section * headings. When absent, the tab falls back to `id.toUpperCase()`. */ label?: string; /** Expected palette length. Drives init + persisted-state validation. */ paletteSize: number; /** * Palette-slot CSS var template. The panel substitutes `{n}` with the * palette index at apply time. * * paletteCssVarTemplate: '--myapp-p{n}' → --myapp-p0, --myapp-p1, ... * paletteCssVarTemplate: '--brand-pa{n}' → --brand-pa0, --brand-pa1, ... */ paletteCssVarTemplate: string; /** * Map of base-role name → CSS custom-property name. A cluster MAY declare * a subset (an empty map is legal); only declared roles are written on apply. */ baseRoles: Partial>; /** Semantic token name → default palette index. */ semanticDefaults: Record; /** Semantic token name → CSS custom-property name. */ semanticCssNames: Record; /** * Fallback palette indices when a scheme omits a base role. Same partial * shape as `baseRoles`. The persist envelope always carries all 5 numeric * fields for round-trip, but inert roles emit zero CSS writes. */ baseDefaults: Partial>; /** Fallback `shikiTheme` when a scheme lacks one. (Inert when no shiki integration.) */ defaultShikiTheme: string; /** * Color-scheme registry. Keyed by display name (`"Default Dark"`, etc.). * Each entry is a `ColorScheme`. The portable contract requires this to be * a plain object — no dynamic loaders. Pass `{}` for clusters that don't * use schemes. */ colorSchemes: Record; /** * Panel-level scheme settings. Carried inside the cluster (rather than as * a separate import) so scheme helpers can read everything from the * cluster argument. */ panelSettings: { /** Scheme name to seed state from when `colorMode` is `false`. */ colorScheme: string; /** * Optional light/dark pairing. When set to an object, the panel honours * `document.documentElement[data-theme]` and switches schemes accordingly * on init. Set to `false` to disable the light/dark UI. */ colorMode: false | { defaultMode: 'light' | 'dark'; lightScheme: string; darkScheme: string }; }; } ``` The runtime type that ships in the package source is `ColorClusterDataConfig`. `ColorClusterConfig` is re-exported from the package root as the public-facing alias for this same shape: ```ts ``` The two names are interchangeable. ### Field reference | Field | Required | Notes | | --- | --- | --- | | `id` | yes | Stable id used for debugging / logging only. | | `label` | no | Human-visible label for section headings. Falls back to `id.toUpperCase()` when absent. | | `paletteSize` | yes | Expected palette length. Drives init + persisted-state validation. | | `paletteCssVarTemplate` | yes | String template with `{n}` placeholder — see [the JSON-serializable constraint below](#json-serializable-constraint). | | `baseRoles` | yes | `Partial>` — only declared roles are written on apply. An empty map is legal. | | `semanticDefaults` | yes | Semantic token name → default palette index. | | `semanticCssNames` | yes | Semantic token name → CSS custom-property name. | | `baseDefaults` | yes | Fallback palette indices when a scheme omits a base role. | | `defaultShikiTheme` | yes | Fallback `shikiTheme` when a scheme lacks one. Inert when no shiki integration. | | `colorSchemes` | yes | Plain object — no dynamic loaders. Pass `{}` for clusters that don't use schemes. | | `panelSettings.colorScheme` | yes | Scheme name to seed state from when `colorMode` is `false`. | | `panelSettings.colorMode` | yes | `false` to disable light/dark UI; an object to honour `data-theme` and switch schemes accordingly on init. | ## `ColorScheme` ```ts background: ColorRef; foreground: ColorRef; cursor: ColorRef; selectionBg: ColorRef; selectionFg: ColorRef; palette: readonly string[]; // length must match cluster.paletteSize shikiTheme: string; semantic?: Record; // keys must be a subset of cluster.semanticDefaults } ``` A `ColorScheme` is a fully-resolved snapshot of a palette plus the role assignments that pin which palette indices the base + semantic roles point at. ### `ColorRef` ```ts ``` A reference to a color, used for base roles and semantic overrides: - A `number` references an index into the scheme's `palette` array (e.g. `0` → `palette[0]`). - A `string` is a literal color value (hex, oklch, etc.). The shorthands `"bg"` and `"fg"` resolve to the scheme's background / foreground respectively. ### Palette length invariant `ColorScheme.palette` length MUST match the cluster's `paletteSize`. Schemes with mismatched palette lengths are rejected at init time. ### Semantic key invariant The keys of `ColorScheme.semantic` MUST be a subset of `cluster.semanticDefaults`. Hosts cannot introduce new semantic tokens via a scheme — the cluster is the authoritative vocabulary. ## JSON-serializable constraint Every field on `ColorClusterConfig` (and on each `ColorScheme` it nests) MUST be JSON-serializable. No function fields, no class instances, no `Symbol` keys, no `undefined` where `null` is meant. This is enforced by Astro frontmatter → component prop handoff: the host adapter stringifies the config into the rendered island and parses it back at runtime. Function fields silently disappear under that round-trip and would surface as cryptic runtime errors. The palette CSS-var name is therefore expressed as a string template, not a function: ```ts // wrong (function — silently dropped by JSON.stringify) // paletteCssVar: (i) => `--myapp-p${i}`, // right (string template, JSON-serializable) paletteCssVarTemplate: '--myapp-p{n}', ``` The panel resolves `{n}` to the palette index at every call site that previously took a function argument (palette apply, clear-applied, scheme diff). The substitution is plain string replacement — no template-engine features. `{n}` is the only placeholder; literal `{n}` text in an output var name is not a use case the contract supports. ## Multi-cluster support The package supports a primary cluster and an optional secondary cluster. `PanelConfig.colorCluster` is the primary cluster (always required). `PanelConfig.secondaryColorCluster` is the secondary cluster slot — host-driven, three states: | `secondaryColorCluster` value | Meaning | Effect | | --- | --- | --- | | `undefined` (field omitted) | Secondary section hidden | The Color tab does not render a secondary palette / semantic section. | | `null` | Explicit opt-out | Same render-side effect as `undefined`; in addition, apply / clear / load skip every secondary code path. The persist envelope's secondary-cluster slice is NOT hydrated. | | `ColorClusterConfig` object | Host-supplied secondary cluster | Same render / apply / clear contract as the primary cluster, scoped to the supplied palette + semantic vocabulary. | The resolution is performed through the `resolveSecondaryColorCluster()` helper exported from the panel config module. Internal call sites (color-tab render, apply-modal flatten, tweak-state apply / clear / load) MUST read through that helper rather than the raw field so every code path treats the three states consistently. The persist envelope's secondary-cluster slice (the slot name is historical and remains stable for storage continuity) is the on-disk shape for the secondary cluster. ## Host-supplied scheme presets `PanelConfig.colorPresets` is the optional, host-supplied preset map surfaced by the Color tab "Scheme..." dropdown. It defaults to `{}` and the package itself ships zero presets — hosts that want a curated preset library (Dracula / Solarized / Tokyo Night / etc.) ship it themselves so consumers do not pay for it by default. | `colorPresets` value | Meaning | Effect | | --- | --- | --- | | `undefined` (field omitted) | Default | Equivalent to `{}` — only `colorCluster.colorSchemes` populates the dropdown. | | `{}` | Explicit empty | Same as `undefined`. | | `Record` | Host-supplied | Each key surfaces as a `` below the cluster's bundled schemes. Sorted alphabetically. | ### Merge order in the dropdown ``` Scheme... ... cluster.colorSchemes (insertion order) ... ... colorPresets (alphabetical) ... ``` ### Key collision If a `colorPresets` entry shares a name with one in `colorCluster.colorSchemes`, the cluster's bundled scheme wins for the load lookup. The bundled cluster scheme is the cluster owner's documented default (typically `"Default"` / `"Default Light"` / `"Default Dark"`) and overrides the optional host preset list. The dropdown still renders both `` entries — visually deduplicated display is out of scope for the Color tab and would require a bespoke `` widget. A duplicate name is the host's signal to rename one of its own preset keys. ### JSON-serializable Every `ColorScheme` MUST satisfy the same JSON-serializable constraint as the cluster. The map is read at render time through `getPanelConfig().colorPresets`, so the standard Astro frontmatter → island handoff applies. ### Lazy attachment via `setPanelColorPresets()` Hosts that ship a large preset library can omit `colorPresets` from the SSR config blob and call `setPanelColorPresets(presets)` from a client-side dynamic import. This keeps the preset payload out of the inline `` and lets the bundler emit it as a separate JS chunk. ```ts void import('./large-preset-library').then(({ presets }) => { setPanelColorPresets(presets); }); ``` The trailing call wins on conflict (no throw, unlike `configurePanel`); a host that pre-calls `setPanelColorPresets` before `configurePanel` is serviced via a holding slot inside the panel-config module. See [`configurePanel`](/pj/zudo-design-token-panel/docs/reference/configure-panel/#setpanelcolorpresetspresets) for the full helper signature. ## Apply behaviour The cluster apply pipeline (`applyColorState(state, cluster)`) walks the cluster fields: - For each palette slot `i` in `0..cluster.paletteSize`, write `cluster.paletteCssVarTemplate.replace('{n}', String(i))` ← `palette[i]`. - For each `(roleKey, cssName)` in `cluster.baseRoles`, write `cssName` ← `palette[state[roleKey]]`. Roles absent from `baseRoles` are not written. - For each `(semanticKey, cssName)` in `cluster.semanticCssNames`, resolve `state.semanticMappings[semanticKey] ?? cluster.semanticDefaults[semanticKey]` through the package's mapping resolver (which handles the `"bg"` / `"fg"` shorthands) and write `cssName` ← resolved hex. - `clearAppliedStyles(clusters)` removes every property the cluster could have set (palette + base roles + semantic). The default wipes both primary and any optional secondary cluster. A cluster MAY declare a subset of base roles (an empty map is legal); only declared roles are written on apply. The persist envelope always carries all 5 numeric fields for round-trip, but inert roles emit zero CSS writes — useful when a host's design system does not surface, say, `cursor` or `selectionFg` as separate tokens. ## Cross-references - [`PanelConfig.colorCluster`](/pj/zudo-design-token-panel/docs/reference/configure-panel/#panelconfig-interface) — primary cluster slot. - [`PanelConfig.secondaryColorCluster`](/pj/zudo-design-token-panel/docs/reference/configure-panel/#panelconfig-interface) — optional secondary slot. - [Token manifest](/pj/zudo-design-token-panel/docs/reference/token-manifest/) — describes the non-cluster `tokens.color` rows that complement the cluster. - [Apply pipeline](/pj/zudo-design-token-panel/docs/reference/apply-pipeline/) — how cluster-driven CSS-var writes flow to the bin server when `applyEndpoint` is configured. --- # Three Frameworks > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/getting-started/three-frameworks The panel's public surface is a single `configurePanel({...})` call plus a Preact-rendered shell, so the wiring shape is the same across hosts. The only differences are *who* calls `configurePanel`, *where* the styles import lives, and *how* the host interacts with view-transitions. This page is the side-by-side overview. For full code, run any of the example apps linked at the bottom of each section. ## Comparison | Step | Astro | Vite + React | Next.js (App Router) | | --- | --- | --- | --- | | Install package | `pnpm add @takazudo/zudo-design-token-panel preact` | same | same | | Define `PanelConfig` | one TS file, host-side | same | same | | Mount the host | `` in layout | call `configurePanel(...)` from entry | call `configurePanel(...)` from a `'use client'` module | | Side-effect script | `void import('.../astro/host-adapter')` next to the host | not required | not required | | Styles import | once on the static graph | once on the static graph | once on the static graph | | View-transition lifecycle | wired automatically by the host adapter when `` is present | n/a | n/a | | Apply-pipeline bin | spawn via `concurrently` in dev script | spawn via `concurrently` in dev script | spawn via `concurrently` in dev script | What stays the same: - The `PanelConfig` shape and all of its fields. - The console API — `window..{show,hide,toggle}DesignPanel()` — installed identically by every host. - Storage-key derivation under `storagePrefix`. - Apply-pipeline behaviour — POST to `applyEndpoint` is identical regardless of host. What differs: - Whether **you** call `configurePanel(...)` (Vite, Next.js) or the **host adapter** calls it for you (Astro). - Whether the page goes through Astro's `` view-transition lifecycle (only Astro). ## Astro The Astro entry handles mounting for you. Drop `` into a shared layout, pair it with the host-adapter script tag, and import the styles. That's the entire integration. ```astro --- // src/layouts/Layout.astro --- void import('@takazudo/zudo-design-token-panel/astro/host-adapter'); ``` Worked example: [`examples/astro`](https://github.com/Takazudo/zudo-design-token-panel/tree/main/examples/astro). ## Vite + React Vite hosts call `configurePanel(...)` themselves at app boot. The panel mounts as a Preact island — your React tree is untouched. ```tsx // src/main.tsx configurePanel(myPanelConfig); createRoot(document.getElementById('root')!).render(); ``` Open devtools, run `window.myapp.toggleDesignPanel()`, and the panel mounts. Worked example: [`examples/vite-react`](https://github.com/Takazudo/zudo-design-token-panel/tree/main/examples/vite-react). ## Next.js (App Router) Next.js needs a `'use client'` boundary so `configurePanel(...)` runs in the browser. The cleanest shape is a tiny client component you mount in your root layout. ```tsx // app/_panel/install-panel.tsx 'use client'; useEffect(() => { configurePanel(myPanelConfig); }, []); return null; } ``` ```tsx // app/layout.tsx return ( {children} ); } ``` `configurePanel(...)` is synchronous and idempotent at the same value. Calling it from `useEffect` avoids running it during a server render and lets you keep the install module otherwise inert. If you'd rather call it inline at module init, you can — just make sure the file is reachable only from the client bundle. Worked example: [`examples/next`](https://github.com/Takazudo/zudo-design-token-panel/tree/main/examples/next). The `examples/` directory linked above is wired up by a separate doc-port topic and may not exist yet at the time you read this. The links are stable — they will resolve once the example apps land on `main`. ## Where to go next - For per-field detail on `PanelConfig`, see the [Configure Panel reference](/pj/zudo-design-token-panel/docs/reference/configure-panel). - For the apply-pipeline bin (`design-token-panel-server`) and routing JSON, see the CLI reference once it lands. - For the smallest possible end-to-end wiring, see [Quickstart](./quickstart). --- # Apply pipeline setup > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/recipes/apply-pipeline-setup The Apply button POSTs a flat CSS-var diff to the host's apply endpoint, which routes the diff to the **bin server** (`design-token-panel-server`), which atomically rewrites your source CSS files. This recipe wires the bin into a non-Astro host (e.g. a Vite SPA, Next.js, or any Node-based dev server) and sets up the three guards: CORS, `--write-root` sandboxing, and the single-source routing JSON. For the full apply contract — request / response envelopes, native- implementation guidance, error shapes — see [Apply pipeline reference](/pj/zudo-design-token-panel/docs/reference/apply-pipeline). ## 1. Install the package as a dev dep ```sh pnpm add -D @takazudo/zudo-design-token-panel ``` The package ships an executable named `design-token-panel-server`. Verify the install with: ```sh pnpm exec design-token-panel-server --help ``` ## 2. Author the routing JSON The routing JSON is a top-level object mapping **CSS-var prefix family** (without leading `--` and trailing `-`) → **repo-relative source-CSS file path**. Both the panel UI (`PanelConfig.applyRouting`) and the bin (`--routing` flag) read the same file — keeping them in lockstep is the whole point. ```json // panel-routing.json { "myapp": "src/styles/tokens.css", "myapp-extra": "src/styles/extra-tokens.css" } ``` **Prefix family, not the full var name.** `myapp` matches every var that starts with `--myapp-` (e.g. `--myapp-spacing-md`, `--myapp-color-primary`). A diff token whose prefix is not in the routing map is rejected with a 400. ## 3. Wire it into your dev script Use `concurrently` (or `npm-run-all`) to run the bin alongside your dev server. Replace `vite` with whichever long-running process your project uses. ```json // package.json { "scripts": { "dev": "concurrently --kill-others-on-fail --names dev,panel \"vite\" \"design-token-panel-server --routing ./panel-routing.json --write-root ./src/styles --allow-origin http://localhost:5173\"" }, "devDependencies": { "@takazudo/zudo-design-token-panel": "^0.1.0", "concurrently": "^9.0.0" } } ``` The flags above are the recommended minimum: - **`--routing ./panel-routing.json`** — required. The single source of truth for prefix → file routing. - **`--write-root ./src/styles`** — optional but strongly recommended. The bin will refuse to write outside this tree, so a typo or path-escape attempt cannot clobber files elsewhere in your repo. Defaults to `--root` (which itself defaults to `process.cwd()`). - **`--allow-origin http://localhost:5173`** — required for any browser to apply. Pass each dev origin you actually serve from. Repeatable. **`--allow-origin` is matched verbatim** on the full scheme + host + port string. `http://localhost:5173` and `http://127.0.0.1:5173` are different origins as far as the bin is concerned. Pass each one you serve from, or your browser will see a 403 with no `Access-Control-Allow-Origin` header. ## 4. Point the panel at the bin Import the same routing JSON into your panel config so the UI and the bin agree without two declarations to keep in sync. ```ts // src/lib/my-panel-config.ts storagePrefix: 'myapp-design-token-panel', consoleNamespace: 'myapp', modalClassPrefix: 'myapp-design-token-panel-modal', schemaId: 'myapp-design-tokens/v1', exportFilenameBase: 'myapp-design-tokens', tokens: myTokens, colorCluster: myColorCluster, applyEndpoint: 'http://127.0.0.1:24681/apply', applyRouting: routing, }; ``` `applyEndpoint` is the URL the **browser** POSTs to. The bin defaults to `127.0.0.1:24681`, so the default `http://127.0.0.1:24681/apply` "just works" — change `--port` or `--host` and you must update `applyEndpoint` to match. ## 5. Vite-only host wiring (no Astro) A Vite host (or any non-Astro host) does not get the `` SSR component. Call `configurePanel` directly at app boot, then import the panel module so its side-effects register: ```ts // src/main.ts configurePanel(myPanelConfig); // Lazy-load the panel module on demand (e.g. when a dev keybinding fires). window.addEventListener('keydown', (event) => { if (event.altKey && event.shiftKey && event.code === 'KeyP') { void import('@takazudo/zudo-design-token-panel').then((mod) => { mod.toggleDesignPanel(); }); } }); ``` **Validate before configure.** If your config crosses any boundary that might mangle it (an ``-hosted JSON blob, a server response, etc.), call `assertValidPanelConfig(value)` first — it throws with a message naming the offending field when something is wrong, instead of failing later with a cryptic runtime error. ## 6. Shut down cleanly `concurrently --kill-others-on-fail` (or `concurrently -k`) ensures `SIGINT` from your terminal propagates to the bin. The bin handles `SIGINT` / `SIGTERM` by stopping accept, draining in-flight requests, and exiting with code `0`. A 5 s timeout force-exits if `close` hangs on a keep-alive socket. If you spawn the bin from a custom Node wrapper instead, forward signals yourself: ```ts // scripts/dev-with-panel.ts const bin = spawn( 'design-token-panel-server', [ '--routing', './panel-routing.json', '--write-root', './src/styles', '--allow-origin', 'http://localhost:5173', ], { stdio: 'inherit', shell: false }, ); const forward = (signal: NodeJS.Signals): void => { bin.kill(signal); }; process.on('SIGINT', forward); process.on('SIGTERM', forward); bin.on('exit', (code) => process.exit(code ?? 0)); ``` ## 7. Smoke-test the wiring Once both processes are up, hit the bin's healthz endpoint to confirm: ```sh curl http://127.0.0.1:24681/healthz # {"ok":true,"writeRoot":"/abs/path/to/src/styles","routing":"/abs/path/to/panel-routing.json","port":24681} ``` Then click **Apply** in the panel. A successful apply returns `{ "ok": true, "updated": [...] }`. See the [apply pipeline reference](/pj/zudo-design-token-panel/docs/reference/apply-pipeline) for the full response shape and every error code. **The bin is dev-only.** It binds to loopback by default and writes to your source tree. Never run it in production, never expose it on the public internet, and never widen `--allow-origin` to `*`. ## Related - [Apply pipeline reference](/pj/zudo-design-token-panel/docs/reference/apply-pipeline) — request / response envelopes, error codes, native-implementation guidance. - [`configurePanel` reference](/pj/zudo-design-token-panel/docs/reference/configure-panel) — `applyEndpoint` and `applyRouting` fields. - [Token manifest reference](/pj/zudo-design-token-panel/docs/reference/token-manifest) — which tokens get included in an Apply diff. --- # Apply pipeline > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/reference/apply-pipeline The **bin server** is the reference implementation for the apply contract. When a user clicks "Apply" in the panel UI, it POSTs a flat CSS-var diff to the host's endpoint, which routes the diff to the bin, which atomically rewrites source files. This page pins the Apply button's two `PanelConfig` fields ([`applyEndpoint`](#applyendpoint) and [`applyRouting`](#applyrouting)), the [request and response envelopes](#request--response-envelopes), the [atomicity contract](#atomic-write-contract), and the [validation rules](#validation-rules). ## `applyEndpoint` | Field | Type | Purpose | | --- | --- | --- | | `applyEndpoint` | `string` | URL the Apply button POSTs the flat cssVar diff to. The host's dev-API handler routes the diff to the bin. | When `undefined`, the Apply button stays disabled with a tooltip — hosts that ship export/import only can omit this field. ## `applyRouting` | Field | Type | Purpose | | --- | --- | --- | | `applyRouting` | `Record` | Map of CSS-var prefix family (without leading `--` and trailing `-`) → repo-relative source-file path. Passed to the bin via `--routing ` flag. | Example: ```ts applyRouting: { myapp: 'src/styles/tokens.css', 'myapp-extra': 'src/styles/extra-tokens.css', } ``` When both `applyEndpoint` and a non-empty `applyRouting` map are set, the Apply button is enabled. When either is missing, the modal still mounts so the user can preview the diff, but the action stays disabled with a tooltip. Both the panel UI (`PanelConfig.applyRouting`) and the bin (`--routing` flag) read the same JSON file. The map is keyed by the CSS-var prefix family without leading `--` and trailing `-`; the value is a repo-relative path to the source file the bin rewrites. Keeping a single JSON file as the source eliminates drift between UI and bin. ## Request & response envelopes The Apply button POSTs to `PanelConfig.applyEndpoint` with a flat JSON diff. ### Request ``` POST Content-Type: application/json { "tokens": { "--myapp-spacing-md": "2rem", "--myapp-extra-slider-length": "200px" } } ``` The `tokens` field is mandatory and must be a JSON object with string keys (CSS custom property names, prefixed with `--`) and string values (CSS strings, no validation at the panel level). ### Response 200 (success) ```json { "ok": true, "updated": [ { "file": "src/styles/tokens.css", "changed": ["--myapp-spacing-md"], "unchanged": ["--myapp-spacing-lg"], "unknown": [] } ], "unknownCssVars": [], "unchangedCssVars": ["--myapp-spacing-lg"] } ``` | Field | Meaning | | --- | --- | | `ok: true` | Marks success. | | `updated[]` | Per-file results. `file` is repo-relative; `changed[]` lists tokens that were rewritten; `unchanged[]` lists tokens found in the file but not in the diff; `unknown[]` lists tokens in the diff that don't exist in the file's `:root` block. | | `unknownCssVars` | Flattened across all files for UI feedback. | | `unchangedCssVars` | Flattened across all files for UI feedback. | ### Response 400 (bad request) ```json { "ok": false, "error": "", "rejected": ["--invalid-token"] } ``` (`rejected` is optional; included where it makes the diagnostic actionable.) Returned for: - Malformed JSON: `"Invalid JSON in request body"` - Body not an object: `"Request body must be a JSON object"` - Missing `tokens` field or not an object: `"tokens must be a JSON object"` - Empty tokens map: `"tokens must contain at least one entry"` - Invalid token names (no `--` prefix, spaces, slashes, etc.) — error message describes the rejection, with optional `rejected[]` array. - Unsupported CSS-var prefix (no route configured): `"Unsupported cssVar prefix"` with `rejected[]` listing the offending prefixes. - Path escape attempt (`../../etc/passwd`): `"Path not allowed: "`. ### Response 403 (Forbidden) ```json { "ok": false, "error": "Origin not allowed" } ``` No `Access-Control-Allow-Origin` header. The bin rejects cross-origin requests. ### Response 405 (Method not allowed) Empty body, `Allow: POST, OPTIONS` header. The endpoint accepts only POST and OPTIONS. ### Response 409 (Conflict) ```json { "ok": false, "error": "No top-level :root { ... } block in " } ``` The target CSS file has no `:root` block. The bin cannot apply token overrides without one. ### Response 500 (Internal server error) ```json { "ok": false, "error": "", "failedFile": "", "restoreFailures": ["", ""] } ``` (`failedFile` and `restoreFailures` are optional; included when the failure context produces them.) Returned for: - File read/parse failure: `"Failed to read or parse source file"` - Write failure with rollback: `"Failed to write file ; previously-written files were restored."` + `failedFile` - Rollback failure: `"Failed to write file ; rollback also failed for N file(s) — disk state is inconsistent. Inspect the listed files manually."` + `failedFile` + `restoreFailures[]` ## Atomic write contract The bin keeps the original file content in memory. It writes the updated content to a temp file, then atomically renames the temp to target. If any write fails, the bin restores every file written so far from the in-memory original. This guarantees one of three terminal states for any Apply request: 1. **Full success.** Every routed file is updated. Response 200. 2. **Clean rollback.** A write fails partway through; every previously-written file is restored. Response 500 with `failedFile` populated. 3. **Inconsistent disk state.** A write fails AND the rollback also fails for one or more files. Response 500 with `failedFile` AND `restoreFailures[]` populated. The user is told to inspect the listed files manually. A non-empty `restoreFailures[]` array in a 500 response means the on-disk state is inconsistent — at least one file was rewritten and could not be restored. Inspect the listed files manually before retrying. The bin does not attempt a second rollback. ## Validation rules The bin validates every request before touching disk. Failures surface as 400 responses with a descriptive message and (where useful) a `rejected[]` array. ### Token name rules - Must start with `--`. - No spaces, slashes, or special characters. - Must split into a recognised prefix family (the part between `--` and the next `-`) for routing. ### Routing rules - Each token's prefix family must appear as a key in `applyRouting`. - Tokens with prefixes not in the routing map are rejected with `"Unsupported cssVar prefix"`. ### 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 with `"Path not allowed: "`. ### Body shape - Body must be a JSON object. - `tokens` must be a JSON object with at least one entry. ## Reference implementation The bin server inside this package (the `design-token-panel-server` bin entry) is the reference implementation. It reads `--routing ` at startup and exposes a Fetch API handler. The handler validates every token name, routes by prefix, resolves absolute paths with sandbox checks, computes rewrites in memory, writes atomically, and responds with the exact shapes pinned above. Read the handler source as the spec. ## Implementing the contract natively (advanced) Hosts physically unable to spawn Node.js can implement the apply contract natively. The implementation must: 1. **Validate token names** — reject names without `--` prefix, with spaces, slashes, or special characters. 2. **Sanitize and route** — split each CSS-var prefix, look up the target file in the routing map, reject prefixes not in the map. 3. **Path safety** — resolve each target path to an absolute path, verify it sits within `writeRoot`, reject path-escape attempts. 4. **Read & parse** — load each CSS file, find the `:root { ... }` block (fail 409 if missing), parse the existing variable values. 5. **Compute rewrite** — for each token in the diff, decide which are already present (`unchanged`), which are new (`unknown`), which are being changed (`changed`). Build the updated `:root` block. 6. **Atomic write** — keep the original file content in memory. Write the updated content to a temp file. Atomically rename temp to target. If any write fails, restore every file written so far from the in-memory original. 7. **Respond** — return the exact JSON envelope shapes pinned above. The panel package's source code (the apply / server / path-safety modules under `src/apply/` and `src/server/`) documents the exact algorithm. Native implementations should mirror it. ## Cross-references - [`PanelConfig.applyEndpoint`](/pj/zudo-design-token-panel/docs/reference/configure-panel/#panelconfig-interface) — endpoint slot. - [`PanelConfig.applyRouting`](/pj/zudo-design-token-panel/docs/reference/configure-panel/#panelconfig-interface) — routing slot. - [Token manifest](/pj/zudo-design-token-panel/docs/reference/token-manifest/) — defines which `--cssVar` names appear in the diff. - [Color cluster](/pj/zudo-design-token-panel/docs/reference/color-cluster/) — defines the cluster-driven `--cssVar` names that can appear in the diff. --- # Lazy color presets > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/recipes/lazy-color-presets Hosts that ship a large preset library (Dracula, Solarized, Tokyo Night, Catppuccin, …) usually do **not** want every preset baked into the SSR config blob — that payload renders inline as `` on every page. `setPanelColorPresets(presets)` solves this: call it from a deferred dynamic import after `configurePanel`, and the bundler emits the preset map as a separate JS chunk. For the merge contract (cluster bundled schemes win on key collision, trailing call wins on conflict between calls), see the [Color cluster reference](/pj/zudo-design-token-panel/docs/reference/color-cluster) section on `colorPresets`. ## Why defer? The simple wiring is to set `colorPresets` directly on `PanelConfig`: ```ts // Always-loaded path — preset map ships in the SSR config blob. // ... colorPresets: { Dracula: dracula, Solarized: solarized, 'Tokyo Night': tokyoNight, }, }; ``` That works, but every page-load pays the JSON cost — even for users who never open the Color tab. The deferred pattern below moves the preset payload off the critical page-load path. ## The deferred pattern Three steps: split the preset map into its own module, import it lazily, call `setPanelColorPresets` with the result. ### Step 1 — own the preset module ```ts // src/lib/color-presets.ts const dracula: ColorScheme = { background: 0, foreground: 7, cursor: 7, selectionBg: 8, selectionFg: 0, palette: [ '#282a36', '#ff5555', '#50fa7b', '#f1fa8c', '#bd93f9', '#ff79c6', '#8be9fd', '#f8f8f2', '#44475a', '#ff5555', '#50fa7b', '#f1fa8c', '#bd93f9', '#ff79c6', '#8be9fd', '#bfbfbf', ], shikiTheme: 'dracula', }; const solarizedDark: ColorScheme = { background: 0, foreground: 7, cursor: 7, selectionBg: 8, selectionFg: 0, palette: [ '#002b36', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#eee8d5', '#073642', '#cb4b16', '#586e75', '#657b83', '#839496', '#6c71c4', '#93a1a1', '#fdf6e3', ], shikiTheme: 'solarized-dark', }; // ...add as many as you like... Dracula: dracula, 'Solarized Dark': solarizedDark, }; ``` ### Step 2 — leave `PanelConfig.colorPresets` empty Omit the field (or set it to `{}`). The SSR config blob now stays small — only the cluster's bundled schemes ride along on first paint. ```ts // src/lib/my-panel-config.ts storagePrefix: 'myapp-design-token-panel', consoleNamespace: 'myapp', modalClassPrefix: 'myapp-design-token-panel-modal', schemaId: 'myapp-design-tokens/v1', exportFilenameBase: 'myapp-design-tokens', tokens: { spacing: [], typography: [], size: [], color: [], }, colorCluster: myColorCluster, // colorPresets intentionally omitted — wired lazily below. }; ``` ### Step 3 — attach the presets after configure `setPanelColorPresets` is exported from the package root. Call it from a deferred dynamic import — the bundler will emit `color-presets.ts` as its own JS chunk, so it never blocks first paint. ```ts // src/main.ts (Vite host) configurePanel(myPanelConfig); // Defer the preset payload to its own chunk. void import('./lib/color-presets').then(({ colorPresets }) => { setPanelColorPresets(colorPresets); }); ``` For Astro hosts the same pattern lives in a hoisted `` block alongside the host-adapter import: ```astro void import('./lib/color-presets').then(({ colorPresets }) => { void import('@takazudo/zudo-design-token-panel').then((mod) => { mod.setPanelColorPresets(colorPresets); }); }); ``` **Order does not matter.** A host that calls `setPanelColorPresets` **before** `configurePanel` is serviced via a holding slot inside `config/panel-config.ts` — the preset map is buffered and merged when configure runs. The trailing call wins if you call `setPanelColorPresets` more than once (no throw, unlike a duplicate `configurePanel`). ## When NOT to defer - **Tiny preset list (≤ 3 entries).** The lazy chunk infrastructure is not worth a few KB. Just inline them on `PanelConfig.colorPresets`. - **Server-rendered preset gallery.** If you build a UI that lists presets at SSR time, deferring would mean the gallery renders empty on first paint and re-renders on hydrate. Inline the map instead. - **Cluster owner's defaults.** Schemes that should be the cluster's documented defaults (e.g. `"Default Light"` / `"Default Dark"`) belong on `colorCluster.colorSchemes`, not on `colorPresets` — they are part of the cluster identity, not optional add-ons. ## Related - [Color cluster reference](/pj/zudo-design-token-panel/docs/reference/color-cluster) — full cluster shape, `colorSchemes` registry, and the preset merge order. - [Custom color cluster](/pj/zudo-design-token-panel/docs/recipes/custom-color-cluster) — wiring the primary cluster (which carries the bundled schemes a preset map cannot override). - [`configurePanel` reference](/pj/zudo-design-token-panel/docs/reference/configure-panel) — `colorPresets` field, lazy attachment, and key-collision rules. --- # Panel CSS tokens > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/reference/panel-css-tokens The panel ships its own bundled CSS — no Tailwind dependency in the consumer. This page pins the panel-private `--tokentweak-*` namespace, the `var(--host, fallback)` indirection ladder that lets a host retheme without touching panel internals, and the bundled stylesheet's modal-selector contract. ## Panel-private namespace The bundled stylesheets declare every panel-chrome variable under a panel-private namespace, scoped to the panel shell + modal class prefix: ```css :where(.tokenpanel-shell, [data-design-token-panel-modal]) { --tokentweak-pad-md: …; --tokentweak-gap-sm: …; --tokentweak-text-body: …; --radius-tokentweak: …; /* …every panel-chrome value lives here */ } ``` ### Naming rules - `--tokentweak-*` is the only allowed prefix for panel-private vars. No consumer-namespaced identifiers may appear in the panel chrome. - The chrome stylesheet (`panel.css`) MUST read only `--tokentweak-*` — it MUST NOT read host vars like `--color-*` or `--font-mono` directly. - The token sheet (`panel-tokens.css`) is the single indirection point where host vars are consumed (see [the indirection ladder below](#host-css-var-indirection-ladder)). ### Files - `panel.css` — chrome layout / typography / controls. - `panel-tokens.css` — the `--tokentweak-*` declarations. Both ship from the package, combined into a single `dist/zudo-design-token-panel.css` by the Vite library build. Vite library mode strips the source `import './styles/panel.css'` from the emitted JS, so the consumer MUST import the combined stylesheet exactly once on their static module graph (typically next to where they mount ``): ```ts ``` The `./styles` sub-export (alias `./styles.css`) resolves to `dist/zudo-design-token-panel.css`. Skipping this import leaves the panel JS fully functional but every chrome rule missing — `.tokenpanel-shell` renders with the host page's transparent background and default font, so the panel appears invisible. The package MUST build and run without Tailwind in the consumer. The panel JSX uses hand-authored CSS classes backed by `--tokentweak-*` vars exclusively. ## Modal class prefix and data-attribute selector `PanelConfig.modalClassPrefix` controls the BEM root for every modal the panel owns (export, import, apply). The host picks any string and the panel emits classes like `${modalClassPrefix}__overlay`, `${modalClassPrefix}__panel`, `${modalClassPrefix}__header`, etc. **The bundled CSS keys on the data attribute, NOT on the class prefix.** Every modal `` element emits `data-design-token-panel-modal=""` (with `data-design-token-panel-modal-variant` set to `"apply"` / `"export"` / `"import"`). `panel.css` anchors all modal chrome rules on `[data-design-token-panel-modal]` and matches sub-elements via `[class*='__title']`-style attribute selectors. This means a host that customises `modalClassPrefix` still inherits the bundled chrome — selecting on the literal class prefix would leave any non-default host with unstyled modals. The class prefix remains useful as a higher-specificity hook for hosts that want to layer custom rules on top of the bundled chrome. ## Host CSS-var indirection ladder The panel-chrome color tokens are declared in `panel-tokens.css` as a `var(--host, fallback)` ladder so a host that does not define `--color-*` / `--font-mono` still gets a sane paint: ```css :where(.tokenpanel-shell, [data-design-token-panel-modal]) { --tokentweak-color-fg: var(--color-fg, oklch(87% 0.01 60)); --tokentweak-color-bg: var(--color-bg, oklch(18% 0.01 50)); --tokentweak-color-muted: var(--color-muted, oklch(70% 0.01 60)); --tokentweak-color-surface: var(--color-surface, oklch(22% 0.01 50)); --tokentweak-color-accent: var(--color-accent, oklch(65% 0.2 45)); --tokentweak-color-accent-hover: var(--color-accent-hover, oklch(55% 0.18 45)); --tokentweak-color-code-bg: var(--color-code-bg, oklch(17% 0.005 50)); --tokentweak-color-code-fg: var(--color-code-fg, oklch(87% 0.01 60)); --tokentweak-color-success: var(--color-success, oklch(65% 0.19 145)); --tokentweak-color-danger: var(--color-danger, oklch(60% 0.2 10)); --tokentweak-color-warning: var(--color-warning, oklch(75% 0.17 75)); --tokentweak-font-mono: var(--font-mono, Menlo, Monaco, Consolas, …); } ``` ### Public surface These are the panel-private variables a host MAY override on the same scope to retheme the panel chrome without touching their own `--color-*` theme: | Variable | Default fallback | Role | | --- | --- | --- | | `--tokentweak-color-fg` | neutral light grey | Foreground text. | | `--tokentweak-color-bg` | dark surface | Panel background. | | `--tokentweak-color-muted` | mid grey | Muted text and dividers. | | `--tokentweak-color-surface` | dark surface variant | Raised surfaces (cards, modals). | | `--tokentweak-color-accent` | warm accent | Primary actions and highlights. | | `--tokentweak-color-accent-hover` | darker accent | Hover state for accent surfaces. | | `--tokentweak-color-code-bg` | very dark surface | Inline / block code background. | | `--tokentweak-color-code-fg` | light text | Inline / block code foreground. | | `--tokentweak-color-success` | green | Success state colour. | | `--tokentweak-color-danger` | red | Danger / error state colour. | | `--tokentweak-color-warning` | amber | Warning state colour. | | `--tokentweak-font-mono` | system monospace stack | Monospace font for code / values. | ### Override layers A host can override at either level: - **At the `--color-*` level** — cascades into the panel via the fallback ladder. Useful when the host wants its own design system to drive the panel's chrome alongside its app surfaces. - **At the `--tokentweak-color-*` level** — panel-only override, bypasses the host theme. Useful when the host wants the panel to look distinct from app surfaces (e.g. a dark panel inside a light app). ### Fallback values The fallback values are picked to be a sensible neutral dark theme so the panel paints readably without any host theme declared. A host can therefore drop the panel into a brand-new project and see a usable panel before shipping a single `--color-*` token. ### Invariant — panel.css MUST NOT read host vars directly `panel.css` MUST NOT read `--color-*` or `--font-mono` directly. The only legal site for those reads is the indirection ladder in `panel-tokens.css`. The package's CI pins this with a grep check: ```bash grep -n 'var(--color-' src/styles/panel.css # → 0 grep -n 'var(--font-mono' src/styles/panel.css # → 0 ``` Reading host vars directly inside `panel.css` would leak the host's design-system identifiers into the panel chrome. Every chrome rule would silently break for hosts whose theme uses a different name. Routing every host read through the `panel-tokens.css` ladder gives one stable seam to evolve without touching the chrome rules. ## Host-adapter side-effect import (paired-unit obligation) Alongside the `./styles` import, the consumer MUST also own a side-effect import for the host-adapter, paired with ``. The component AND a sibling `` block loading `@takazudo/zudo-design-token-panel/astro/host-adapter` are a single unit — both lines are required, always together. Required wiring shape: ```astro void import('@takazudo/zudo-design-token-panel/astro/host-adapter'); ``` ### Why a dynamic `void import('...')`? Both forms work — the package's `package.json` lists `dist/astro/host-adapter.js` in `sideEffects` so Rollup preserves consumer-side imports of the host-adapter regardless of whether the result is used. The dynamic form is the recommended canonical wiring because it loads the host-adapter chunk off the critical page-load path (mirrors the existing color-presets lazy-loader pattern) and is robust to future packaging changes that could miss-configure `sideEffects`. ### Why not a single page-level static import? Browser caching makes the duplicated `import()` cheap (one network fetch per session), and the wrapper component is the single authoritative mount point so duplicating the import there is a non-issue. ### What happens if you skip it? Skipping this import leaves the JSON config payload from `` on the page with no JS to read it, so calling `window..showDesignPanel()` throws `ReferenceError`. Symptom in deployed builds: silent failure, no panel chrome ever paints. The `./astro/host-adapter` sub-export points at the built `dist/astro/host-adapter.js` file plus its `.d.ts` types. ## Consumer-controlled tokens The tokens the panel writes to (the `cssVar` field on each `TokenDef`, plus the cluster's `paletteCssVarTemplate`, base-role names, and semantic-CSS names) are entirely consumer-controlled. Hosts pick names like `--myapp-spacing-hgap-md`, `--myapp-p0`, `--myapp-semantic-bg` themselves; the panel just writes them through `setProperty` on `:root`. The package contract is therefore: - **Read:** the panel never reads consumer CSS variables (it carries its own defaults via `TokenDef.default`). - **Write:** the panel only writes the consumer-supplied `cssVar` strings, one per overridden token, plus the cluster's palette / base / semantic vars on apply. ## Cross-references - [`PanelConfig.modalClassPrefix`](/pj/zudo-design-token-panel/docs/reference/configure-panel/#panelconfig-interface) — BEM root for modal classes (the data attribute is what the bundled CSS keys on). - [Token manifest](/pj/zudo-design-token-panel/docs/reference/token-manifest/) — declares the `cssVar` names the panel writes to `:root`. - [Color cluster](/pj/zudo-design-token-panel/docs/reference/color-cluster/) — declares the palette / base-role / semantic CSS-var names the panel writes on apply. --- # design-token-panel-server CLI > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/reference/cli-design-token-panel-server ## Overview `design-token-panel-server` is the small Node `http` server that backs the in-browser **design token panel**. It wraps the framework-agnostic apply handler shipped from `zudo-design-token-panel/server` in a tiny CLI bin so you can run it next to a Vite / Astro / framework dev server during local development. Use it when you want the panel running in a browser tab to write changes back to real CSS files on disk — for example, while iterating on tokens in a design system, or wiring the panel into an existing frontend app. The bin exposes exactly three HTTP endpoints: - `POST /apply` — apply a token mutation to disk - `OPTIONS /apply` — CORS preflight for the above - `GET /healthz` — readiness probe It writes only inside the directory you pass via `--write-root`, and it only honors browser POSTs whose `Origin` header matches one of the values you pass via `--allow-origin`. Everything else is rejected. This bin is a **development-time** tool. It does not authenticate callers beyond the `--allow-origin` allowlist, has no rate limiting, and writes files synchronously. Do not expose it on a public network or use it in production. ## Synopsis ```text design-token-panel-server [options] ``` A minimum useful invocation always includes both required flags plus at least one allowed origin (otherwise no browser POST will succeed): ```bash design-token-panel-server \ --write-root tokens \ --routing ./my.routing.json \ --allow-origin http://localhost:5173 ``` Run with `--help` (or `-h`) at any time to print the same usage text the bin ships with: ```bash design-token-panel-server --help ``` ## Flags All flags are documented below with type, default, and an example. Required flags are called out explicitly. The bin will exit with status `1` and a short stderr message if a required flag is missing or a value fails to validate. ### Path and routing flags #### `--root ` - **Type:** path (string) - **Default:** `process.cwd()` (the directory you invoke the bin from) - **Required:** No Repo root used as the CWD reference for resolving `--write-root` and `--routing` when they are passed as relative paths. If you invoke the bin from somewhere other than your repository root, pass `--root` to anchor those resolutions explicitly. ```bash design-token-panel-server \ --root /Users/me/work/my-app \ --write-root tokens \ --routing config/panel.routing.json \ --allow-origin http://localhost:5173 ``` #### `--write-root ` - **Type:** path (string), absolute or relative to `--root` - **Default:** _(none — required)_ - **Required:** **Yes** The single directory the bin is allowed to write into. Any file the apply handler resolves to that escapes this directory is rejected before the write happens. Treat this as a sandbox for the bin's filesystem authority. `--write-root` is the **only** thing standing between an unexpected routing entry (or a malicious payload from an attacker who tricked the browser) and arbitrary files on your machine. Always point it at the narrowest directory that contains every file the panel is supposed to edit — typically a `tokens/` or `src/styles/tokens/` directory. Never point it at your repo root. ```bash # Sandbox writes into ./tokens (relative to --root) design-token-panel-server \ --write-root tokens \ --routing ./panel.routing.json \ --allow-origin http://localhost:5173 # Or pass an absolute path design-token-panel-server \ --write-root /Users/me/work/my-app/src/styles/tokens \ --routing /Users/me/work/my-app/panel.routing.json \ --allow-origin http://localhost:5173 ``` #### `--routing ` - **Type:** path (string), absolute or relative to `--root` - **Default:** _(none — required)_ - **Required:** **Yes** Path to the routing JSON file that maps token-prefix → repo-relative CSS file path. The file is loaded **once at startup** via `loadRoutingFromFile`; restart the bin to pick up changes. ```bash design-token-panel-server \ --write-root tokens \ --routing ./panel.routing.json \ --allow-origin http://localhost:5173 ``` For the schema of the routing JSON file and the request/response shape of `POST /apply`, see [Apply pipeline reference](/pj/zudo-design-token-panel/docs/reference/apply-pipeline). ### Network flags #### `--port ` - **Type:** integer in `0..65535` - **Default:** `24681` - **Required:** No TCP port to bind. Pass `0` to let the OS pick a free port — the bin will print the actual bound port on its startup log line, which is useful for integration tests and one-off scripted runs. ```bash # Bind a fixed port design-token-panel-server --port 34434 \ --write-root tokens --routing ./panel.routing.json \ --allow-origin http://localhost:5173 # Let the OS pick — read the bound port from the startup log design-token-panel-server --port 0 \ --write-root tokens --routing ./panel.routing.json \ --allow-origin http://localhost:5173 ``` If the requested port is already in use, the bin exits `1` with the message `port already in use`. #### `--host ` - **Type:** host/interface string - **Default:** `127.0.0.1` - **Required:** No Host/interface to bind. The default keeps the server on loopback so only processes on the same machine can reach it. Pass `0.0.0.0` to expose it on the LAN — for example, when testing the panel from a phone or a co-worker's machine on the same network. ```bash # LAN-accessible (only do this on a trusted network) design-token-panel-server --host 0.0.0.0 \ --write-root tokens --routing ./panel.routing.json \ --allow-origin http://192.168.1.20:5173 ``` Binding to `0.0.0.0` exposes the bin to anyone on your local network. Combine it with a tightly scoped `--allow-origin` list and **never** run the bin on `0.0.0.0` on an untrusted network (cafés, hotels, conferences). ### CORS and access flags #### `--allow-origin ` - **Type:** exact-match origin string (e.g. `http://localhost:5173`) - **Default:** _(none)_ - **Required:** Effectively yes for any browser use — see warning - **Repeatable:** Yes — pass it multiple times to allow multiple origins Origin allowed to call `POST /apply` and to receive a 204 from `OPTIONS /apply`. The match is **case-sensitive on the entire scheme + host + port string**. Wildcards and pattern matching are intentionally not supported; the operator declares each allowed origin verbatim. ```bash # Single origin (typical local dev) design-token-panel-server \ --write-root tokens --routing ./panel.routing.json \ --allow-origin http://localhost:5173 # Multiple origins (e.g. Vite + Storybook side by side) design-token-panel-server \ --write-root tokens --routing ./panel.routing.json \ --allow-origin http://localhost:5173 \ --allow-origin http://localhost:6006 ``` If you start the bin without any `--allow-origin`, the startup log prints a `WARNING: no --allow-origin set; browser POST /apply will be rejected.` banner and every subsequent browser POST will fail with `403 Origin not allowed`. The CLI does not require the flag at parse time — but no production-style use of the panel will work without it. ### Logging and help flags #### `--quiet` - **Type:** boolean flag - **Default:** `false` - **Required:** No Suppress per-request logs. The startup line and error logs still print; only the per-apply summary log line (`[design-token-panel] applied N tokens to M files ...`) and the startup `listening on ...` banner are silenced. ```bash design-token-panel-server --quiet \ --write-root tokens --routing ./panel.routing.json \ --allow-origin http://localhost:5173 ``` #### `--help` / `-h` - **Type:** boolean flag - **Default:** `false` - **Required:** No Print the built-in usage text to stdout and exit `0`. Short-circuits all validation, so `--help` works even with no other flags set. ```bash design-token-panel-server --help design-token-panel-server -h ``` ## Endpoint contract The bin serves exactly three routes. Any other path returns `404 { ok: false, error: "Not found" }`. Any other method on `/apply` returns `405 { ok: false, error: "Method not allowed" }` with an `Allow: POST, OPTIONS` header. ### `POST /apply` Apply a token mutation to disk via the wrapped apply handler. **Request:** - Header `Content-Type` must include `application/json` (case-insensitive). Otherwise the bin returns `415 { ok: false, error: "Content-Type must be application/json" }`. - Header `Origin` must exactly match one of the configured `--allow-origin` values. Otherwise the bin returns `403 { ok: false, error: "Origin not allowed" }`. - Body: JSON payload accepted by the apply handler. See [Apply pipeline reference](/pj/zudo-design-token-panel/docs/reference/apply-pipeline) for the full payload schema. **Response:** - On success: `200` with the JSON body produced by the apply handler. The body has the shape: ```json { "ok": true, "updated": [ { "file": "tokens/colors.css", "changed": ["--color-bg", "--color-fg"] }, { "file": "tokens/spacing.css", "changed": [] } ] } ``` - The four `Access-Control-Allow-*` headers (see preflight below) are also set on the success response, so a fetch from an allowed browser origin receives a fully CORS-compliant reply. - On error: the apply handler's status code (typically `4xx`) and JSON body pass through unchanged. When `--quiet` is not set, a successful apply also writes one summary line to the bin's stdout, e.g.: ```text [design-token-panel] applied 2 tokens to 2 files (changed: tokens/colors.css) ``` ### `OPTIONS /apply` (CORS preflight) Standard CORS preflight. The bin checks the request `Origin` header against the `--allow-origin` allowlist: - **Allowed origin:** `204 No Content` with the four CORS headers below. - **Not allowed (or no Origin header):** `403 { ok: false, error: "Origin not allowed" }`. The CORS headers returned on a successful preflight (and echoed on a successful POST) are: | Header | Value | | ------------------------------- | ----------------- | | `Access-Control-Allow-Origin` | _(echoed origin)_ | | `Access-Control-Allow-Methods` | `POST, OPTIONS` | | `Access-Control-Allow-Headers` | `content-type` | | `Access-Control-Max-Age` | `600` | `Access-Control-Allow-Origin` is the **exact** origin string the client sent, never `*`. Because the bin only echoes origins that already passed `isOriginAllowed`, the operator's `--allow-origin` list is the single source of truth for what gets reflected. ### `GET /healthz` Readiness probe. Always returns `200` with the runtime configuration: ```json { "ok": true, "writeRoot": "/Users/me/work/my-app/tokens", "routing": "/Users/me/work/my-app/panel.routing.json", "port": 24681 } ``` `writeRoot` and `routing` are the fully-resolved absolute paths, which is useful for confirming you started the bin against the right files. `port` is the **requested** port from `--port` — for the actual bound port when you used `--port 0`, read the startup log line instead. ## Examples ### Wiring the bin into a Vite dev script Run the bin alongside your Vite dev server using a process runner like `npm-run-all` or `concurrently`: ```json { "scripts": { "dev": "npm-run-all --parallel dev:vite dev:panel", "dev:vite": "vite", "dev:panel": "design-token-panel-server --write-root src/styles/tokens --routing ./panel.routing.json --allow-origin http://localhost:5173" } } ``` Vite serves on `http://localhost:5173` and the panel UI loaded by Vite posts to the bin on `http://127.0.0.1:24681`. The `--allow-origin` matches Vite's dev server origin so the browser POST is accepted. For a step-by-step walkthrough including the routing JSON file format and panel mount, see the [Apply pipeline setup recipe](/pj/zudo-design-token-panel/docs/recipes/apply-pipeline-setup). ### CORS configuration for a remote host When the panel is hosted somewhere other than `localhost` (for example, a preview deploy or another developer's machine on the LAN), bind the bin to a routable interface and allow that origin explicitly: ```bash design-token-panel-server \ --host 0.0.0.0 \ --port 24681 \ --write-root tokens \ --routing ./panel.routing.json \ --allow-origin https://preview.example.test \ --allow-origin http://192.168.1.20:5173 ``` Exposing the bin on `0.0.0.0` makes it reachable by every host on the network. Use this only on networks you control, and keep the `--allow-origin` list as small as possible. ### `--write-root` sandbox for safe iteration When experimenting with a routing file you don't fully trust yet, point `--write-root` at a throwaway directory so a misconfigured prefix can't overwrite real source files: ```bash mkdir -p /tmp/zdtp-sandbox/tokens design-token-panel-server \ --root /tmp/zdtp-sandbox \ --write-root tokens \ --routing /tmp/zdtp-sandbox/panel.routing.json \ --allow-origin http://localhost:5173 \ --quiet ``` Apply a few mutations from the panel, inspect the diff in `/tmp/zdtp-sandbox/tokens`, and only when the routing file matches your intent should you switch `--root` and `--write-root` over to the real project. ## See also - [Apply pipeline reference](/pj/zudo-design-token-panel/docs/reference/apply-pipeline) — the request/response schema and routing file format consumed by `POST /apply`. - [Apply pipeline setup recipe](/pj/zudo-design-token-panel/docs/recipes/apply-pipeline-setup) — end-to-end walkthrough of wiring the bin into an existing frontend app. --- # Recipes > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/recipes These are small, copy-pasteable recipes for common integration tasks against `@takazudo/zudo-design-token-panel` as currently exported. Each recipe stands on its own — start with whichever scenario matches what you are doing right now and follow the cross-links into the [reference pages](/pj/zudo-design-token-panel/docs/reference/configure-panel) when you need the full API surface. ## When to use which recipe - Wiring a fresh palette + semantic table for a new project → [Custom color cluster](/pj/zudo-design-token-panel/docs/recipes/custom-color-cluster). - Adding or removing token tabs, coining custom group ids, or controlling group order → [Custom token manifest](/pj/zudo-design-token-panel/docs/recipes/custom-token-manifest). - Decide whether to ship a secondary cluster, hide it, or hard opt-out → [Secondary cluster: configure or disable](/pj/zudo-design-token-panel/docs/recipes/secondary-cluster-or-disable). - Wire the apply-to-disk pipeline into a non-Astro dev script → [Apply pipeline setup](/pj/zudo-design-token-panel/docs/recipes/apply-pipeline-setup). - Defer a large preset library out of the SSR config blob → [Lazy color presets](/pj/zudo-design-token-panel/docs/recipes/lazy-color-presets). Every snippet on these pages compiles against the package's current public exports. If one ever drifts, file an issue — the recipes are the canonical "does this still work" smoke test for the contract. --- # Examples > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/getting-started/examples Reference integrations for `zudo-design-token-panel` across three popular stacks. Each example wires the panel against a host-supplied `TokenManifest` and `ColorClusterConfig`, plus the companion bin server for applying tweaks back to source. The example apps are being ported alongside the initial OSS release — the links below point at the `examples/` directory in the GitHub repo and may 404 until the example apps land. Track progress on [super-epic issue #2](https://github.com/Takazudo/zudo-design-token-panel/issues/2). ## Try it - [Astro example](https://github.com/Takazudo/zudo-design-token-panel/tree/main/examples/astro) — Drop the panel into an Astro site. Demonstrates host-config wiring with an Astro dev server and the bin apply pipeline. - [Vite + React example](https://github.com/Takazudo/zudo-design-token-panel/tree/main/examples/vite-react) — Use the panel from a Vite + React app. Shows how the Preact runtime coexists with React via the compat shim. - [Next.js example](https://github.com/Takazudo/zudo-design-token-panel/tree/main/examples/next) — Wire the panel into a Next.js app router project, including the bin server side-by-side with the Next dev server. --- # Changelog > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/changelog Notable changes to `zudo-design-token-panel`. Format loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The canonical file lives at the [repo root `CHANGELOG.md`](https://github.com/Takazudo/zudo-design-token-panel/blob/main/CHANGELOG.md); this section mirrors per-version entries for in-site browsing. ## Releases - [v0.1.0](./v0.1.0) — Initial OSS port (2026-04-27). --- # Claude > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/claude Claude Code configuration reference. ## Resources --- # zudo-doc-design-system > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/claude-skills/zudo-doc-design-system # zudo-doc CSS & Component Rules **IMPORTANT**: These rules are mandatory for all code changes in this project that touch CSS, Tailwind classes, color tokens, or component markup. Read the relevant section before making changes. ## How to Use Based on the topic, read the specific reference doc: | Topic | File | |-------|------| | Spacing, typography, layout tokens | `src/content/docs/reference/design-system.mdx` | | Component-first methodology | `src/content/docs/reference/component-first.mdx` | | Color tokens, palette, schemes | `src/content/docs/reference/color.mdx` | Read ONLY the file relevant to your task. Apply its rules strictly. ## Quick Rules (always apply) ### Component First (no custom CSS classes) - **NEVER** create CSS module files, custom class names, or separate stylesheets - **ALWAYS** use Tailwind utility classes directly in component markup - The component itself is the abstraction — `.card`, `.btn-primary` are forbidden - Use props for variants, not CSS modifiers ### Design Tokens (no arbitrary values) - **NEVER** use Tailwind default colors (`bg-gray-500`, `text-blue-600`) — they are reset to `initial` - **NEVER** use arbitrary values (`text-[0.875rem]`, `p-[1.2rem]`) when a token exists - **ALWAYS** use project tokens: `text-fg`, `bg-surface`, `border-muted`, `p-hsp-md`, `text-small` - Spacing: `hsp-*` (horizontal), `vsp-*` (vertical) — see design-system.mdx for full list - Typography: `text-caption`, `text-small`, `text-body`, `text-heading` etc. ### Color Tokens (three-tier system) - **Tier 1** (palette): `p0`–`p15` — raw colors, use only when no semantic token fits - **Tier 2** (semantic): `text-fg`, `bg-surface`, `border-muted`, `text-accent` — prefer these - **NEVER** use hardcoded hex values in components - Palette index convention (consistent across all themes): - p1=danger, p2=success, p3=warning, p4=info, p5=accent - p8=muted, p9=background, p10=surface, p11=text primary ### Search & highlight tokens (role-split) Highlight roles are deliberately split across dedicated semantic tokens — do **not** share one token across unrelated highlight UIs. - `matched-keyword-bg` / `matched-keyword-fg` — background and foreground of the search panel `` element. Driven by `--color-matched-keyword-bg` / `--color-matched-keyword-fg`; live-editable in the Design Token Panel. This is the single source of truth for "why is this color yellow in the search results" — the panel swatch matches the rendered highlight 1:1. - `warning` — drives admonitions (`:::warning`), find-in-page (`.find-match`, `.find-match-active`), and any UI that is semantically a warning. Do **not** reuse it for new UI-chrome highlights. **Rule**: when a new highlight role appears (new kind of mark, new pill, new callout), add a dedicated semantic token rather than bolting it onto `--color-warning` or another existing token. Each visible highlight color should map to exactly one panel swatch. ### hover:underline on link-like elements Any element that navigates (rendered as `` or behaves as a link) MUST have `hover:underline focus-visible:underline`. Keyboard users need the same affordance as mouse users — never add `hover:underline` without the `focus-visible:underline` pair. - **Links (do underline)**: doc content links, sidebar items, header main-nav, header overflow menu items, color-tweak panel unselected tabs, search result rows, footer links, doc history entries, breadcrumb trails, mobile TOC entries. - **Controls (do NOT underline)**: buttons, toggles, sidebar resizer, palette selectors, color swatches, close icons. These use border/bg hover instead. Precedents to copy the pattern from: `src/components/header.astro`, `src/components/site-tree-nav.tsx`, `src/components/footer.astro`. See also: `/css-wisdom` for light-mode / dark-mode contrast rules and the broader three-tier token strategy. ### Astro vs React - Default to **Astro components** (`.astro`) — zero JS, server-rendered - Use **React islands** (`client:load`) only when client-side interactivity is needed - Both follow the same utility-class approach --- # zudo-doc-translate > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/claude-skills/zudo-doc-translate # zudo-doc Translation Skill Translate documentation between English and Japanese following project-specific conventions. ## i18n Structure - English docs: `src/content/docs/` — routes at `/docs/...` - Japanese docs: `src/content/docs-ja/` — routes at `/ja/docs/...` - Directory structures must mirror each other exactly (same filenames, same folder hierarchy) - Locale settings: `locales` in `src/config/settings.ts` - Astro i18n config: `astro.config.ts` with `prefixDefaultLocale: false` (English has no prefix, Japanese uses `/ja/`) ## Translation Rules ### Keep in English (do NOT translate) - Component names: ``, ``, ``, ``, ``, ``, ``, `` - Code blocks — code is universal - File paths: `src/content/docs/...`, `.claude/skills/...`, etc. - CLI commands: `pnpm dev`, `pnpm build`, etc. - Technical terms that are standard in English (e.g., component, props, frontmatter, slug) - Frontmatter field keys (`title`, `description`, `sidebar_position`, `category`) ### Translate - Frontmatter field values (e.g., the `title` value, the `description` value) - The `title` prop of admonition components (e.g., ``) - Prose content, headings, list items, table cells (except as noted below) ### Table conventions - In tables with a "Required" column: use **"Yes"** / **"No"** directly, NOT "はい" / "いいえ" — Japanese conversational yes/no is unnatural in technical documentation ### Internal links - Adjust link paths when translating: - En→Ja: `/docs/getting-started` → `/ja/docs/getting-started` - Ja→En: `/ja/docs/getting-started` → `/docs/getting-started` ## File Naming - Japanese files use the **same filenames** as English (e.g., `writing-docs.mdx`) - Only the parent directory differs: `docs/` vs `docs-ja/` - Example: `src/content/docs/guides/writing-docs.mdx` → `src/content/docs-ja/guides/writing-docs.mdx` ## Workflow ### En→Ja Translation 1. Read the English source file from `src/content/docs/` 2. Check if the corresponding Japanese file already exists in `src/content/docs-ja/` - If it exists, read it first — use it as a base and update from the English source rather than overwriting from scratch - If it does not exist, create the file at the equivalent path in `src/content/docs-ja/` 3. Translate the content following the rules above 4. Verify internal links point to `/ja/docs/...` ### Ja→En Translation 1. Read the Japanese source file from `src/content/docs-ja/` 2. Check if the corresponding English file already exists in `src/content/docs/` - If it exists, read it first — use it as a base and update from the Japanese source rather than overwriting from scratch - If it does not exist, create the file at the equivalent path in `src/content/docs/` 3. Translate the content following the rules above 4. Verify internal links point to `/docs/...` (no `/ja/` prefix) ### Post-Translation Checks - Frontmatter keys are unchanged (only values translated) - All admonition component names remain in English - Code blocks are untouched - Internal links use the correct locale prefix - Directory structure mirrors the source language --- # zudo-doc-version-bump > Source: https://takazudomodular.com/pj/zudo-design-token-panel/docs/claude-skills/zudo-doc-version-bump # /zudo-doc-version-bump Bump the version, generate changelog doc pages, commit, tag, and create a GitHub release. ## Preconditions Before doing anything else, verify ALL of the following. If any check fails, stop and tell the user. 1. Current branch is `main` 2. Working tree is clean (`git status --porcelain` returns empty) 3. At least one `v*` tag exists (`git tag -l 'v*'`). If no tag exists, tell the user to create the initial tag first (e.g. `git tag v0.1.0 && git push --tags`). Find the latest version tag: ```bash git tag -l 'v*' --sort=-v:refname | head -1 ``` ## Analyze changes since last tag Run: ```bash git log ..HEAD --oneline ``` and ```bash git diff ..HEAD --stat ``` Categorize each commit by its conventional-commit prefix: - **Breaking Changes**: commits with an exclamation mark suffix (e.g. `feat!:`) or BREAKING CHANGE in body - **Features**: `feat:` prefix - **Bug Fixes**: `fix:` prefix - **Other Changes**: everything else (`docs:`, `chore:`, `refactor:`, `ci:`, `test:`, `style:`, `perf:`, etc.) ## Propose version bump Based on the changes: - If there are breaking changes → propose **major** bump - If there are features (no breaking) → propose **minor** bump - Otherwise → propose **patch** bump If the user passed an argument (`major`, `minor`, or `patch`), use that directly instead of proposing. Present the proposal to the user: ``` Proposed bump: {current} → {new} ({type}) Breaking Changes: - description (hash) Features: - description (hash) Bug Fixes: - description (hash) Other Changes: - description (hash) ``` Only show sections that have entries. **Wait for user confirmation before proceeding.** If this is a **major** version bump, ask the user whether they want to archive the current docs as a versioned snapshot (i.e. run with `--snapshot`). Explain that this copies the current docs to a versioned directory for the old version. ## Run version-bump.sh Run the existing version bump script to update package.json and create changelog entry files: ```bash ./scripts/version-bump.sh {NEW_VERSION} # Or with snapshot for major bumps: ./scripts/version-bump.sh {NEW_VERSION} --snapshot ``` This script: 1. Updates `version` in `package.json` 2. Creates `src/content/docs/changelog/{NEW_VERSION}.mdx` (EN) 3. Creates `src/content/docs-ja/changelog/{NEW_VERSION}.mdx` (JA) 4. With `--snapshot`: copies current docs to versioned directories and prints settings.ts entry to add ## Fill in changelog content After the script creates the template files, **replace the placeholder content** with the actual categorized changes from the commit analysis. ### English changelog (`src/content/docs/changelog/{NEW_VERSION}.mdx`) ```mdx --- title: {NEW_VERSION} description: Release notes for {NEW_VERSION}. sidebar_position: {value from script} --- Released: {YYYY-MM-DD} ### Breaking Changes - Description (commit-hash) ### Features - Description (commit-hash) ### Bug Fixes - Description (commit-hash) ### Other Changes - Description (commit-hash) ``` ### Japanese changelog (`src/content/docs-ja/changelog/{NEW_VERSION}.mdx`) ```mdx --- title: {NEW_VERSION} description: {NEW_VERSION}のリリースノート。 sidebar_position: {value from script} --- リリース日: {YYYY-MM-DD} ### 破壊的変更 - Description (commit-hash) ### 機能 - Description (commit-hash) ### バグ修正 - Description (commit-hash) ### その他の変更 - Description (commit-hash) ``` Rules: - Only include sections that have entries - Use today's date for the release date - Each entry should be the commit subject with the short hash in parentheses ## Build and test Run the full build and test suite to make sure everything is good: ```bash pnpm b4push ``` If anything fails, fix the issue and re-run. Do not proceed with committing until all checks pass. ## Commit changes Stage and commit **all** version bump changes — include any files modified by b4push formatting fixes: ```bash git add package.json src/content/docs/changelog/{NEW_VERSION}.mdx src/content/docs-ja/changelog/{NEW_VERSION}.mdx # Also stage any other modified files (e.g. formatting fixes from b4push) git diff --name-only | xargs git add git commit -m "chore: Bump version to v{NEW_VERSION}" ``` ## Push and wait for CI Push the commits first (without the tag) and wait for CI to pass: ```bash git push ``` Then check CI status. Use `gh run list --branch main --limit 1 --json status,conclusion,headSha` and verify the `headSha` matches the pushed commit. Poll every 30 seconds, with a **maximum of 10 minutes**. If CI is still running after 10 minutes, ask the user whether to keep waiting or proceed. If CI fails, investigate the failure with `gh run view --log-failed`, fix the issue, commit, and push again. **Do not tag or publish until CI is green.** ## Tag, push tag, and create GitHub release **Ask the user for confirmation before tagging.** ```bash git tag v{NEW_VERSION} git push --tags ``` After pushing the tag, create a GitHub release. Use `awk` to strip only the YAML frontmatter (first `---` to second `---`) from the changelog file: ```bash NOTES=$(awk 'BEGIN{f=0} /^---$/{f++; next} f>=2' src/content/docs/changelog/{NEW_VERSION}.mdx) gh release create v{NEW_VERSION} --title "v{NEW_VERSION}" --notes "$NOTES" ``` ## Publish to npm (if applicable) If the package is **not** marked as `"private": true` in `package.json`, tell the user to publish: ``` The package is ready for npm publishing. Run: pnpm publish (This requires browser-based 2FA and must be done manually.) ``` If the package is `"private": true`, skip this step and inform the user: ``` Package is marked as private — skipping npm publish. ``` ## Done Report the summary: - Version bumped: `{OLD_VERSION}` → `{NEW_VERSION}` - Changelog created (EN + JA) - Git tag: `v{NEW_VERSION}` - GitHub release: link to the release - npm publish status (published / skipped for private package)