@takazudo/zdtp 0.2.3 → 0.3.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 CHANGED
@@ -1,7 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Features
6
+
7
+ - **Multi-instance support.** `configurePanel(config)` now returns a `PanelInstanceHandle` and supports multiple independent panel instances on one page. Calling it with a distinct `storagePrefix` registers a new instance (independent storage keys, DOM root, toggle event, apply target). Calling it again with the same prefix and structurally-equal config is a no-op that returns the same handle (covers Astro view-transition reruns). Calling it with the same prefix but a structurally-different config throws immediately (`RECONFIGURE_RULE = 'reject-with-error'`); call `handle.destroy()` first to re-configure a prefix. Additive and backward-compatible — single-panel hosts observe no change. ([#353](https://github.com/Takazudo/zudo-design-token-panel/issues/353))
8
+
9
+ - **`PanelInstanceHandle`.** `configurePanel` now returns a handle with `{ instanceId, open(), close(), toggle(), destroy() }`. `instanceId` equals `storagePrefix`. `destroy()` deregisters the instance, unmounts its Preact tree, removes its DOM root, and unbinds its toggle-event listener — freeing the prefix for re-configuration. ([#353](https://github.com/Takazudo/zudo-design-token-panel/issues/353))
10
+
11
+ - **Per-instance toggle events.** The default instance (the historical `storagePrefix`) keeps `toggle-design-token-panel` unchanged. Any instance with a non-default prefix listens on `config.toggleEvent` when supplied, or `toggle-${storagePrefix}` by default — giving each instance its own independent toggle channel. `PanelConfig.toggleEvent?: string` is a new optional field. ([#354](https://github.com/Takazudo/zudo-design-token-panel/issues/354))
12
+
13
+ - **`PanelConfig.applySink`.** An optional `{ apply(pairs), clear(names) }` sink routes this instance's CSS-var writes and clears through a caller-supplied object instead of `document.documentElement`. Useful for shadow DOM, iframe, or test-spy contexts. `apply` = upsert; `clear` = remove. Reset sends the full token-name set for the instance to `sink.clear` so the sink target is completely cleaned. Sink errors are non-fatal (`console.warn`). The host owns the sink target's lifecycle. `applySink` carries function references and must not be passed through the Astro inline JSON config. ([#355](https://github.com/Takazudo/zudo-design-token-panel/issues/355))
14
+
3
15
  ## 0.2.3
4
16
 
17
+ ### Bug Fixes
18
+
19
+ - A host `color-scheme-changed` event (light/dark toggle) no longer wipes the user's `spacing` / `typography` / `size` tweaks from the live panel. The scheme-change handler now clears only the color cluster's inline `:root` vars and re-seeds only the `color` (and optional `secondary`) slices, leaving the scheme-independent non-color slices — and their applied inline vars — intact. Previously it called the full `clearAppliedStyles()` + `freshTweakState()`, which stripped every spacing/font/size var and emptied those live slices, so the next in-panel edit permanently persisted the loss. A new internal `clearAppliedColorStyles()` performs the color-only clear; full resets (Reset / Apply) keep using `clearAppliedStyles()`. ([#347](https://github.com/Takazudo/zudo-design-token-panel/issues/347))
20
+
5
21
  ### Features
6
22
 
7
23
  - Make `ColorScheme.shikiTheme` optional so hosts can pass their color-scheme maps without a dummy `shikiTheme` or an `as unknown as` cast — the runtime already falls back to the cluster's `defaultShikiTheme`. The hydrated `ColorTweakState.shikiTheme` stays required (it is always defaulted, and `TweakState` is re-exported, so keeping it required avoids widening the public `state.color.shikiTheme` type). (e057388, fd87423, [#342](https://github.com/Takazudo/zudo-design-token-panel/issues/342))
package/README.md CHANGED
@@ -432,31 +432,98 @@ The zfb (zudo-front-builder) integration is documented in that project's own rep
432
432
 
433
433
  ## 5. `configurePanel()` and the `PanelConfig` shape
434
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.
435
+ `configurePanel(config)` is the multi-instance init. It returns a `PanelInstanceHandle`. 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
436
 
437
437
  ```ts
438
438
  import { configurePanel, type PanelConfig } from '@takazudo/zdtp';
439
439
 
440
- configurePanel(myPanelConfig);
440
+ const handle = configurePanel(myPanelConfig);
441
+ // handle.instanceId === myPanelConfig.storagePrefix
442
+ // handle.open() / close() / toggle() / destroy()
441
443
  ```
442
444
 
443
445
  ### 5.1 Behaviour
444
446
 
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.
447
+ - **Multi-instance.** Call `configurePanel` with a **distinct** `storagePrefix` to register an independent panel instance (independent storage keys, DOM root, toggle event, and apply target). No throw.
448
+ - **Idempotent for same prefix+config.** Calling `configurePanel` again with the same `storagePrefix` and structurally-equal config values is a no-op and returns the same handle. This covers Astro view-transition reruns that re-parse the inline JSON config.
449
+ - **Same-prefix-different-config THROWS.** Calling `configurePanel` with an already-registered prefix but a different config throws immediately. To re-configure a prefix, call `handle.destroy()` first, then `configurePanel` again.
446
450
  - **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).
451
+ - **JSON-serializable input (except `applySink`).** Every nested field other than `applySink` MUST round-trip through `JSON.stringify` / `JSON.parse` without loss. `applySink` carries function references and must not be passed through the Astro inline JSON config.
448
452
 
449
- ### 5.2 Field summary
453
+ ### 5.2 `PanelInstanceHandle`
454
+
455
+ ```ts
456
+ export interface PanelInstanceHandle {
457
+ /** Stable instance id — equal to the instance's `storagePrefix`. */
458
+ readonly instanceId: string;
459
+ open(): void; // show this instance's panel
460
+ close(): void; // hide this instance's panel
461
+ toggle(): void; // toggle open/closed
462
+ /**
463
+ * Deregister this instance. Unmounts Preact tree, removes DOM root,
464
+ * unbinds toggle-event listener. Prefix can then be re-configured.
465
+ */
466
+ destroy(): void;
467
+ }
468
+ ```
469
+
470
+ ### 5.3 Field summary
450
471
 
451
472
  | Field | Type | Purpose |
452
473
  | -------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
453
- | `storagePrefix` | `string` | Base for every derived `localStorage` key. See §9. |
474
+ | `storagePrefix` | `string` | Base for every derived `localStorage` key. Also the instance id. See §9. |
454
475
  | `consoleNamespace` | `string` | Global object the package installs `showDesignPanel` / `hideDesignPanel` / `toggleDesignPanel` on (e.g. `consoleNamespace: 'myapp'` → `window.myapp.showDesignPanel`). |
455
476
  | `modalClassPrefix` | `string` | BEM root class for every modal the panel owns (export, import, apply). Emits `${prefix}__overlay`, `${prefix}__panel`, etc. |
456
477
  | `schemaId` | `string` | `$schema` value emitted into export JSON and required on import. |
457
478
  | `exportFilenameBase` | `string` | Default download filename base — exports save as `${exportFilenameBase}.json`. |
479
+ | `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`. |
458
480
  | `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
481
  | `colorPresets` | `Record<string, ColorScheme>` (optional) | Optional named scheme presets surfaced in the Color tab "Scheme..." dropdown. Defaults to `{}`. See §7.5. |
482
+ | `applySink` | `ApplySink` (optional) | Optional sink that routes this instance's CSS-var writes off `:root`. See §5.4. Not JSON-serializable — do not include in Astro inline config. |
483
+
484
+ ### 5.4 `applySink` — optional write target
485
+
486
+ When `PanelConfig.applySink` is set for an instance, all CSS-var writes and clears for that instance route through the sink rather than `document.documentElement`. This enables embedding the panel in a shadow root, iframe, or test spy without touching `:root`.
487
+
488
+ ```ts
489
+ export interface ApplySink {
490
+ /** Upsert the given var name→value pairs on the sink target. */
491
+ apply(pairs: ReadonlyArray<readonly [string, string]>): void;
492
+ /** Remove the given var names from the sink target. */
493
+ clear(names: readonly string[]): void;
494
+ }
495
+ ```
496
+
497
+ Key behaviours:
498
+
499
+ - `apply` = **upsert**: set each named CSS var on the sink target.
500
+ - `clear` = **remove**: remove each named CSS var from the sink target.
501
+ - **Reset clears the full token set**: when the user clicks Reset, `sink.clear` receives every var the instance can own (not just dirty vars) so the sink target is fully cleaned.
502
+ - **Sink errors are non-fatal**: `console.warn` is emitted and the apply pipeline continues.
503
+ - **The host owns the sink target's lifecycle.** Keep the sink target alive as long as the panel instance is alive.
504
+ - **Not JSON-serializable.** Supply it after `configurePanel` via a custom adapter or by constructing the config object with the sink already attached.
505
+
506
+ ```ts
507
+ // Example: routing to a shadow root
508
+ const shadow = shadowHost.attachShadow({ mode: 'open' });
509
+
510
+ const handle = configurePanel({
511
+ storagePrefix: 'myapp-shadow-panel',
512
+ // ...other required fields...
513
+ applySink: {
514
+ apply(pairs) {
515
+ for (const [name, value] of pairs) {
516
+ (shadow.host as HTMLElement).style.setProperty(name, value);
517
+ }
518
+ },
519
+ clear(names) {
520
+ for (const name of names) {
521
+ (shadow.host as HTMLElement).style.removeProperty(name);
522
+ }
523
+ },
524
+ },
525
+ });
526
+ ```
460
527
 
461
528
  ### 5.3 Mount strategy & auto-mount
462
529
 
@@ -577,13 +644,15 @@ The panel walks each `TierItem` on apply:
577
644
 
578
645
  - If `readonly`, the row is display-only — no writes.
579
646
  - 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.
647
+ `id`, the panel writes `item.cssVar` ← value. Otherwise it removes the
648
+ inline property so the consumer's stylesheet default wins.
582
649
  - For a **ref-tier item** (`referencesTier` set): the persisted value is the id
583
650
  of an item in the referenced base tier. The apply pipeline emits
584
651
  `var(--base-tier-cssvar)` as the written value.
585
652
 
586
- The write target is always `:root`. No shadow DOM, no scoped overrides — the panel ships a global tweak intentionally.
653
+ The default write target is `:root` (`document.documentElement`). An instance
654
+ with `PanelConfig.applySink` set routes writes through the sink instead — see
655
+ §5.4 for the full sink contract.
587
656
 
588
657
  ---
589
658
 
@@ -842,7 +911,9 @@ The `visible` key uses a literal `:` separator, not `-`. Every other derived key
842
911
 
843
912
  Tweak state is **global**, not scheme-scoped: the keys above are derived only from `storagePrefix` and carry no color-scheme name. A single envelope holds every slice — the `color` slice stores **absolute** colors (palette + role/semantic indices resolved against that palette), while the `spacing` / `typography` / `size` slices store token overrides that are independent of the active scheme.
844
913
 
845
- Because the stored palette is absolute, it cannot simply be re-applied on top of a different scheme. So when the host dispatches a `color-scheme-changed` event (e.g. a light/dark toggle), the panel **re-seeds its live state from the newly active scheme**: it removes the inline `:root` overrides it had applied (`clearAppliedStyles`) and re-initializes the live envelope from that scheme (`freshTweakState`). Palette tweaks are therefore intentionally **not** carried across a scheme switch — the panel adopts the new scheme's palette rather than layering the previous absolute colors on top of it. This is the panel's deliberate global model; it does **not** persist a separate color slice per scheme.
914
+ Because the stored palette is absolute, it cannot simply be re-applied on top of a different scheme. So when the host dispatches a `color-scheme-changed` event (e.g. a light/dark toggle), the panel **re-seeds its live color state from the newly active scheme**: it removes only the inline `:root` color-cluster overrides it had applied (`clearAppliedColorStyles`) and re-initializes only the live `color` (and optional `secondary`) slices from that scheme. Palette tweaks are therefore intentionally **not** carried across a scheme switch — the panel adopts the new scheme's palette rather than layering the previous absolute colors on top of it. This is the panel's deliberate global model; it does **not** persist a separate color slice per scheme.
915
+
916
+ The `spacing` / `typography` / `size` slices are scheme-INDEPENDENT, so they are deliberately left untouched by the re-seed: their live values and their applied inline `:root` vars survive a scheme toggle (the scheme-change handler narrows the clear to the color cluster(s) for exactly this reason — see #347). A full panel reset (Reset / Apply) still wipes every slice via `clearAppliedStyles`.
846
917
 
847
918
  Two consequences worth knowing as a host integrator:
848
919
 
@@ -26,6 +26,7 @@
26
26
  * the native `close` event exactly once; `onClose` then fires exactly once
27
27
  * per dismissal regardless of the path taken.
28
28
  */
29
+ import { type PanelConfig } from './config/panel-config';
29
30
  import { type ColorTweakState, type TweakState } from './state/tweak-state';
30
31
  export interface ApplyModalProps {
31
32
  state: TweakState;
@@ -44,6 +45,15 @@ export interface ApplyModalProps {
44
45
  * clear any inline-applied styles, and reset in-memory state to empty.
45
46
  */
46
47
  onApplied: () => void;
48
+ /**
49
+ * The mounted panel instance's config (multi-instance, #357). When supplied,
50
+ * the modal resolves its apply routing / endpoint / secondary cluster / modal
51
+ * classes from THIS instance rather than the active default instance — so an
52
+ * Apply on panel A targets A's endpoint+routing, not whichever instance was
53
+ * configured last. Omitted (e.g. a direct test render) → `getPanelConfig()`,
54
+ * preserving the single-panel path.
55
+ */
56
+ instanceConfig?: PanelConfig;
47
57
  }
48
58
  export declare function ApplyModal(props: ApplyModalProps): import("preact").JSX.Element;
49
59
  //# sourceMappingURL=apply-modal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"apply-modal.d.ts","sourceRoot":"","sources":["../src/apply-modal.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAWH,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAK5E,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,eAAe,CAAC;IAChC;;;;OAIG;IACH,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAmJD,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,gCA0YhD"}
1
+ {"version":3,"file":"apply-modal.d.ts","sourceRoot":"","sources":["../src/apply-modal.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAKH,OAAO,EAKL,KAAK,WAAW,EACjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAC;AAK7B,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,eAAe,CAAC;IAChC;;;;OAIG;IACH,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,WAAW,CAAC;CAC9B;AA2JD,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,gCAwZhD"}
@@ -1,34 +1,34 @@
1
- import { c as u, g as l, a as f, b as p, d as h, e as m } from "../panel-config-DP2_P7dD.js";
2
- import { Z as w } from "../tweak-state-DSJa5csL.js";
3
- const r = "tokenpanel-config";
4
- function P() {
1
+ import { c as f, g as c, a as p, b as h, d as m, e as w } from "../panel-config-BOyB8Egg.js";
2
+ import { Z as P } from "../tweak-state-DMwBHPow.js";
3
+ const i = "tokenpanel-config";
4
+ function y() {
5
5
  if (typeof document > "u")
6
6
  throw new Error(
7
7
  "[design-token-panel] host-adapter loaded without a document; expected to run in a browser context."
8
8
  );
9
- const e = document.getElementById(r);
9
+ const e = document.getElementById(i);
10
10
  if (!e)
11
11
  throw new Error(
12
- `[design-token-panel] Inline config script #${r} not found. Ensure <DesignTokenPanelHost config={...} /> is rendered on this page before the host script runs.`
12
+ `[design-token-panel] Inline config script #${i} not found. Ensure <DesignTokenPanelHost config={...} /> is rendered on this page before the host script runs.`
13
13
  );
14
- const o = e.textContent ?? "";
14
+ const t = e.textContent ?? "";
15
15
  let n;
16
16
  try {
17
- n = JSON.parse(o);
18
- } catch (t) {
17
+ n = JSON.parse(t);
18
+ } catch (o) {
19
19
  throw new Error(
20
- `[design-token-panel] Failed to parse inline config from #${r}: ${t.message}`
20
+ `[design-token-panel] Failed to parse inline config from #${i}: ${o.message}`
21
21
  );
22
22
  }
23
- return m(n), n;
23
+ return w(n), n;
24
24
  }
25
- function y(e) {
25
+ function _(e) {
26
26
  return e.__zudoDesignTokenPanelAdapter || (e.__zudoDesignTokenPanelAdapter = {}), e.__zudoDesignTokenPanelAdapter;
27
27
  }
28
- function _(e, o) {
29
- const n = y(e);
30
- let t = n[o];
31
- return t || (t = { bound: !1, modulePromise: null }, n[o] = t), t;
28
+ function b(e, t) {
29
+ const n = _(e);
30
+ let o = n[t];
31
+ return o || (o = { bound: !1, modulePromise: null }, n[t] = o), o;
32
32
  }
33
33
  function k(e) {
34
34
  try {
@@ -37,19 +37,19 @@ function k(e) {
37
37
  return !1;
38
38
  }
39
39
  }
40
- function b(e, o) {
40
+ function C(e, t) {
41
41
  try {
42
42
  const n = window.localStorage;
43
- return n.getItem(o) !== null || n.getItem(e) !== null;
43
+ return n.getItem(t) !== null || n.getItem(e) !== null;
44
44
  } catch {
45
45
  return !1;
46
46
  }
47
47
  }
48
- async function i(e) {
49
- return e.modulePromise === null && (e.modulePromise = import("@takazudo/zdtp").then((o) => {
48
+ async function r(e) {
49
+ return e.modulePromise === null && (e.modulePromise = import("@takazudo/zdtp").then((t) => {
50
50
  try {
51
- const n = l(), t = o.__panelConfigForTest();
52
- n !== t && console.warn(
51
+ const n = c(), o = t.__panelConfigForTest();
52
+ n !== o && console.warn(
53
53
  "[design-token-panel] Singleton-sharing check failed: the host adapter and the panel module observed different PanelConfig singletons. This indicates the package's `config/panel-config` module is no longer code-split into a single shared chunk. The panel may behave correctly today, but storage keys / namespaces / branding could diverge between the two surfaces in future bundles."
54
54
  );
55
55
  } catch (n) {
@@ -57,25 +57,23 @@ async function i(e) {
57
57
  "[design-token-panel] Singleton-sharing check could not run (likely an older dist without the __panelConfigForTest accessor): " + n.message
58
58
  );
59
59
  }
60
- return o;
60
+ return t;
61
61
  })), e.modulePromise;
62
62
  }
63
- function T(e, o, n) {
64
- const t = e[o] ?? {};
65
- t.showDesignPanel = async () => {
66
- (await i(n)).showDesignTokenPanel();
67
- }, t.hideDesignPanel = async () => {
68
- (await i(n)).hideDesignTokenPanel();
69
- }, t.toggleDesignPanel = async () => {
70
- (await i(n)).toggleDesignPanel();
71
- }, e[o] = t;
63
+ function I(e, t, n, o) {
64
+ const a = e[t] ?? {};
65
+ a.showDesignPanel = async () => {
66
+ await r(n), o.open();
67
+ }, a.hideDesignPanel = async () => {
68
+ await r(n), o.close();
69
+ }, a.toggleDesignPanel = async () => {
70
+ await r(n), o.toggle();
71
+ }, e[t] = a;
72
72
  }
73
73
  (function() {
74
- const o = P(), n = o.legacyIdRenameMap ? o : { ...o, legacyIdRenameMap: { ...w } };
75
- u(n);
76
- const t = l(), a = window, s = _(a, t.storagePrefix);
77
- if (T(a, t.consoleNamespace, s), s.bound) return;
74
+ const t = y(), n = t.legacyIdRenameMap ? t : { ...t, legacyIdRenameMap: { ...P } }, o = f(n), a = c(), l = window, s = b(l, a.storagePrefix);
75
+ if (I(l, a.consoleNamespace, s, o), s.bound) return;
78
76
  s.bound = !0;
79
- const c = f(t), d = p(t), g = h(t);
80
- (k(c) || b(d, g)) && i(s);
77
+ const d = p(a), g = h(a), u = m(a);
78
+ (k(d) || C(g, u)) && r(s);
81
79
  })();
@@ -1,4 +1,4 @@
1
- import { s as t } from "../panel-config-DP2_P7dD.js";
1
+ import { s as t } from "../panel-config-BOyB8Egg.js";
2
2
  import { c as d } from "../color-schemes-CgzOBqGO.js";
3
3
  import { F as r, G as m, a as g, S as z } from "../manifest-DCReQE0k.js";
4
4
  const e = {