@takazudo/zdtp 0.4.4 → 0.4.6
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 +46 -0
- package/README.md +159 -12
- package/dist/apply/apply-token-overrides.d.ts +139 -32
- package/dist/astro/host-adapter.js +71 -58
- package/dist/astro/index.js +1 -1
- package/dist/{autoload-state-CmhI7q9j.js → autoload-state-D26ML6sW.js} +1 -1
- package/dist/bin/server.js +116 -112
- package/dist/config/panel-config.d.ts +68 -1
- package/dist/controls/actions-menu-popover.d.ts +26 -0
- package/dist/controls/help-icon.d.ts +43 -0
- package/dist/controls/tooltip.d.ts +20 -2
- package/dist/export-modal.d.ts +5 -3
- package/dist/import-modal.d.ts +9 -9
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3383 -3012
- package/dist/load-routing-LJ72261l.js +421 -0
- package/dist/{panel-config-CXTCcYQs.js → panel-config-Bw7D-25C.js} +381 -271
- package/dist/server/create-apply-handler.d.ts +6 -0
- package/dist/server/index.js +1 -1
- package/dist/state/tweak-state.d.ts +137 -14
- package/dist/tabs/notes-tab.d.ts +25 -0
- package/dist/testing.js +7 -7
- package/dist/tokens/manifest.d.ts +8 -0
- package/dist/tokens/tier-model.d.ts +36 -0
- package/dist/tweak-state-CD1y56zR.js +1958 -0
- package/dist/utils/design-token-serde.d.ts +20 -12
- package/dist/utils/sanitize-html.d.ts +27 -0
- package/dist/utils/unit-cycle.d.ts +61 -0
- package/dist/zdtp.css +1 -1
- package/package.json +1 -1
- package/dist/load-routing-DtNTKoLW.js +0 -353
- package/dist/tweak-state-BeXkzoj8.js +0 -1858
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.6
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Notes tab — host-configurable token notes as the panel's top page ([#515](https://github.com/Takazudo/zudo-design-token-panel/issues/515)) (4169a05)
|
|
8
|
+
- Palette tab: groups collapsed by default with boxed toggle headers ([#517](https://github.com/Takazudo/zudo-design-token-panel/issues/517)) (2ff5a1e)
|
|
9
|
+
- Container-query responsive header & tabs at narrow panel widths, plus "zdtp" panel title + square corners ([#518](https://github.com/Takazudo/zudo-design-token-panel/issues/518)) (25dff1a)
|
|
10
|
+
- Click-to-cycle unit suffix on value inputs ([#519](https://github.com/Takazudo/zudo-design-token-panel/issues/519)) (b05581d)
|
|
11
|
+
- "?" help tooltips for Literal and Per-mode rows in the Color tab ([#520](https://github.com/Takazudo/zudo-design-token-panel/issues/520)) (c85fd69)
|
|
12
|
+
- `zdtp.show()` — fixed-name global open API ([#523](https://github.com/Takazudo/zudo-design-token-panel/issues/523)) (9b3c636)
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Tooltip repositions correctly after panel resize (ResizeObserver initial-delivery fix) ([#516](https://github.com/Takazudo/zudo-design-token-panel/issues/516)) (d10fe16)
|
|
17
|
+
- Closed a C0-control URL-scheme bypass in the notes-tab HTML sanitizer ([#515](https://github.com/Takazudo/zudo-design-token-panel/issues/515)) (19f7147)
|
|
18
|
+
- Corrected kebab-close dead zone + title truncation in the responsive header ([#518](https://github.com/Takazudo/zudo-design-token-panel/issues/518)) (bfc4b81)
|
|
19
|
+
- `window.zdtp` guards against a null instance and clarifies multi-instance behavior ([#523](https://github.com/Takazudo/zudo-design-token-panel/issues/523)) (bcc71aa)
|
|
20
|
+
|
|
21
|
+
### Other Changes
|
|
22
|
+
|
|
23
|
+
- docs: notes-tab demo in doc-site manifest; help-icon resize-pin trade-off + sanitizer intent documented (82cf7d8, fe1b477)
|
|
24
|
+
- test: VRT baselines re-recorded for zdtp title + square corners; broad coverage added for notes tab, help icons, unit cycling
|
|
25
|
+
- chore: removed consumed `_temp-resource/` prototype scaffold (ccbd18a)
|
|
26
|
+
|
|
27
|
+
## 0.4.5
|
|
28
|
+
|
|
29
|
+
### Features
|
|
30
|
+
|
|
31
|
+
- Per-scheme/per-mode keyed color persistence — envelope v4 ([#509](https://github.com/Takazudo/zudo-design-token-panel/issues/509)) (653c4ab)
|
|
32
|
+
- Distinguish declared-outside-scanned-block from genuinely-absent unknowns ([#508](https://github.com/Takazudo/zudo-design-token-panel/issues/508)) (441fa93)
|
|
33
|
+
- Scan top-level `@theme` blocks in `applyTokenOverrides` ([#507](https://github.com/Takazudo/zudo-design-token-panel/issues/507)) (d6c6692)
|
|
34
|
+
- Add `colorExtras.semanticDefaults` config-time override map ([#499](https://github.com/Takazudo/zudo-design-token-panel/issues/499)) (543136e)
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- Harden v4 persistence load edge cases ([#509](https://github.com/Takazudo/zudo-design-token-panel/issues/509) audit, epic [#502](https://github.com/Takazudo/zudo-design-token-panel/issues/502) review) (2b07460)
|
|
39
|
+
- Scope color-scheme clearing to panel-written values (closes [#506](https://github.com/Takazudo/zudo-design-token-panel/issues/506)) (f434c8c)
|
|
40
|
+
- Validate persisted semantic index mappings against live cluster paletteSize ([#503](https://github.com/Takazudo/zudo-design-token-panel/issues/503)) (915ffbf)
|
|
41
|
+
|
|
42
|
+
### Other Changes
|
|
43
|
+
|
|
44
|
+
- docs: sync README + doc-site with `@theme` apply, v4 persistence, schemaId ([#511](https://github.com/Takazudo/zudo-design-token-panel/issues/511)) (3c3fd57)
|
|
45
|
+
- test: cross-cutting interaction coverage for [#510](https://github.com/Takazudo/zudo-design-token-panel/issues/510) confirm pass (9a5d7fe)
|
|
46
|
+
- docs: truth-up schemaId docs, tighten Import modal copy, export `SCHEMA_V1`/`SCHEMA_V2`/`SCHEMA_V3` ([#505](https://github.com/Takazudo/zudo-design-token-panel/issues/505)) (8088281)
|
|
47
|
+
- docs: replace zmodular references with Takazudo Modular markdown links (afcfea2)
|
|
48
|
+
|
|
3
49
|
## 0.4.4
|
|
4
50
|
|
|
5
51
|
### Features
|
package/README.md
CHANGED
|
@@ -53,6 +53,8 @@ The package builds against Preact (declared as a `peerDependency`) and ships its
|
|
|
53
53
|
|
|
54
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
55
|
|
|
56
|
+
**Browser floor:** the panel's own header/tabbar chrome adapts to the panel's *width* (not the viewport) via CSS container queries, which are Baseline 2023 (Safari 16+). The panel is a developer tool used on evergreen browsers, so older browsers simply keep the wide-layout chrome with no JS fallback.
|
|
57
|
+
|
|
56
58
|
---
|
|
57
59
|
|
|
58
60
|
## 2. Install
|
|
@@ -161,6 +163,8 @@ export const panelConfig: PanelConfig = {
|
|
|
161
163
|
|
|
162
164
|
For the full token-overrides payload schema and the `PanelConfig` shape, see §5 and §6.
|
|
163
165
|
|
|
166
|
+
**Which CSS blocks the rewriter scans.** Each routed file's rewrite only reaches two locations: the FIRST top-level `:root { ... }` block and the FIRST top-level `@theme { ... }` block (bare `@theme`, or with one modifier such as `@theme inline` — the shape Tailwind v4 prescribes when theme values reference other variables). Later blocks of either kind, and anything nested under `@media` / `@layer` / `@supports`, are not scanned. A cssVar declared in BOTH blocks is rewritten only in `:root` (`:root`-first-match-wins). A file with neither block returns a 409 — see §11 for the Tailwind `@theme` worked example, and [Apply pipeline reference](/docs/reference/apply-pipeline) for the full response-field contract, including the `unknownOutsideBlockCssVars` diagnostic for a cssVar that's declared somewhere in the file but outside both scanned blocks.
|
|
167
|
+
|
|
164
168
|
### 3.3 Security model
|
|
165
169
|
|
|
166
170
|
The bin is **dev-only** and is built with three independent guards:
|
|
@@ -273,7 +277,7 @@ export const myPanelConfig: PanelConfig = {
|
|
|
273
277
|
storagePrefix: 'myapp-design-token-panel',
|
|
274
278
|
consoleNamespace: 'myapp',
|
|
275
279
|
modalClassPrefix: 'myapp-design-token-panel-modal',
|
|
276
|
-
schemaId: 'myapp-design-tokens/v1',
|
|
280
|
+
schemaId: 'myapp-design-tokens/v1', // display-only label — see §5.3; import/export always use the canonical SCHEMA_V1/V2/V3
|
|
277
281
|
exportFilenameBase: 'myapp-design-tokens',
|
|
278
282
|
tabs: [spacingTab /*, fontTab, sizeTab, colorTab, ... */],
|
|
279
283
|
};
|
|
@@ -324,6 +328,22 @@ That is the entire integration. `<DesignTokenPanelHost>` emits a JSON `<script>`
|
|
|
324
328
|
window.myapp.toggleDesignPanel();
|
|
325
329
|
```
|
|
326
330
|
|
|
331
|
+
Or use the fixed-name `zdtp` global — no need to remember your own
|
|
332
|
+
`consoleNamespace`:
|
|
333
|
+
|
|
334
|
+
```js
|
|
335
|
+
// In the browser devtools console — works regardless of consoleNamespace
|
|
336
|
+
zdtp.show();
|
|
337
|
+
zdtp.hide();
|
|
338
|
+
zdtp.toggle();
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`zdtp.show()` / `hide()` / `toggle()` are console sugar for the exact same
|
|
342
|
+
open/close verbs `window.myapp.*` exposes (see §10 for the full contract) —
|
|
343
|
+
`window.myapp.*` keeps working unchanged. On an Astro page, `zdtp.*` is
|
|
344
|
+
available as soon as the host-adapter script has run, even before the panel
|
|
345
|
+
bundle itself has loaded.
|
|
346
|
+
|
|
327
347
|
Or wire a hidden keyboard shortcut / dev-only button to call the same helper.
|
|
328
348
|
|
|
329
349
|
### 4.1.4 Stylesheet (self-injected — no consumer import required)
|
|
@@ -470,6 +490,13 @@ export interface PanelInstanceHandle {
|
|
|
470
490
|
}
|
|
471
491
|
```
|
|
472
492
|
|
|
493
|
+
`open()` / `close()` / `toggle()` are the same primitives both
|
|
494
|
+
`window.myapp.*` (§10) and the fixed-name `window.zdtp.*` global (§10) wrap
|
|
495
|
+
for the **default** (most-recently-configured) instance. On a page with more
|
|
496
|
+
than one instance, call `handle.open()` / `.close()` / `.toggle()` directly
|
|
497
|
+
on the specific instance's handle — `window.zdtp.*` always targets the
|
|
498
|
+
default instance only.
|
|
499
|
+
|
|
473
500
|
### 5.3 Field summary
|
|
474
501
|
|
|
475
502
|
| Field | Type | Purpose |
|
|
@@ -477,7 +504,7 @@ export interface PanelInstanceHandle {
|
|
|
477
504
|
| `storagePrefix` | `string` | Base for every derived `localStorage` key. Also the instance id. See §9. |
|
|
478
505
|
| `consoleNamespace` | `string` | Global object the package installs `showDesignPanel` / `hideDesignPanel` / `toggleDesignPanel` on (e.g. `consoleNamespace: 'myapp'` → `window.myapp.showDesignPanel`). |
|
|
479
506
|
| `modalClassPrefix` | `string` | BEM root class for every modal the panel owns (export, import, apply). Emits `${prefix}__overlay`, `${prefix}__panel`, etc. |
|
|
480
|
-
| `schemaId` | `string` |
|
|
507
|
+
| `schemaId` | `string` | **Display-only** label surfaced in the Import modal's copy. `serialize()` always emits the canonical `SCHEMA_V2`/`SCHEMA_V3` and `deserialize()` always validates against the canonical `SCHEMA_V1`/`V2`/`V3` — neither reads this field. See §14's migration recipe. |
|
|
481
508
|
| `exportFilenameBase` | `string` | Default download filename base — exports save as `${exportFilenameBase}.json`. |
|
|
482
509
|
| `toggleEvent` | `string` (optional) | Window-event name that toggles THIS instance. Defaults to `toggle-${storagePrefix}` for non-default instances; the default instance keeps `toggle-design-token-panel`. |
|
|
483
510
|
| `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. |
|
|
@@ -533,7 +560,7 @@ const handle = configurePanel({
|
|
|
533
560
|
The Astro entry point (`<DesignTokenPanelHost>`) handles mounting for you. Internally:
|
|
534
561
|
|
|
535
562
|
- 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.
|
|
536
|
-
- The panel module is **dynamically imported on first need**: when the user calls a console helper, OR when first-paint detects any of four gate signals in `localStorage` — `${storagePrefix}:visible` set to `1`, persisted overrides (`${storagePrefix}-state-v3`), the owner-autoload flag (`${storagePrefix}:autoload` set to `1`), or the element-path inspector enabled.
|
|
563
|
+
- The panel module is **dynamically imported on first need**: when the user calls a console helper, OR when first-paint detects any of four gate signals in `localStorage` — `${storagePrefix}:visible` set to `1`, persisted overrides (checked as `${storagePrefix}-state-v4`, falling back to `-state-v3` / `-state-v2` for pre-migration sessions — see §9), the owner-autoload flag (`${storagePrefix}:autoload` set to `1`), or the element-path inspector enabled.
|
|
537
564
|
- 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. **General visitors** (none of the four signals set) pay zero bundle cost.
|
|
538
565
|
|
|
539
566
|
For a Vite-only / non-Astro host, mount it yourself by importing the adapter module after `configurePanel(...)`. See §11.5.
|
|
@@ -693,6 +720,13 @@ export interface ColorClusterExtras {
|
|
|
693
720
|
colorScheme: string;
|
|
694
721
|
colorMode: false | { defaultMode: 'light' | 'dark'; lightScheme: string; darkScheme: string };
|
|
695
722
|
};
|
|
723
|
+
/**
|
|
724
|
+
* Optional config-time override map for a semantic tier's derived defaults,
|
|
725
|
+
* keyed by semantic item id. This is the ONLY way to ship a `{ literal }` /
|
|
726
|
+
* `{ literal: { light, dark } }` / `{ ref }` default — `TierItem.default`
|
|
727
|
+
* itself stays a plain string, consumed generically by every tab kind.
|
|
728
|
+
*/
|
|
729
|
+
semanticDefaults?: Record<string, SemanticValue>;
|
|
696
730
|
}
|
|
697
731
|
|
|
698
732
|
export interface ColorScheme {
|
|
@@ -712,6 +746,30 @@ export interface ColorScheme {
|
|
|
712
746
|
> re-exported from the package root:
|
|
713
747
|
> `import type { ColorClusterConfig } from '@takazudo/zdtp'`.
|
|
714
748
|
|
|
749
|
+
**`colorExtras.semanticDefaults`.** Without this field, a semantic tier's default `SemanticValue` is DERIVED from each `TierItem.default` string (palette-index lookup, or a literal fallback). `semanticDefaults` lets a host override that derivation for specific semantic item ids and ship a default the plain-string `default` field can't express — most usefully a per-mode literal that resolves via CSS `light-dark()`:
|
|
750
|
+
|
|
751
|
+
```ts
|
|
752
|
+
const colorTab: TabConfig = {
|
|
753
|
+
id: 'color',
|
|
754
|
+
label: 'Color',
|
|
755
|
+
tiers: [paletteTier, semanticTier],
|
|
756
|
+
colorExtras: {
|
|
757
|
+
id: 'myapp',
|
|
758
|
+
baseRoles: { background: '--myapp-bg', foreground: '--myapp-fg' },
|
|
759
|
+
baseDefaults: {},
|
|
760
|
+
defaultShikiTheme: 'dracula',
|
|
761
|
+
colorSchemes: { /* ... */ },
|
|
762
|
+
panelSettings: { colorScheme: 'Default', colorMode: false },
|
|
763
|
+
semanticDefaults: {
|
|
764
|
+
// Keyed by the semantic TierItem's `id`, not its cssVar.
|
|
765
|
+
danger: { literal: { light: '#b91c1c', dark: '#f87171' } },
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
A key present in `semanticDefaults` wins verbatim over the derived value; keys not listed fall back to the normal derivation from `TierItem.default`.
|
|
772
|
+
|
|
715
773
|
### 7.2 JSON-serializable constraint (important)
|
|
716
774
|
|
|
717
775
|
**Every field on the color `TabConfig` (including `colorExtras` and every
|
|
@@ -888,9 +946,10 @@ Behaviour notes:
|
|
|
888
946
|
|
|
889
947
|
| Logical key | Derivation | Purpose |
|
|
890
948
|
| ----------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
891
|
-
| `state-
|
|
892
|
-
| `state-
|
|
893
|
-
| `state-
|
|
949
|
+
| `state-v4` | `${storagePrefix}-state-v4` | Current unified envelope. Same top-level slices as v3, EXCEPT `color` (and optional `secondary`) is an identity-keyed map — one `ColorTweakState` slot per active-scheme identity — instead of a single flat object. See "Per-scheme color persistence" below. |
|
|
950
|
+
| `state-v3` | `${storagePrefix}-state-v3` | Legacy pre-v4 format (flat, single-slot `color`). Migrated into `state-v4` on first load; the v3 key itself is left in place (not deleted) so a downgrade can still read it. |
|
|
951
|
+
| `state-v2` | `${storagePrefix}-state-v2` | Legacy pre-v3 format. Migrated into `state-v3` (and from there into `state-v4`) on first load, then deleted. |
|
|
952
|
+
| `state-v1` | `${storagePrefix}-state` | Legacy pre-v2 flat-state format (Color-only). Migrated into `state-v3` (and from there into `state-v4`) on first load, then deleted. |
|
|
894
953
|
| `open` | `${storagePrefix}-open` | Mirror of the panel's `open` boolean (synchronous mount-time read — preserves user intent across reloads, fixes #1549). |
|
|
895
954
|
| `position` | `${storagePrefix}-position` | Drag position `{ top, right }` so the panel reappears where the user left it. |
|
|
896
955
|
| `visible` | `${storagePrefix}:visible` | Adapter-level visibility-intent flag, owned by the lazy-load gate. |
|
|
@@ -899,6 +958,7 @@ Behaviour notes:
|
|
|
899
958
|
For example, with `storagePrefix: 'myapp-design-token-panel'`:
|
|
900
959
|
|
|
901
960
|
```
|
|
961
|
+
myapp-design-token-panel-state-v4
|
|
902
962
|
myapp-design-token-panel-state-v3
|
|
903
963
|
myapp-design-token-panel-state-v2
|
|
904
964
|
myapp-design-token-panel-state
|
|
@@ -912,18 +972,40 @@ myapp-design-token-panel:autoload
|
|
|
912
972
|
|
|
913
973
|
The `visible` and `autoload` keys use a literal `:` separator, not `-`. Every other derived key uses `-`. The colon form is intentional for these adapter-level flags — see [`PORTABLE-CONTRACT.md`](./PORTABLE-CONTRACT.md) §2 for the historical reason. Don't try to "normalize" them; the unit tests assert this specific shape.
|
|
914
974
|
|
|
915
|
-
###
|
|
975
|
+
### Per-scheme color persistence (v4 envelope)
|
|
976
|
+
|
|
977
|
+
Color/secondary tweaks are persisted **per (scheme, mode) identity**, inside the single `state-v4` storage key — there is no per-scheme key fan-out. The identity is exactly the active scheme name the panel already resolves for seeding (`getActiveSchemeName`): a cluster without `colorMode` resolves one constant identity (one slot, effectively the old global behavior); a cluster with light/dark `colorMode` resolves a distinct identity per side, so a light tweak and a dark tweak occupy independent slots in the same envelope:
|
|
978
|
+
|
|
979
|
+
```jsonc
|
|
980
|
+
{
|
|
981
|
+
"color": {
|
|
982
|
+
"Default Light": { "palette": [...], "semanticMappings": {...}, /* ... */ },
|
|
983
|
+
"Default Dark": { "palette": [...], "semanticMappings": {...}, /* ... */ }
|
|
984
|
+
},
|
|
985
|
+
"secondary": { "Default Light": { /* ... */ } }, // optional, same identity keying
|
|
986
|
+
"spacing": { /* ... */ },
|
|
987
|
+
"typography": { /* ... */ },
|
|
988
|
+
"size": { /* ... */ }
|
|
989
|
+
}
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
The `spacing` / `typography` / `size` (and generic `tabs`) slices stay **global and unkeyed**, exactly as in v1–v3 — a scheme/mode toggle never touches them.
|
|
916
993
|
|
|
917
|
-
|
|
994
|
+
When the host dispatches a `color-scheme-changed` event (e.g. a light/dark toggle), the panel re-seeds its live color state for the newly active identity:
|
|
918
995
|
|
|
919
|
-
|
|
996
|
+
- If that identity already has a persisted slot, the slot's stored `color` (and `secondary`) state is loaded and re-applied — a per-scheme tweak now **survives** a round-trip through another scheme and back.
|
|
997
|
+
- If the identity has no slot yet (never tweaked under this scheme before), the panel cold-seeds from that scheme's defaults instead — the pre-existing "adopt the new scheme's palette" behavior for a scheme you haven't touched.
|
|
920
998
|
|
|
921
|
-
|
|
999
|
+
Editing color under scheme A only ever overwrites scheme A's slot (`writeMergedV4` merge-saves: it reads the existing envelope, replaces just the active identity's slot, and preserves every other identity's slot untouched) — it never mutates scheme B's stored state. A full panel reset (Reset / Apply) still wipes every slice via `clearAppliedStyles`.
|
|
1000
|
+
|
|
1001
|
+
**Migration.** On first load with no `state-v4` key, the legacy v1/v2/v3 chain runs unchanged (v3 wins over v2 over v1, exactly as before) and the resulting single flat `TweakState` is filed into `state-v4` under whichever identity is active at that moment — every subsequent load reads `state-v4` first. The legacy v3/v2/v1 keys are left in place by the v4 migration step (only the v1→v2→v3 chain deletes as it goes), so a host that needs to downgrade can still read them.
|
|
1002
|
+
|
|
1003
|
+
**Host-owned `color-scheme` is never touched.** A host that manages its own `<html style="color-scheme">` (e.g. a site-level light/dark toggle) has that inline style tracked separately from the panel's own writes; the panel's mount/clear paths only ever remove a `color-scheme` value that the panel itself applied, never a host-owned one (#501).
|
|
922
1004
|
|
|
923
1005
|
Two consequences worth knowing as a host integrator:
|
|
924
1006
|
|
|
925
|
-
- The event handler does not rewrite
|
|
926
|
-
- Do **not** manually delete the tweak keys on a scheme toggle. The panel already
|
|
1007
|
+
- The event handler does not rewrite `localStorage` directly beyond what's described above — it reads/re-seeds the live (in-memory) state and clears the inline overrides for the previous identity. The next in-panel edit (or a full reload via `loadPersistedState`) is what persists the current identity's state.
|
|
1008
|
+
- Do **not** manually delete the tweak keys on a scheme toggle. The panel already handles per-identity persistence on the event, and a manual envelope delete would also discard the scheme-independent `spacing` / `typography` / `size` overrides.
|
|
927
1009
|
|
|
928
1010
|
---
|
|
929
1011
|
|
|
@@ -943,6 +1025,27 @@ window.myapp.disableAutoload(); // disarm owner-mode; unmounts the panel
|
|
|
943
1025
|
|
|
944
1026
|
All helpers 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.
|
|
945
1027
|
|
|
1028
|
+
### Fixed-name global: `window.zdtp`
|
|
1029
|
+
|
|
1030
|
+
`consoleNamespace` is a **required** field on `PanelConfig` — every consumer picks its own value, so `window[consoleNamespace].*` needs the host's chosen namespace before you can open the panel from the console. `window.zdtp` is an additive, fixed-name alias that needs none:
|
|
1031
|
+
|
|
1032
|
+
```ts
|
|
1033
|
+
zdtp.show(); // open the panel (lazy-loads the bundle on first call, same as showDesignPanel())
|
|
1034
|
+
zdtp.hide(); // close the panel
|
|
1035
|
+
zdtp.toggle(); // toggle open/closed
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
- **`show` / `hide` / `toggle` only.** There is no `zdtp.enableAutoload()` — owner-autoload (§10.1) stays on `window[consoleNamespace].*` and the package-root `enableAutoload()` / `disableAutoload()` exports.
|
|
1039
|
+
- **`window[consoleNamespace].*` is unchanged** — `window.zdtp` is sugar layered on top of it, not a replacement. Both stay available side by side.
|
|
1040
|
+
- **Targets the default instance**, exactly like the package-root `showDesignTokenPanel()` / `hideDesignTokenPanel()` / `toggleDesignPanel()` exports and `window[consoleNamespace].*` itself. On an Astro host specifically, the alias binds to whichever instance's adapter script installs it first, rather than re-resolving "the current default" on every call — see `PORTABLE-CONTRACT.md` §6.5 for the exact per-install-site rule. For a page with more than one panel instance, use `configurePanel(cfg)`'s returned handle (§5.2) instead — it is unambiguous regardless of install order.
|
|
1041
|
+
- **Available on both integration paths:**
|
|
1042
|
+
- Non-Astro hosts get it as soon as `@takazudo/zdtp`'s package-root module has loaded (it installs the alias at module init).
|
|
1043
|
+
- Astro hosts get it as soon as the host-adapter `<script>` has run — **before** the panel bundle itself has loaded. The first `zdtp.*` call lazy-imports the bundle, exactly like `window[consoleNamespace].*`.
|
|
1044
|
+
- **Never clobbers a host-defined `window.zdtp`.** If your page already has its own `window.zdtp` for something unrelated, the package leaves it alone and logs a `console.warn` instead of overwriting it — including the edge case of choosing `consoleNamespace: 'zdtp'` yourself.
|
|
1045
|
+
- **Auto-remember applies too** — `zdtp.show()` arms the `:autoload` flag exactly like `showDesignPanel()` (§10.1's Auto-remember footgun note applies here as well).
|
|
1046
|
+
|
|
1047
|
+
See `PORTABLE-CONTRACT.md` §6.5 for the full install-site and no-clobber/no-double-install contract.
|
|
1048
|
+
|
|
946
1049
|
### Backward-compatible: the on-demand flow is unchanged
|
|
947
1050
|
|
|
948
1051
|
The pre-existing on-demand usage — calling `window.<consoleNamespace>.showDesignPanel()` / `hideDesignPanel()` / `toggleDesignPanel()` to lazy-import and initialize the panel — is **unchanged and fully supported**. Owner-autoload (§10.1) is an opt-in layered on top, not a replacement. If you never call `enableAutoload()`, the panel behaves exactly as it always did: nothing loads for any visitor until the first console call.
|
|
@@ -1032,6 +1135,39 @@ Because the fallback ladder is host-CSS-var-driven, a brand-new consumer can mou
|
|
|
1032
1135
|
|
|
1033
1136
|
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`.
|
|
1034
1137
|
|
|
1138
|
+
### 11.2 Apply-to-disk for a Tailwind v4 `@theme` block
|
|
1139
|
+
|
|
1140
|
+
The section above is about the panel's OWN chrome — it never needed Tailwind. This section is about the opposite direction: a Tailwind v4 **consumer** whose design tokens live inside `@theme { ... }` (so Tailwind can generate utility classes from them) rather than `:root`.
|
|
1141
|
+
|
|
1142
|
+
The apply-to-disk rewriter (§3.2) scans the first top-level `:root` block AND the first top-level `@theme` block, so a Tailwind v4 token file like this applies out of the box (issue #496's repro):
|
|
1143
|
+
|
|
1144
|
+
```css
|
|
1145
|
+
:root {
|
|
1146
|
+
--palette-cool-700: oklch(0.21 0.03 264);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
@theme {
|
|
1150
|
+
--spacing-md: 0.75rem;
|
|
1151
|
+
--color-ink: light-dark(var(--palette-cool-700), var(--palette-cool-50));
|
|
1152
|
+
}
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
POSTing overrides for `--palette-cool-700` (in `:root`), `--spacing-md`, or `--color-ink` (both in `@theme`) all land in `changed` — no indirection required. A file that is 100% `@theme` (no `:root` block at all) also applies cleanly; only a file with **neither** block 409s.
|
|
1156
|
+
|
|
1157
|
+
**Legacy workaround (no longer required).** Before the rewriter learned to scan `@theme`, the only way to make a Tailwind-`@theme` token editable was to declare the editable value in `:root` and have `@theme` alias it via `var(...)`:
|
|
1158
|
+
|
|
1159
|
+
```css
|
|
1160
|
+
:root {
|
|
1161
|
+
--spacing-md-editable: 0.75rem; /* apply pipeline writes here */
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
@theme {
|
|
1165
|
+
--spacing-md: var(--spacing-md-editable); /* Tailwind reads the alias */
|
|
1166
|
+
}
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
This indirection is no longer necessary — declare the token directly inside `@theme` as shown above. The alias pattern still works (it's just two ordinary top-level blocks), so existing consumers are not required to migrate.
|
|
1170
|
+
|
|
1035
1171
|
---
|
|
1036
1172
|
|
|
1037
1173
|
## 12. Bundler notes
|
|
@@ -1112,6 +1248,17 @@ This recipe walks through wiring the panel into a project that does not currentl
|
|
|
1112
1248
|
|
|
1113
1249
|
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`).
|
|
1114
1250
|
|
|
1251
|
+
`schemaId` is display-only (see §5.3) — it never gates import/export, so picking any string here is safe. If you want the Import modal's copy to name the ACTUAL schema your export/import round-trips through, import `SCHEMA_V1` / `SCHEMA_V2` / `SCHEMA_V3` from the package root and set `schemaId` to one of them instead of an arbitrary string:
|
|
1252
|
+
|
|
1253
|
+
```ts
|
|
1254
|
+
import { SCHEMA_V3 } from '@takazudo/zdtp';
|
|
1255
|
+
|
|
1256
|
+
export const myPanelConfig: PanelConfig = {
|
|
1257
|
+
// ...
|
|
1258
|
+
schemaId: SCHEMA_V3, // truthful label — matches what serialize()/deserialize() actually validate against
|
|
1259
|
+
};
|
|
1260
|
+
```
|
|
1261
|
+
|
|
1115
1262
|
3. **Author your token manifest in the host project.**
|
|
1116
1263
|
|
|
1117
1264
|
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.
|
|
@@ -2,79 +2,186 @@
|
|
|
2
2
|
* Pure, IO-free CSS custom-property value replacement.
|
|
3
3
|
*
|
|
4
4
|
* `applyTokenOverrides(source, overrides)` rewrites the values of one or more
|
|
5
|
-
* CSS custom properties inside the FIRST top-level `:root { ... }` block
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* CSS custom properties inside the FIRST top-level `:root { ... }` block AND
|
|
6
|
+
* the FIRST top-level `@theme { ... }` block (Tailwind v4 design-token scope)
|
|
7
|
+
* of a CSS file, preserving surrounding whitespace and trailing inline
|
|
8
|
+
* comments. The module is the regex foundation that the dev-API endpoint
|
|
8
9
|
* wraps; it performs no IO, no DOM access, and is safe to import in any
|
|
9
10
|
* environment.
|
|
10
11
|
*
|
|
11
12
|
* Behavior summary
|
|
12
13
|
* ----------------
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
14
|
+
* - Two top-level blocks are scanned: the FIRST top-level `:root { ... }` block
|
|
15
|
+
* and the FIRST top-level `@theme { ... }` block (first-of-each-kind only).
|
|
16
|
+
* Later blocks of either kind, and tokens inside `@media`, `@layer`,
|
|
17
|
+
* `@supports`, or nested blocks, are IGNORED.
|
|
18
|
+
* - `:root`-first-match-wins: for each override var, `:root` is tried first and
|
|
19
|
+
* `@theme` is the fallback. A var declared in BOTH blocks is rewritten only
|
|
20
|
+
* in `:root`.
|
|
21
|
+
* - `@theme` accepts an optional modifier — `@theme`, `@theme inline`,
|
|
22
|
+
* `@theme static`, `@theme reference`, or a combination — because Tailwind v4
|
|
23
|
+
* prescribes `@theme inline` precisely when theme values reference other
|
|
24
|
+
* variables (the mainstream case). `:root` matching stays strict: grouped
|
|
25
|
+
* selectors such as `:root, html { ... }` are NOT treated as a `:root` block.
|
|
15
26
|
* - Per-cssVar regex: `(--name:)\s*([\s\S]+?);` with the name regex-escaped.
|
|
16
27
|
* The non-greedy value capture stops at the first `;`, which preserves any
|
|
17
28
|
* trailing `/* ... */` inline comment on the same logical line.
|
|
18
29
|
* - A whitespace-trimmed comparison between the old value and the override
|
|
19
30
|
* decides whether the cssVar lands in `changed` or `unchanged`.
|
|
20
31
|
* - Idempotent: applying the same overrides twice in a row reports every key
|
|
21
|
-
* as `unchanged` on the second call and produces byte-identical output.
|
|
32
|
+
* as `unchanged` on the second call and produces byte-identical output. The
|
|
33
|
+
* two non-overlapping regions are spliced back in DESCENDING start order so
|
|
34
|
+
* the earlier region's offsets stay valid through the later region's splice.
|
|
35
|
+
* - `unknownOutsideBlock` (#508): a whole-file scan (ignoring the "first
|
|
36
|
+
* block of each kind" and "no nesting" restrictions above) reclassifies any
|
|
37
|
+
* `unknown` key that IS declared somewhere in the file, so callers can tell
|
|
38
|
+
* "declared, but outside a scanned block" apart from "genuinely absent".
|
|
22
39
|
*
|
|
23
40
|
* Known limitations (if a richer transform is ever needed, swap this module
|
|
24
41
|
* for a real CSS parser such as postcss):
|
|
25
42
|
* - Only `:root` as a bare selector is recognized. Grouped selectors such as
|
|
26
43
|
* `:root, html { ... }` are NOT treated as a `:root` block.
|
|
27
|
-
* -
|
|
28
|
-
*
|
|
44
|
+
* - Only the FIRST top-level block of each kind is scanned; a second top-level
|
|
45
|
+
* `:root` or `@theme` block is ignored.
|
|
46
|
+
* - Nested `:root` / `@theme` under `@media` / `@layer` / `@supports` is
|
|
47
|
+
* intentionally skipped; see the test matrix in the companion `__tests__`
|
|
48
|
+
* file.
|
|
29
49
|
* - Values containing a literal `;` inside a string or comment would confuse
|
|
30
50
|
* the non-greedy regex. Not supported.
|
|
31
|
-
* - If the same cssVar is declared multiple times
|
|
51
|
+
* - If the same cssVar is declared multiple times inside a single scanned
|
|
32
52
|
* block, only the FIRST occurrence is rewritten.
|
|
33
53
|
*/
|
|
34
54
|
export interface ApplyResult {
|
|
35
55
|
/** The new CSS file contents (byte-identical to `source` when nothing
|
|
36
|
-
* changed, including the "no
|
|
56
|
+
* changed, including the "no scannable block" case). */
|
|
37
57
|
updated: string;
|
|
38
58
|
/** cssVar names actually rewritten (trimmed new value differs from old). */
|
|
39
59
|
changed: string[];
|
|
40
|
-
/** cssVar names present in
|
|
41
|
-
* the override. */
|
|
60
|
+
/** cssVar names present in a scanned block whose trimmed value already
|
|
61
|
+
* matched the override. */
|
|
42
62
|
unchanged: string[];
|
|
43
|
-
/** cssVar names in `overrides` that were not found in the first
|
|
44
|
-
* `:root` block
|
|
45
|
-
*
|
|
63
|
+
/** cssVar names in `overrides` that were not found in EITHER the first
|
|
64
|
+
* top-level `:root` block or the first top-level `@theme` block (the name
|
|
65
|
+
* is absent, or neither block exists). */
|
|
46
66
|
unknown: string[];
|
|
67
|
+
/**
|
|
68
|
+
* Subset of `unknown` (#508): cssVar names that ARE declared somewhere in
|
|
69
|
+
* `source` — found by a whole-file scan, not restricted to the two scanned
|
|
70
|
+
* blocks — but outside both of them, so nothing was rewritten. Typical
|
|
71
|
+
* causes: the declaration sits in a nested block (`@media`/`@layer`/
|
|
72
|
+
* `@supports`), a grouped selector (`:root, html { ... }`), or a SECOND
|
|
73
|
+
* top-level `:root`/`@theme` block (the scanner only reads the first of
|
|
74
|
+
* each kind). Lets callers distinguish "declared, but Apply doesn't rewrite
|
|
75
|
+
* this location" from "genuinely absent / typo'd name" — the remaining
|
|
76
|
+
* `unknown` entries not in this list.
|
|
77
|
+
*/
|
|
78
|
+
unknownOutsideBlock: string[];
|
|
47
79
|
}
|
|
48
80
|
/**
|
|
49
|
-
* Thrown by {@link applyTokenOverridesOrThrow} when the source has
|
|
50
|
-
* top-level `:root { ... }` block
|
|
51
|
-
* diagnostic message without having to probe the
|
|
81
|
+
* Thrown by {@link applyTokenOverridesOrThrow} when the source has neither a
|
|
82
|
+
* top-level `:root { ... }` block nor a top-level `@theme { ... }` block. Lets
|
|
83
|
+
* the dev-API handler surface a diagnostic message without having to probe the
|
|
84
|
+
* result shape.
|
|
52
85
|
*/
|
|
53
|
-
export declare class
|
|
86
|
+
export declare class NoTokenBlockError extends Error {
|
|
54
87
|
constructor(message?: string);
|
|
55
88
|
}
|
|
56
89
|
/**
|
|
57
|
-
*
|
|
58
|
-
* `
|
|
90
|
+
* @deprecated Retained as an alias for {@link NoTokenBlockError} so existing
|
|
91
|
+
* `instanceof` / import sites keep working. The scan now covers `@theme`
|
|
92
|
+
* blocks too, so "NoRootBlock" is no longer literally accurate.
|
|
93
|
+
*/
|
|
94
|
+
export declare const NoRootBlockError: typeof NoTokenBlockError;
|
|
95
|
+
export interface BlockBounds {
|
|
96
|
+
/** Index of the first character INSIDE the block (just after the opening
|
|
97
|
+
* `{`). */
|
|
98
|
+
contentStart: number;
|
|
99
|
+
/** Index of the matching `}` (exclusive end of block content). */
|
|
100
|
+
contentEnd: number;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Bounds of the first top-level `:root { ... }` block, or `null`. Grouped
|
|
104
|
+
* selectors (`:root, html { ... }`) are intentionally NOT matched.
|
|
105
|
+
*/
|
|
106
|
+
export declare function findFirstTopLevelRootBlock(source: string): BlockBounds | null;
|
|
107
|
+
/**
|
|
108
|
+
* Bounds of the first top-level `@theme { ... }` block, or `null`. Tolerates an
|
|
109
|
+
* optional modifier (`@theme inline`, `@theme static`, ...) between `@theme`
|
|
110
|
+
* and the opening `{`.
|
|
111
|
+
*/
|
|
112
|
+
export declare function findFirstTopLevelThemeBlock(source: string): BlockBounds | null;
|
|
113
|
+
export type SingleReplaceStatus = 'changed' | 'unchanged' | 'unknown';
|
|
114
|
+
export interface SingleReplaceResult {
|
|
115
|
+
content: string;
|
|
116
|
+
status: SingleReplaceStatus;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Replace every CSS block comment (`/* ... */`) with same-length whitespace
|
|
120
|
+
* so the per-declaration regex below cannot match a fake declaration that
|
|
121
|
+
* lives INSIDE a comment. Same-length is critical: the masked string is used
|
|
122
|
+
* only as a search space, but the resulting `match.index` is then sliced
|
|
123
|
+
* straight back into the original (un-masked) `content`, so masked and
|
|
124
|
+
* original must agree byte-for-byte on offsets.
|
|
125
|
+
*
|
|
126
|
+
* Pre-fix behaviour: a `:root` containing
|
|
127
|
+
*
|
|
128
|
+
* /* --zd-foo: red; */
|
|
129
|
+
* --zd-foo: blue;
|
|
59
130
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
|
|
64
|
-
|
|
131
|
+
* would have its commented-out value rewritten by the regex (it matches the
|
|
132
|
+
* FIRST occurrence), leaving the live declaration untouched and silently
|
|
133
|
+
* corrupting the user-authored comment.
|
|
134
|
+
*/
|
|
135
|
+
export declare function maskComments(input: string): string;
|
|
136
|
+
/**
|
|
137
|
+
* Replace the value of a single custom property inside `content`. The name
|
|
138
|
+
* may be supplied with or without the leading `--`. Returns the updated
|
|
139
|
+
* content along with a status flag used to populate `changed` / `unchanged`
|
|
140
|
+
* / `unknown` in the top-level {@link ApplyResult}.
|
|
141
|
+
*
|
|
142
|
+
* The match is run against a comment-masked copy of `content` so a
|
|
143
|
+
* commented-out declaration cannot shadow a live one (B4 fix). Edits are
|
|
144
|
+
* still spliced back into the original `content` so trailing inline comments
|
|
145
|
+
* survive byte-for-byte.
|
|
146
|
+
*/
|
|
147
|
+
export declare function replaceOne(content: string, cssVarName: string, newValue: string): SingleReplaceResult;
|
|
148
|
+
/**
|
|
149
|
+
* Rewrite `overrides` into the first top-level `:root { ... }` block AND the
|
|
150
|
+
* first top-level `@theme { ... }` block of `source`, returning an
|
|
151
|
+
* {@link ApplyResult} describing the outcome.
|
|
152
|
+
*
|
|
153
|
+
* Per override var, `:root` is tried first and `@theme` is the fallback, so a
|
|
154
|
+
* var present in both blocks is rewritten only in `:root` (`:root`-first-match-
|
|
155
|
+
* wins). The two non-overlapping block regions are spliced back into `source`
|
|
156
|
+
* in DESCENDING start order so the earlier region's offsets stay valid.
|
|
157
|
+
*
|
|
158
|
+
* If `source` has neither block, the function returns `source` unchanged with
|
|
159
|
+
* every key routed to `unknown` — the companion {@link applyTokenOverridesOrThrow}
|
|
160
|
+
* raises {@link NoTokenBlockError} in that case so callers that need a hard
|
|
161
|
+
* failure can distinguish it from a file where merely none of the overrides
|
|
162
|
+
* matched.
|
|
65
163
|
*/
|
|
66
164
|
export declare function applyTokenOverrides(source: string, overrides: Record<string, string>): ApplyResult;
|
|
67
165
|
/**
|
|
68
166
|
* Throwing variant. Same shape as {@link applyTokenOverrides} except it
|
|
69
|
-
* raises {@link
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* problem rather than
|
|
167
|
+
* raises {@link NoTokenBlockError} when the source has neither a top-level
|
|
168
|
+
* `:root { ... }` block nor a top-level `@theme { ... }` block, instead of
|
|
169
|
+
* returning every override under `unknown`. Use this from server-side handlers
|
|
170
|
+
* where "no writable token block" is a fatal configuration problem rather than
|
|
171
|
+
* an expected state.
|
|
73
172
|
*/
|
|
74
173
|
export declare function applyTokenOverridesOrThrow(source: string, overrides: Record<string, string>): ApplyResult;
|
|
75
174
|
/**
|
|
76
175
|
* Low-level predicate exposed primarily for callers that want to report a
|
|
77
176
|
* precise diagnostic without running a full rewrite. Returns `true` iff the
|
|
78
|
-
* source contains at least one top-level
|
|
177
|
+
* source contains at least one scannable top-level token block — a `:root`
|
|
178
|
+
* block OR a `@theme` block. Consistent with the {@link applyTokenOverridesOrThrow}
|
|
179
|
+
* throw condition (which fires only when this returns `false`).
|
|
180
|
+
*/
|
|
181
|
+
export declare function hasApplicableTokenBlock(source: string): boolean;
|
|
182
|
+
/**
|
|
183
|
+
* @deprecated Renamed to {@link hasApplicableTokenBlock} now that the scan
|
|
184
|
+
* covers `@theme` blocks too. Retained as an alias so existing call sites keep
|
|
185
|
+
* working; it returns `true` for a `@theme`-only file (no `:root`).
|
|
79
186
|
*/
|
|
80
|
-
export declare
|
|
187
|
+
export declare const hasTopLevelRootBlock: typeof hasApplicableTokenBlock;
|