@takazudo/zdtp 0.1.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +107 -0
- package/LICENSE +21 -0
- package/README.md +1044 -0
- package/dist/apply/apply-token-overrides.d.ts +81 -0
- package/dist/apply/apply-token-overrides.d.ts.map +1 -0
- package/dist/apply/build-apply-overrides.d.ts +71 -0
- package/dist/apply/build-apply-overrides.d.ts.map +1 -0
- package/dist/apply/route-tokens-to-files.d.ts +65 -0
- package/dist/apply/route-tokens-to-files.d.ts.map +1 -0
- package/dist/apply/tier-resolver.d.ts +75 -0
- package/dist/apply/tier-resolver.d.ts.map +1 -0
- package/dist/apply-modal.d.ts +49 -0
- package/dist/apply-modal.d.ts.map +1 -0
- package/dist/astro/DesignTokenPanelHost.astro +76 -0
- package/dist/astro/host-adapter.d.ts +47 -0
- package/dist/astro/host-adapter.d.ts.map +1 -0
- package/dist/astro/host-adapter.js +81 -0
- package/dist/astro/index.d.ts +30 -0
- package/dist/astro/index.d.ts.map +1 -0
- package/dist/astro/index.js +54 -0
- package/dist/bin/cors.d.ts +24 -0
- package/dist/bin/cors.d.ts.map +1 -0
- package/dist/bin/parse-args.d.ts +38 -0
- package/dist/bin/parse-args.d.ts.map +1 -0
- package/dist/bin/server.d.ts +23 -0
- package/dist/bin/server.d.ts.map +1 -0
- package/dist/bin/server.js +283 -0
- package/dist/color-schemes-CgzOBqGO.js +85 -0
- package/dist/components/color-picker/color-picker.d.ts +64 -0
- package/dist/components/color-picker/color-picker.d.ts.map +1 -0
- package/dist/components/color-picker/custom-slider.d.ts +25 -0
- package/dist/components/color-picker/custom-slider.d.ts.map +1 -0
- package/dist/components/color-picker/index.d.ts +3 -0
- package/dist/components/color-picker/index.d.ts.map +1 -0
- package/dist/config/cluster-config.d.ts +135 -0
- package/dist/config/cluster-config.d.ts.map +1 -0
- package/dist/config/color-scheme-utils.d.ts +105 -0
- package/dist/config/color-scheme-utils.d.ts.map +1 -0
- package/dist/config/color-schemes.d.ts +83 -0
- package/dist/config/color-schemes.d.ts.map +1 -0
- package/dist/config/panel-config.d.ts +258 -0
- package/dist/config/panel-config.d.ts.map +1 -0
- package/dist/controls/pill-slider-row.d.ts +37 -0
- package/dist/controls/pill-slider-row.d.ts.map +1 -0
- package/dist/controls/role-button.d.ts +18 -0
- package/dist/controls/role-button.d.ts.map +1 -0
- package/dist/controls/sanitize-css-value.d.ts +22 -0
- package/dist/controls/sanitize-css-value.d.ts.map +1 -0
- package/dist/controls/select-row.d.ts +26 -0
- package/dist/controls/select-row.d.ts.map +1 -0
- package/dist/controls/slider-row.d.ts +34 -0
- package/dist/controls/slider-row.d.ts.map +1 -0
- package/dist/controls/text-row.d.ts +28 -0
- package/dist/controls/text-row.d.ts.map +1 -0
- package/dist/controls/tier-ref-selector.d.ts +33 -0
- package/dist/controls/tier-ref-selector.d.ts.map +1 -0
- package/dist/controls/tooltip.d.ts +44 -0
- package/dist/controls/tooltip.d.ts.map +1 -0
- package/dist/export-modal.d.ts +33 -0
- package/dist/export-modal.d.ts.map +1 -0
- package/dist/highlight/find-elements.d.ts +113 -0
- package/dist/highlight/find-elements.d.ts.map +1 -0
- package/dist/highlight/highlight-orchestrator.d.ts +24 -0
- package/dist/highlight/highlight-orchestrator.d.ts.map +1 -0
- package/dist/highlight/highlight-overlay.d.ts +43 -0
- package/dist/highlight/highlight-overlay.d.ts.map +1 -0
- package/dist/highlight/highlight-settings-popover.d.ts +25 -0
- package/dist/highlight/highlight-settings-popover.d.ts.map +1 -0
- package/dist/highlight/highlight-state.d.ts +87 -0
- package/dist/highlight/highlight-state.d.ts.map +1 -0
- package/dist/highlight/highlight-toggle-button.d.ts +34 -0
- package/dist/highlight/highlight-toggle-button.d.ts.map +1 -0
- package/dist/import-modal.d.ts +33 -0
- package/dist/import-modal.d.ts.map +1 -0
- package/dist/index.d.ts +105 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5480 -0
- package/dist/load-routing-XyKgftpe.js +353 -0
- package/dist/manifest-DCReQE0k.js +24 -0
- package/dist/panel-config-DP2_P7dD.js +332 -0
- package/dist/panel.d.ts +3 -0
- package/dist/panel.d.ts.map +1 -0
- package/dist/route-tokens-to-files-1DvmWBdj.js +41 -0
- package/dist/server/create-apply-handler.d.ts +48 -0
- package/dist/server/create-apply-handler.d.ts.map +1 -0
- package/dist/server/index.d.ts +24 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +10 -0
- package/dist/server/load-routing.d.ts +39 -0
- package/dist/server/load-routing.d.ts.map +1 -0
- package/dist/server/path-safety.d.ts +36 -0
- package/dist/server/path-safety.d.ts.map +1 -0
- package/dist/server/serialize-write.d.ts +7 -0
- package/dist/server/serialize-write.d.ts.map +1 -0
- package/dist/state/persist.d.ts +39 -0
- package/dist/state/persist.d.ts.map +1 -0
- package/dist/state/tweak-state.d.ts +344 -0
- package/dist/state/tweak-state.d.ts.map +1 -0
- package/dist/tabs/_generic-item-editor.d.ts +23 -0
- package/dist/tabs/_generic-item-editor.d.ts.map +1 -0
- package/dist/tabs/color-tab.d.ts +55 -0
- package/dist/tabs/color-tab.d.ts.map +1 -0
- package/dist/tabs/font-tab.d.ts +22 -0
- package/dist/tabs/font-tab.d.ts.map +1 -0
- package/dist/tabs/generic-tab.d.ts +43 -0
- package/dist/tabs/generic-tab.d.ts.map +1 -0
- package/dist/tabs/size-tab.d.ts +20 -0
- package/dist/tabs/size-tab.d.ts.map +1 -0
- package/dist/tabs/spacing-tab.d.ts +23 -0
- package/dist/tabs/spacing-tab.d.ts.map +1 -0
- package/dist/testing.d.ts +81 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +23 -0
- package/dist/tokens/manifest.d.ts +148 -0
- package/dist/tokens/manifest.d.ts.map +1 -0
- package/dist/tokens/tier-model.d.ts +89 -0
- package/dist/tokens/tier-model.d.ts.map +1 -0
- package/dist/tweak-state-DSJa5csL.js +624 -0
- package/dist/utils/color-convert.d.ts +7 -0
- package/dist/utils/color-convert.d.ts.map +1 -0
- package/dist/utils/color-hsla.d.ts +9 -0
- package/dist/utils/color-hsla.d.ts.map +1 -0
- package/dist/utils/color-oklch.d.ts +51 -0
- package/dist/utils/color-oklch.d.ts.map +1 -0
- package/dist/utils/design-token-serde.d.ts +193 -0
- package/dist/utils/design-token-serde.d.ts.map +1 -0
- package/dist/utils/structural-equal.d.ts +22 -0
- package/dist/utils/structural-equal.d.ts.map +1 -0
- package/dist/zdtp.css +1 -0
- package/package.json +102 -0
package/README.md
ADDED
|
@@ -0,0 +1,1044 @@
|
|
|
1
|
+
# @takazudo/zdtp
|
|
2
|
+
|
|
3
|
+
A live-tweak design-token panel for Astro sites. Drop a single `<DesignTokenPanelHost>` component into your layout, hand it a `PanelConfig`, and your users get an in-page UI for adjusting CSS custom properties (spacing, typography, sizing, color palette + semantic roles). Changes apply to `:root` instantly, persist to `localStorage`, and survive view transitions and hard reloads.
|
|
4
|
+
|
|
5
|
+
The package is portable: every project-specific identifier is driven by the host's `PanelConfig`. Storage keys, console namespace, modal class prefix, schema id, and the entire tab configuration (tiers, items, color cluster extras) are all configured by the consumer. Every config field is JSON-serializable so the configuration crosses the Astro frontmatter → client island boundary without losing fidelity.
|
|
6
|
+
|
|
7
|
+
The panel uses an **abstract token tier model**: all token categories — spacing, typography, size, and color — are expressed as `TabConfig` / `TierConfig` / `TierItem` structures on `PanelConfig.tabs`. A "ref tier" mechanism lets semantic tokens reference base tokens, and the apply pipeline emits `var(--base-cssvar)` for ref-tier items. See `PORTABLE-CONTRACT.md` §3 for the full tier model spec.
|
|
8
|
+
|
|
9
|
+
The authoritative API spec is [`PORTABLE-CONTRACT.md`](./PORTABLE-CONTRACT.md). This README is the consumer-oriented translation. When the two disagree, the contract wins — please file an issue against this README.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 0. Architecture at a glance
|
|
14
|
+
|
|
15
|
+
The design-token panel is a browser-based UI that writes token overrides to `:root`, with an optional **apply pipeline** for persisting those overrides back to disk source files.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
┌─ Your dev server (Astro / Vite / any host) ──────┐
|
|
19
|
+
│ │
|
|
20
|
+
│ Panel UI (browser) ←─────────────────────────> │ Host adapter (side-effect import)
|
|
21
|
+
│ ↓ (user tweaks) │
|
|
22
|
+
│ POST /apply (JSON diff) │ Apply endpoint (routes tokens to files)
|
|
23
|
+
│ ↓ │
|
|
24
|
+
└──────────────────────────────────────────────────┘
|
|
25
|
+
│
|
|
26
|
+
│ (HTTP)
|
|
27
|
+
↓
|
|
28
|
+
┌─ design-token-panel bin server ───────────────┐
|
|
29
|
+
│ Receives POST /apply with token diff │
|
|
30
|
+
│ Validates tokens & paths │
|
|
31
|
+
│ Rewrites source CSS files atomically │
|
|
32
|
+
│ (respects --write-root sandbox) │
|
|
33
|
+
└──────────────────────────────────────────────┘
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The **host adapter** import (`@takazudo/zdtp/astro/host-adapter`) is a separate concern: it reads the inline config, installs `window.<namespace>.*`, and gates lazy-load of the panel module. The **apply pipeline** (bin server + endpoint) is optional — hosts that only want export/import omit `applyEndpoint` and `applyRouting` from `PanelConfig`.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 1. What it is
|
|
41
|
+
|
|
42
|
+
A Preact-rendered side panel that:
|
|
43
|
+
|
|
44
|
+
- Reads a host-supplied **tab configuration** (`PanelConfig.tabs`) — an array of `TabConfig` entries where each tab owns one or more `TierConfig` objects, each holding an array of `TierItem` entries. Sliders (`length` / `number`), selects, text inputs, color pickers, and pill toggles are all supported via a discriminated `TierValueKind`.
|
|
45
|
+
- Supports **abstract tier references**: a `TierConfig` can carry `referencesTier` to point at a base tier; the apply pipeline emits `var(--base-cssvar)` for each ref-tier item, encoding semantic → base token aliasing in the config data model.
|
|
46
|
+
- Renders a **color tab** (id `'color'`) from the same tab model — palette and semantic tokens are `TierItem` arrays, and the structural metadata (base roles, scheme registry, panel settings) lives in `TabConfig.colorExtras`.
|
|
47
|
+
- Writes every override to `document.documentElement.style.setProperty(...)` against the consumer-supplied CSS-var names — so your stylesheet can be plain CSS, CSS Modules, Tailwind, or anything else.
|
|
48
|
+
- Persists state to `localStorage` under a host-chosen prefix and re-applies overrides synchronously on next page load (no FOUT — this is a hard requirement of the contract).
|
|
49
|
+
- Exposes a small console API (`window.<namespace>.showDesignPanel()` etc.) so a developer can pop the panel without it being mounted on every page.
|
|
50
|
+
- Plugs into Astro's view-transition lifecycle (`astro:before-swap` / `astro:page-load`) so soft navigation does not double-mount the panel.
|
|
51
|
+
|
|
52
|
+
The package builds against Preact (declared as a `peerDependency`) and ships its own bundled CSS scoped under the `--tokentweak-*` namespace. **It does not require Tailwind** in the consumer; see §11.
|
|
53
|
+
|
|
54
|
+
> Visual: a screenshot or short capture would go here. Skipped in the v1 README — a placeholder is worse than nothing. See the external example repos linked in §15 for live demos.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 2. Install
|
|
59
|
+
|
|
60
|
+
Install from npm. Preact is a peer dependency — bring your own copy so the panel shares one runtime with any other Preact islands you mount.
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
pnpm add @takazudo/zdtp preact
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```jsonc
|
|
67
|
+
// consumer/package.json
|
|
68
|
+
{
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"@takazudo/zdtp": "^0.1.0",
|
|
71
|
+
},
|
|
72
|
+
"peerDependencies": {
|
|
73
|
+
"preact": "^10.29.1",
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Peer dependencies
|
|
79
|
+
|
|
80
|
+
| Peer | Range | Why |
|
|
81
|
+
| -------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
82
|
+
| `preact` | `^10.29.1` | The panel UI is rendered with Preact. The consumer must bring its own copy so the panel and any other Preact islands share one runtime. |
|
|
83
|
+
|
|
84
|
+
The package's CSS is self-contained — it ships its own bundled stylesheet under the panel-private `--tokentweak-*` namespace and does not depend on any host design-system package.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 3. Apply pipeline (the bin)
|
|
89
|
+
|
|
90
|
+
The **bin server** is the recommended — and only supported — way to apply panel tweaks back to disk. When a user clicks "Apply" in the panel UI, the token diff POSTs to a small loopback HTTP server that the bin runs alongside your dev server. The bin computes every file rewrite in memory first and only then commits to disk via atomic temp-file renames, so a failed apply never leaves a half-rewritten CSS file behind.
|
|
91
|
+
|
|
92
|
+
### 3.1 CLI usage
|
|
93
|
+
|
|
94
|
+
Install the package alongside your existing dev tooling:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
pnpm add -D @takazudo/zdtp
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The package ships an executable named `zdtp-server`. Print the help text from inside your consumer repo with either:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
npx zdtp-server --help
|
|
104
|
+
# or, with pnpm:
|
|
105
|
+
pnpm exec zdtp-server --help
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Flags:**
|
|
109
|
+
|
|
110
|
+
| Flag | Required | Purpose | Default |
|
|
111
|
+
|---|---|---|---|
|
|
112
|
+
| `--routing <path>` | yes | Path to the routing JSON file (cluster id → repo-relative CSS file path). Absolute, or relative to `--root`. See §3.2. | — |
|
|
113
|
+
| `--write-root <dir>` | no | Sandbox directory: the bin refuses to write outside this tree. Absolute, or relative to `--root`. | `--root` |
|
|
114
|
+
| `--root <dir>` | no | Repo-root reference used to resolve `--routing` and `--write-root`. | `process.cwd()` |
|
|
115
|
+
| `--port <number>` | no | TCP port to bind. `0` asks the OS for an ephemeral port (the bin logs the assigned port on startup). | `24681` |
|
|
116
|
+
| `--host <addr>` | no | Bind address. Use `0.0.0.0` to expose on the LAN (off by default). | `127.0.0.1` |
|
|
117
|
+
| `--allow-origin <origin>` | repeatable | Origin allowed to POST to `/apply` (scheme + host + port, no trailing slash). At least one is required for any browser to apply. | none (all origins denied) |
|
|
118
|
+
| `--quiet` | no | Suppress the startup banner and the per-apply summary log line. | off |
|
|
119
|
+
| `--help`, `-h` | no | Print usage and exit 0. | — |
|
|
120
|
+
|
|
121
|
+
Generic invocation, run from your consumer repo root:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
npx zdtp-server \
|
|
125
|
+
--routing ./panel-routing.json \
|
|
126
|
+
--write-root ./tokens \
|
|
127
|
+
--allow-origin http://localhost:5173
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The bin reads the routing JSON once at startup and does not hot-reload it. Restart the bin if you edit the routing file.
|
|
131
|
+
|
|
132
|
+
### 3.2 Routing configuration
|
|
133
|
+
|
|
134
|
+
The routing JSON is a top-level object mapping **cluster id** → **repo-relative CSS file path**. Each path receives the apply pipeline's serialised writes for that cluster. The cluster ids must match the cluster ids used in your `PanelConfig.applyRouting` (the panel UI loads from this same JSON file — see §5).
|
|
135
|
+
|
|
136
|
+
Generic example:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"main": "tokens/tokens.css",
|
|
141
|
+
"secondary": "tokens/secondary-tokens.css"
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Each path is resolved against `--root` (which defaults to `process.cwd()`) and must end up inside `--write-root` after resolution — see §3.3.
|
|
146
|
+
|
|
147
|
+
The host imports the same file as a static JSON module so the panel UI and the bin agree on the routing without two declarations to keep in sync:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import routing from './panel-routing.json' assert { type: 'json' };
|
|
151
|
+
import type { PanelConfig } from '@takazudo/zdtp';
|
|
152
|
+
|
|
153
|
+
export const panelConfig: PanelConfig = {
|
|
154
|
+
// ... other fields ...
|
|
155
|
+
applyRouting: routing,
|
|
156
|
+
applyEndpoint: 'http://127.0.0.1:24681/apply',
|
|
157
|
+
};
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`applyEndpoint` is the URL the **browser** POSTs to. By default the bin listens on `http://127.0.0.1:24681`, so the panel default of `http://127.0.0.1:24681/apply` "just works" when you keep the bin's defaults. Change `--port` or `--host` and you must update `applyEndpoint` to match.
|
|
161
|
+
|
|
162
|
+
For the full token-overrides payload schema and the `PanelConfig` shape, see §5 and §6.
|
|
163
|
+
|
|
164
|
+
### 3.3 Security model
|
|
165
|
+
|
|
166
|
+
The bin is **dev-only** and is built with three independent guards:
|
|
167
|
+
|
|
168
|
+
- **Loopback default.** Binds to `127.0.0.1` so only requests from the same machine are accepted. Override with `--host` if you actually want LAN access.
|
|
169
|
+
- **Write sandbox (`--write-root`).** Every routing entry is resolved against `--root` and verified to sit strictly inside `--write-root` before any file I/O happens. An entry whose resolved path escapes the sandbox — via `..` segments or an absolute path that points elsewhere — fails the apply with a 400 and a descriptive error message. `--write-root` defaults to `--root`, so routing entries are sandboxed to your repo root unless you narrow it further.
|
|
170
|
+
- **CORS allow-list.** By default, **all origins are denied**. To let a browser POST to `/apply` you must list its origin explicitly with `--allow-origin <url>` (repeatable). Without a matching `--allow-origin`, the OPTIONS preflight returns 403 and POST returns 403 — no `Access-Control-Allow-Origin` header is emitted. Origin matching is **verbatim** on the full scheme + host + port string: `http://localhost:5173` and `http://127.0.0.1:5173` are different origins.
|
|
171
|
+
|
|
172
|
+
**Atomic writes.** The bin serialises per-file writes through a small mutex and uses a write-temp-file-then-rename strategy, so a failure mid-write never leaves a half-rewritten CSS file on disk. If any file in a multi-file apply fails to write, every file that was already persisted is restored from the in-memory snapshot taken before the apply started.
|
|
173
|
+
|
|
174
|
+
### 3.4 Lifecycle & signal handling
|
|
175
|
+
|
|
176
|
+
While the bin is running it exposes a tiny HTTP surface:
|
|
177
|
+
|
|
178
|
+
- **`GET /healthz`** — returns `200 OK` with `{"ok":true,"writeRoot":"…","routing":"…","port":…}` once the listener is up. Useful for dev-server readiness checks.
|
|
179
|
+
- **`OPTIONS /apply`** — CORS preflight. Returns `204` with `Access-Control-Allow-{Origin,Methods,Headers,Max-Age}` headers when the request's `Origin` is on the allow-list, and `403` otherwise.
|
|
180
|
+
- **`POST /apply`** — applies a token-overrides payload via the apply pipeline. The body is `application/json` with a top-level `tokens` object whose keys are CSS-var names (e.g. `--brand-primary`) and whose values are CSS values. See §6 for the full token-manifest schema and §6.6 for apply-time behaviour. A non-JSON content type returns `415`; an unallowed origin returns `403`.
|
|
181
|
+
- **Anything else** — `404` for unknown paths and `405` for unsupported methods on `/apply`.
|
|
182
|
+
|
|
183
|
+
The bin is intended to run as a subprocess of your dev server (`concurrently`, `npm-run-all`, a custom Node wrapper, etc.) and exits cleanly under host control:
|
|
184
|
+
|
|
185
|
+
- **SIGINT / SIGTERM.** The HTTP server stops accepting new connections, in-flight requests are allowed to drain, then the process exits 0. A 5-second belt-and-suspenders timeout force-exits if `close` hangs on a lingering keep-alive socket.
|
|
186
|
+
- **EADDRINUSE.** If the requested `--port` is already bound, the bin writes a friendly `port <n> already in use` line to stderr and exits with code 1, so the host supervisor can decide whether to retry or escalate.
|
|
187
|
+
|
|
188
|
+
### 3.5 Running the bin from a non-Astro host (Vite, Next, Rollup, anything)
|
|
189
|
+
|
|
190
|
+
The bin is framework-agnostic. Any consumer that has a long-running dev server can launch the bin alongside it. Two common shapes:
|
|
191
|
+
|
|
192
|
+
**A) `concurrently` (or `npm-run-all`) in `package.json`:**
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"scripts": {
|
|
197
|
+
"dev": "concurrently --kill-others-on-fail --names dev,panel \"vite\" \"zdtp-server --routing panel-routing.json --write-root ./tokens --allow-origin http://localhost:5173\""
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`--kill-others-on-fail` (or `concurrently -k`) ensures SIGINT propagates from the host runner to the bin when you Ctrl-C the dev server.
|
|
203
|
+
|
|
204
|
+
**B) A small Node wrapper that spawns the bin as a child process and proxies `SIGINT`:**
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
// scripts/dev-with-panel.ts
|
|
208
|
+
import { spawn } from 'node:child_process';
|
|
209
|
+
|
|
210
|
+
const bin = spawn(
|
|
211
|
+
'zdtp-server',
|
|
212
|
+
[
|
|
213
|
+
'--routing', 'panel-routing.json',
|
|
214
|
+
'--write-root', './tokens',
|
|
215
|
+
'--allow-origin', 'http://localhost:5173',
|
|
216
|
+
],
|
|
217
|
+
{ stdio: 'inherit', shell: false },
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const forward = (signal: NodeJS.Signals): void => {
|
|
221
|
+
bin.kill(signal);
|
|
222
|
+
};
|
|
223
|
+
process.on('SIGINT', forward);
|
|
224
|
+
process.on('SIGTERM', forward);
|
|
225
|
+
|
|
226
|
+
bin.on('exit', (code) => process.exit(code ?? 0));
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Whichever shape you pick, the wiring is the same: the bin listens on `http://127.0.0.1:24681/apply` by default, and the panel runtime POSTs to `PanelConfig.applyEndpoint`. Keep the bin's defaults and the default endpoint matches; if you change `--port` or `--host`, update `applyEndpoint` accordingly.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 4. Consumer recipes
|
|
234
|
+
|
|
235
|
+
The panel package is portable — every config field is host-supplied. Below are worked integration paths for different contexts.
|
|
236
|
+
|
|
237
|
+
### 4.1 Consumer recipes — Astro
|
|
238
|
+
|
|
239
|
+
Minimal end-to-end wiring — five steps, drop-in for a new Astro project.
|
|
240
|
+
|
|
241
|
+
### 4.1.1 Define your panel config
|
|
242
|
+
|
|
243
|
+
`PanelConfig.tabs` is the required data field. Each `TabConfig` carries one or
|
|
244
|
+
more `TierConfig` objects, each holding an array of `TierItem` entries. The
|
|
245
|
+
color tab (id `'color'`) additionally requires a `colorExtras` field.
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
// src/lib/my-panel-config.ts
|
|
249
|
+
import type { PanelConfig, TabConfig, TierConfig, TierItem } from '@takazudo/zdtp/astro';
|
|
250
|
+
|
|
251
|
+
const spacingTier: TierConfig = {
|
|
252
|
+
id: 'base',
|
|
253
|
+
label: 'Base spacing',
|
|
254
|
+
items: [
|
|
255
|
+
{
|
|
256
|
+
id: 'spacing-md',
|
|
257
|
+
cssVar: '--myapp-spacing-md',
|
|
258
|
+
label: 'Spacing M',
|
|
259
|
+
group: 'hsp',
|
|
260
|
+
default: '1rem',
|
|
261
|
+
type: { kind: 'length', min: 0, max: 4, step: 0.0625, unit: 'rem' },
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const spacingTab: TabConfig = {
|
|
267
|
+
id: 'spacing',
|
|
268
|
+
label: 'Spacing',
|
|
269
|
+
tiers: [spacingTier],
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
export const myPanelConfig: PanelConfig = {
|
|
273
|
+
storagePrefix: 'myapp-design-token-panel',
|
|
274
|
+
consoleNamespace: 'myapp',
|
|
275
|
+
modalClassPrefix: 'myapp-design-token-panel-modal',
|
|
276
|
+
schemaId: 'myapp-design-tokens/v1',
|
|
277
|
+
exportFilenameBase: 'myapp-design-tokens',
|
|
278
|
+
tabs: [spacingTab /*, fontTab, sizeTab, colorTab, ... */],
|
|
279
|
+
};
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
For a full worked color tab example (palette tier + semantic tier +
|
|
283
|
+
`colorExtras`), see the external example repos linked in §15.
|
|
284
|
+
|
|
285
|
+
### 4.1.2 Drop the host into your layout
|
|
286
|
+
|
|
287
|
+
The `<DesignTokenPanelHost>` component AND a paired `<script>` block that loads the host adapter are a **single unit** — both lines are required, always together. Do not omit the script tag.
|
|
288
|
+
|
|
289
|
+
```astro
|
|
290
|
+
---
|
|
291
|
+
// src/layouts/Layout.astro
|
|
292
|
+
import { ClientRouter } from 'astro:transitions';
|
|
293
|
+
import { DesignTokenPanelHost } from '@takazudo/zdtp/astro';
|
|
294
|
+
import { myPanelConfig } from '../lib/my-panel-config';
|
|
295
|
+
import '@takazudo/zdtp/styles';
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
<!doctype html>
|
|
299
|
+
<html lang="en">
|
|
300
|
+
<head>
|
|
301
|
+
<meta charset="utf-8" />
|
|
302
|
+
<ClientRouter />
|
|
303
|
+
</head>
|
|
304
|
+
<body>
|
|
305
|
+
<slot />
|
|
306
|
+
<DesignTokenPanelHost config={myPanelConfig} />
|
|
307
|
+
</body>
|
|
308
|
+
</html>
|
|
309
|
+
|
|
310
|
+
<script>
|
|
311
|
+
// Required side-effect load — see §3.5 for the rationale. Use a dynamic
|
|
312
|
+
// `void import(...)` here, NOT a top-level `import '...';` statement.
|
|
313
|
+
void import('@takazudo/zdtp/astro/host-adapter');
|
|
314
|
+
</script>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
That is the entire integration. `<DesignTokenPanelHost>` emits a JSON `<script>` with the serialized config; the paired `<script>` block above loads the host adapter — which reads that JSON, calls `configurePanel(...)` synchronously, installs `window.myapp.{showDesignPanel, hideDesignPanel, toggleDesignPanel}`, and lazy-loads the panel module only when the user has saved overrides or opens it via the console API.
|
|
318
|
+
|
|
319
|
+
### 4.1.3 Open the panel
|
|
320
|
+
|
|
321
|
+
```js
|
|
322
|
+
// In the browser devtools console
|
|
323
|
+
window.myapp.toggleDesignPanel();
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Or wire a hidden keyboard shortcut / dev-only button to call the same helper.
|
|
327
|
+
|
|
328
|
+
### 4.1.4 Bundled CSS
|
|
329
|
+
|
|
330
|
+
The package builds in Vite library mode, which extracts every CSS side-effect import from the source into a single emitted stylesheet at `dist/design-token-panel.css` and **strips the `import './styles/panel.css'` line from the emitted JS**. That means the consumer's bundler has no static reference to follow and the CSS will not arrive on its own — you MUST import the bundled stylesheet exactly once from somewhere on the consumer's static module graph (typically next to where you mount `<DesignTokenPanelHost>`):
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
// Astro frontmatter, Vite entry, anywhere on the static import chain
|
|
334
|
+
import '@takazudo/zdtp/styles';
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
The `./styles` (alias `./styles.css`) sub-export resolves to `./dist/design-token-panel.css` — the single combined chrome + tokens file Vite emits at build time. `package.json` still declares `sideEffects: ["**/*.css"]` so production bundlers don't tree-shake the import away.
|
|
338
|
+
|
|
339
|
+
If you skip this line, the panel's JS will still run, `window.<ns>.showDesignPanel()` will mount `#…-design-token-panel-root`, and the shell DOM will render — but with no chrome rules applied (transparent background, default page font), so it appears invisible. See §13 for further notes on bundler behaviour.
|
|
340
|
+
|
|
341
|
+
### 4.1.5 Why the host-adapter import lives in your wrapper
|
|
342
|
+
|
|
343
|
+
The host-adapter `<script>` block in §4.1.2 is the second half of the paired-unit contract. It must live in YOUR layout, not inside the package's `<DesignTokenPanelHost>` component.
|
|
344
|
+
|
|
345
|
+
The package's distributed Astro surface ships built `dist/astro/*` files, and the package-side hoisted `<script>` from those built files does not reliably reach production page bundles — Vite/Rollup processes the import, recognizes it as resolving to a file outside the consumer's source tree, emits an empty chunk, and never links it from any page entry. Owning the host-adapter import in the consumer wrapper sidesteps that pipeline issue.
|
|
346
|
+
|
|
347
|
+
The recommended form is a dynamic `void import('...')` — it loads the host-adapter chunk off the critical path (mirroring the existing color-presets lazy-loader pattern) and is robust to future packaging changes. A top-level `import '...';` also works because the package's `sideEffects` list explicitly includes `dist/astro/host-adapter.js` so Rollup preserves the import.
|
|
348
|
+
|
|
349
|
+
For the regression-guard tests that pin this contract, see `package-exports.test.ts` under the package's test suite.
|
|
350
|
+
|
|
351
|
+
### 4.2 Consumer recipes — any framework / Rust SSG
|
|
352
|
+
|
|
353
|
+
The Astro recipe above shows the case where a host owns the config import and the host-adapter side-effect import. For non-Astro hosts (Vite SPA, Rust SSG, custom framework) the pattern is the same, just without Astro-specific syntax.
|
|
354
|
+
|
|
355
|
+
**Worked example:** the [Astro](https://github.com/Takazudo/zudo-design-token-panel-example-astro), [Vite + React](https://github.com/Takazudo/zudo-design-token-panel-example-vite-react), and [Next.js](https://github.com/Takazudo/zudo-design-token-panel-example-nextjs) example repos prove the contract end-to-end. Each ships:
|
|
356
|
+
|
|
357
|
+
- A host-side config file with deliberately different names (e.g. `--astro-palette-{n}`, `astro` namespace).
|
|
358
|
+
- A routing JSON file at the example's root.
|
|
359
|
+
- A bin invocation via `concurrently` in the dev script, pointing at that routing file.
|
|
360
|
+
|
|
361
|
+
Copy an example's structure when porting the panel into a new host.
|
|
362
|
+
|
|
363
|
+
If you are building a **Rust SSG** or other non-Node host, the bin still runs as a sidecar Node.js subprocess (started by your host's build orchestration). The same routing JSON and host-adapter setup applies — the only difference is your host ships its own config format (not TypeScript) and you invoke the bin via your build system's subprocess spawner rather than npm scripts.
|
|
364
|
+
|
|
365
|
+
### 4.3 Recipe — Rust SSG (zfb)
|
|
366
|
+
|
|
367
|
+
Worked example for the case where the host is a Rust dev server (e.g. [zfb / zudo-front-builder](https://github.com/Takazudo/zudo-front-builder)) rather than a Node-based runner. The bin itself is unchanged — it remains a Node.js subprocess invoked as `node path/to/dist/bin/server.js ...`. The Rust host's only job is to spawn that Node process, forward shutdown signals to it, and configure `--allow-origin` so the browser POST from the panel UI is accepted.
|
|
368
|
+
|
|
369
|
+
The actual flag surface is `--routing`, `--write-root`, and `--allow-origin` (required), plus optional `--root`, `--host`, `--port`, `--quiet`. See `src/bin/parse-args.ts` for the authoritative list. **`--allow-origin` is repeatable and is required for any browser to issue the apply POST**, so pass your dev server's exact origin (scheme + host + port, no trailing slash).
|
|
370
|
+
|
|
371
|
+
#### Async (preferred): `tokio::process::Command`
|
|
372
|
+
|
|
373
|
+
```rust
|
|
374
|
+
use std::process::Stdio;
|
|
375
|
+
use tokio::process::{Child, Command};
|
|
376
|
+
use tokio::signal::unix::{signal, SignalKind};
|
|
377
|
+
|
|
378
|
+
async fn spawn_design_token_panel_bin() -> std::io::Result<Child> {
|
|
379
|
+
// Resolve the bin path however your host prefers — e.g. `node_modules/.bin`
|
|
380
|
+
// discovery, a config-supplied absolute path, or a fixed workspace layout.
|
|
381
|
+
let bin = "node_modules/@takazudo/zdtp/dist/bin/server.js";
|
|
382
|
+
|
|
383
|
+
// (Path is shown explicitly; in practice your host's npm script runner
|
|
384
|
+
// resolves the bin via the package's `bin` field and `node_modules/.bin`.)
|
|
385
|
+
|
|
386
|
+
let mut child = Command::new("node")
|
|
387
|
+
.arg(bin)
|
|
388
|
+
.arg("--routing").arg("./design-tokens.routing.json")
|
|
389
|
+
.arg("--write-root").arg("./src/styles")
|
|
390
|
+
// Repeat --allow-origin for each origin your dev UI runs on.
|
|
391
|
+
.arg("--allow-origin").arg("http://localhost:8080")
|
|
392
|
+
.stdout(Stdio::inherit())
|
|
393
|
+
.stderr(Stdio::inherit())
|
|
394
|
+
// Important: do NOT set `kill_on_drop(true)` here — we forward signals
|
|
395
|
+
// explicitly below so the bin can finish in-flight writes cleanly.
|
|
396
|
+
.spawn()?;
|
|
397
|
+
|
|
398
|
+
// Forward Ctrl-C / SIGTERM so the bin shuts down gracefully when the
|
|
399
|
+
// Rust dev server exits.
|
|
400
|
+
let pid = child.id().map(|p| p as i32);
|
|
401
|
+
tokio::spawn(async move {
|
|
402
|
+
let mut sigint = signal(SignalKind::interrupt()).expect("sigint handler");
|
|
403
|
+
let mut sigterm = signal(SignalKind::terminate()).expect("sigterm handler");
|
|
404
|
+
tokio::select! {
|
|
405
|
+
_ = sigint.recv() => {}
|
|
406
|
+
_ = sigterm.recv() => {}
|
|
407
|
+
}
|
|
408
|
+
if let Some(pid) = pid {
|
|
409
|
+
// SAFETY: we only signal a child we just spawned, and `kill(2)` with
|
|
410
|
+
// SIGTERM is the documented graceful-shutdown path for the bin.
|
|
411
|
+
unsafe { libc::kill(pid, libc::SIGTERM); }
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
Ok(child)
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
#### Sync fallback: `std::process::Command`
|
|
420
|
+
|
|
421
|
+
If your host is sync (no Tokio runtime), `std::process::Command` works the same way — spawn the Node process with identical args, then handle SIGINT / SIGTERM on whatever signal-handling primitive your host already uses (e.g. `ctrlc::set_handler` or a hand-rolled `signal_hook` listener), and call `libc::kill(child.id() as i32, libc::SIGTERM)` from the handler. The flag surface and lifecycle contract are identical to the async case.
|
|
422
|
+
|
|
423
|
+
#### Note on origin matching
|
|
424
|
+
|
|
425
|
+
`--allow-origin` is matched verbatim against the `Origin` request header — `http://localhost:8080` and `http://127.0.0.1:8080` are different origins. Pass each dev origin you actually serve from. Cross-origin POSTs without a matching `--allow-origin` value receive a 403 with no `Access-Control-Allow-Origin` header.
|
|
426
|
+
|
|
427
|
+
#### Upstream tracking
|
|
428
|
+
|
|
429
|
+
The zfb (zudo-front-builder) integration is documented in that project's own repository; this README is docs-only and does not require any zfb repo changes.
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## 5. `configurePanel()` and the `PanelConfig` shape
|
|
434
|
+
|
|
435
|
+
`configurePanel(config)` is the configure-once init. The Astro host adapter calls it for you (it reads the inline JSON config emitted by `<DesignTokenPanelHost>` and forwards it). For a non-Astro host, you would call `configurePanel(myPanelConfig)` yourself before the panel adapter is dynamically imported.
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
import { configurePanel, type PanelConfig } from '@takazudo/zdtp';
|
|
439
|
+
|
|
440
|
+
configurePanel(myPanelConfig);
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### 5.1 Behaviour
|
|
444
|
+
|
|
445
|
+
- **One-shot per page lifecycle.** Calling `configurePanel` twice with identical values is a no-op. Calling it twice with different values throws — silently overwriting a previously-configured cluster mid-session is the failure mode the contract explicitly rules out.
|
|
446
|
+
- **Synchronous, no I/O.** The call must be cheap enough to run inline at module init.
|
|
447
|
+
- **JSON-serializable input.** Every nested field MUST round-trip through `JSON.stringify` / `JSON.parse` without loss. No function fields, no class instances, no `Symbol` keys, no `undefined`-where-`null`-is-meant. This is the hard precondition for the Astro frontmatter → client island handoff (§8).
|
|
448
|
+
|
|
449
|
+
### 5.2 Field summary
|
|
450
|
+
|
|
451
|
+
| Field | Type | Purpose |
|
|
452
|
+
| -------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
453
|
+
| `storagePrefix` | `string` | Base for every derived `localStorage` key. See §9. |
|
|
454
|
+
| `consoleNamespace` | `string` | Global object the package installs `showDesignPanel` / `hideDesignPanel` / `toggleDesignPanel` on (e.g. `consoleNamespace: 'myapp'` → `window.myapp.showDesignPanel`). |
|
|
455
|
+
| `modalClassPrefix` | `string` | BEM root class for every modal the panel owns (export, import, apply). Emits `${prefix}__overlay`, `${prefix}__panel`, etc. |
|
|
456
|
+
| `schemaId` | `string` | `$schema` value emitted into export JSON and required on import. |
|
|
457
|
+
| `exportFilenameBase` | `string` | Default download filename base — exports save as `${exportFilenameBase}.json`. |
|
|
458
|
+
| `tabs` | `readonly TabConfig[]` | **Required.** Tab strip data — each entry is a tab with one or more `TierConfig` objects. The color tab (id `'color'`) additionally requires `colorExtras`. See §6. |
|
|
459
|
+
| `colorPresets` | `Record<string, ColorScheme>` (optional) | Optional named scheme presets surfaced in the Color tab "Scheme..." dropdown. Defaults to `{}`. See §7.5. |
|
|
460
|
+
|
|
461
|
+
### 5.3 Mount strategy & auto-mount
|
|
462
|
+
|
|
463
|
+
The Astro entry point (`<DesignTokenPanelHost>`) handles mounting for you. Internally:
|
|
464
|
+
|
|
465
|
+
- The console API (`showDesignPanel` etc.) is **always installed eagerly**, even when the panel module has not loaded — calling them is what triggers the lazy import for cold-start users.
|
|
466
|
+
- The panel module is **dynamically imported on first need**: either when the user calls a console helper, OR when first-paint detects either a `${storagePrefix}:visible` flag set to `1` or any `${storagePrefix}-state-v2` payload in `localStorage`.
|
|
467
|
+
- This gating keeps the panel out of the initial JS bundle for first-time visitors while still re-applying overrides on hard reload for users who have tweaked things.
|
|
468
|
+
|
|
469
|
+
For a Vite-only / non-Astro host, mount it yourself by importing the adapter module after `configurePanel(...)`. See §11.5.
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
## 6. Tab / tier model schema
|
|
474
|
+
|
|
475
|
+
All token categories are expressed through the tab/tier model on
|
|
476
|
+
`PanelConfig.tabs`. See `PORTABLE-CONTRACT.md` §3 for the authoritative
|
|
477
|
+
spec; this section is the consumer-oriented summary.
|
|
478
|
+
|
|
479
|
+
### 6.1 `TierItem`
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
export type TierValueKind =
|
|
483
|
+
| { kind: 'length'; min: number; max: number; step: number; unit: string }
|
|
484
|
+
| { kind: 'number'; min: number; max: number; step: number }
|
|
485
|
+
| { kind: 'select'; options: readonly string[] }
|
|
486
|
+
| { kind: 'text' }
|
|
487
|
+
| { kind: 'cursor' }
|
|
488
|
+
| { kind: 'content' }
|
|
489
|
+
| { kind: 'mask-image' }
|
|
490
|
+
| { kind: 'color' };
|
|
491
|
+
|
|
492
|
+
export interface TierItem {
|
|
493
|
+
/** Stable id used as the key in persisted state (e.g. `hsp-2xs`). */
|
|
494
|
+
id: string;
|
|
495
|
+
/** CSS custom property written to `:root` (e.g. `--myapp-spacing-md`). */
|
|
496
|
+
cssVar: string;
|
|
497
|
+
/** Display label shown in the panel row. */
|
|
498
|
+
label: string;
|
|
499
|
+
/** Optional manifest group — tab components use this for section headers. */
|
|
500
|
+
group?: string;
|
|
501
|
+
/** Default value as a CSS string (`0.125rem`, `12px`, etc.). */
|
|
502
|
+
default: string;
|
|
503
|
+
/** Discriminated union describing the control kind and its metadata. */
|
|
504
|
+
type: TierValueKind;
|
|
505
|
+
/** Opt-in pill toggle. */
|
|
506
|
+
pill?: { value: string; customDefault: string };
|
|
507
|
+
/** Read-only items are displayed but not editable. */
|
|
508
|
+
readonly?: true;
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### 6.2 `TierConfig` and `TabConfig`
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
export interface TierConfig {
|
|
516
|
+
id: string;
|
|
517
|
+
label: string;
|
|
518
|
+
items: readonly TierItem[];
|
|
519
|
+
/**
|
|
520
|
+
* When set, this tier's items hold references — each item's `default` is
|
|
521
|
+
* the id of an item in the tier whose id matches `referencesTier`. The
|
|
522
|
+
* apply pipeline emits `var(--target-cssvar)` for ref-tier items.
|
|
523
|
+
*/
|
|
524
|
+
referencesTier?: string;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export interface TabConfig {
|
|
528
|
+
id: string;
|
|
529
|
+
label: string;
|
|
530
|
+
tiers: readonly TierConfig[];
|
|
531
|
+
/** Tier ids hidden behind an Advanced disclosure. */
|
|
532
|
+
advancedTiers?: readonly string[];
|
|
533
|
+
/** Required on color tabs (id 'color' / 'color-secondary'). */
|
|
534
|
+
colorExtras?: ColorClusterExtras;
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### 6.3 Worked example — spacing tab
|
|
539
|
+
|
|
540
|
+
```ts
|
|
541
|
+
// src/lib/my-tabs/spacing-tab.ts
|
|
542
|
+
import type { TabConfig } from '@takazudo/zdtp';
|
|
543
|
+
|
|
544
|
+
export const spacingTab: TabConfig = {
|
|
545
|
+
id: 'spacing',
|
|
546
|
+
label: 'Spacing',
|
|
547
|
+
tiers: [
|
|
548
|
+
{
|
|
549
|
+
id: 'base',
|
|
550
|
+
label: 'Base spacing',
|
|
551
|
+
items: [
|
|
552
|
+
{
|
|
553
|
+
id: 'spacing-md',
|
|
554
|
+
cssVar: '--myapp-spacing-md',
|
|
555
|
+
label: 'Spacing M',
|
|
556
|
+
group: 'hsp',
|
|
557
|
+
default: '1rem',
|
|
558
|
+
type: { kind: 'length', min: 0, max: 4, step: 0.0625, unit: 'rem' },
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
},
|
|
562
|
+
],
|
|
563
|
+
};
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### 6.4 Helpers (re-exported)
|
|
567
|
+
|
|
568
|
+
| Helper | Signature | Purpose |
|
|
569
|
+
| ------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
570
|
+
| `parseNumericValue` | `(value: string) => number \| null` | Strip the numeric portion from a CSS length string (`'1.5rem'` → `1.5`). Returns `null` for unparseable input (e.g. `clamp(...)`). |
|
|
571
|
+
| `formatValue` | `(n: number, unit: string) => string` | Re-format a numeric slider value back into the stored string form (`(1.5, 'rem')` → `'1.5rem'`). |
|
|
572
|
+
| `isLengthKind` / `isNumberKind` / `isSelectKind` / `isTextKind` / `isColorKind` / `isCursorKind` / `isContentKind` / `isMaskImageKind` | `(v: TierValueKind) => boolean` | Kind narrowing helpers for `TierValueKind`. |
|
|
573
|
+
|
|
574
|
+
### 6.5 Apply behaviour
|
|
575
|
+
|
|
576
|
+
The panel walks each `TierItem` on apply:
|
|
577
|
+
|
|
578
|
+
- If `readonly`, the row is display-only — no writes.
|
|
579
|
+
- For a **base tier item**: if the override map has a non-empty string for
|
|
580
|
+
`id`, the panel calls `document.documentElement.style.setProperty(item.cssVar, value)`.
|
|
581
|
+
Otherwise it removes the inline property so the consumer's stylesheet default wins.
|
|
582
|
+
- For a **ref-tier item** (`referencesTier` set): the persisted value is the id
|
|
583
|
+
of an item in the referenced base tier. The apply pipeline emits
|
|
584
|
+
`var(--base-tier-cssvar)` as the written value.
|
|
585
|
+
|
|
586
|
+
The write target is always `:root`. No shadow DOM, no scoped overrides — the panel ships a global tweak intentionally.
|
|
587
|
+
|
|
588
|
+
---
|
|
589
|
+
|
|
590
|
+
## 7. Color tab schema
|
|
591
|
+
|
|
592
|
+
The color tab — palette + base roles + semantic table + scheme registry — is
|
|
593
|
+
expressed as a `TabConfig` with `id: 'color'` and a `colorExtras` field.
|
|
594
|
+
Palette and semantic tokens are `TierItem` entries in the tab's `tiers`;
|
|
595
|
+
`colorExtras` carries the structural metadata. See `PORTABLE-CONTRACT.md` §4
|
|
596
|
+
for the authoritative spec; this section is the consumer-oriented summary.
|
|
597
|
+
|
|
598
|
+
### 7.1 `ColorClusterExtras`
|
|
599
|
+
|
|
600
|
+
```ts
|
|
601
|
+
export type BaseRoleKey = 'background' | 'foreground' | 'cursor' | 'selectionBg' | 'selectionFg';
|
|
602
|
+
|
|
603
|
+
export interface ColorClusterExtras {
|
|
604
|
+
/** Stable id — used for debugging / logging only. */
|
|
605
|
+
id: string;
|
|
606
|
+
/** Optional human-visible label for the Color tab section headings. */
|
|
607
|
+
label?: string;
|
|
608
|
+
/** base-role name → CSS custom-property name. A cluster MAY declare a subset. */
|
|
609
|
+
baseRoles: Partial<Record<BaseRoleKey, string>>;
|
|
610
|
+
/** Fallback palette indices when a scheme omits a base role. */
|
|
611
|
+
baseDefaults: Partial<Record<BaseRoleKey, number>>;
|
|
612
|
+
/** Fallback `shikiTheme` when a scheme lacks one. */
|
|
613
|
+
defaultShikiTheme: string;
|
|
614
|
+
/**
|
|
615
|
+
* Bundled scheme registry — keyed by display name. The Scheme… dropdown
|
|
616
|
+
* lists these. Pass `{}` for clusters that don't use schemes.
|
|
617
|
+
*/
|
|
618
|
+
colorSchemes: Record<string, ColorScheme>;
|
|
619
|
+
/** Panel-level scheme settings (seed scheme name + optional light/dark pairing). */
|
|
620
|
+
panelSettings: {
|
|
621
|
+
colorScheme: string;
|
|
622
|
+
colorMode: false | { defaultMode: 'light' | 'dark'; lightScheme: string; darkScheme: string };
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export interface ColorScheme {
|
|
627
|
+
background: number | string;
|
|
628
|
+
foreground: number | string;
|
|
629
|
+
cursor: number | string;
|
|
630
|
+
selectionBg: number | string;
|
|
631
|
+
selectionFg: number | string;
|
|
632
|
+
palette: readonly string[]; // length must match the palette tier's item count
|
|
633
|
+
shikiTheme: string;
|
|
634
|
+
semantic?: Record<string, number | string>;
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
> **Public alias** — the runtime type in `src/config/` is
|
|
639
|
+
> `ColorClusterDataConfig`. `ColorClusterConfig` is the public-facing alias
|
|
640
|
+
> re-exported from the package root:
|
|
641
|
+
> `import type { ColorClusterConfig } from '@takazudo/zdtp'`.
|
|
642
|
+
|
|
643
|
+
### 7.2 JSON-serializable constraint (important)
|
|
644
|
+
|
|
645
|
+
**Every field on the color `TabConfig` (including `colorExtras` and every
|
|
646
|
+
`ColorScheme` it nests) MUST be JSON-serializable.** No function fields, no
|
|
647
|
+
class instances. The Astro adapter stringifies the whole config into a
|
|
648
|
+
`<script type="application/json">` element and `JSON.parse`s it back at
|
|
649
|
+
runtime.
|
|
650
|
+
|
|
651
|
+
Palette CSS-var names are expressed as `TierItem.cssVar` strings (one per
|
|
652
|
+
palette slot), not as a function template — functions silently disappear under
|
|
653
|
+
the JSON round-trip.
|
|
654
|
+
|
|
655
|
+
### 7.3 Apply behaviour
|
|
656
|
+
|
|
657
|
+
For each palette `TierItem` in the palette tier, the panel writes
|
|
658
|
+
`item.cssVar` ← `palette[i]` from the active scheme / user override. For
|
|
659
|
+
each declared base role, it writes the role's CSS-var ← `palette[state[roleKey]]`.
|
|
660
|
+
For each semantic `TierItem`, it resolves the mapping and writes
|
|
661
|
+
`item.cssVar` ← resolved hex.
|
|
662
|
+
|
|
663
|
+
Roles absent from `colorExtras.baseRoles` are not written, so a minimalist
|
|
664
|
+
cluster (just `background` + `foreground`) is fine.
|
|
665
|
+
|
|
666
|
+
### 7.5 Host-supplied scheme presets — `colorPresets`
|
|
667
|
+
|
|
668
|
+
The Color tab's "Scheme..." dropdown surfaces named `ColorScheme` entries. Two sources feed it:
|
|
669
|
+
|
|
670
|
+
1. **`colorCluster.colorSchemes`** — the cluster's bundled scheme registry. Always present, typically holds your default scheme(s) (`"Default"`, `"Default Light"` / `"Default Dark"`).
|
|
671
|
+
2. **`PanelConfig.colorPresets`** — an optional, host-supplied preset map for an additional, larger preset library. Defaults to `{}` — the package itself ships zero presets.
|
|
672
|
+
|
|
673
|
+
This split exists so a host that just wants the panel for a single scheme (zero or one cluster scheme) does not pay for a long preset blob, while a host that wants to ship a "playground" of curated schemes (Dracula / Solarized / Tokyo Night / etc.) drops them into a single config field.
|
|
674
|
+
|
|
675
|
+
```ts
|
|
676
|
+
// src/lib/my-panel-config.ts
|
|
677
|
+
import type {
|
|
678
|
+
PanelConfig,
|
|
679
|
+
ColorScheme,
|
|
680
|
+
} from '@takazudo/zdtp/astro';
|
|
681
|
+
|
|
682
|
+
const myPresets: Record<string, ColorScheme> = {
|
|
683
|
+
Dracula: {
|
|
684
|
+
background: '#282a36',
|
|
685
|
+
foreground: 7,
|
|
686
|
+
cursor: 7,
|
|
687
|
+
selectionBg: '#44475a',
|
|
688
|
+
selectionFg: '#ffffff',
|
|
689
|
+
palette: [
|
|
690
|
+
// 16 hex strings — length must match colorCluster.paletteSize
|
|
691
|
+
'#21222c',
|
|
692
|
+
'#ff5555',
|
|
693
|
+
'#50fa7b',
|
|
694
|
+
'#f1fa8c',
|
|
695
|
+
'#bd93f9',
|
|
696
|
+
'#ff79c6',
|
|
697
|
+
'#8be9fd',
|
|
698
|
+
'#f8f8f2',
|
|
699
|
+
'#6272a4',
|
|
700
|
+
'#ff6e6e',
|
|
701
|
+
'#69ff94',
|
|
702
|
+
'#ffffa5',
|
|
703
|
+
'#d6acff',
|
|
704
|
+
'#ff92df',
|
|
705
|
+
'#a4ffff',
|
|
706
|
+
'#ffffff',
|
|
707
|
+
],
|
|
708
|
+
shikiTheme: 'dracula',
|
|
709
|
+
semantic: { primary: 4, accent: 5 },
|
|
710
|
+
},
|
|
711
|
+
// ... more presets
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
export const myPanelConfig: PanelConfig = {
|
|
715
|
+
// ... storagePrefix, tokens, colorCluster, etc.
|
|
716
|
+
colorPresets: myPresets,
|
|
717
|
+
};
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
**Dropdown layout** — `<option>`s render in this order:
|
|
721
|
+
|
|
722
|
+
1. The disabled `Scheme...` placeholder.
|
|
723
|
+
2. Each `colorExtras.colorSchemes` entry (from the color `TabConfig`), in insertion order.
|
|
724
|
+
3. An `<hr />` separator.
|
|
725
|
+
4. Each `colorPresets` entry, sorted alphabetically.
|
|
726
|
+
|
|
727
|
+
**Key collision** — if a `colorPresets` key matches a `colorExtras.colorSchemes` key, the bundled scheme wins for `handleLoadPreset`. Rename one of the keys if you want both to be selectable.
|
|
728
|
+
|
|
729
|
+
**JSON-serializable** — every `ColorScheme` is plain JSON, same as the rest of the config (§7.2). The host-supplied preset map crosses the Astro frontmatter → island boundary as part of the serialised `PanelConfig`.
|
|
730
|
+
|
|
731
|
+
> **Note on preset libraries.** The package ships zero baked-in scheme presets — the long preset blob (Dracula / Solarized / Tokyo Night / etc.) historically baked into earlier internal versions has been moved out of the package so consumers do not pay for a preset library they do not use. Hosts that want a curated preset list ship it themselves through `panelConfig.colorPresets`.
|
|
732
|
+
|
|
733
|
+
---
|
|
734
|
+
|
|
735
|
+
## 8. Astro wiring
|
|
736
|
+
|
|
737
|
+
```astro
|
|
738
|
+
---
|
|
739
|
+
import { DesignTokenPanelHost } from '@takazudo/zdtp/astro';
|
|
740
|
+
import { myPanelConfig } from '../lib/my-panel-config';
|
|
741
|
+
---
|
|
742
|
+
|
|
743
|
+
<DesignTokenPanelHost config={myPanelConfig} />
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
### 8.1 The `config` prop
|
|
747
|
+
|
|
748
|
+
`<DesignTokenPanelHost>` accepts the full `PanelConfig` from §5. The component renders two sibling `<script>` blocks:
|
|
749
|
+
|
|
750
|
+
1. An inline `<script type="application/json" id="tokenpanel-config">` carrying `JSON.stringify(config)` (with `<` defensively escaped to `<` for HTML-parsing safety).
|
|
751
|
+
2. An Astro `<script>` that imports the host adapter side-effect-style. The consumer's Astro toolchain bundles this into the page's client JS.
|
|
752
|
+
|
|
753
|
+
The adapter reads the JSON, calls `configurePanel(...)`, installs the console API, and gates the lazy import.
|
|
754
|
+
|
|
755
|
+
### 8.2 JSON-serializability constraint
|
|
756
|
+
|
|
757
|
+
Astro stringifies props at render time. **Functions, class instances, and `undefined` values silently disappear** when the config crosses the SSR → client boundary. Always design your `PanelConfig` with `JSON.parse(JSON.stringify(config))` round-trip in mind. Every `TierItem.cssVar` field must be a plain string — no template-function patterns survive the JSON boundary.
|
|
758
|
+
|
|
759
|
+
### 8.3 View-transition lifecycle
|
|
760
|
+
|
|
761
|
+
When the consumer site renders Astro's `<ClientRouter />`, the panel's host adapter automatically wires:
|
|
762
|
+
|
|
763
|
+
- `astro:before-swap` → unmount the Preact tree (`render(null, root)`), remove the host node, snapshot visibility intent so the remount decision survives the body swap.
|
|
764
|
+
- `astro:page-load` → re-apply persisted overrides + re-materialise the shell when either the visibility flag or the persisted-overrides flag is set.
|
|
765
|
+
|
|
766
|
+
No additional wiring needed in your layout beyond importing `<ClientRouter />` from `astro:transitions`.
|
|
767
|
+
|
|
768
|
+
### 8.4 Where to mount
|
|
769
|
+
|
|
770
|
+
The conventional placement is **at the end of `<body>`** in your shared layout. Mounting it earlier still works but the render order looks better when the panel is the last child of `<body>`.
|
|
771
|
+
|
|
772
|
+
### 8.5 Non-Astro hosts (Vite-only)
|
|
773
|
+
|
|
774
|
+
The `./astro` sub-export is the only place that imports anything Astro-flavoured. The package's main entry (`@takazudo/zdtp`) is framework-agnostic: call `configurePanel(...)` yourself, then `import('@takazudo/zdtp')` to materialise the panel. The `astro:before-swap` / `astro:page-load` listeners no-op outside an Astro context but the storage / mount / apply paths work identically.
|
|
775
|
+
|
|
776
|
+
#### Soft-nav lifecycle for non-Astro hosts (`setLifecycleAdapter`)
|
|
777
|
+
|
|
778
|
+
For hosts that own a client-side router (zfb, custom SPA, etc.), persisted overrides need to re-apply after every soft navigation. Register a `LifecycleAdapter` so the panel's internal handlers route through your router's hooks instead of Astro's document events:
|
|
779
|
+
|
|
780
|
+
```ts
|
|
781
|
+
import {
|
|
782
|
+
setLifecycleAdapter,
|
|
783
|
+
type LifecycleAdapter,
|
|
784
|
+
} from '@takazudo/zdtp';
|
|
785
|
+
|
|
786
|
+
// Example: a zfb-style host that emits 'zfb:before-swap' / 'zfb:page-load'
|
|
787
|
+
// CustomEvents on the document.
|
|
788
|
+
const adapter: LifecycleAdapter = {
|
|
789
|
+
onBeforeSwap(callback) {
|
|
790
|
+
document.addEventListener('zfb:before-swap', callback);
|
|
791
|
+
return () => document.removeEventListener('zfb:before-swap', callback);
|
|
792
|
+
},
|
|
793
|
+
onPageLoad(callback) {
|
|
794
|
+
document.addEventListener('zfb:page-load', callback);
|
|
795
|
+
return () => document.removeEventListener('zfb:page-load', callback);
|
|
796
|
+
},
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
setLifecycleAdapter(adapter);
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
Each installer must return a cleanup fn that unbinds the listener — the panel calls it on re-registration and on `setLifecycleAdapter(null)`.
|
|
803
|
+
|
|
804
|
+
Behaviour notes:
|
|
805
|
+
|
|
806
|
+
- When NO adapter is registered (initial state, or after `setLifecycleAdapter(null)`), the panel falls back to its built-in `astro:before-swap` / `astro:page-load` document listeners. Backwards-compatible — existing Astro consumers see zero behaviour change.
|
|
807
|
+
- When an adapter IS registered, the astro fallback is **actively unbound** so the internal handlers never double-fire on hosts that emit both event sets.
|
|
808
|
+
- `setLifecycleAdapter(null)` re-installs the astro fallback. Useful for tests and re-init scenarios.
|
|
809
|
+
- Re-registration (calling `setLifecycleAdapter` twice with different adapters) drains the previous adapter's cleanup fns before binding the new one — no listener leaks.
|
|
810
|
+
|
|
811
|
+
---
|
|
812
|
+
|
|
813
|
+
## 9. Storage-key derivation
|
|
814
|
+
|
|
815
|
+
`storagePrefix` is the single knob that controls every persisted key. The panel derives keys from this base at runtime.
|
|
816
|
+
|
|
817
|
+
| Logical key | Derivation | Purpose |
|
|
818
|
+
| ----------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
819
|
+
| `state-v3` | `${storagePrefix}-state-v3` | Current unified envelope (color + spacing + typography + size + panelPosition + generic `tabs` map). Added in abstract-token-tiers epic. |
|
|
820
|
+
| `state-v2` | `${storagePrefix}-state-v2` | Legacy pre-v3 format. Migrated into `state-v3` on first load, then deleted. |
|
|
821
|
+
| `state-v1` | `${storagePrefix}-state` | Legacy pre-v2 flat-state format (Color-only). Migrated into `state-v3` on first load, then deleted. |
|
|
822
|
+
| `open` | `${storagePrefix}-open` | Mirror of the panel's `open` boolean (synchronous mount-time read — preserves user intent across reloads, fixes #1549). |
|
|
823
|
+
| `position` | `${storagePrefix}-position` | Drag position `{ top, right }` so the panel reappears where the user left it. |
|
|
824
|
+
| `visible` | `${storagePrefix}:visible` | Adapter-level visibility-intent flag, owned by the lazy-load gate. |
|
|
825
|
+
|
|
826
|
+
For example, with `storagePrefix: 'myapp-design-token-panel'`:
|
|
827
|
+
|
|
828
|
+
```
|
|
829
|
+
myapp-design-token-panel-state-v3
|
|
830
|
+
myapp-design-token-panel-state-v2
|
|
831
|
+
myapp-design-token-panel-state
|
|
832
|
+
myapp-design-token-panel-open
|
|
833
|
+
myapp-design-token-panel-position
|
|
834
|
+
myapp-design-token-panel:visible
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
### Note: colon vs dash on the `visible` key
|
|
838
|
+
|
|
839
|
+
The `visible` key uses a literal `:` separator, not `-`. Every other derived key uses `-`. This is intentional — see [`PORTABLE-CONTRACT.md`](./PORTABLE-CONTRACT.md) §2 for the historical reason. Don't try to "normalize" it; the unit tests assert this specific shape.
|
|
840
|
+
|
|
841
|
+
---
|
|
842
|
+
|
|
843
|
+
## 10. Console API contract
|
|
844
|
+
|
|
845
|
+
Once `configurePanel` has run, the package installs three async helpers on the global `window[consoleNamespace]` object:
|
|
846
|
+
|
|
847
|
+
```ts
|
|
848
|
+
window.myapp.showDesignPanel(); // open the panel
|
|
849
|
+
window.myapp.hideDesignPanel(); // close the panel
|
|
850
|
+
window.myapp.toggleDesignPanel(); // toggle
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
All three are **async** — the first call lazy-imports the panel module. Subsequent calls share the memoised module promise and resolve synchronously after the first import completes.
|
|
854
|
+
|
|
855
|
+
### Co-existing helpers on the same namespace
|
|
856
|
+
|
|
857
|
+
The adapter **merges** its three helpers into any existing object at `window[namespace]` rather than overwriting the namespace wholesale. This means a host can share a namespace between multiple dev tools (e.g. `window.myapp.ogpDebug.show()` from a separate package, alongside `window.myapp.showDesignPanel()` from this one) without collisions.
|
|
858
|
+
|
|
859
|
+
### Default
|
|
860
|
+
|
|
861
|
+
There is no default `consoleNamespace` exposed to consumers — the field is required on `PanelConfig`. Pick a short, unambiguous string (typically your app's slug).
|
|
862
|
+
|
|
863
|
+
---
|
|
864
|
+
|
|
865
|
+
## 11. Tailwind not required
|
|
866
|
+
|
|
867
|
+
The panel ships its own bundled CSS scoped under a panel-private namespace:
|
|
868
|
+
|
|
869
|
+
```css
|
|
870
|
+
:where(.tokenpanel-shell, [data-design-token-panel-modal]) {
|
|
871
|
+
--tokentweak-color-fg: var(--color-fg, #b8b8b8);
|
|
872
|
+
--tokentweak-color-bg: var(--color-bg, #181818);
|
|
873
|
+
--tokentweak-color-surface: var(--color-surface, #1c1c1c);
|
|
874
|
+
--tokentweak-color-accent: var(--color-accent, #d69a66);
|
|
875
|
+
--tokentweak-font-mono: var(--font-mono, Menlo, Monaco, Consolas, …);
|
|
876
|
+
--tokentweak-pad-md: …;
|
|
877
|
+
--tokentweak-gap-sm: …;
|
|
878
|
+
--tokentweak-text-body: …;
|
|
879
|
+
--radius-tokentweak: …;
|
|
880
|
+
/* …one custom property per panel-chrome value */
|
|
881
|
+
}
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
- **Naming:** every panel-private CSS variable uses the `--tokentweak-*` prefix. Consumer-namespaced identifiers do not appear in the panel chrome — `panel.css` reads only `--tokentweak-*`.
|
|
885
|
+
- **Files:** `panel.css` (chrome layout / typography / controls) + `panel-tokens.css` (the `--tokentweak-*` declarations). Both ship from the package and the consumer pulls them in via `sideEffects`.
|
|
886
|
+
- **No Tailwind dependency in the consumer.** The panel chrome uses hand-authored CSS classes backed by `--tokentweak-*` variables. You can integrate the panel into a Tailwind site, a CSS Modules site, a vanilla CSS site, or anything in between.
|
|
887
|
+
|
|
888
|
+
### 11.1 Two-layer override model for chrome colors
|
|
889
|
+
|
|
890
|
+
The panel-chrome color tokens (`--tokentweak-color-fg`, `--tokentweak-color-bg`, `--tokentweak-color-muted`, `--tokentweak-color-surface`, `--tokentweak-color-accent`, `--tokentweak-color-accent-hover`, `--tokentweak-color-code-bg`, `--tokentweak-color-code-fg`, `--tokentweak-color-success`, `--tokentweak-color-danger`, `--tokentweak-color-warning`, `--tokentweak-font-mono`) are declared as a `var(--host, fallback)` ladder. This gives hosts two override layers and works out-of-the-box when neither is supplied:
|
|
891
|
+
|
|
892
|
+
| Host declares | Outcome |
|
|
893
|
+
|---|---|
|
|
894
|
+
| Nothing | Built-in fallback paints the panel (a sensible neutral dark theme). |
|
|
895
|
+
| `--color-fg` (and friends) at `:root` | Host theme cascades into the panel — no panel-side change needed. |
|
|
896
|
+
| `--tokentweak-color-fg` directly | Panel-only override that bypasses the host's `--color-*` theme. Useful when you want the panel to look different from your site shell (e.g., a brand-neutral inspector overlay). |
|
|
897
|
+
|
|
898
|
+
Because the fallback ladder is host-CSS-var-driven, a brand-new consumer can mount the panel without declaring any `--color-*` tokens and still get readable chrome.
|
|
899
|
+
|
|
900
|
+
The CSS variables the panel **writes to** (the `cssVar` field on each `TokenDef`, the cluster's `paletteCssVarTemplate`, base-role names, and semantic CSS names) are entirely consumer-controlled. The package never reads those consumer CSS variables; it only writes through `setProperty` on `:root`.
|
|
901
|
+
|
|
902
|
+
---
|
|
903
|
+
|
|
904
|
+
## 12. Bundler notes
|
|
905
|
+
|
|
906
|
+
The package builds in **Vite library mode**, which has a quirk that's important to understand: it extracts every `import './something.css'` from the source and emits a single combined stylesheet (`dist/design-token-panel.css`), but **removes the import statements from the emitted JS files**. The `dist/index.js` and `dist/astro/host-adapter.js` therefore have no static reference back to the CSS — `sideEffects: ["**/*.css"]` in `package.json` only protects existing imports from tree-shaking; it cannot resurrect an import the build step has already deleted.
|
|
907
|
+
|
|
908
|
+
Net effect: the consumer MUST add a one-line side-effect import to their static module graph, as described in §3.4. The `./styles` sub-export is the canonical entry:
|
|
909
|
+
|
|
910
|
+
```ts
|
|
911
|
+
import '@takazudo/zdtp/styles';
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
`./styles.css` is provided as an alias for clarity in tooling that prefers explicit extensions:
|
|
915
|
+
|
|
916
|
+
```ts
|
|
917
|
+
import '@takazudo/zdtp/styles.css';
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
If you forget the import, the JS layer still works — `window.<ns>.showDesignPanel()` mounts the shell DOM correctly — but every chrome rule is missing, so the panel appears invisible against the host page background. The fix is the import, not bundler reconfiguration.
|
|
921
|
+
|
|
922
|
+
(Historical note: an earlier draft of this section claimed `sideEffects` alone was sufficient and consumers did not need to import CSS. That was incorrect — Vite library mode's CSS-extraction behaviour means `sideEffects` is necessary but not sufficient. The §3.4 + this section now reflect the actual contract.)
|
|
923
|
+
|
|
924
|
+
### 12.1 Host-adapter side-effect import (paired-unit contract)
|
|
925
|
+
|
|
926
|
+
The package's distributed Astro surface ships built `dist/astro/*` files. The host-adapter (`dist/astro/host-adapter.js`) is the runtime that reads the inline JSON config emitted by `<DesignTokenPanelHost>` and installs `window.<consoleNamespace>.*`. Earlier package versions emitted a hoisted `<script>import './host-adapter';</script>` block from inside `DesignTokenPanelHost.astro` to load it; that block did not reliably reach production page bundles — Vite/Rollup processed the import, recognized it as resolving to a sibling JS file outside the consumer's source tree, emitted an empty chunk, and never linked it from any page entry.
|
|
927
|
+
|
|
928
|
+
For consumer-side imports, the package's `package.json` lists `dist/astro/host-adapter.js` in the `sideEffects` array so Rollup preserves the import even when its result is discarded (the host adapter has top-level execution that registers the console API — Rollup's tree-shaker has no way to know that without the metadata hint).
|
|
929
|
+
|
|
930
|
+
Net effect: the consumer MUST own the host-adapter import in their wrapper layout. Use the dynamic `void import('...')` form below — it loads the host-adapter chunk off the critical path (mirrors the existing color-presets lazy-loader) and is robust to future packaging changes that could miss-configure `sideEffects`. A top-level `import '...';` works too with the current `sideEffects` list, but the dynamic form is the recommended canonical wiring.
|
|
931
|
+
|
|
932
|
+
```astro
|
|
933
|
+
<script>
|
|
934
|
+
void import('@takazudo/zdtp/astro/host-adapter');
|
|
935
|
+
</script>
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
This is the second half of the paired-unit contract from §3.2 (`<DesignTokenPanelHost>` AND the host-adapter `<script>` block — always together). If you forget it, the `<DesignTokenPanelHost>` JSON config payload still ships, but no JS reads it, so calling `window.<consoleNamespace>.showDesignPanel()` throws `ReferenceError`.
|
|
939
|
+
|
|
940
|
+
For the regression-guard tests that pin this contract, see `package-exports.test.ts` under the package's test suite.
|
|
941
|
+
|
|
942
|
+
---
|
|
943
|
+
|
|
944
|
+
## 13. Troubleshooting
|
|
945
|
+
|
|
946
|
+
### 13.1 FOUT (flash of unstyled tokens) on hard navigation
|
|
947
|
+
|
|
948
|
+
**Symptom:** on first paint after a hard reload, the page renders with the consumer's default token values for a beat before snapping to the user's saved overrides.
|
|
949
|
+
|
|
950
|
+
**Resolution:** the host adapter eagerly re-applies persisted overrides during the lazy-load gate (probes `${storagePrefix}-state-v2` and `${storagePrefix}:visible` synchronously from `localStorage`). If you still see a flash, your `<DesignTokenPanelHost>` is being rendered too late in the document (e.g. inside a deferred island) — move it to the layout's `<body>` and verify the inline `<script type="application/json" id="tokenpanel-config">` is in the initial HTML.
|
|
951
|
+
|
|
952
|
+
### 13.2 Auto-mount race on first reload
|
|
953
|
+
|
|
954
|
+
**Symptom:** the panel does not re-open on the first reload after the user closed it, even though `${storagePrefix}-open` is set in `localStorage`.
|
|
955
|
+
|
|
956
|
+
**Resolution:** the open boolean is mirrored to `localStorage` synchronously and read at mount time so the next mount opens directly into the user's last state without a post-render toggle dispatch. If the symptom persists, confirm the storage key matches what the contract derives (§8) and that nothing else in the page is clearing the key on load.
|
|
957
|
+
|
|
958
|
+
### 13.3 Live-apply regression test approach
|
|
959
|
+
|
|
960
|
+
**Symptom:** after a panel-package change, you want to confirm the live-apply pipeline (storage → adapter → `:root`) is unbroken end-to-end.
|
|
961
|
+
|
|
962
|
+
**Resolution:** the canonical regression test is each external example repo's `apply-roundtrip.spec.ts` Playwright spec under `tests/e2e/`. It boots the example's preview build, seeds a v2 state under the example's storage prefix, hard-reloads, and asserts the adapter rehydrated and applied the override against the example's palette and semantic CSS variable names. The contract: storage prefix, palette template, semantic CSS names — change one of those and this spec fails first. See §15 for links to the five external example repos.
|
|
963
|
+
|
|
964
|
+
---
|
|
965
|
+
|
|
966
|
+
## 14. Migration recipe — adopting the panel into an existing consumer
|
|
967
|
+
|
|
968
|
+
This recipe walks through wiring the panel into a project that does not currently use it. If you previously consumed an internal pre-OSS snapshot of the panel where storage keys, console namespace, modal class prefix, and cluster identifiers were hardcoded literals, the same steps apply — your job is to lift those literals into a `PanelConfig` value.
|
|
969
|
+
|
|
970
|
+
1. **Install the package.**
|
|
971
|
+
|
|
972
|
+
```sh
|
|
973
|
+
pnpm add @takazudo/zdtp preact
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
2. **Define your `PanelConfig` literals.**
|
|
977
|
+
|
|
978
|
+
Pick identifiers for `storagePrefix`, `consoleNamespace`, `modalClassPrefix`, `schemaId`, `exportFilenameBase`, and your palette CSS-var family (`paletteCssVarTemplate`). Pull these into a host-side config file (e.g. `src/lib/panel-config.ts`). If you are migrating from an internal snapshot fork and want to preserve users' saved state across the migration, keep the legacy values verbatim; otherwise pick fresh, neutral identifiers (e.g. `myapp-design-token-panel`).
|
|
979
|
+
|
|
980
|
+
3. **Author your token manifest in the host project.**
|
|
981
|
+
|
|
982
|
+
The package itself ships zero baked-in manifest data. Define `SPACING_TOKENS`, `FONT_TOKENS`, `SIZE_TOKENS`, `COLOR_TOKENS` (or whatever names you prefer) in your project and wire them up as `tokens.spacing`, `tokens.typography`, `tokens.size`, `tokens.color`. The host is the source of truth.
|
|
983
|
+
|
|
984
|
+
4. **Author your color cluster in the host project.**
|
|
985
|
+
|
|
986
|
+
Build a `ColorClusterConfig` value with your palette, base roles, semantic table, and scheme registry. **The palette CSS-var name MUST be a string template, not a function:**
|
|
987
|
+
|
|
988
|
+
```ts
|
|
989
|
+
// wrong (function — does not survive Astro frontmatter → island handoff)
|
|
990
|
+
// paletteCssVar: (i) => `--myapp-p${i}`,
|
|
991
|
+
|
|
992
|
+
// right (string template, JSON-serializable)
|
|
993
|
+
paletteCssVarTemplate: '--myapp-p{n}',
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
Every other field on the cluster is a plain value already.
|
|
997
|
+
|
|
998
|
+
5. **Drop `<DesignTokenPanelHost>` into your layout.**
|
|
999
|
+
|
|
1000
|
+
```astro
|
|
1001
|
+
---
|
|
1002
|
+
import { DesignTokenPanelHost } from '@takazudo/zdtp/astro';
|
|
1003
|
+
import { myPanelConfig } from '../lib/panel-config';
|
|
1004
|
+
---
|
|
1005
|
+
|
|
1006
|
+
<DesignTokenPanelHost config={myPanelConfig} />
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
The host adapter does the rest (calls `configurePanel`, installs the console API, gates lazy-load). Don't forget the paired host-adapter `<script>` block — see §4.1.2.
|
|
1010
|
+
|
|
1011
|
+
6. **Verify storage keys derive to the expected literals.**
|
|
1012
|
+
|
|
1013
|
+
Open devtools → Application → Local Storage. Confirm you see keys derived under your `storagePrefix` and that any pre-existing user state (under the legacy prefix, if you preserved it) is migrated forward through the v1 → v2 path on first load.
|
|
1014
|
+
|
|
1015
|
+
7. **Run the live-apply e2e spec.**
|
|
1016
|
+
|
|
1017
|
+
Use any of the external example repos' Playwright spec at `tests/e2e/apply-roundtrip.spec.ts` (see §15 for the five repo links) as a template: seed a `${storagePrefix}-state-v2` payload, hard-reload, assert your palette and semantic CSS variables on `:root` reflect the seeded values.
|
|
1018
|
+
|
|
1019
|
+
For edge cases hit during the migration, see CONTRIBUTING and the doc-site reference pages for each `PanelConfig` field.
|
|
1020
|
+
|
|
1021
|
+
---
|
|
1022
|
+
|
|
1023
|
+
## 15. Worked examples
|
|
1024
|
+
|
|
1025
|
+
The canonical worked examples live in five dedicated sibling repos. Each is an independent consumer app that demonstrates the panel against a different host framework. Each renders a tiny page with cards, buttons, and palette swatches whose styles reference its own demo CSS variables, and each ships a Playwright spec at `tests/e2e/apply-roundtrip.spec.ts` that asserts the live-apply pipeline.
|
|
1026
|
+
|
|
1027
|
+
- [`zudo-design-token-panel-example-astro`](https://github.com/Takazudo/zudo-design-token-panel-example-astro) — Astro + Preact island. Uses `storagePrefix: 'astro-example-tokens'`, `consoleNamespace: 'astro'`, `paletteCssVarTemplate: '--astro-palette-{n}'`.
|
|
1028
|
+
- [`zudo-design-token-panel-example-vite-react`](https://github.com/Takazudo/zudo-design-token-panel-example-vite-react) — Vite + React (panel mounted as a Preact island, React tree untouched). Uses the `vr` namespace.
|
|
1029
|
+
- [`zudo-design-token-panel-example-nextjs`](https://github.com/Takazudo/zudo-design-token-panel-example-nextjs) — Next.js (App Router) + React, panel as a `'use client'` boundary. Uses the `nx` namespace.
|
|
1030
|
+
- [`zudo-design-token-panel-example-zfb`](https://github.com/Takazudo/zudo-design-token-panel-example-zfb) — [zfb](https://github.com/Takazudo/zudo-front-builder) (Preact host). Uses the `devMiddleware` plugin hook for the apply proxy.
|
|
1031
|
+
- [`zudo-design-token-panel-example-zfb-tailwind`](https://github.com/Takazudo/zudo-design-token-panel-example-zfb-tailwind) — zfb + Tailwind v4. Extends the zfb example by registering panel tokens into Tailwind's design-system namespaces via an `@theme` block.
|
|
1032
|
+
|
|
1033
|
+
To run any example locally, clone the repo and follow its README — typically:
|
|
1034
|
+
|
|
1035
|
+
```sh
|
|
1036
|
+
git clone https://github.com/Takazudo/zudo-design-token-panel-example-astro.git
|
|
1037
|
+
cd zudo-design-token-panel-example-astro
|
|
1038
|
+
pnpm install
|
|
1039
|
+
pnpm dev
|
|
1040
|
+
# open the printed dev URL
|
|
1041
|
+
# in devtools console: window.<consoleNamespace>.toggleDesignPanel()
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
Use the closest example as a copy-paste template when wiring the panel into your own project — they are the smallest end-to-end consumers that exercise every contract surface.
|