@takazudo/zdtp 0.5.0 → 0.5.2

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
@@ -4,6 +4,77 @@ All notable changes to `@takazudo/zdtp` are documented in this file.
4
4
 
5
5
  The format is based on Keep a Changelog, and release notes are generated from the changelog MDX pages.
6
6
 
7
+ ## [0.5.2] - 2026-09-08
8
+
9
+ ### Features
10
+
11
+ - Header actions now carry a stable `data-zdtp-action` attribute on both affordances — the inline header link and the compact kebab popover — with the ids `export`, `import`, `apply`, and `reset`. The ids are stable across releases; the display labels are not. This gives consumer tests a selector that survives a label change, which is what broke in [#831](https://github.com/Takazudo/zudo-design-token-panel/issues/831). The hook removes the dependence on the label; it does **not** remove the responsive collapse, so a locator still needs to be visibility-qualified and panel-scoped — see the recipe [Reaching header actions](https://zdtp.zudolab.dev/docs/recipes/reaching-header-actions/) and `PORTABLE-CONTRACT.md` §7.6. (91916b4, [#840](https://github.com/Takazudo/zudo-design-token-panel/issues/840))
12
+
13
+ ### Fixed
14
+
15
+ - The popover action items and the header region items are now keyed by their stable action id rather than by their display label. Two actions sharing a label previously produced duplicate keys, so reconciliation could reuse the wrong node and leave an item with a stale handler or `aria-disabled` state. (c3384ae)
16
+ - The 0.5.1 changelog said the old header-action selector "worked through 0.4.14". It worked through **0.5.0** — `v0.4.15` and `v0.5.0` both shipped before the collapse landed. (c3384ae)
17
+
18
+ ### Other Changes
19
+
20
+ - The documentation site moved to `https://zdtp.zudolab.dev/`, and the playground and minimal example to `https://zdtp-playground.zudolab.dev/` and `https://zdtp-minimal.zudolab.dev/`. The package `homepage` field follows. (0b9e596, ef41bf2, [#835](https://github.com/Takazudo/zudo-design-token-panel/issues/835))
21
+ - The 0.5.1 release notes now flag the header-action collapse as consumer-affecting, with the exact `1135px` container-query threshold and the fact that it measures the shell's content-box inline size rather than the viewport. (6215888, [#841](https://github.com/Takazudo/zudo-design-token-panel/issues/841))
22
+ - New bilingual recipe, [Reaching header actions](https://zdtp.zudolab.dev/docs/recipes/reaching-header-actions/), covering both a label-based pattern for existing releases and the id-based pattern from this one. (c701369, [#842](https://github.com/Takazudo/zudo-design-token-panel/issues/842))
23
+ - Twenty dead `*.pages.dev` example-demo links on the Examples page were repointed to their live `zudolab.dev` hosts. (c83ffa3, [#836](https://github.com/Takazudo/zudo-design-token-panel/issues/836))
24
+
25
+ ## [0.5.1] - 2026-09-06
26
+
27
+ The panel now fits its token grids, card controls, and header controls at the minimum panel width instead of overflowing. Multiple panels keep stable spawn positions: spawn ordinals are persisted and survive Astro client-side navigation. Eager loading is bounded to state keys the panel can actually read, and the specimen stylesheet gained the boundaries the docs-adoption check required.
28
+
29
+ ### Breaking Changes
30
+
31
+ - **Narrowed the accepted-key set of `EAGER_LOAD_GATE_STATE_FAMILY.matchesKey`, a public exported API.** 0.5.0 accepted `${storagePrefix}-state` optionally followed by `-v` and one or more digits. 0.5.1 accepts only exact matches against the new `READABLE_STATE_KEY_SUFFIXES` registry — `-state`, `-state-v2`, `-state-v3`, `-state-v4`. So `${storagePrefix}-state` and `${storagePrefix}-state-v2` still match, while `${storagePrefix}-state-v1`, `${storagePrefix}-state-v0`, `${storagePrefix}-state-v01`, and `${storagePrefix}-state-v123` matched in 0.5.0 and do not in 0.5.1. The narrowing is deliberate — the synchronous eager gate must probe only the key versions the loader can actually read — but it is consumer-observable, the TypeScript signature is unchanged so a type check will not surface it, and it should have ridden a minor release rather than this patch. **Migration**: hosts asserting on `-state-vN` keys should derive them from the new `READABLE_STATE_KEY_SUFFIXES` export instead of matching a numeric pattern, so their assertions self-update at the next storage-format bump. (bcc55fd, [#790](https://github.com/Takazudo/zudo-design-token-panel/issues/790), [#793](https://github.com/Takazudo/zudo-design-token-panel/issues/793), [#814](https://github.com/Takazudo/zudo-design-token-panel/issues/814))
32
+
33
+ - **Header action links now collapse behind the kebab menu at a panel content width of 1135px and below, making previously-clickable controls unreachable by their old selectors.** _(Retroactive correction added 2026-09-08. This behaviour shipped in 0.5.1 but was recorded only under **Fixed**, as `fix(panel): yield header controls before floating panel overflow`; it belongs here.)_ The header action links — Export, Load from JSON…, Apply, Reset — are hidden and served instead from the kebab trigger `.tokenpanel-actions-menu-btn` (`aria-label="Panel actions"`), which opens `.tokenpanel-actions-popover`. The rule is `@container tokenpanel (max-width: 1135px)` against `.tokenpanel-shell` (`container: tokenpanel / inline-size`). Two properties of that threshold matter: it engages at **1135px and below** — exactly 1135px collapses, 1136px does not — and it measures the **shell's own content-box inline size, not the viewport**, so the panel's borders count against the measured width. 1135px is wide enough that an ordinary desktop panel is already past it; a shell at 1024px is not a narrow or mobile case. The surprise is not that a responsive collapse exists but that it engages at a width most people would call desktop. Collapsed links are `display: none` and therefore leave the accessibility tree, so a query that worked through 0.5.0 times out on 0.5.1:
34
+
35
+ ```js
36
+ await page.getByRole('button', { name: 'Reset', exact: true }).click();
37
+ ```
38
+
39
+ Like the `matchesKey` narrowing above, this is consumer-observable, the TypeScript signature is unchanged so a type check will not surface it, and it should have ridden a minor release rather than this patch. **Migration**: open the kebab menu before reaching for an action, or drive the panel at a content width above the threshold — the recipe [Reaching header actions](https://zdtp.zudolab.dev/docs/recipes/reaching-header-actions/) covers both, with the boundary arithmetic. A stable `data-zdtp-action` attribute for these controls landed in 0.5.2; it does not exist in 0.5.1. (06cbb0e, [#787](https://github.com/Takazudo/zudo-design-token-panel/issues/787), [#831](https://github.com/Takazudo/zudo-design-token-panel/issues/831))
40
+
41
+ ### Fixed
42
+
43
+ - fix: fit token grids and card controls at minimum panel width (8db4235)
44
+ - fix(panel): yield header controls before floating panel overflow (06cbb0e)
45
+ - fix: validate doc adoption boundaries (a11fd6a)
46
+
47
+ ### Other Changes
48
+
49
+ - Merge pull request [#801](https://github.com/Takazudo/zudo-design-token-panel/pull/801) from Takazudo/base/sweep-260906-2 (2631545)
50
+ - Merge pull request [#813](https://github.com/Takazudo/zudo-design-token-panel/pull/813) from Takazudo/base/sweep-260906-2-doc-zdtp-050 (406fb79)
51
+ - Merge branch 'topic/795-doc-adoption-confirm' into base/sweep-260906-2-doc-zdtp-050 (1f5abd9)
52
+ - Merge branch 'topic/795-doc-pin-manifest' into base/sweep-260906-2-doc-zdtp-050 (2fe626b)
53
+ - Adopt zdtp 0.5.0 in docs (8026c66)
54
+ - Merge pull request [#810](https://github.com/Takazudo/zudo-design-token-panel/pull/810) from Takazudo/base/sweep-260906-2-storage-bootstrap (e8edd31)
55
+ - Merge branch 'topic/790-storage-bootstrap-confirm' into base/sweep-260906-2-storage-bootstrap (aca17d0)
56
+ - Confirm storage bootstrap regressions (1fdb75e)
57
+ - Merge branch 'topic/790-readable-gate-probe' into base/sweep-260906-2-storage-bootstrap (be49753)
58
+ - Merge branch 'topic/790-astro-ordinal-lifecycle' into base/sweep-260906-2-storage-bootstrap (48a76ab)
59
+ - Keep spawn positions stable across Astro navigation (2a6672b)
60
+ - Merge branch 'topic/790-persisted-spawn-ordinal' into base/sweep-260906-2-storage-bootstrap (03deeaa)
61
+ - Persist panel spawn ordinals (3f1caa3)
62
+ - Merge pull request [#805](https://github.com/Takazudo/zudo-design-token-panel/pull/805) from Takazudo/base/sweep-260906-2-panel-width-layout (b62b94d)
63
+ - test: follow compact panel actions in walking skeleton (de19350)
64
+ - Merge branch 'topic/783-containment-confirm' into base/sweep-260906-2-panel-width-layout (1cdbb29)
65
+ - test: update width-layout VRT baselines (f54695a)
66
+ - test: reconcile compact label containment assertion (777ac84)
67
+ - Merge branch 'topic/783-grid-implementation' into base/sweep-260906-2-panel-width-layout (5283cfc)
68
+ - test: isolate and verify persisted highlight toggle state (373239c)
69
+ - test: reconcile card fit fixtures with manifest and query contracts (6345370)
70
+ - Merge branch 'topic/783-header-implementation' into base/sweep-260906-2-panel-width-layout (eacf1ef)
71
+ - test(panel): correct header portal and edge-hit assertions (8fafedd)
72
+ - Merge pull request [#800](https://github.com/Takazudo/zudo-design-token-panel/pull/800) from Takazudo/base/sweep-260906-2-docs-consistency (82073a5)
73
+ - Merge branch 'topic/780-docs-version-skew-note' into base/sweep-260906-2-docs-consistency (d838cc2)
74
+ - Merge branch 'topic/780-readme-section-renumber' into base/sweep-260906-2-docs-consistency (22dd16d)
75
+ - docs: explain release skew in getting-started notes (b62ffb9)
76
+ - docs: renumber README color presets section (fb19bed)
77
+
7
78
  ## [0.5.0] - 2026-09-06
8
79
 
9
80
  `@takazudo/zdtp/constants` now exposes `DEFAULT_STORAGE_PREFIX`, `DEFAULT_TOGGLE_EVENT`, `resolveToggleEventName`, `EAGER_LOAD_GATE_KEY_SUFFIXES`, and `EAGER_LOAD_GATE_STATE_FAMILY` without importing the panel. Lazy hosts can share toggle-event rules and eager-load metadata; state-family matching still requires the documented content checks. The storage-key documentation is also corrected: position, size, density, and all three highlight keys already existed in published 0.4.14.
@@ -276,6 +276,7 @@ derives the keys at runtime from this single base.
276
276
  | `state-v1` | `${storagePrefix}-state` | tweak-state (legacy) | Pre-v2 flat-state format (Color-only). When selected, it is written to `state-v3` and the v1 key is deleted, then the result is copied into v4. |
277
277
  | `open` | `${storagePrefix}-open` | panel | Mirror of the panel's `open` boolean state (so the next mount opens directly into the user's last state without a post-render toggle dispatch). |
278
278
  | `position` | `${storagePrefix}-position` | panel | Drag position (`{ top, left }`) so the panel reappears where the user left it. |
279
+ | `spawn-ordinal` | `${storagePrefix}-spawn-ordinal` | mount registry | Instance cascade identity: a JSON integer from 0 through 31, retained by Reset and restored across Astro body swaps. Invalid values fall back to lowest-free allocation. |
279
280
  | `size` | `${storagePrefix}-size` | panel | Floating shell dimensions (`{ width, height }`) in pixels. |
280
281
  | `dock` | `${storagePrefix}-dock` | panel | Presentation mode: `'float'`, `'right'`, `'bottom'`, or `'mini'`. |
281
282
  | `dock-size` | `${storagePrefix}-dock-size` | panel | Right/bottom dock dimensions (`{ right, bottom }`), defaulting to `{ right: 440, bottom: 340 }`. |
@@ -316,6 +317,7 @@ myapp-design-token-panel-state-v2
316
317
  myapp-design-token-panel-state
317
318
  myapp-design-token-panel-open
318
319
  myapp-design-token-panel-position
320
+ myapp-design-token-panel-spawn-ordinal
319
321
  myapp-design-token-panel-size
320
322
  myapp-design-token-panel-dock
321
323
  myapp-design-token-panel-dock-size
@@ -336,8 +338,9 @@ myapp-design-token-panel-highlight-active # sessionStorage
336
338
 
337
339
  Unit tests in the package verify these derivations with literal-equality
338
340
  checks. The v4 precedence and legacy v1/v2/v3 migration paths at first load
339
- are part of the test matrix; the version-agnostic `${storagePrefix}-state`
340
- family probe in §6.2 continues to cover this key and future versions.
341
+ are part of the test matrix. The bounded probe in §6.2 covers exactly the
342
+ state versions the loader can read; every future format bump must update the
343
+ shared readable-suffix registry in the same change.
341
344
 
342
345
  ### Current `state-v4` envelope
343
346
 
@@ -389,11 +392,20 @@ computed at open time as one coherent rectangle:
389
392
  against the persisted size, not against the default one.
390
393
  - **The fallback is instance-aware.** Each additional panel instance
391
394
  concurrently mounted on the page offsets its own fallback position by 24px
392
- on both axes, keyed to mount order with lowest-free-slot reuse (a released
393
- slot — e.g. from `destroy()` — is reused by the next instance that mounts,
394
- rather than the ordinal growing forever). This exists only to keep
395
- simultaneously-opened instances from landing exactly on top of one
396
- another; it has no effect once a `position` value is persisted.
395
+ on both axes. Its `storagePrefix` persists the allocated ordinal, so an Astro
396
+ body swap restores the same cascade position even when remount order differs
397
+ from original open order. An absent, malformed, negative, fractional, or
398
+ out-of-range stored value uses mount order with lowest-free-slot reuse and
399
+ records the result. The stored format is a JSON integer from 0 through 31;
400
+ 31 is the cap because 32 identities span at most 744px at the 24px step,
401
+ already the useful cascade range on ordinary desktop viewports. A released
402
+ live slot — e.g. from `destroy()` or a shell that does not re-materialise
403
+ after navigation — can be reused immediately; a retained stored identity
404
+ reserves nothing by itself and Reset does not delete it. If two prefixes
405
+ contain the same ordinal, the already-live holder keeps it and the later
406
+ claimant takes and persists the lowest free slot. This exists only to keep
407
+ simultaneously-opened instances from landing exactly on top of one another;
408
+ it has no effect once a `position` value is persisted.
397
409
  - **A persisted `position` value always wins over the cascade.** The 24px
398
410
  offset applies only to the computed fallback, never to a stored value —
399
411
  once `position` is written, that instance reopens at the exact stored
@@ -503,7 +515,7 @@ export interface TierConfig {
503
515
  }
504
516
 
505
517
  For the full `SemanticValue` mapping and emission behavior, see the maintained
506
- [Color-cluster reference](https://zudo-design-token-panel.takazudomodular.com/docs/reference/color-cluster/).
518
+ [Color-cluster reference](https://zdtp.zudolab.dev/docs/reference/color-cluster/).
507
519
 
508
520
  /**
509
521
  * Color-cluster extras — the non-tier fields required for the color tab.
@@ -1130,9 +1142,10 @@ if (
1130
1142
  is applied a second time to the `${storagePrefix}-open` mirror (dash-form,
1131
1143
  §2) that `panel.tsx` writes alongside it. Either key holding `'1'` means the
1132
1144
  panel was open before the last navigation.
1133
- - `hasPersistedOverrides()` — scans every `localStorage` key matching the
1134
- `${storagePrefix}-state` family (dash-form, §2: `-state` (v1) through every
1135
- `-state-vN`) and returns `true` when at least one holds a non-empty envelope
1145
+ - `hasPersistedOverrides()` — performs bounded `getItem` probes for the
1146
+ `${storagePrefix}-state` keys the loader can read (dash-form, §2: `-state`
1147
+ (v1), `-state-v2`, `-state-v3`, and `-state-v4`) and returns `true` when at
1148
+ least one holds a non-empty envelope
1136
1149
  (malformed JSON also counts as `true` — fail open, so the panel loads and
1137
1150
  can migrate or reject the payload rather than stranding the user with data
1138
1151
  it can never see). This is a **content check**, not a presence check on a
@@ -1143,13 +1156,18 @@ if (
1143
1156
  sink (or default `:root`) even when the panel stays hidden, otherwise
1144
1157
  hard-nav produces a FOUT.
1145
1158
 
1146
- `EAGER_LOAD_GATE_STATE_FAMILY.matchesKey(storagePrefix, key)` only recognizes
1147
- the exact `${storagePrefix}-state` / `${storagePrefix}-state-vN` key shape. It
1148
- does not read storage or perform the content check. The consumer must apply
1149
- the accompanying `valueRules`: raw empty strings, JSON `null`, and empty
1159
+ `READABLE_STATE_KEY_SUFFIXES` is the dependency-free registry of those four
1160
+ readable suffixes. Every storage-format bump MUST update it in the same
1161
+ change as the loader. `EAGER_LOAD_GATE_STATE_FAMILY.matchesKey(storagePrefix,
1162
+ key)` recognizes only exact keys constructed from that registry; unreadable
1163
+ future versions and `${storagePrefix}-spawn-ordinal` do not activate. It does
1164
+ not read storage or perform the content check. The consumer must apply the
1165
+ accompanying `valueRules`: raw empty strings, JSON `null`, and empty
1150
1166
  objects/arrays do not activate; non-empty collections, every other parsed
1151
1167
  primitive (including `false`, `0`, and JSON `""`), and malformed JSON do
1152
- activate.
1168
+ activate. The top-level emptiness check is defensive-only: zdtp-written v4
1169
+ envelopes always contain their top-level buckets, and Reset removes state
1170
+ keys rather than writing empty envelopes.
1153
1171
  - `shouldAutoload()` — reads `${storagePrefix}:autoload` (colon-form, §2).
1154
1172
  Returns `true` when the flag is `'1'` (explicit, written by `enableAutoload()`)
1155
1173
  OR `'auto'` (auto-remembered, written by opening the panel — see "Auto-remember
@@ -1179,7 +1197,7 @@ that decision.
1179
1197
  | Signal | Key derivation | Owner |
1180
1198
  |--------|---------------|-------|
1181
1199
  | `wasVisible` | `${storagePrefix}:visible`, OR its `${storagePrefix}-open` mirror | adapter |
1182
- | `hasPersistedOverrides` | Content check across the `${storagePrefix}-state` family (`-state`, `-state-v2`, `-state-v3`, `-state-v4`, ... — every version, not a fixed list) | tweak-state |
1200
+ | `hasPersistedOverrides` | Bounded content checks for the readable suffix registry (`-state`, `-state-v2`, `-state-v3`, `-state-v4`) | tweak-state |
1183
1201
  | `shouldAutoload` | `${storagePrefix}:autoload`, matching `'1'` or `'auto'` | autoload-state |
1184
1202
  | `loadElementPathEnabled` | `${storagePrefix}-elpath-enabled` | element-path-state |
1185
1203
  | `loadDomTweakerEnabled` | `${storagePrefix}-domtweaker-enabled` | dom-tweaker-state |
@@ -1497,6 +1515,47 @@ pipeline:
1497
1515
  </script>
1498
1516
  ```
1499
1517
 
1518
+ ### 7.6 `data-zdtp-action` — stable header-action DOM hook
1519
+
1520
+ The four header actions carry a stable per-action attribute so a consumer test
1521
+ can select one without matching its display label:
1522
+
1523
+ ```html
1524
+ <div role="button" class="tokenpanel-action-link" data-zdtp-action="export">Export</div>
1525
+ ```
1526
+
1527
+ | Action label | `data-zdtp-action` |
1528
+ | ----------------- | ------------------ |
1529
+ | `Export` | `export` |
1530
+ | `Load from JSON…` | `import` |
1531
+ | `Apply` | `apply` |
1532
+ | `Reset` | `reset` |
1533
+
1534
+ **The ids are stable; the labels are not.** Labels are display strings and may
1535
+ change at any minor version (note the ellipsis in `Load from JSON…`, which is
1536
+ `…` U+2026, not three periods). Select on the attribute, never on the text.
1537
+
1538
+ Both affordances emit it with identical values: the header row and the compact
1539
+ kebab popover that takes over visually below the header's container-query
1540
+ threshold.
1541
+
1542
+ **The hook does not remove the visibility branch.** `ShellHeader` *always*
1543
+ renders the header action row; the container query only hides it with
1544
+ `display: none`. So while the compact popover is open, `[data-zdtp-action="reset"]`
1545
+ matches **two** elements — the hidden header control and the popover item. A
1546
+ bare attribute selector is therefore not unambiguous at narrow widths; qualify
1547
+ it by visibility (Playwright's `:visible` / `.filter({ visible: true })`, or a
1548
+ non-zero bounding box) to land on exactly one.
1549
+
1550
+ Scope — this hook covers the header actions and their compact popover only,
1551
+ which means every layout sharing `ShellHeader`: floating, right-docked, and
1552
+ bottom-docked. Deliberately **not** covered:
1553
+
1554
+ - The **command palette**, which renders its own Export / Import / Apply entries
1555
+ and whose reset entries are per-tab (`Reset {tab.label}`), not the header's
1556
+ reset-all.
1557
+ - **Mini mode**, which renders Apply and has no header.
1558
+
1500
1559
  ---
1501
1560
 
1502
1561
  ## 8. Storage-key continuity & migration paths
@@ -1642,6 +1701,28 @@ Items this contract deliberately does NOT pin down:
1642
1701
 
1643
1702
  ---
1644
1703
 
1704
+ ## 11. Versioning & compatibility
1705
+
1706
+ The package is pre-1.0 (`0.x`). This section states what a consumer may rely
1707
+ on across a release bump.
1708
+
1709
+ - **The versioning promise.** On 0.x, patch releases preserve documented
1710
+ public contracts and accepted consumer inputs. Consumer-breaking contract
1711
+ changes, including input narrowing, require a minor release. Patches remain
1712
+ free to fix bugs, and a bug fix can change observable behavior; the promise
1713
+ only bounds what a patch may take *away* from a consumer.
1714
+ - **How breaking changes are signalled.** A consumer-breaking contract change
1715
+ gets its own heading in the release notes, with a migration pointer naming
1716
+ the new adoption path.
1717
+ - **What consumers should do.** A host mirroring the eager-load gate should
1718
+ derive its checks from the exported registries
1719
+ (`READABLE_STATE_KEY_SUFFIXES`, `EAGER_LOAD_GATE_KEY_SUFFIXES`) documented
1720
+ in §6.2, rather than matching a pattern of its own — that way the host's
1721
+ assertions self-update on the next storage-format bump instead of silently
1722
+ drifting from the package's actual behavior.
1723
+
1724
+ ---
1725
+
1645
1726
  ## Appendix A — section index
1646
1727
 
1647
1728
  Cross-reference table — what each section pins down.
@@ -1650,7 +1731,7 @@ Cross-reference table — what each section pins down.
1650
1731
  | ------------------------------------------------------------------------------------------- | ------------- |
1651
1732
  | `configurePanel({...})` signature, multi-instance, `PanelInstanceHandle`, per-instance toggle events | §1 |
1652
1733
  | Storage-key derivation | §2, §8 |
1653
- | Default first-open geometry (coherent size+position, viewport containment, cascade, persisted-position precedence) | §2.1 |
1734
+ | Default first-open geometry (coherent size+position, viewport containment, persisted cascade identity, persisted-position precedence) | §2.1 |
1654
1735
  | `PanelDockConfig`, dock modes, body-margin reflow, edge claims, and dock storage | §1, §2, §7 |
1655
1736
  | `TabConfig` / `TierConfig` / `TierItem` / `TierValueKind` interfaces and apply behaviour | §3 |
1656
1737
  | `TierConfig.preview` / `previewBase` matrix and host-page specimen lifecycle | §3.2 |
@@ -1670,7 +1751,9 @@ Cross-reference table — what each section pins down.
1670
1751
  | Modal class prefix and `data-design-token-panel-modal` selector contract | §7.3 |
1671
1752
  | Self-contained panel chrome palette (no host theme reads) | §7.4 |
1672
1753
  | Host-adapter side-effect import (paired-unit obligation) | §7.5 |
1754
+ | `data-zdtp-action` stable header-action hook (ids stable, labels not; two matches while the compact popover is open) | §7.6 |
1673
1755
  | v4 envelope precedence, v1/v2/v3 storage migration, and typography-id rename map | §2, §8.3, §8.4 |
1674
1756
  | JSON export/import schemas v1/v2/v3 (serde) | §9 |
1675
1757
  | Out-of-scope / deferred concerns | §10 |
1758
+ | Versioning promise, breaking-change signalling, and consumer guidance for mirroring the eager-load gate registries | §11 |
1676
1759
  | Feature walkthrough and shortcut table | [Panel UX tour](/docs/recipes/panel-ux-tour) |
package/README.md CHANGED
@@ -588,7 +588,7 @@ default instance only.
588
588
  | `exportFilenameBase` | `string` | Default download filename base — exports save as `${exportFilenameBase}.json`. |
589
589
  | `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`. |
590
590
  | `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. |
591
- | `colorPresets` | `Record<string, ColorScheme>` (optional) | Optional named scheme presets surfaced in the Color tab "Scheme..." dropdown. Defaults to `{}`. See §7.5. |
591
+ | `colorPresets` | `Record<string, ColorScheme>` (optional) | Optional named scheme presets surfaced in the Color tab "Scheme..." dropdown. Defaults to `{}`. See §7.4. |
592
592
  | `applyEndpoint` | `string` (optional) | Browser POST target for apply previews and confirmed writes. See §3 and [Apply pipeline reference](/docs/reference/apply-pipeline). |
593
593
  | `applyRouting` | `Record<string, string>` (optional) | CSS-var prefix-family to repo-relative CSS-file routing map used by the apply endpoint. |
594
594
  | `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. |
@@ -646,7 +646,7 @@ const handle = configurePanel({
646
646
  The Astro entry point (`<DesignTokenPanelHost>`) handles mounting for you. Internally:
647
647
 
648
648
  - 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.
649
- - The panel module is **dynamically imported on first need**: when the user calls a console helper, OR when first-paint detects any of these gate signals in `localStorage` — `${storagePrefix}:visible` set to `1` or its `${storagePrefix}-open` mirror set to `1`, persisted overrides (a content check across the `${storagePrefix}-state` family — `-state` (v1) through every `-state-vN` — rather than a presence check on a specific version key; see §9), the owner-autoload flag (`${storagePrefix}:autoload` set to `'1'` or `'auto'`), or the element-path inspector enabled. `${storagePrefix}-domtweaker-enabled` is also a gate signal when `domTweaker` is configured.
649
+ - The panel module is **dynamically imported on first need**: when the user calls a console helper, OR when first-paint detects any of these gate signals in `localStorage` — `${storagePrefix}:visible` set to `1` or its `${storagePrefix}-open` mirror set to `1`, persisted overrides (bounded content checks for the state formats the loader can read: `-state` (v1), `-state-v2`, `-state-v3`, and `-state-v4`; see §9), the owner-autoload flag (`${storagePrefix}:autoload` set to `'1'` or `'auto'`), or the element-path inspector enabled. `${storagePrefix}-domtweaker-enabled` is also a gate signal when `domTweaker` is configured.
650
650
  - This gating keeps the panel bundle out of the initial JS payload for first-time visitors while still re-applying overrides on hard reload for users who have tweaked things. **General visitors** (none of these signals set) pay no panel-bundle cost; the small host adapter/config bootstrap still runs.
651
651
 
652
652
  For a Vite-only / non-Astro host, mount it yourself by importing the adapter module after `configurePanel(...)`. See §8.5.
@@ -657,7 +657,7 @@ The first time an instance opens with no persisted position, the panel picks its
657
657
 
658
658
  - **Size first, position from that size.** The default size follows the historical `min(1200, 0.8·vw) × min(800, 0.8·vh)` rule, clamped to a minimum floor and the viewport. The default position then centers _that same clamped rectangle_. Position and size can no longer disagree about the panel's width — they used to be computed separately, which could center a wider phantom panel than the one actually rendered and spawn it partly off-screen on a narrow viewport.
659
659
  - **Always contained in the viewport.** The centered position is run through a containment clamp before use, so the whole panel — not just a grabbable strip of it — sits inside `[0, innerWidth]` × `[0, innerHeight]`. This holds at every viewport width, including phone widths, and it holds for a *persisted* size too: an instance that has a saved size but no saved position (the user resized the panel without ever dragging it) is centered and contained against that saved size, not against the default one.
660
- - **Instance-aware: concurrent panels cascade apart.** Each additional concurrently-mounted instance offsets its own fresh-open position by 24px on both axes. The offset comes from mount order with lowest-free-slot reuse — the first free ordinal is claimed on mount and given back on unmount, so opening a third panel after closing the first reuses ordinal 0 rather than always growing. Two instances opened at once therefore don't spawn stacked exactly on top of each other.
660
+ - **Instance-aware: concurrent panels cascade apart.** Each additional concurrently-mounted instance offsets its own fresh-open position by 24px on both axes. Each `storagePrefix` persists its allocated ordinal, so an Astro body swap restores the same cascade position even when remount order differs from original open order. With no valid stored ordinal, allocation uses mount order with lowest-free-slot reuse and then records the result. Unmounting always releases the live claim: a departed instance's retained identity does not reserve a slot, and a later panel may reuse it. If two prefixes contain the same ordinal, the already-live holder keeps it and the later claimant takes and persists the lowest free slot. Two instances opened at once therefore don't spawn stacked exactly on top of each other.
661
661
  - **A persisted position always wins.** The cascade offset applies only to the fallback used when an instance has no saved position yet. A panel the user has dragged (and which got saved) reopens exactly where it was left, regardless of how many sibling instances happen to be mounted.
662
662
  - **Containment beats distinctness.** These two guarantees are not both promised unconditionally — on a viewport with no spare room, the 24px cascade shrinks toward whatever slack is left, down to a 0px offset, rather than pushing a panel outside the viewport. Two instances' first-open positions may then coincide. Each axis degrades independently: at a 320px viewport width the horizontal offset collapses to 0 once the minimum-width floor eats all the spare width, while the vertical offset may still cascade its full 24px if vertical slack remains.
663
663
  - **A different, stricter rule than the drag-recovery clamp.** Once a panel has been dragged, its position is governed by a separate, deliberately permissive clamp: it only guarantees a small grip of the panel's header stays on-screen so the user can drag it back, and otherwise allows the panel to hang off any edge. That drag clamp is unchanged and keeps applying after a drag; the full-containment guarantee above is specific to the first-open fallback position and does not carry over once the user has moved the panel.
@@ -904,7 +904,7 @@ literal mapping, or `light-dark(light, dark)` for a per-mode literal.
904
904
  Roles absent from `colorExtras.baseRoles` are not written, so a minimalist
905
905
  cluster (just `background` + `foreground`) is fine.
906
906
 
907
- ### 7.5 Host-supplied scheme presets — `colorPresets`
907
+ ### 7.4 Host-supplied scheme presets — `colorPresets`
908
908
 
909
909
  The Color tab's "Scheme..." dropdown surfaces named `ColorScheme` entries. Two sources feed it:
910
910
 
@@ -1063,6 +1063,7 @@ Behaviour notes:
1063
1063
  | `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. |
1064
1064
  | `open` | `${storagePrefix}-open` | Mirror of the panel's `open` boolean, read synchronously at mount so user intent survives reloads. |
1065
1065
  | `position` | `${storagePrefix}-position` | Drag position `{ top, left }` so the panel reappears where the user left it. |
1066
+ | `spawn-ordinal` | `${storagePrefix}-spawn-ordinal` | Instance cascade identity: a JSON integer from 0 through 31, retained by Reset and restored across Astro body swaps. Invalid values fall back to lowest-free allocation. |
1066
1067
  | `size` | `${storagePrefix}-size` | Floating shell dimensions `{ width, height }` in pixels. |
1067
1068
  | `dock` | `${storagePrefix}-dock` | Presentation mode: `'float'`, `'right'`, `'bottom'`, or `'mini'`. |
1068
1069
  | `dock-size` | `${storagePrefix}-dock-size` | Right/bottom dock dimensions `{ right, bottom }`, defaulting to `{ right: 440, bottom: 340 }`. |
@@ -1089,6 +1090,7 @@ myapp-design-token-panel-state-v2
1089
1090
  myapp-design-token-panel-state
1090
1091
  myapp-design-token-panel-open
1091
1092
  myapp-design-token-panel-position
1093
+ myapp-design-token-panel-spawn-ordinal
1092
1094
  myapp-design-token-panel-size
1093
1095
  myapp-design-token-panel-dock
1094
1096
  myapp-design-token-panel-dock-size
@@ -1410,7 +1412,7 @@ For the regression-guard tests that pin this contract, see `package-exports.test
1410
1412
 
1411
1413
  **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.
1412
1414
 
1413
- **Resolution:** the host adapter eagerly re-applies persisted overrides during the lazy-load gate (it content-checks the exact `${storagePrefix}-state` / `${storagePrefix}-state-vN` family and the visibility, autoload, and enabled-feature signals 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.
1415
+ **Resolution:** the host adapter eagerly re-applies persisted overrides during the lazy-load gate (it performs bounded content checks for the readable `${storagePrefix}-state`, `-state-v2`, `-state-v3`, and `-state-v4` keys, plus the visibility, autoload, and enabled-feature signals 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.
1414
1416
 
1415
1417
  ### 13.2 Auto-mount race on first reload
1416
1418
 
@@ -1,8 +1,9 @@
1
- import { c as y, g as m, a as w, b, i as P } from "../panel-config-BSu6TUht.js";
2
- import { g, Z as k } from "../tweak-state-B7ok3Ob3.js";
3
- import { s as A, l as D, a as E, i as c, r as f, c as _, b as I, d } from "../autoload-state-DmTY6tRy.js";
1
+ import { c as y, g as m, a as w, b, i as P } from "../panel-config-CqbuB0nh.js";
2
+ import { g, Z as A } from "../tweak-state-c53KtLHq.js";
3
+ import { s as D, l as E, a as _, i as c, r as f, c as k, b as S, d } from "../autoload-state-BI4-9A58.js";
4
+ import { READABLE_STATE_KEY_SUFFIXES as T } from "../constants.js";
4
5
  const u = "tokenpanel-config";
5
- function S() {
6
+ function I() {
6
7
  if (typeof document > "u")
7
8
  throw new Error(
8
9
  "[design-token-panel] host-adapter loaded without a document; expected to run in a browser context."
@@ -12,24 +13,24 @@ function S() {
12
13
  throw new Error(
13
14
  `[design-token-panel] Inline config script #${u} not found. Ensure <DesignTokenPanelHost config={...} /> is rendered on this page before the host script runs.`
14
15
  );
15
- const o = e.textContent ?? "";
16
- let n;
16
+ const n = e.textContent ?? "";
17
+ let o;
17
18
  try {
18
- n = JSON.parse(o);
19
+ o = JSON.parse(n);
19
20
  } catch (a) {
20
21
  throw new Error(
21
22
  `[design-token-panel] Failed to parse inline config from #${u}: ${a.message}`
22
23
  );
23
24
  }
24
- return b(n), n;
25
+ return b(o), o;
25
26
  }
26
- function T(e) {
27
+ function v(e) {
27
28
  return e.__zudoDesignTokenPanelAdapter || (e.__zudoDesignTokenPanelAdapter = {}), e.__zudoDesignTokenPanelAdapter;
28
29
  }
29
- function v(e, o) {
30
- const n = T(e);
31
- let a = n[o];
32
- return a || (a = { bound: !1, modulePromise: null }, n[o] = a), a;
30
+ function C(e, n) {
31
+ const o = v(e);
32
+ let a = o[n];
33
+ return a || (a = { bound: !1, modulePromise: null }, o[n] = a), a;
33
34
  }
34
35
  function p(e) {
35
36
  try {
@@ -38,58 +39,53 @@ function p(e) {
38
39
  return !1;
39
40
  }
40
41
  }
41
- function C(e) {
42
- return e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
43
- }
44
42
  function O(e) {
45
43
  if (e === null || e === "") return !1;
46
- let o;
44
+ let n;
47
45
  try {
48
- o = JSON.parse(e);
46
+ n = JSON.parse(e);
49
47
  } catch {
50
48
  return !0;
51
49
  }
52
- return o === null ? !1 : Array.isArray(o) ? o.length > 0 : typeof o == "object" ? Object.keys(o).length > 0 : !0;
50
+ return n === null ? !1 : Array.isArray(n) ? n.length > 0 : typeof n == "object" ? Object.keys(n).length > 0 : !0;
53
51
  }
54
52
  function x(e) {
55
53
  try {
56
- const o = new RegExp(`^${C(e.storagePrefix)}-state(-v\\d+)?$`), n = window.localStorage;
57
- for (let a = 0; a < n.length; a++) {
58
- const t = n.key(a);
59
- if (t !== null && o.test(t) && O(n.getItem(t)))
54
+ const n = window.localStorage;
55
+ for (const o of Object.values(T))
56
+ if (O(n.getItem(e.storagePrefix + o)))
60
57
  return !0;
61
- }
62
58
  return !1;
63
59
  } catch {
64
60
  return !1;
65
61
  }
66
62
  }
67
63
  async function s(e) {
68
- return e.modulePromise === null && (e.modulePromise = import("@takazudo/zdtp").then((o) => {
64
+ return e.modulePromise === null && (e.modulePromise = import("@takazudo/zdtp").then((n) => {
69
65
  try {
70
- const n = m(), a = o.__panelConfigForTest();
71
- n !== a && console.warn(
66
+ const o = m(), a = n.__panelConfigForTest();
67
+ o !== a && console.warn(
72
68
  "[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."
73
69
  );
74
- } catch (n) {
70
+ } catch (o) {
75
71
  console.warn(
76
- "[design-token-panel] Singleton-sharing check could not run (likely an older dist without the __panelConfigForTest accessor): " + n.message
72
+ "[design-token-panel] Singleton-sharing check could not run (likely an older dist without the __panelConfigForTest accessor): " + o.message
77
73
  );
78
74
  }
79
- return o;
75
+ return n;
80
76
  })), e.modulePromise;
81
77
  }
82
- function R(e, o, n, a, t) {
83
- const i = e[o] ?? {};
78
+ function N(e, n, o, a, t) {
79
+ const i = e[n] ?? {};
84
80
  i.showDesignPanel = async () => {
85
81
  const r = d();
86
- await s(n), c(r) && (f(t), a.open());
82
+ await s(o), c(r) && (f(t), a.open());
87
83
  }, i.hideDesignPanel = async () => {
88
84
  const r = d();
89
- await s(n), c(r) && a.close();
85
+ await s(o), c(r) && a.close();
90
86
  }, i.toggleDesignPanel = async () => {
91
87
  const r = d();
92
- if (await s(n), !c(r)) return;
88
+ if (await s(o), !c(r)) return;
93
89
  a.toggle();
94
90
  let l = !1;
95
91
  try {
@@ -98,17 +94,17 @@ function R(e, o, n, a, t) {
98
94
  }
99
95
  l && f(t);
100
96
  }, i.enableAutoload = async () => {
101
- const r = d(), l = await s(n);
97
+ const r = d(), l = await s(o);
102
98
  c(r) && l.enableAutoload(t);
103
99
  }, i.disableAutoload = async () => {
104
100
  const r = d();
105
- if (n.modulePromise !== null) {
106
- const l = await s(n);
101
+ if (o.modulePromise !== null) {
102
+ const l = await s(o);
107
103
  if (!c(r)) return;
108
104
  l.disableAutoload(t);
109
105
  return;
110
106
  }
111
- _(t), I(!1, t);
107
+ k(t), S(!1, t);
112
108
  try {
113
109
  window.localStorage.setItem(w(t), "0");
114
110
  } catch {
@@ -117,30 +113,30 @@ function R(e, o, n, a, t) {
117
113
  window.localStorage.removeItem(g(t));
118
114
  } catch {
119
115
  }
120
- }, e[o] = i;
116
+ }, e[n] = i;
121
117
  }
122
- function N(e, o) {
118
+ function F(e, n) {
123
119
  P({
124
120
  show: async () => {
125
- const n = d();
126
- await s(e), c(n) && o.open();
121
+ const o = d();
122
+ await s(e), c(o) && n.open();
127
123
  },
128
124
  hide: async () => {
129
- const n = d();
130
- await s(e), c(n) && o.close();
125
+ const o = d();
126
+ await s(e), c(o) && n.close();
131
127
  },
132
128
  toggle: async () => {
133
- const n = d();
134
- await s(e), c(n) && o.toggle();
129
+ const o = d();
130
+ await s(e), c(o) && n.toggle();
135
131
  }
136
132
  });
137
133
  }
138
134
  (function() {
139
- const o = S(), n = o.legacyIdRenameMap ? o : { ...o, legacyIdRenameMap: { ...k } }, a = y(n), t = m(), i = window, r = v(i, t.storagePrefix);
140
- if (R(i, t.consoleNamespace, r, a, t), N(r, a), r.bound) return;
135
+ const n = I(), o = n.legacyIdRenameMap ? n : { ...n, legacyIdRenameMap: { ...A } }, a = y(o), t = m(), i = window, r = C(i, t.storagePrefix);
136
+ if (N(i, t.consoleNamespace, r, a, t), F(r, a), r.bound) return;
141
137
  r.bound = !0;
142
138
  const l = w(t);
143
- (p(l) || p(g(t)) || x(t) || A(t) || D(t) || t.domTweaker !== void 0 && E(t)) && s(r).catch((h) => {
139
+ (p(l) || p(g(t)) || x(t) || D(t) || E(t) || t.domTweaker !== void 0 && _(t)) && s(r).catch((h) => {
144
140
  console.error("[design-token-panel] Eager panel-module load failed.", h);
145
141
  });
146
142
  })();
@@ -1,4 +1,4 @@
1
- import { s as t } from "../panel-config-BSu6TUht.js";
1
+ import { s as t } from "../panel-config-CqbuB0nh.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-DvuKi7I4.js";
4
4
  const e = {
@@ -1,4 +1,4 @@
1
- import { g as n, j as o } from "./panel-config-BSu6TUht.js";
1
+ import { g as n, j as o } from "./panel-config-CqbuB0nh.js";
2
2
  function c() {
3
3
  return typeof document < "u" && document !== null && typeof document.getElementById == "function";
4
4
  }
@@ -471,6 +471,13 @@ export declare function storageKey_stateV3(cfg: PanelConfig): string;
471
471
  export declare function storageKey_stateV4(cfg: PanelConfig): string;
472
472
  /** Legacy v1 key (Color-only flat state). Migrated into v2 on first load, then deleted. */
473
473
  export declare function storageKey_stateV1(cfg: PanelConfig): string;
474
+ /**
475
+ * Persisted identity used to keep a fresh panel's cascade position stable
476
+ * across an Astro body swap. This is deliberately separate from the unified
477
+ * tweak-state envelopes: it identifies an instance; it is not user-authored
478
+ * token state and does not participate in state-schema migration.
479
+ */
480
+ export declare function storageKey_spawnOrdinal(cfg: PanelConfig): string;
474
481
  /** Mirror of the panel's `open` boolean (synchronous mount-time read). */
475
482
  export declare function storageKey_open(cfg: PanelConfig): string;
476
483
  /** Drag position `{ top, left }` so the panel reappears where the user left it. */
@@ -43,17 +43,37 @@ export declare const EAGER_LOAD_GATE_KEY_SUFFIXES: {
43
43
  };
44
44
  };
45
45
  /**
46
- * The sixth eager-load signal: exact -state / -state-vN keys, for every numeric
47
- * version, with a content check. Missing keys and raw empty strings are blank;
46
+ * The single registry of state-key suffixes the current loader can read.
47
+ *
48
+ * EVERY storage-format version bump must update this registry in the same
49
+ * change that teaches the loader to read that version. The synchronous eager
50
+ * gate intentionally probes only these bounded, readable versions; speculative
51
+ * future versions would promise recovery that the loader cannot perform.
52
+ */
53
+ export declare const READABLE_STATE_KEY_SUFFIXES: {
54
+ readonly v1: "-state";
55
+ readonly v2: "-state-v2";
56
+ readonly v3: "-state-v3";
57
+ readonly v4: "-state-v4";
58
+ };
59
+ /**
60
+ * The sixth eager-load signal: exact readable state keys, with a content
61
+ * check. Missing keys and raw empty strings are blank;
48
62
  * JSON null and empty objects/arrays do not activate. Non-empty collections and
49
63
  * all other parsed primitives (even false, 0, or JSON "") activate. Malformed
50
64
  * JSON fails open so the panel can migrate or reject the stored payload.
51
65
  *
52
- * matchesKey compares the prefix literally before matching the suffix, so regex
53
- * metacharacters in a prefix are safe and sibling-instance keys are excluded.
66
+ * matchesKey compares complete strings constructed from the literal prefix, so
67
+ * regex metacharacters are safe and sibling-instance keys are excluded.
54
68
  * These are storage signal descriptions, not a storage reader or panel bootstrap.
55
69
  */
56
70
  export declare const EAGER_LOAD_GATE_STATE_FAMILY: {
71
+ readonly keySuffixes: {
72
+ readonly v1: "-state";
73
+ readonly v2: "-state-v2";
74
+ readonly v3: "-state-v3";
75
+ readonly v4: "-state-v4";
76
+ };
57
77
  readonly matchesKey: (storagePrefix: string, key: string) => boolean;
58
78
  readonly valueRules: {
59
79
  readonly blank: false;