@takazudo/zdtp 0.4.13 → 0.4.15

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.
Files changed (113) hide show
  1. package/CHANGELOG.md +143 -0
  2. package/PORTABLE-CONTRACT.md +366 -94
  3. package/README.md +218 -96
  4. package/dist/apply/build-apply-overrides.d.ts +2 -2
  5. package/dist/apply/compute-hunks.d.ts +23 -0
  6. package/dist/apply/last-applied.d.ts +5 -0
  7. package/dist/apply/reconcile-applied.d.ts +19 -0
  8. package/dist/apply-modal.d.ts +2 -48
  9. package/dist/astro/host-adapter.js +3 -3
  10. package/dist/astro/index.js +2 -2
  11. package/dist/{autoload-state-CfL2VUe1.js → autoload-state-7BQYXI7Y.js} +1 -1
  12. package/dist/bin/server.js +52 -50
  13. package/dist/bulk/bulk-actions.d.ts +45 -0
  14. package/dist/bulk/index.d.ts +2 -0
  15. package/dist/chain/index.d.ts +2 -0
  16. package/dist/chain/token-chain-context.d.ts +19 -0
  17. package/dist/chain/token-chain-popover.d.ts +22 -0
  18. package/dist/changed/contribution.d.ts +32 -0
  19. package/dist/changed/footer-content.d.ts +16 -0
  20. package/dist/changed/index.d.ts +4 -0
  21. package/dist/changed/tab-badge.d.ts +8 -0
  22. package/dist/changed/tab-filter.d.ts +8 -0
  23. package/dist/config/panel-config.d.ts +22 -25
  24. package/dist/controls/actions-menu-popover.d.ts +2 -0
  25. package/dist/controls/role-button.d.ts +5 -2
  26. package/dist/controls/tooltip.d.ts +8 -4
  27. package/dist/element-inspect/element-inspect-context.d.ts +22 -0
  28. package/dist/element-inspect/element-inspect-orchestrator.d.ts +17 -0
  29. package/dist/element-inspect/element-inspect-overlay.d.ts +16 -0
  30. package/dist/element-inspect/element-inspect-toggle-button.d.ts +3 -0
  31. package/dist/element-inspect/element-inspect-view.d.ts +6 -0
  32. package/dist/element-inspect/find-tokens-for-element.d.ts +23 -0
  33. package/dist/element-inspect/index.d.ts +8 -0
  34. package/dist/highlight/find-elements.d.ts +14 -3
  35. package/dist/highlight/highlight-orchestrator.d.ts +7 -3
  36. package/dist/highlight/highlight-state.d.ts +3 -2
  37. package/dist/highlight/highlight-toggle-button.d.ts +3 -0
  38. package/dist/highlight/walk-css-rules.d.ts +2 -0
  39. package/dist/history/buttons.d.ts +28 -0
  40. package/dist/history/index.d.ts +3 -0
  41. package/dist/history/rail.d.ts +13 -0
  42. package/dist/history/snapshots.d.ts +57 -0
  43. package/dist/host/host-mutations.d.ts +23 -0
  44. package/dist/index-BOOmFtJh.js +12165 -0
  45. package/dist/{index-BAY1JOUl.js → index-C2DIYD0d.js} +2 -2
  46. package/dist/index.js +14 -14
  47. package/dist/load-routing-BtCE1hGI.js +547 -0
  48. package/dist/{manifest-DCReQE0k.js → manifest-DvuKi7I4.js} +11 -7
  49. package/dist/{panel-config-COHesW2B.js → panel-config-CRf-ONSV.js} +404 -308
  50. package/dist/picker/alt-click-picker.d.ts +10 -2
  51. package/dist/picker/arming-coordinator.d.ts +2 -0
  52. package/dist/picker/index.d.ts +2 -2
  53. package/dist/search/command-palette.d.ts +21 -0
  54. package/dist/search/contribution.d.ts +7 -0
  55. package/dist/search/fuzzy.d.ts +10 -0
  56. package/dist/search/index.d.ts +4 -0
  57. package/dist/search/match-bar.d.ts +14 -0
  58. package/dist/search/search-header.d.ts +7 -0
  59. package/dist/search/token-search.d.ts +36 -0
  60. package/dist/server/create-apply-handler.d.ts +33 -0
  61. package/dist/server/index.d.ts +2 -1
  62. package/dist/server/index.js +1 -1
  63. package/dist/shell/dock-mode-switch.d.ts +6 -0
  64. package/dist/shell/footer.d.ts +1 -0
  65. package/dist/shell/ghost-idle.d.ts +24 -0
  66. package/dist/shell/header.d.ts +5 -0
  67. package/dist/shell/layer-activity.d.ts +6 -0
  68. package/dist/shell/mini-pill.d.ts +21 -0
  69. package/dist/shell/regions.d.ts +26 -0
  70. package/dist/shell/shortcut-dispatcher.d.ts +17 -0
  71. package/dist/shell/tab-bar.d.ts +17 -0
  72. package/dist/shell/tab-overflow-popover.d.ts +13 -0
  73. package/dist/specimen/on-page-specimen.d.ts +19 -0
  74. package/dist/specimen/preview-glyphs.d.ts +2 -0
  75. package/dist/specimen/specimen-state.d.ts +12 -0
  76. package/dist/specimen/specimen-tab-body.d.ts +18 -0
  77. package/dist/specimen/specimen-toolbar.d.ts +10 -0
  78. package/dist/specimen/specimen-values.d.ts +9 -0
  79. package/dist/state/history.d.ts +64 -0
  80. package/dist/state/persist.d.ts +10 -14
  81. package/dist/state/transaction.d.ts +24 -0
  82. package/dist/state/tweak-state.d.ts +16 -0
  83. package/dist/styles/z-index-tokens.d.ts +2 -0
  84. package/dist/tabs/color-tab.d.ts +10 -1
  85. package/dist/tabs/flat/flat-tab.d.ts +27 -0
  86. package/dist/tabs/flat/index.d.ts +8 -0
  87. package/dist/tabs/flat/scroll-to-token-row.d.ts +3 -0
  88. package/dist/tabs/flat/tier-section.d.ts +17 -0
  89. package/dist/tabs/flat/token-controller.d.ts +23 -0
  90. package/dist/tabs/flat/token-row.d.ts +12 -0
  91. package/dist/tabs/flat/types.d.ts +25 -0
  92. package/dist/tabs/font-tab.d.ts +13 -12
  93. package/dist/tabs/generic-tab.d.ts +8 -38
  94. package/dist/tabs/palette/palette-check-view.d.ts +5 -1
  95. package/dist/tabs/palette/palette-edit-view.d.ts +9 -1
  96. package/dist/tabs/palette/palette-tab.d.ts +12 -1
  97. package/dist/tabs/size-tab.d.ts +7 -10
  98. package/dist/tabs/spacing-tab.d.ts +7 -13
  99. package/dist/testing.js +3 -3
  100. package/dist/tokens/tier-model.d.ts +6 -0
  101. package/dist/{tweak-state-BKNxtHzb.js → tweak-state-BGp_7Tfh.js} +473 -423
  102. package/dist/utils/numeric-transform.d.ts +32 -0
  103. package/dist/utils/token-diff.d.ts +25 -0
  104. package/dist/utils/token-graph.d.ts +23 -0
  105. package/dist/utils/token-index.d.ts +30 -0
  106. package/dist/zdtp.css +1 -1
  107. package/package.json +3 -1
  108. package/dist/controls/pill-slider-row.d.ts +0 -36
  109. package/dist/controls/select-row.d.ts +0 -25
  110. package/dist/controls/slider-row.d.ts +0 -29
  111. package/dist/controls/text-row.d.ts +0 -27
  112. package/dist/index-U7Ol7tgA.js +0 -7591
  113. package/dist/load-routing-D4H2VOl5.js +0 -426
@@ -25,6 +25,11 @@ one page: call it with a distinct `storagePrefix` to register a new instance;
25
25
  call it with the same prefix and equal config for an idempotent no-op.
26
26
 
27
27
  ```ts
28
+ export interface PanelDockConfig {
29
+ /** Reserve host-document space for a right/bottom dock. Defaults to body-margin. */
30
+ reflow?: 'body-margin' | 'none';
31
+ }
32
+
28
33
  export interface PanelConfig {
29
34
  /** Base for every derived storage key. Also the instance id. See §2. */
30
35
  storagePrefix: string;
@@ -32,7 +37,7 @@ export interface PanelConfig {
32
37
  consoleNamespace: string;
33
38
  /** BEM-style prefix used by every modal in the panel (export / import / apply). */
34
39
  modalClassPrefix: string;
35
- /** `$schema` value emitted into export JSON and required on import. */
40
+ /** Display-only label returned by getDesignTokenSchema(); not used by built-in UI or serde validation. */
36
41
  schemaId: string;
37
42
  /** Default filename base — exports save as `${exportFilenameBase}.json`. */
38
43
  exportFilenameBase: string;
@@ -63,10 +68,9 @@ export interface PanelConfig {
63
68
  */
64
69
  colorPresets?: Record<string, ColorScheme>;
65
70
  /**
66
- * Optional dev-API endpoint URL. When the host wires the panel into a
67
- * project that ships its own design-tokens-apply route, supply the URL
68
- * here; the Apply button POSTs its diff payload to it. When `undefined`,
69
- * the Apply button stays disabled with a tooltip.
71
+ * Optional dev-API endpoint URL. When supplied together with a non-empty
72
+ * `applyRouting` map, the Apply button POSTs its diff payload to it. When
73
+ * either field is absent, the button stays disabled with a tooltip.
70
74
  */
71
75
  applyEndpoint?: string;
72
76
  /**
@@ -96,10 +100,12 @@ export interface PanelConfig {
96
100
  /** Optional host Tailwind v4 theme CSS used by the lazy side for suggestions. */
97
101
  themeCss?: string;
98
102
  };
103
+ /** Optional dock integration; omitted values use body-margin host reflow. */
104
+ dock?: PanelDockConfig;
99
105
  /**
100
106
  * Optional apply sink. Routes this instance's CSS-var writes and clears
101
107
  * through the caller-supplied object instead of `document.documentElement`.
102
- * See §3.5 for the full sink contract.
108
+ * See §3.6 for the full sink contract.
103
109
  *
104
110
  * NOTE: this field carries a function reference and is therefore NOT
105
111
  * JSON-serializable. It cannot pass through Astro's inline JSON config.
@@ -126,7 +132,7 @@ export interface PanelConfig {
126
132
 
127
133
  /**
128
134
  * Apply sink — routes CSS-var writes for one panel instance somewhere other
129
- * than the host `:root`. See §3.5.
135
+ * than the host `:root`. See §3.6.
130
136
  */
131
137
  export interface ApplySink {
132
138
  /** Upsert the given var name→value pairs on the sink target. */
@@ -270,9 +276,22 @@ derives the keys at runtime from this single base.
270
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. |
271
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). |
272
278
  | `position` | `${storagePrefix}-position` | panel | Drag position (`{ top, left }`) so the panel reappears where the user left it. |
279
+ | `size` | `${storagePrefix}-size` | panel | Floating shell dimensions (`{ width, height }`) in pixels. |
280
+ | `dock` | `${storagePrefix}-dock` | panel | Presentation mode: `'float'`, `'right'`, `'bottom'`, or `'mini'`. |
281
+ | `dock-size` | `${storagePrefix}-dock-size` | panel | Right/bottom dock dimensions (`{ right, bottom }`), defaulting to `{ right: 440, bottom: 340 }`. |
282
+ | `density` | `${storagePrefix}-density` | panel | Tab-grid density preference (`0`, `1`, or `2`). |
283
+ | `ghost` | `${storagePrefix}-ghost` | shell | Ghost-when-idle preference (`'1'` when enabled). |
284
+ | `specimen` | `${storagePrefix}-specimen` | specimen | Font specimen toolbar JSON: `{ text, preset, overridden, width }`; width is clamped to 240–720. |
285
+ | `snapshot-a` | `${storagePrefix}-snapshot-a` | snapshots | Persisted A snapshot: `{ state, identity, savedAt, edits }`. |
286
+ | `snapshot-b` | `${storagePrefix}-snapshot-b` | snapshots | Persisted B snapshot: `{ state, identity, savedAt, edits }`. |
287
+ | `last-applied` | `${storagePrefix}-last-applied` | apply | Flat comparison baseline; a successful apply resets it to `{}` while unconfirmed overrides remain in live state. |
273
288
  | `visible` | `${storagePrefix}:visible` | adapter | Adapter-level visibility-intent flag, owned by the lazy-load gate (§6). |
274
289
  | `autoload` | `${storagePrefix}:autoload` | autoload-state | Owner-mode autoload flag. `'1'` (explicit, set by `enableAutoload()`) or `'auto'` (auto-remembered, set by opening the panel — see §6.2) both mean "load the panel bundle eagerly and mount CLOSED on every page load." `enableAutoload()` / `disableAutoload()` manage the explicit value; `disableAutoload()` clears either. See §6.2. |
290
+ | `elpath-enabled` | `${storagePrefix}-elpath-enabled` | element-path-state | Element-path picker enabled bit. |
275
291
  | `domtweaker-enabled` | `${storagePrefix}-domtweaker-enabled` | dom-tweaker-state | DOM Tweaker enabled bit. `'1'` means "mount the closed shell and load the DOM Tweaker lazy boundary." Only meaningful when `PanelConfig.domTweaker` is present. |
292
+ | `highlight-slots` | `${storagePrefix}-highlight-slots` | highlight-state | Ten highlight slot colors in local storage. |
293
+ | `highlight-outline-width` | `${storagePrefix}-highlight-outline-width` | highlight-state | Global highlight outline width in local storage, clamped to 1–20. |
294
+ | `highlight-active` | `${storagePrefix}-highlight-active` | highlight-state | Active CSS-variable-to-slot map in session storage. |
276
295
 
277
296
  **Constraint — colon, not dash, for `visible` and `autoload`.** Both adapter-
278
297
  level flags use a `:` separator; every other derived key uses `-`. The colon
@@ -290,9 +309,22 @@ myapp-design-token-panel-state-v2
290
309
  myapp-design-token-panel-state
291
310
  myapp-design-token-panel-open
292
311
  myapp-design-token-panel-position
312
+ myapp-design-token-panel-size
313
+ myapp-design-token-panel-dock
314
+ myapp-design-token-panel-dock-size
315
+ myapp-design-token-panel-density
316
+ myapp-design-token-panel-ghost
317
+ myapp-design-token-panel-specimen
318
+ myapp-design-token-panel-snapshot-a
319
+ myapp-design-token-panel-snapshot-b
320
+ myapp-design-token-panel-last-applied
293
321
  myapp-design-token-panel:visible
294
322
  myapp-design-token-panel:autoload
323
+ myapp-design-token-panel-elpath-enabled
295
324
  myapp-design-token-panel-domtweaker-enabled
325
+ myapp-design-token-panel-highlight-slots
326
+ myapp-design-token-panel-highlight-outline-width
327
+ myapp-design-token-panel-highlight-active # sessionStorage
296
328
  ```
297
329
 
298
330
  Unit tests in the package verify these derivations with literal-equality
@@ -387,14 +419,14 @@ public surface:
387
419
  ```ts
388
420
  // Value-kind discriminated union — describes how a tier item is edited.
389
421
  export type TierValueKind =
390
- | { kind: 'length'; step: number; unit: string }
391
- | { kind: 'number'; step: number }
422
+ | { kind: 'length'; step: number; unit: string; units?: readonly string[] }
423
+ | { kind: 'number'; step: number; unit?: string }
392
424
  | { kind: 'select'; options: readonly string[] }
393
425
  | { kind: 'text' }
394
426
  | { kind: 'cursor' }
395
427
  | { kind: 'content' }
396
428
  | { kind: 'mask-image' }
397
- | { kind: 'color' };
429
+ | { kind: 'color'; format?: 'hex' | 'oklch' };
398
430
 
399
431
  export interface PillSpec {
400
432
  value: string;
@@ -414,12 +446,10 @@ export type SemanticValue =
414
446
  export interface TierItem {
415
447
  /** Stable id used as the key in persisted state (e.g. `hsp-2xs`). */
416
448
  id: string;
417
- /** CSS custom property written to `:root` (e.g. `--myapp-spacing-hgap-2xs`). */
449
+ /** CSS custom property written to the default root or configured apply sink (e.g. `--myapp-spacing-hgap-2xs`). */
418
450
  cssVar: string;
419
451
  /** Display label shown in the panel row. */
420
452
  label: string;
421
- /** Optional manifest group — tab components use this for section headers. */
422
- group?: string;
423
453
  /** Default value as a CSS string (`0.125rem`, `12px`, etc.). */
424
454
  default: string;
425
455
  /** Discriminated union describing the control kind and its metadata. */
@@ -459,6 +489,10 @@ export interface TierConfig {
459
489
  * this is a multi-source allow-list for individual semantic mappings.
460
490
  */
461
491
  referencesRamps?: readonly { tab?: string; tier: string }[];
492
+ /** Optional visual treatment rendered for this tier's values. */
493
+ preview?: 'size' | 'line-height' | 'family' | 'weight' | 'bar' | 'radius' | 'duration';
494
+ /** CSS variable used as the font-size base for a preview, when supplied. */
495
+ previewBase?: string;
462
496
  }
463
497
 
464
498
  For the full `SemanticValue` mapping and emission behavior, see the maintained
@@ -477,38 +511,78 @@ export interface ColorClusterExtras {
477
511
  defaultShikiTheme: string;
478
512
  colorSchemes: Record<string, ColorScheme>;
479
513
  panelSettings: ClusterPanelSettings;
514
+ /** Optional semantic-item-id → SemanticValue defaults override map. */
515
+ semanticDefaults?: Record<string, SemanticValue>;
480
516
  }
481
517
 
482
518
  /** Top-level tab entry on PanelConfig.tabs. */
483
519
  export interface TabConfig {
484
- /** Stable id. Reserved ids: 'color' (primary color tab), 'color-secondary'. */
520
+ /** Stable id. Dedicated panel ids are 'color', 'font', 'spacing', 'size', 'palette', and 'notes'. */
485
521
  id: string;
486
522
  /** Display label rendered on the tab strip. */
487
523
  label: string;
488
524
  /** Ordered list of tiers within this tab. */
489
525
  tiers: readonly TierConfig[];
490
- /** Tier ids whose rows are hidden behind an Advanced <details> disclosure. */
491
- advancedTiers?: readonly string[];
492
526
  /**
493
- * Required on color tabs (id 'color' / 'color-secondary'). Carries the
527
+ * Required on color entries (id 'color' / 'color-secondary'). Carries the
494
528
  * structural metadata (base roles, scheme registry, panel settings) for the
495
529
  * color tab's palette picker and semantic table. Absent on non-color tabs.
496
530
  */
497
531
  colorExtras?: ColorClusterExtras;
532
+ /** Required on the reserved `notes` tab; forbidden on other tabs. */
533
+ notesExtras?: NotesExtras;
534
+ }
535
+
536
+ export interface NotesExtras {
537
+ title: string;
538
+ html: string;
498
539
  }
499
540
  ```
500
541
 
501
- ### 3.2 Reserved tab ids
542
+ ### 3.2 Preview matrix
543
+
544
+ `preview` is opt-in. Omitting it preserves the ordinary flat tier rows, and a
545
+ tier remains one visible heading; previews do not introduce a collapsed or
546
+ progressive-disclosure tier. The current values are:
547
+
548
+ | `preview` | Valid value kind | Runtime treatment |
549
+ | --- | --- | --- |
550
+ | `'size'` | `length`, or a `referencesTier` resolving to `length` | One type-size sample per item, sorted by resolved pixel size. |
551
+ | `'line-height'` | `number` | Paragraph sample with a leading guide; `previewBase` selects a font-size CSS variable when supplied. |
552
+ | `'family'` | `text` | The first item's font-family value supplies the style for every size and line-height sample. |
553
+ | `'weight'` | `select` or `number` | The first item's font-weight value supplies the style for every sample. |
554
+ | `'bar'` | `length` | Compact bar sized from the token value. |
555
+ | `'radius'` | `length` | Compact rounded-corner glyph. |
556
+ | `'duration'` | `length` or `number` with unit `ms` or `s` | Compact timing glyph using the token's duration value. |
557
+
558
+ The font specimen toolbar persists `{ text, preset, overridden, width }` under
559
+ `${storagePrefix}-specimen`; width is clamped to 240–720 pixels. **Render on
560
+ page** mounts a read-only specimen in the host document under
561
+ `.tokenpanel-on-page-specimen[data-zdtp-specimen]`, temporarily forces the
562
+ right dock, and restores the prior mode when the specimen is disabled, closed,
563
+ unmounted, or loses its dock claim. The specimen is excluded from token scans
564
+ and page pickers.
565
+
566
+ ### 3.3 Reserved tab ids
502
567
 
503
568
  | Tab id | Meaning |
504
569
  | ------------------ | -------------------------------------------------- |
505
570
  | `color` | Primary color tab — palette + base roles + semantics + scheme picker. Requires `colorExtras`. |
506
- | `color-secondary` | Secondary color tab (same shape as `color`). Requires `colorExtras`. |
571
+ | `font` | Dedicated typography tab with family/weight previews and the host-page specimen option. |
572
+ | `spacing` | Dedicated spacing tab with the numeric bulk editor. |
573
+ | `size` | Dedicated size tab with the numeric bulk editor. |
574
+ | `palette` | Dedicated palette editor for a generic palette tab. |
575
+ | `notes` | Dedicated notes tab; it renders content but carries no token state or apply/export overrides. |
576
+
577
+ The panel always prepends a synthetic `Inspect` tab for element inspection; it
578
+ is not supplied in `PanelConfig.tabs`. A configured `color-secondary` entry is
579
+ the companion color-cluster data source consumed by the primary Color tab (and
580
+ requires `colorExtras`); it is not an additional dedicated body dispatcher.
507
581
 
508
- Any other id dispatches to `GenericTab`, which renders the tab's `tiers`
509
- using kind-appropriate editors.
582
+ Any other configured id dispatches to `GenericTab`, which renders the tab's
583
+ `tiers` using kind-appropriate editors.
510
584
 
511
- ### 3.3 Validation rules
585
+ ### 3.4 Validation rules
512
586
 
513
587
  `assertValidPanelConfig` enforces these structural rules at the host-adapter
514
588
  trust boundary:
@@ -523,7 +597,7 @@ trust boundary:
523
597
  - `referencesTier` must name an existing tier in the same tab, and the
524
598
  referencing tier's kind must match the referenced tier's kind.
525
599
 
526
- ### 3.4 Apply behaviour for ref-tier items
600
+ ### 3.5 Apply behaviour for ref-tier items
527
601
 
528
602
  When a `TierConfig` carries `referencesTier`, the apply pipeline treats each
529
603
  item's persisted value as the id of an item in the referenced tier. The
@@ -532,9 +606,9 @@ emitted CSS override is `var(--target-cssvar)` where `target-cssvar` is the
532
606
 
533
607
  By default the write target is `:root` (`document.documentElement`). When a
534
608
  `PanelConfig.applySink` is configured for the instance, writes are routed
535
- through the sink instead — see §3.5.
609
+ through the sink instead — see §3.6.
536
610
 
537
- ### 3.5 `applySink` — optional CSS-var write target
611
+ ### 3.6 `applySink` — optional CSS-var write target
538
612
 
539
613
  When `PanelConfig.applySink` is set, all CSS-var writes and clears for that
540
614
  panel instance route through the sink rather than `document.documentElement`.
@@ -596,7 +670,7 @@ const handle = configurePanel({
596
670
  });
597
671
  ```
598
672
 
599
- ### 3.6 Helpers (re-exported from the package root)
673
+ ### 3.7 Helpers (re-exported from the package root)
600
674
 
601
675
  ```ts
602
676
  export function isLengthKind(v: TierValueKind): boolean;
@@ -609,6 +683,23 @@ export function isContentKind(v: TierValueKind): boolean;
609
683
  export function isMaskImageKind(v: TierValueKind): boolean;
610
684
  ```
611
685
 
686
+ ### 3.8 Canonical state transaction
687
+
688
+ All panel mutations use one transaction path, including ordinary row edits,
689
+ bulk actions, imports, snapshot restore, undo, redo, and reset. A transaction
690
+ applies the new state and CSS-variable writes, saves the persisted envelope,
691
+ updates component state, and then records the history entry in that order.
692
+ History is identity-aware and held in memory only; the persisted A/B snapshots
693
+ and token envelope are separate storage concerns. After a disk apply, the
694
+ implementation resets the last-applied baseline to `{}` and reconciles only
695
+ variables confirmed as written, so retained or unrouted overrides stay dirty.
696
+ Because disk Apply does not write base-role variables, a confirmed semantic
697
+ write derived from an unchanged `bg` or `fg` alias also reconciles that alias's
698
+ role-index dependency. Before resetting the dependency, other unwritten
699
+ semantic aliases using the same role are materialized to their resolved
700
+ numeric palette index. Their emitted value and dirty state are preserved for a
701
+ later Apply.
702
+
612
703
  ---
613
704
 
614
705
  ## 4. Color tab contract
@@ -659,9 +750,11 @@ export interface ColorClusterExtras {
659
750
  * on init. Set to `false` to disable scheme-to-`data-theme` binding; this
660
751
  * does not disable per-mode literal editing or emitted `light-dark(...)`
661
752
  * values.
662
- */
753
+ */
663
754
  colorMode: false | { defaultMode: 'light' | 'dark'; lightScheme: string; darkScheme: string };
664
755
  };
756
+ /** Optional semantic-item-id → SemanticValue defaults override map. */
757
+ semanticDefaults?: Record<string, SemanticValue>;
665
758
  }
666
759
  ```
667
760
 
@@ -676,8 +769,8 @@ export interface ColorScheme {
676
769
  cursor: ColorRef;
677
770
  selectionBg: ColorRef;
678
771
  selectionFg: ColorRef;
679
- palette: readonly string[]; // length must match the palette tier's item count
680
- shikiTheme: string;
772
+ palette: readonly string[]; // the public type requires exactly 16 entries
773
+ shikiTheme?: string;
681
774
  semantic?: Record<string, ColorRef>;
682
775
  }
683
776
  ```
@@ -744,13 +837,19 @@ The apply pipeline for color tabs:
744
837
 
745
838
  - For each palette `TierItem` in the palette tier, write
746
839
  `item.cssVar` ← `palette[i]` from the active scheme / user override.
747
- - For each `(roleKey, cssName)` in `colorExtras.baseRoles`, write
748
- `cssName` ← `palette[state[roleKey]]`.
840
+ - For each `(roleKey, cssName)` in `colorExtras.baseRoles`, the live DOM apply
841
+ path writes `cssName` ← `palette[state[roleKey]]`.
749
842
  - For each semantic `TierItem`, resolve
750
843
  `state.semanticMappings[key] ?? colorExtras.semanticDefaults[key]`
751
- through `resolveMapping` and write `item.cssVar` ← resolved hex.
844
+ through `resolveMapping` and write the emitted CSS value: `var(...)` for a
845
+ palette/reference mapping, a literal string for a literal mapping, or
846
+ `light-dark(light, dark)` for a per-mode literal.
752
847
  - `clearAppliedStyles()` removes every property the cluster could have set.
753
848
 
849
+ The disk `buildApplyOverrides` payload intentionally emits palette and
850
+ semantic CSS variables only; base-role values are runtime wiring and are not
851
+ included in source-file rewrites.
852
+
754
853
  ### 4.6 `applyEndpoint` and `applyRouting`
755
854
 
756
855
  The Apply modal's button is gated on two `PanelConfig` fields:
@@ -773,6 +872,13 @@ The **bin server** is the reference implementation for the apply contract.
773
872
  ### 5.1 Request & response envelopes
774
873
 
775
874
  The Apply button POSTs to `PanelConfig.applyEndpoint` with a flat JSON diff.
875
+ The client sends only changed tokens. A token is changed exactly when the CSS
876
+ value it would emit differs from the value its baseline would emit. Therefore
877
+ an empty flat override or one equal to its manifest default is omitted;
878
+ semantic role aliases and palette indices compare by their resolved palette
879
+ slot, while literal and ref mappings compare structurally. Palette slots use
880
+ the active color-identity baseline. The Apply payload may also contain a
881
+ secondary color cluster, diffed against that cluster's configured defaults.
776
882
 
777
883
  **Request**
778
884
 
@@ -788,6 +894,60 @@ Content-Type: application/json
788
894
  }
789
895
  ```
790
896
 
897
+ The same endpoint accepts two optional coordination fields:
898
+
899
+ - `dryRun: true` computes a preview without creating a temporary file,
900
+ renaming a file, or otherwise mutating disk. A dry run may mix routed and
901
+ unrouted tokens; unrouted entries are diagnostics in the successful preview
902
+ rather than a whole-request error.
903
+ - `expectDigests` maps the repo-relative `file` values from a preceding preview
904
+ to their SHA-256 `digest` values. On a real write, every supplied digest is
905
+ checked after all files have been read and before any file is written.
906
+
907
+ **Response 200 (dry run)**
908
+
909
+ ```json
910
+ {
911
+ "ok": true,
912
+ "dryRun": true,
913
+ "files": [
914
+ {
915
+ "file": "src/styles/tokens.css",
916
+ "blockKind": "root",
917
+ "digest": "<64 lowercase SHA-256 hex characters>",
918
+ "changed": ["--myapp-spacing-md"],
919
+ "unchanged": ["--myapp-spacing-lg"],
920
+ "unknown": [],
921
+ "unknownOutsideBlock": [],
922
+ "hunks": [
923
+ {
924
+ "cssVar": "--myapp-spacing-md",
925
+ "line": 12,
926
+ "before": " --myapp-spacing-md: 1rem;",
927
+ "after": " --myapp-spacing-md: 2rem;",
928
+ "context": {
929
+ "before": [" --myapp-spacing-sm: 0.5rem;"],
930
+ "after": [" --myapp-spacing-lg: 3rem;"]
931
+ }
932
+ }
933
+ ]
934
+ }
935
+ ],
936
+ "rejected": ["--unrouted-token"],
937
+ "rejectedReasons": ["--unrouted-token: no route configured for prefix family (...)"]
938
+ }
939
+ ```
940
+
941
+ `blockKind` identifies the scanned block containing the requested declaration;
942
+ `:root` wins when a request changes declarations in both supported block kinds.
943
+ For an unknown-only file result, the first available kind (`root`, then
944
+ `theme`) is reported. `line` is one-based. `before` and `after` are complete
945
+ declaration lines, while each context array contains the adjacent proposed-file
946
+ line on that side. `hunks` contains exactly one entry per changed cssVar; two
947
+ declarations on one physical line therefore produce two independently-keyed
948
+ hunks with the same line number. Unknown and unchanged tokens do not produce
949
+ hunks.
950
+
791
951
  **Response 200 (success)**
792
952
 
793
953
  ```json
@@ -798,11 +958,13 @@ Content-Type: application/json
798
958
  "file": "src/styles/tokens.css",
799
959
  "changed": ["--myapp-spacing-md"],
800
960
  "unchanged": ["--myapp-spacing-lg"],
801
- "unknown": []
961
+ "unknown": [],
962
+ "unknownOutsideBlock": []
802
963
  }
803
964
  ],
804
965
  "unknownCssVars": [],
805
- "unchangedCssVars": ["--myapp-spacing-lg"]
966
+ "unchangedCssVars": ["--myapp-spacing-lg"],
967
+ "unknownOutsideBlockCssVars": []
806
968
  }
807
969
  ```
808
970
 
@@ -817,8 +979,10 @@ Content-Type: application/json
817
979
  ```
818
980
 
819
981
  Returned for: malformed JSON, missing `tokens` field, empty tokens map,
820
- invalid token names (no `--` prefix, spaces, slashes), unsupported CSS-var
821
- prefix, path escape attempts.
982
+ invalid token names (no `--` prefix, spaces, slashes), or path escape attempts.
983
+ For a real write, an unsupported CSS-var prefix is also a 400 error. A dry run
984
+ keeps unsupported prefixes in its `rejected` / `rejectedReasons` diagnostics
985
+ so the caller can preview the rest of the request.
822
986
 
823
987
  **Response 403 (Forbidden)**
824
988
 
@@ -833,9 +997,30 @@ Empty body, `Allow: POST, OPTIONS` header.
833
997
  **Response 409 (Conflict)**
834
998
 
835
999
  ```json
836
- { "ok": false, "error": "No top-level :root { ... } block in <file>" }
1000
+ { "ok": false, "error": "No top-level :root { ... } or @theme { ... } block in <file>" }
1001
+ ```
1002
+
1003
+ The handler scans only the first top-level `:root` block and the first
1004
+ top-level `@theme` block (bare or with one modifier). `:root` wins when the
1005
+ same variable is declared in both. Later blocks and nested blocks are not
1006
+ rewritable. A dry run can succeed with only unrouted tokens; those tokens are
1007
+ listed in `rejected` and `rejectedReasons`, while a file with no supported
1008
+ block is a `409`.
1009
+
1010
+ A real write whose current file content does not match a supplied preview
1011
+ digest returns the following envelope before any target is written:
1012
+
1013
+ ```json
1014
+ {
1015
+ "ok": false,
1016
+ "reason": "stale-file",
1017
+ "files": ["src/styles/tokens.css"]
1018
+ }
837
1019
  ```
838
1020
 
1021
+ The client must request a new dry run and ask the user to review the refreshed
1022
+ hunks. Omitting `expectDigests` preserves the legacy write behaviour.
1023
+
839
1024
  **Response 500 (Internal server error)**
840
1025
 
841
1026
  ```json
@@ -863,14 +1048,19 @@ Hosts physically unable to spawn Node.js must:
863
1048
  the routing map, reject prefixes not in the map.
864
1049
  3. Path safety — resolve each target path to an absolute path, verify it sits
865
1050
  within `writeRoot`, reject path-escape attempts.
866
- 4. Read & parse — load each CSS file, find the `:root { ... }` block (fail
867
- 409 if missing), parse the existing variable values.
868
- 5. Compute rewrite — compute `changed` / `unchanged` / `unknown`, build the
869
- updated `:root` block.
870
- 6. Atomic write — keep the original file content in memory. Write updated
1051
+ 4. Read & parse — load each CSS file, find the first top-level `:root { ... }`
1052
+ or first top-level `@theme { ... }` block (bare or with one modifier; fail
1053
+ 409 if neither exists), parse the existing variable values.
1054
+ 5. Compute rewrite — compute `changed` / `unchanged` / `unknown` and
1055
+ `unknownOutsideBlock`, build the updated `:root` / `@theme` content, its
1056
+ per-cssVar hunks, and the SHA-256 of the original bytes.
1057
+ 6. Preview/stale gate — for `dryRun: true`, return the preview immediately. For
1058
+ a real write with `expectDigests`, compare every supplied digest and return
1059
+ the stale-file 409 before the first mutation when any target differs.
1060
+ 7. Atomic write — keep the original file content in memory. Write updated
871
1061
  content to a temp file. Atomically rename temp to target. If any write
872
1062
  fails, restore every previously-written file.
873
- 7. Respond — return the exact JSON envelope shapes pinned in §5.1.
1063
+ 8. Respond — return the exact JSON envelope shapes pinned in §5.1.
874
1064
 
875
1065
  ### 5.4 Routing config — single source of truth
876
1066
 
@@ -933,15 +1123,17 @@ if (
933
1123
  specific version key — an empty `{}` / `[]` / `null` / `''` does NOT trigger
934
1124
  it. (zdtp itself never writes such a value: `clearPersistedState()` removes
935
1125
  the `-state` keys outright, so this guard only covers envelopes written by
936
- hand or by another tool.) Overrides MUST be re-applied to `:root` even when
937
- the panel stays hidden, otherwise hard-nav produces a FOUT.
1126
+ hand or by another tool.) Overrides MUST be re-applied to the configured
1127
+ sink (or default `:root`) even when the panel stays hidden, otherwise
1128
+ hard-nav produces a FOUT.
938
1129
  - `shouldAutoload()` — reads `${storagePrefix}:autoload` (colon-form, §2).
939
1130
  Returns `true` when the flag is `'1'` (explicit, written by `enableAutoload()`)
940
1131
  OR `'auto'` (auto-remembered, written by opening the panel — see "Auto-remember
941
1132
  on open" below). This is the owner-mode signal: the panel bundle fetches
942
1133
  eagerly and mounts CLOSED so the element-path inspector is armed even though
943
- the panel UI is hidden. General visitors (no flag, or `'0'`) pay zero bundle
944
- cost. A downstream host that wants to distinguish the two populations can
1134
+ the panel UI is hidden. General visitors (no flag, or `'0'`) pay no
1135
+ panel-bundle cost; the small host adapter/config bootstrap still runs. A
1136
+ downstream host that wants to distinguish the two populations can
945
1137
  test `=== '1'` directly — see "Auto-remember on open" for the caveat.
946
1138
  - `loadElementPathEnabled()` — reads the element-path inspector's persistence
947
1139
  key. Returns `true` when the inspector was left enabled. Ensures the Preact
@@ -952,9 +1144,11 @@ if (
952
1144
  enabled. Ensures the Preact shell is mounted and the lazy boundary is
953
1145
  imported even when the panel UI is hidden.
954
1146
 
955
- When none of the five signals is present — the common case for first-time
1147
+ When none of the six signals is present — the common case for first-time
956
1148
  visitors and general site visitors on a public site with owner-autoload — the
957
- panel bundle is NOT fetched and the page is completely free of panel JS.
1149
+ panel bundle is NOT fetched, no panel stylesheet is injected, and no panel
1150
+ root is mounted. The small host adapter/config bootstrap still runs to make
1151
+ that decision.
958
1152
 
959
1153
  #### Storage-key table for §6.2 signals
960
1154
 
@@ -1006,7 +1200,9 @@ panel bundle is NOT fetched and the page is completely free of panel JS.
1006
1200
  #### Auto-remember on open
1007
1201
 
1008
1202
  Any action that shows the panel (`showDesignPanel()`, `toggleDesignPanel()`,
1009
- or the panel's header button) MUST also write
1203
+ the fixed-name `window.zdtp.show()` / `.toggle()` aliases, an instance
1204
+ handle's `open()` / `toggle()`, the panel header's open action, or an instance
1205
+ toggle event) MUST also write
1010
1206
  `${storagePrefix}:autoload = 'auto'` (auto-remembered provenance, distinct
1011
1207
  from the `'1'` that `enableAutoload()` writes) — implemented by
1012
1208
  `rememberAutoload()`. This ensures that once the owner has opened the panel
@@ -1034,13 +1230,26 @@ already hold `'1'`, and that provenance was never recorded, so it cannot be
1034
1230
  reclassified. The `=== '1'` discrimination applies only to opens made from
1035
1231
  this version onward.
1036
1232
 
1233
+ #### Shared Alt+click picker ownership
1234
+
1235
+ Element path, DOM Tweaker, and element inspect share one document-level
1236
+ Alt+click coordinator. Only one owner may be armed at a time; a new request
1237
+ revokes the previous owner's armed state before it starts. Panel-owned surfaces
1238
+ and the host-page specimen are excluded from all three pickers.
1239
+
1240
+ | Feature | Activation | Result |
1241
+ | --- | --- | --- |
1242
+ | Element path | Owner autoload or its panel toggle, then `Alt+click` | Copies an annotated selector/path block. |
1243
+ | DOM Tweaker | Configured feature toggle, then `Alt+click` | Opens the Tailwind class editor and live utility preview. |
1244
+ | Element inspect | Header toggle or `I`, then click; `Alt` also arms the coordinator | Opens the reserved inspect tab with computed and inherited token rows. |
1245
+
1037
1246
  ### 6.3 Astro view-transition lifecycle
1038
1247
 
1039
1248
  The adapter's existing `astro:before-swap` and `astro:page-load` listeners
1040
1249
  stay. They are Astro-specific and only register when `document` is available:
1041
1250
 
1042
1251
  - `astro:before-swap` → unmount the Preact tree, remove the host node, snapshot/restore visibility intent.
1043
- - `astro:page-load` → re-apply persisted overrides + re-materialise the shell when any of the four gate signals (§6.2) is true.
1252
+ - `astro:page-load` → re-apply persisted overrides + re-materialise the shell when any of the six gate signals (§6.2) is true.
1044
1253
 
1045
1254
  ### 6.4 Console API
1046
1255
 
@@ -1104,11 +1313,12 @@ window.zdtp.toggle = () => void | Promise<void>; // toggle the panel
1104
1313
  always installs first ("first install wins" — see the next point). The
1105
1314
  package-root install site is therefore reached only by non-Astro hosts.
1106
1315
  - **Never clobbers a host-defined `window.zdtp`.** If `window.zdtp` already
1107
- exists and was not installed by this package, the install is skipped with
1108
- a `console.warn` — the host's own global is left untouched. (This also
1109
- covers the edge case of a host that picks `consoleNamespace: 'zdtp'`: the
1110
- namespace object installed at §6.4 is not this package's alias marker, so
1111
- the second install site treats it as host-owned and skips.)
1316
+ exposes callable `show` / `hide` / `toggle` methods, the install is skipped
1317
+ silently — this supports hosts that pre-claim the alias before lazily loading
1318
+ the panel bundle. Other pre-existing values are also left untouched, but
1319
+ produce a `console.warn`. (This includes the edge case of a host that picks
1320
+ `consoleNamespace: 'zdtp'`: the namespace object installed at §6.4 does not
1321
+ expose the fixed-name alias shape, so the second install site warns and skips.)
1112
1322
  - **Auto-remember carries over for free.** `zdtp.show()` routes through the
1113
1323
  same `showDesignTokenPanel()` / `handle.open()` core as every other open
1114
1324
  path, so it arms `${storagePrefix}:autoload = 'auto'` exactly like
@@ -1121,32 +1331,55 @@ window.zdtp.toggle = () => void | Promise<void>; // toggle the panel
1121
1331
 
1122
1332
  ### 7.1 Panel-private namespace
1123
1333
 
1124
- The panel ships its own bundled CSS. All panel-chrome variables use the
1125
- `--tokentweak-*` prefix, scoped to the panel shell + modal class prefix:
1334
+ The panel ships its own bundled CSS. Panel-private color, font, spacing,
1335
+ typography, and z-index variables use the `--tokentweak-*` prefix; the shared
1336
+ radius token is `--radius-tokentweak`. They are scoped to the panel shell,
1337
+ mini pill, modal class prefix, and the body-level popover/tooltip/inspector
1338
+ surfaces:
1126
1339
 
1127
1340
  ```css
1128
1341
  :where(.tokenpanel-shell, [data-design-token-panel-modal]) {
1129
- --tokentweak-pad-md: …;
1130
- --tokentweak-gap-sm: …;
1131
- --tokentweak-color-fg: #b8b8b8;
1342
+ /* base-0 is the darkest ground; stops ascend toward the foreground. */
1343
+ --tokentweak-palette-base-5: oklch(0.8 0 0);
1344
+ --tokentweak-color-fg: var(--tokentweak-palette-base-5);
1345
+ --tokentweak-color-accent-bar: #efb477;
1132
1346
  /* …every panel-chrome value lives here */
1133
1347
  }
1134
1348
  ```
1135
1349
 
1136
1350
  - **No Tailwind dependency.** The package builds and runs without Tailwind in
1137
1351
  the consumer.
1138
- - **Consumer import required.** The `./styles` sub-export must be imported
1139
- exactly once from the consumer's static module graph:
1352
+ - **Self-injected by the panel entry.** The panel calls `ensurePanelStyles()`
1353
+ when it first mounts, so a consumer does not need a CSS import. The `./styles`
1354
+ sub-export remains available when a host wants to pull the stylesheet into its
1355
+ own static CSS pipeline; importing it twice is unnecessary.
1140
1356
 
1141
1357
  ```ts
1142
- import '@takazudo/zdtp/styles';
1358
+ import '@takazudo/zdtp/styles'; // optional static-CSS path
1143
1359
  ```
1144
1360
 
1361
+ The semantic color layer includes `--tokentweak-color-fg`, `bg`, `muted`,
1362
+ `border`, `surface`, `accent`, `accent-bar`, `accent-hover`, `code-bg`,
1363
+ `code-fg`, `success`, `danger`, and `warning`, backed by the private
1364
+ `--tokentweak-palette-base-*` OKLCH ramp, plus `--tokentweak-font-mono`. Spacing,
1365
+ typography, radius, and stacking values use the same prefix (`pad-*`, `gap-*`,
1366
+ `text-*`, `--radius-tokentweak`, and `z-*`). The chrome does not read host
1367
+ `--color-*` or `--font-mono` variables; hosts that retheme it assign the
1368
+ `--tokentweak-*` names directly on one of the listed scopes.
1369
+
1370
+ Docking is a host-document contract rather than a panel-private token:
1371
+ `--zdtp-dock-inset-right` and `--zdtp-dock-inset-bottom` are published on the
1372
+ host root while the corresponding dock claim is active. The on-page specimen
1373
+ uses `.tokenpanel-on-page-specimen[data-zdtp-specimen]`; it inherits the host
1374
+ font/foreground, is excluded from token scans and page pickers, and is removed
1375
+ when the specimen is disabled, closed, unmounted, or loses its dock claim.
1376
+
1145
1377
  ### 7.2 Consumer's editable tokens
1146
1378
 
1147
1379
  The tokens the panel writes to (the `cssVar` field on each `TierItem`) are
1148
- entirely consumer-controlled. The package just writes them through `setProperty`
1149
- on `:root`.
1380
+ entirely consumer-controlled. The package writes them through the configured
1381
+ `applySink`, or through `setProperty` on the default `:root` target when no
1382
+ sink is supplied.
1150
1383
 
1151
1384
  - **Read:** the panel never reads consumer CSS variables (it carries its own
1152
1385
  defaults via `TierItem.default`).
@@ -1162,20 +1395,30 @@ rules on `[data-design-token-panel-modal]`.
1162
1395
 
1163
1396
  ### 7.4 Self-contained panel chrome palette (no host theme reads)
1164
1397
 
1165
- The panel-chrome color tokens are declared in `panel-tokens.css` as
1166
- concrete dark-palette values so the panel paints as a neutral dark surface
1398
+ The panel-chrome color tokens are declared in `panel-tokens.css` as semantic
1399
+ aliases onto a private OKLCH ramp so the panel paints as a neutral dark surface
1167
1400
  regardless of what the host's `--color-*` tokens resolve to:
1168
1401
 
1169
1402
  ```css
1170
1403
  :where(.tokenpanel-shell, [data-design-token-panel-modal]) {
1171
- --tokentweak-color-fg: #b8b8b8;
1172
- --tokentweak-color-bg: #181818;
1173
- --tokentweak-color-muted: #888888;
1174
- --tokentweak-color-surface: #1c1c1c;
1404
+ /* base-0 is the darkest ground; stops ascend toward the foreground. */
1405
+ --tokentweak-palette-base-0: oklch(0.18 0 0);
1406
+ --tokentweak-palette-base-1: oklch(0.25 0 0);
1407
+ --tokentweak-palette-base-2: oklch(0.34 0 0);
1408
+ --tokentweak-palette-base-3: oklch(0.536 0 0);
1409
+ --tokentweak-palette-base-4: oklch(0.66 0 0);
1410
+ --tokentweak-palette-base-5: oklch(0.8 0 0);
1411
+ --tokentweak-palette-base-6: oklch(0.91 0 0);
1412
+ --tokentweak-color-fg: var(--tokentweak-palette-base-5);
1413
+ --tokentweak-color-bg: var(--tokentweak-palette-base-0);
1414
+ --tokentweak-color-muted: var(--tokentweak-palette-base-4);
1415
+ --tokentweak-color-border: var(--tokentweak-palette-base-3);
1416
+ --tokentweak-color-surface: var(--tokentweak-palette-base-1);
1175
1417
  --tokentweak-color-accent: #d69a66;
1418
+ --tokentweak-color-accent-bar: #efb477;
1176
1419
  --tokentweak-color-accent-hover: #a7c0e3;
1177
- --tokentweak-color-code-bg: #383838;
1178
- --tokentweak-color-code-fg: #e0e0e0;
1420
+ --tokentweak-color-code-bg: var(--tokentweak-palette-base-2);
1421
+ --tokentweak-color-code-fg: var(--tokentweak-palette-base-6);
1179
1422
  --tokentweak-color-success: #93bb77;
1180
1423
  --tokentweak-color-danger: #da6871;
1181
1424
  --tokentweak-color-warning: #dfbb77;
@@ -1192,9 +1435,17 @@ in a demo — MUST NOT bleed into the panel chrome.
1192
1435
  **Override surface for hosts:** a host that wants to retheme the panel
1193
1436
  chrome assigns directly to the `--tokentweak-color-*` /
1194
1437
  `--tokentweak-font-mono` names on `.tokenpanel-shell`,
1195
- `[data-design-token-panel-modal]`, or any ancestor (`:where()` keeps
1196
- specificity at 0). This single name layer is the entire host-override
1197
- contract for panel chrome — `--color-*` reads are not part of it.
1438
+ `.tokenpanel-mini-pill`, `[data-design-token-panel-modal]`, the highlight
1439
+ settings and chain popovers, the color picker, tooltip, element-path label and
1440
+ toast, or an element-inspect surface (or any ancestor). `:where()` keeps
1441
+ specificity at 0. This single name layer is the entire host-override contract
1442
+ for panel chrome — `--color-*` reads are not part of it.
1443
+
1444
+ Hosts may instead override a `--tokentweak-palette-base-*` stop to update all
1445
+ roles that alias it; a direct semantic `--tokentweak-color-*` assignment still
1446
+ wins. The border role is intentionally separate: `--tokentweak-color-muted`
1447
+ now recolors secondary text only. A host that previously used it for both text
1448
+ and 1px dividers must also assign `--tokentweak-color-border`.
1198
1449
 
1199
1450
  **Invariant:** the panel package MUST NOT read `--color-*` or
1200
1451
  `--font-mono` anywhere. Both `panel.css` and `panel-tokens.css` are pinned
@@ -1209,8 +1460,10 @@ grep -n 'var(--font-mono' src/styles/panel-tokens.css # → 0
1209
1460
 
1210
1461
  ### 7.5 Host-adapter side-effect import (paired-unit obligation)
1211
1462
 
1212
- Alongside the `./styles` import, the consumer MUST own a side-effect import
1213
- for the host-adapter, paired with `<DesignTokenPanelHost>`:
1463
+ The consumer MUST own a side-effect import for the host-adapter, paired with
1464
+ `<DesignTokenPanelHost>`. The `./styles` import is optional because the panel
1465
+ entry self-injects its stylesheet; use it only when the host wants a static CSS
1466
+ pipeline:
1214
1467
 
1215
1468
  ```astro
1216
1469
  <DesignTokenPanelHost config={myPanelConfig} />
@@ -1224,11 +1477,13 @@ for the host-adapter, paired with `<DesignTokenPanelHost>`:
1224
1477
 
1225
1478
  ## 8. Storage-key continuity & migration paths
1226
1479
 
1227
- ### 8.1 No default `PanelConfig`
1480
+ ### 8.1 Minimal fallback before configuration
1228
1481
 
1229
- The package ships **zero** baked-in identifiers. The host MUST configure the
1230
- panel explicitly. A package import without an explicit configure-call surfaces
1231
- a clear runtime error.
1482
+ The package ships **zero** host-specific identifiers. Before the first explicit
1483
+ `configurePanel` call, `getPanelConfig()` returns a minimal sentinel with empty
1484
+ token manifests and a stub color cluster so imports and adapter boot can remain
1485
+ safe; it is not a useful consumer configuration. Hosts MUST configure the
1486
+ panel explicitly to render their own tabs and token values.
1232
1487
 
1233
1488
  ### 8.2 Storage-key derivation is literal
1234
1489
 
@@ -1285,7 +1540,7 @@ The historical zdtp-internal map is exported as `ZDTP_LEGACY_TYPOGRAPHY_RENAME_M
1285
1540
 
1286
1541
  ---
1287
1542
 
1288
- ## 9. JSON export / import schema (serde v2)
1543
+ ## 9. JSON export / import schema (serde)
1289
1544
 
1290
1545
  ### 9.1 Schema versioning
1291
1546
 
@@ -1293,9 +1548,12 @@ The historical zdtp-internal map is exported as `ZDTP_LEGACY_TYPOGRAPHY_RENAME_M
1293
1548
  | ----------------------- | ------- | ------------------------------------------------------ |
1294
1549
  | `zudo-design-tokens/v1` | Legacy | Flat top-level `color`/`spacing`/`typography`/`size` keys |
1295
1550
  | `zudo-design-tokens/v2` | Current | `tabs` wrapper keyed by tab id; cssVar-keyed leaves |
1551
+ | `zudo-design-tokens/v3` | Current | v2 structure with object-valued semantic color mappings |
1296
1552
 
1297
- `serialize()` always emits v2. `deserialize()` accepts both v1 and v2 and
1298
- normalises to an internal `TweakState`.
1553
+ `serialize()` emits v2 for states whose semantic mappings are representable by
1554
+ the v2 shape, and upgrades to v3 when an object-valued semantic mapping needs
1555
+ the v3 shape. `deserialize()` accepts v1, v2, and v3 and normalises each to an
1556
+ internal `TweakState`.
1299
1557
 
1300
1558
  ### 9.2 v2 format
1301
1559
 
@@ -1331,7 +1589,14 @@ Key decisions:
1331
1589
 
1332
1590
  `serialize()` only emits tokens the user has changed relative to manifest
1333
1591
  defaults. Pass `includeDefaults: true` to dump the full state. A tab key is
1334
- omitted entirely when nothing in it differs.
1592
+ omitted entirely when nothing in it differs. "Changed" uses the canonical
1593
+ emitted-value definition in §5.1. Export parity covers tokens representable by
1594
+ the current schema: flat tabs and the primary color cluster. The secondary
1595
+ color cluster is Apply-only and is not added to the export schema here.
1596
+
1597
+ A sparse persisted override equal to today's manifest default remains stored
1598
+ but invisible to the UI, diff-only export, and Apply. It can intentionally
1599
+ resurface if a later manifest changes that default.
1335
1600
 
1336
1601
  ---
1337
1602
 
@@ -1341,10 +1606,12 @@ Items this contract deliberately does NOT pin down:
1341
1606
 
1342
1607
  - **Persist envelope internal shape** — frozen at the current shape so
1343
1608
  existing user state round-trips without migration.
1344
- - **Schema id versioning.** `schemaId` is a configure-time string; bumping
1345
- it is the host's responsibility.
1609
+ - **Schema id versioning.** `schemaId` is a configure-time display label
1610
+ returned by `getDesignTokenSchema()`; it does not select or version the
1611
+ serializer. The package-owned `SCHEMA_V1` / `SCHEMA_V2` / `SCHEMA_V3`
1612
+ constants govern export and import validation.
1346
1613
  - **Shadow-DOM scoping.** The panel writes to `:root` by default; hosts
1347
- that need scoped writes use `PanelConfig.applySink` (§3.5). The sink
1614
+ that need scoped writes use `PanelConfig.applySink` (§3.6). The sink
1348
1615
  target's lifecycle is owned by the host — not pinned here.
1349
1616
  - **Theme-API surface.** The panel does not expose a programmatic API for
1350
1617
  reading the current overrides outside the persist envelope.
@@ -1360,21 +1627,26 @@ Cross-reference table — what each section pins down.
1360
1627
  | `configurePanel({...})` signature, multi-instance, `PanelInstanceHandle`, per-instance toggle events | §1 |
1361
1628
  | Storage-key derivation | §2, §8 |
1362
1629
  | Default first-open geometry (coherent size+position, viewport containment, cascade, persisted-position precedence) | §2.1 |
1630
+ | `PanelDockConfig`, dock modes, body-margin reflow, edge claims, and dock storage | §1, §2, §7 |
1363
1631
  | `TabConfig` / `TierConfig` / `TierItem` / `TierValueKind` interfaces and apply behaviour | §3 |
1364
- | `applySink` — optional CSS-var write target (upsert / clear / Reset full set) | §3.5 |
1632
+ | `TierConfig.preview` / `previewBase` matrix and host-page specimen lifecycle | §3.2 |
1633
+ | `applySink` — optional CSS-var write target (upsert / clear / Reset full set) | §3.6 |
1365
1634
  | `ColorClusterExtras` shape and multi-cluster support | §4.1, §4.3 |
1366
1635
  | JSON-serializable constraint on color tab config | §4.2 |
1367
1636
  | `colorPresets` and `setPanelColorPresets()` lazy attachment | §4.4 |
1368
1637
  | Color apply behaviour | §4.5 |
1369
- | Apply pipeline request / response envelopes | §5.1 |
1638
+ | Apply pipeline request / response envelopes, dry-run hunks/digests, stale-write 409 | §5.1 |
1639
+ | Canonical transaction order and in-memory undo/redo history | §3.8 |
1370
1640
  | Reference-implementation algorithm + native-implementation guidance | §5.2, §5.3 |
1371
1641
  | Routing config single-source | §5.4 |
1372
- | Astro `<DesignTokenPanelHost>` prop, lazy-load gate (4-signal), owner-autoload, console API | §6 |
1642
+ | Astro `<DesignTokenPanelHost>` prop, lazy-load gate (6-signal), owner-autoload, console API | §6 |
1643
+ | Shared Alt+click owner for element path, DOM Tweaker, and element inspect | §6.2 |
1373
1644
  | Fixed-name global open API (`window.zdtp.show/hide/toggle`) | §6.5 |
1374
1645
  | `--tokentweak-*` namespace and Tailwind-free CSS contract | §7.1 |
1375
1646
  | Modal class prefix and `data-design-token-panel-modal` selector contract | §7.3 |
1376
1647
  | Self-contained panel chrome palette (no host theme reads) | §7.4 |
1377
1648
  | Host-adapter side-effect import (paired-unit obligation) | §7.5 |
1378
1649
  | v4 envelope precedence, v1/v2/v3 storage migration, and typography-id rename map | §2, §8.3, §8.4 |
1379
- | JSON export/import schema v2 (serde v2) | §9 |
1650
+ | JSON export/import schemas v1/v2/v3 (serde) | §9 |
1380
1651
  | Out-of-scope / deferred concerns | §10 |
1652
+ | Feature walkthrough and shortcut table | [Panel UX tour](/docs/recipes/panel-ux-tour) |