@takazudo/zdtp 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.5
4
+
5
+ ### Features
6
+
7
+ - Per-scheme/per-mode keyed color persistence — envelope v4 ([#509](https://github.com/Takazudo/zudo-design-token-panel/issues/509)) (653c4ab)
8
+ - Distinguish declared-outside-scanned-block from genuinely-absent unknowns ([#508](https://github.com/Takazudo/zudo-design-token-panel/issues/508)) (441fa93)
9
+ - Scan top-level `@theme` blocks in `applyTokenOverrides` ([#507](https://github.com/Takazudo/zudo-design-token-panel/issues/507)) (d6c6692)
10
+ - Add `colorExtras.semanticDefaults` config-time override map ([#499](https://github.com/Takazudo/zudo-design-token-panel/issues/499)) (543136e)
11
+
12
+ ### Fixed
13
+
14
+ - Harden v4 persistence load edge cases ([#509](https://github.com/Takazudo/zudo-design-token-panel/issues/509) audit, epic [#502](https://github.com/Takazudo/zudo-design-token-panel/issues/502) review) (2b07460)
15
+ - Scope color-scheme clearing to panel-written values (closes [#506](https://github.com/Takazudo/zudo-design-token-panel/issues/506)) (f434c8c)
16
+ - Validate persisted semantic index mappings against live cluster paletteSize ([#503](https://github.com/Takazudo/zudo-design-token-panel/issues/503)) (915ffbf)
17
+
18
+ ### Other Changes
19
+
20
+ - docs: sync README + doc-site with `@theme` apply, v4 persistence, schemaId ([#511](https://github.com/Takazudo/zudo-design-token-panel/issues/511)) (3c3fd57)
21
+ - test: cross-cutting interaction coverage for [#510](https://github.com/Takazudo/zudo-design-token-panel/issues/510) confirm pass (9a5d7fe)
22
+ - docs: truth-up schemaId docs, tighten Import modal copy, export `SCHEMA_V1`/`SCHEMA_V2`/`SCHEMA_V3` ([#505](https://github.com/Takazudo/zudo-design-token-panel/issues/505)) (8088281)
23
+ - docs: replace zmodular references with Takazudo Modular markdown links (afcfea2)
24
+
3
25
  ## 0.4.4
4
26
 
5
27
  ### Features
package/README.md CHANGED
@@ -161,6 +161,8 @@ export const panelConfig: PanelConfig = {
161
161
 
162
162
  For the full token-overrides payload schema and the `PanelConfig` shape, see §5 and §6.
163
163
 
164
+ **Which CSS blocks the rewriter scans.** Each routed file's rewrite only reaches two locations: the FIRST top-level `:root { ... }` block and the FIRST top-level `@theme { ... }` block (bare `@theme`, or with one modifier such as `@theme inline` — the shape Tailwind v4 prescribes when theme values reference other variables). Later blocks of either kind, and anything nested under `@media` / `@layer` / `@supports`, are not scanned. A cssVar declared in BOTH blocks is rewritten only in `:root` (`:root`-first-match-wins). A file with neither block returns a 409 — see §11 for the Tailwind `@theme` worked example, and [Apply pipeline reference](/docs/reference/apply-pipeline) for the full response-field contract, including the `unknownOutsideBlockCssVars` diagnostic for a cssVar that's declared somewhere in the file but outside both scanned blocks.
165
+
164
166
  ### 3.3 Security model
165
167
 
166
168
  The bin is **dev-only** and is built with three independent guards:
@@ -273,7 +275,7 @@ export const myPanelConfig: PanelConfig = {
273
275
  storagePrefix: 'myapp-design-token-panel',
274
276
  consoleNamespace: 'myapp',
275
277
  modalClassPrefix: 'myapp-design-token-panel-modal',
276
- schemaId: 'myapp-design-tokens/v1',
278
+ schemaId: 'myapp-design-tokens/v1', // display-only label — see §5.3; import/export always use the canonical SCHEMA_V1/V2/V3
277
279
  exportFilenameBase: 'myapp-design-tokens',
278
280
  tabs: [spacingTab /*, fontTab, sizeTab, colorTab, ... */],
279
281
  };
@@ -477,7 +479,7 @@ export interface PanelInstanceHandle {
477
479
  | `storagePrefix` | `string` | Base for every derived `localStorage` key. Also the instance id. See §9. |
478
480
  | `consoleNamespace` | `string` | Global object the package installs `showDesignPanel` / `hideDesignPanel` / `toggleDesignPanel` on (e.g. `consoleNamespace: 'myapp'` → `window.myapp.showDesignPanel`). |
479
481
  | `modalClassPrefix` | `string` | BEM root class for every modal the panel owns (export, import, apply). Emits `${prefix}__overlay`, `${prefix}__panel`, etc. |
480
- | `schemaId` | `string` | `$schema` value emitted into export JSON and required on import. |
482
+ | `schemaId` | `string` | **Display-only** label surfaced in the Import modal's copy. `serialize()` always emits the canonical `SCHEMA_V2`/`SCHEMA_V3` and `deserialize()` always validates against the canonical `SCHEMA_V1`/`V2`/`V3` — neither reads this field. See §14's migration recipe. |
481
483
  | `exportFilenameBase` | `string` | Default download filename base — exports save as `${exportFilenameBase}.json`. |
482
484
  | `toggleEvent` | `string` (optional) | Window-event name that toggles THIS instance. Defaults to `toggle-${storagePrefix}` for non-default instances; the default instance keeps `toggle-design-token-panel`. |
483
485
  | `tabs` | `readonly TabConfig[]` | **Required.** Tab strip data — each entry is a tab with one or more `TierConfig` objects. The color tab (id `'color'`) additionally requires `colorExtras`. See §6. |
@@ -533,7 +535,7 @@ const handle = configurePanel({
533
535
  The Astro entry point (`<DesignTokenPanelHost>`) handles mounting for you. Internally:
534
536
 
535
537
  - The console API (`showDesignPanel` etc.) is **always installed eagerly**, even when the panel module has not loaded — calling them is what triggers the lazy import for cold-start users.
536
- - The panel module is **dynamically imported on first need**: when the user calls a console helper, OR when first-paint detects any of four gate signals in `localStorage` — `${storagePrefix}:visible` set to `1`, persisted overrides (`${storagePrefix}-state-v3`), the owner-autoload flag (`${storagePrefix}:autoload` set to `1`), or the element-path inspector enabled.
538
+ - The panel module is **dynamically imported on first need**: when the user calls a console helper, OR when first-paint detects any of four gate signals in `localStorage` — `${storagePrefix}:visible` set to `1`, persisted overrides (checked as `${storagePrefix}-state-v4`, falling back to `-state-v3` / `-state-v2` for pre-migration sessions — see §9), the owner-autoload flag (`${storagePrefix}:autoload` set to `1`), or the element-path inspector enabled.
537
539
  - This gating keeps the panel out of the initial JS bundle for first-time visitors while still re-applying overrides on hard reload for users who have tweaked things. **General visitors** (none of the four signals set) pay zero bundle cost.
538
540
 
539
541
  For a Vite-only / non-Astro host, mount it yourself by importing the adapter module after `configurePanel(...)`. See §11.5.
@@ -693,6 +695,13 @@ export interface ColorClusterExtras {
693
695
  colorScheme: string;
694
696
  colorMode: false | { defaultMode: 'light' | 'dark'; lightScheme: string; darkScheme: string };
695
697
  };
698
+ /**
699
+ * Optional config-time override map for a semantic tier's derived defaults,
700
+ * keyed by semantic item id. This is the ONLY way to ship a `{ literal }` /
701
+ * `{ literal: { light, dark } }` / `{ ref }` default — `TierItem.default`
702
+ * itself stays a plain string, consumed generically by every tab kind.
703
+ */
704
+ semanticDefaults?: Record<string, SemanticValue>;
696
705
  }
697
706
 
698
707
  export interface ColorScheme {
@@ -712,6 +721,30 @@ export interface ColorScheme {
712
721
  > re-exported from the package root:
713
722
  > `import type { ColorClusterConfig } from '@takazudo/zdtp'`.
714
723
 
724
+ **`colorExtras.semanticDefaults`.** Without this field, a semantic tier's default `SemanticValue` is DERIVED from each `TierItem.default` string (palette-index lookup, or a literal fallback). `semanticDefaults` lets a host override that derivation for specific semantic item ids and ship a default the plain-string `default` field can't express — most usefully a per-mode literal that resolves via CSS `light-dark()`:
725
+
726
+ ```ts
727
+ const colorTab: TabConfig = {
728
+ id: 'color',
729
+ label: 'Color',
730
+ tiers: [paletteTier, semanticTier],
731
+ colorExtras: {
732
+ id: 'myapp',
733
+ baseRoles: { background: '--myapp-bg', foreground: '--myapp-fg' },
734
+ baseDefaults: {},
735
+ defaultShikiTheme: 'dracula',
736
+ colorSchemes: { /* ... */ },
737
+ panelSettings: { colorScheme: 'Default', colorMode: false },
738
+ semanticDefaults: {
739
+ // Keyed by the semantic TierItem's `id`, not its cssVar.
740
+ danger: { literal: { light: '#b91c1c', dark: '#f87171' } },
741
+ },
742
+ },
743
+ };
744
+ ```
745
+
746
+ A key present in `semanticDefaults` wins verbatim over the derived value; keys not listed fall back to the normal derivation from `TierItem.default`.
747
+
715
748
  ### 7.2 JSON-serializable constraint (important)
716
749
 
717
750
  **Every field on the color `TabConfig` (including `colorExtras` and every
@@ -888,9 +921,10 @@ Behaviour notes:
888
921
 
889
922
  | Logical key | Derivation | Purpose |
890
923
  | ----------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
891
- | `state-v3` | `${storagePrefix}-state-v3` | Current unified envelope (color + spacing + typography + size + panelPosition + generic `tabs` map). Added in abstract-token-tiers epic. |
892
- | `state-v2` | `${storagePrefix}-state-v2` | Legacy pre-v3 format. Migrated into `state-v3` on first load, then deleted. |
893
- | `state-v1` | `${storagePrefix}-state` | Legacy pre-v2 flat-state format (Color-only). Migrated into `state-v3` on first load, then deleted. |
924
+ | `state-v4` | `${storagePrefix}-state-v4` | Current unified envelope. Same top-level slices as v3, EXCEPT `color` (and optional `secondary`) is an identity-keyed map — one `ColorTweakState` slot per active-scheme identity — instead of a single flat object. See "Per-scheme color persistence" below. |
925
+ | `state-v3` | `${storagePrefix}-state-v3` | Legacy pre-v4 format (flat, single-slot `color`). Migrated into `state-v4` on first load; the v3 key itself is left in place (not deleted) so a downgrade can still read it. |
926
+ | `state-v2` | `${storagePrefix}-state-v2` | Legacy pre-v3 format. Migrated into `state-v3` (and from there into `state-v4`) on first load, then deleted. |
927
+ | `state-v1` | `${storagePrefix}-state` | Legacy pre-v2 flat-state format (Color-only). Migrated into `state-v3` (and from there into `state-v4`) on first load, then deleted. |
894
928
  | `open` | `${storagePrefix}-open` | Mirror of the panel's `open` boolean (synchronous mount-time read — preserves user intent across reloads, fixes #1549). |
895
929
  | `position` | `${storagePrefix}-position` | Drag position `{ top, right }` so the panel reappears where the user left it. |
896
930
  | `visible` | `${storagePrefix}:visible` | Adapter-level visibility-intent flag, owned by the lazy-load gate. |
@@ -899,6 +933,7 @@ Behaviour notes:
899
933
  For example, with `storagePrefix: 'myapp-design-token-panel'`:
900
934
 
901
935
  ```
936
+ myapp-design-token-panel-state-v4
902
937
  myapp-design-token-panel-state-v3
903
938
  myapp-design-token-panel-state-v2
904
939
  myapp-design-token-panel-state
@@ -912,18 +947,40 @@ myapp-design-token-panel:autoload
912
947
 
913
948
  The `visible` and `autoload` keys use a literal `:` separator, not `-`. Every other derived key uses `-`. The colon form is intentional for these adapter-level flags — see [`PORTABLE-CONTRACT.md`](./PORTABLE-CONTRACT.md) §2 for the historical reason. Don't try to "normalize" them; the unit tests assert this specific shape.
914
949
 
915
- ### Scheme changes and the global tweak model
950
+ ### Per-scheme color persistence (v4 envelope)
916
951
 
917
- Tweak state is **global**, not scheme-scoped: the keys above are derived only from `storagePrefix` and carry no color-scheme name. A single envelope holds every slice — the `color` slice stores **absolute** colors (palette + role/semantic indices resolved against that palette), while the `spacing` / `typography` / `size` slices store token overrides that are independent of the active scheme.
952
+ Color/secondary tweaks are persisted **per (scheme, mode) identity**, inside the single `state-v4` storage key — there is no per-scheme key fan-out. The identity is exactly the active scheme name the panel already resolves for seeding (`getActiveSchemeName`): a cluster without `colorMode` resolves one constant identity (one slot, effectively the old global behavior); a cluster with light/dark `colorMode` resolves a distinct identity per side, so a light tweak and a dark tweak occupy independent slots in the same envelope:
918
953
 
919
- Because the stored palette is absolute, it cannot simply be re-applied on top of a different scheme. So when the host dispatches a `color-scheme-changed` event (e.g. a light/dark toggle), the panel **re-seeds its live color state from the newly active scheme**: it removes only the inline `:root` color-cluster overrides it had applied (`clearAppliedColorStyles`) and re-initializes only the live `color` (and optional `secondary`) slices from that scheme. Palette tweaks are therefore intentionally **not** carried across a scheme switch — the panel adopts the new scheme's palette rather than layering the previous absolute colors on top of it. This is the panel's deliberate global model; it does **not** persist a separate color slice per scheme.
954
+ ```jsonc
955
+ {
956
+ "color": {
957
+ "Default Light": { "palette": [...], "semanticMappings": {...}, /* ... */ },
958
+ "Default Dark": { "palette": [...], "semanticMappings": {...}, /* ... */ }
959
+ },
960
+ "secondary": { "Default Light": { /* ... */ } }, // optional, same identity keying
961
+ "spacing": { /* ... */ },
962
+ "typography": { /* ... */ },
963
+ "size": { /* ... */ }
964
+ }
965
+ ```
966
+
967
+ The `spacing` / `typography` / `size` (and generic `tabs`) slices stay **global and unkeyed**, exactly as in v1–v3 — a scheme/mode toggle never touches them.
920
968
 
921
- The `spacing` / `typography` / `size` slices are scheme-INDEPENDENT, so they are deliberately left untouched by the re-seed: their live values and their applied inline `:root` vars survive a scheme toggle (the scheme-change handler narrows the clear to the color cluster(s) for exactly this reason — see #347). A full panel reset (Reset / Apply) still wipes every slice via `clearAppliedStyles`.
969
+ When the host dispatches a `color-scheme-changed` event (e.g. a light/dark toggle), the panel re-seeds its live color state for the newly active identity:
970
+
971
+ - If that identity already has a persisted slot, the slot's stored `color` (and `secondary`) state is loaded and re-applied — a per-scheme tweak now **survives** a round-trip through another scheme and back.
972
+ - If the identity has no slot yet (never tweaked under this scheme before), the panel cold-seeds from that scheme's defaults instead — the pre-existing "adopt the new scheme's palette" behavior for a scheme you haven't touched.
973
+
974
+ Editing color under scheme A only ever overwrites scheme A's slot (`writeMergedV4` merge-saves: it reads the existing envelope, replaces just the active identity's slot, and preserves every other identity's slot untouched) — it never mutates scheme B's stored state. A full panel reset (Reset / Apply) still wipes every slice via `clearAppliedStyles`.
975
+
976
+ **Migration.** On first load with no `state-v4` key, the legacy v1/v2/v3 chain runs unchanged (v3 wins over v2 over v1, exactly as before) and the resulting single flat `TweakState` is filed into `state-v4` under whichever identity is active at that moment — every subsequent load reads `state-v4` first. The legacy v3/v2/v1 keys are left in place by the v4 migration step (only the v1→v2→v3 chain deletes as it goes), so a host that needs to downgrade can still read them.
977
+
978
+ **Host-owned `color-scheme` is never touched.** A host that manages its own `<html style="color-scheme">` (e.g. a site-level light/dark toggle) has that inline style tracked separately from the panel's own writes; the panel's mount/clear paths only ever remove a `color-scheme` value that the panel itself applied, never a host-owned one (#501).
922
979
 
923
980
  Two consequences worth knowing as a host integrator:
924
981
 
925
- - The event handler does not rewrite the `localStorage` envelope — it only re-seeds the live (in-memory) state and clears the inline overrides. The persisted envelope remains until the next in-panel edit re-persists the (re-seeded) state, or a full reload re-applies it via `loadPersistedState`.
926
- - Do **not** manually delete the tweak keys on a scheme toggle. The panel already re-seeds on the event, and a manual envelope delete would also discard the scheme-independent `spacing` / `typography` / `size` overrides.
982
+ - The event handler does not rewrite `localStorage` directly beyond what's described above — it reads/re-seeds the live (in-memory) state and clears the inline overrides for the previous identity. The next in-panel edit (or a full reload via `loadPersistedState`) is what persists the current identity's state.
983
+ - Do **not** manually delete the tweak keys on a scheme toggle. The panel already handles per-identity persistence on the event, and a manual envelope delete would also discard the scheme-independent `spacing` / `typography` / `size` overrides.
927
984
 
928
985
  ---
929
986
 
@@ -1032,6 +1089,39 @@ Because the fallback ladder is host-CSS-var-driven, a brand-new consumer can mou
1032
1089
 
1033
1090
  The CSS variables the panel **writes to** (the `cssVar` field on each `TokenDef`, the cluster's `paletteCssVarTemplate`, base-role names, and semantic CSS names) are entirely consumer-controlled. The package never reads those consumer CSS variables; it only writes through `setProperty` on `:root`.
1034
1091
 
1092
+ ### 11.2 Apply-to-disk for a Tailwind v4 `@theme` block
1093
+
1094
+ The section above is about the panel's OWN chrome — it never needed Tailwind. This section is about the opposite direction: a Tailwind v4 **consumer** whose design tokens live inside `@theme { ... }` (so Tailwind can generate utility classes from them) rather than `:root`.
1095
+
1096
+ The apply-to-disk rewriter (§3.2) scans the first top-level `:root` block AND the first top-level `@theme` block, so a Tailwind v4 token file like this applies out of the box (issue #496's repro):
1097
+
1098
+ ```css
1099
+ :root {
1100
+ --palette-cool-700: oklch(0.21 0.03 264);
1101
+ }
1102
+
1103
+ @theme {
1104
+ --spacing-md: 0.75rem;
1105
+ --color-ink: light-dark(var(--palette-cool-700), var(--palette-cool-50));
1106
+ }
1107
+ ```
1108
+
1109
+ POSTing overrides for `--palette-cool-700` (in `:root`), `--spacing-md`, or `--color-ink` (both in `@theme`) all land in `changed` — no indirection required. A file that is 100% `@theme` (no `:root` block at all) also applies cleanly; only a file with **neither** block 409s.
1110
+
1111
+ **Legacy workaround (no longer required).** Before the rewriter learned to scan `@theme`, the only way to make a Tailwind-`@theme` token editable was to declare the editable value in `:root` and have `@theme` alias it via `var(...)`:
1112
+
1113
+ ```css
1114
+ :root {
1115
+ --spacing-md-editable: 0.75rem; /* apply pipeline writes here */
1116
+ }
1117
+
1118
+ @theme {
1119
+ --spacing-md: var(--spacing-md-editable); /* Tailwind reads the alias */
1120
+ }
1121
+ ```
1122
+
1123
+ This indirection is no longer necessary — declare the token directly inside `@theme` as shown above. The alias pattern still works (it's just two ordinary top-level blocks), so existing consumers are not required to migrate.
1124
+
1035
1125
  ---
1036
1126
 
1037
1127
  ## 12. Bundler notes
@@ -1112,6 +1202,17 @@ This recipe walks through wiring the panel into a project that does not currentl
1112
1202
 
1113
1203
  Pick identifiers for `storagePrefix`, `consoleNamespace`, `modalClassPrefix`, `schemaId`, `exportFilenameBase`, and your palette CSS-var family (`paletteCssVarTemplate`). Pull these into a host-side config file (e.g. `src/lib/panel-config.ts`). If you are migrating from an internal snapshot fork and want to preserve users' saved state across the migration, keep the legacy values verbatim; otherwise pick fresh, neutral identifiers (e.g. `myapp-design-token-panel`).
1114
1204
 
1205
+ `schemaId` is display-only (see §5.3) — it never gates import/export, so picking any string here is safe. If you want the Import modal's copy to name the ACTUAL schema your export/import round-trips through, import `SCHEMA_V1` / `SCHEMA_V2` / `SCHEMA_V3` from the package root and set `schemaId` to one of them instead of an arbitrary string:
1206
+
1207
+ ```ts
1208
+ import { SCHEMA_V3 } from '@takazudo/zdtp';
1209
+
1210
+ export const myPanelConfig: PanelConfig = {
1211
+ // ...
1212
+ schemaId: SCHEMA_V3, // truthful label — matches what serialize()/deserialize() actually validate against
1213
+ };
1214
+ ```
1215
+
1115
1216
  3. **Author your token manifest in the host project.**
1116
1217
 
1117
1218
  The package itself ships zero baked-in manifest data. Define `SPACING_TOKENS`, `FONT_TOKENS`, `SIZE_TOKENS`, `COLOR_TOKENS` (or whatever names you prefer) in your project and wire them up as `tokens.spacing`, `tokens.typography`, `tokens.size`, `tokens.color`. The host is the source of truth.
@@ -2,79 +2,186 @@
2
2
  * Pure, IO-free CSS custom-property value replacement.
3
3
  *
4
4
  * `applyTokenOverrides(source, overrides)` rewrites the values of one or more
5
- * CSS custom properties inside the FIRST top-level `:root { ... }` block of a
6
- * CSS file, preserving surrounding whitespace and trailing inline comments.
7
- * The module is the regex foundation that the dev-API endpoint
5
+ * CSS custom properties inside the FIRST top-level `:root { ... }` block AND
6
+ * the FIRST top-level `@theme { ... }` block (Tailwind v4 design-token scope)
7
+ * of a CSS file, preserving surrounding whitespace and trailing inline
8
+ * comments. The module is the regex foundation that the dev-API endpoint
8
9
  * wraps; it performs no IO, no DOM access, and is safe to import in any
9
10
  * environment.
10
11
  *
11
12
  * Behavior summary
12
13
  * ----------------
13
- * - Only the FIRST top-level `:root { ... }` block is modified. Tokens inside
14
- * `@media`, `@layer`, `@supports`, or nested `:root` blocks are IGNORED.
14
+ * - Two top-level blocks are scanned: the FIRST top-level `:root { ... }` block
15
+ * and the FIRST top-level `@theme { ... }` block (first-of-each-kind only).
16
+ * Later blocks of either kind, and tokens inside `@media`, `@layer`,
17
+ * `@supports`, or nested blocks, are IGNORED.
18
+ * - `:root`-first-match-wins: for each override var, `:root` is tried first and
19
+ * `@theme` is the fallback. A var declared in BOTH blocks is rewritten only
20
+ * in `:root`.
21
+ * - `@theme` accepts an optional modifier — `@theme`, `@theme inline`,
22
+ * `@theme static`, `@theme reference`, or a combination — because Tailwind v4
23
+ * prescribes `@theme inline` precisely when theme values reference other
24
+ * variables (the mainstream case). `:root` matching stays strict: grouped
25
+ * selectors such as `:root, html { ... }` are NOT treated as a `:root` block.
15
26
  * - Per-cssVar regex: `(--name:)\s*([\s\S]+?);` with the name regex-escaped.
16
27
  * The non-greedy value capture stops at the first `;`, which preserves any
17
28
  * trailing `/​* ... *​/` inline comment on the same logical line.
18
29
  * - A whitespace-trimmed comparison between the old value and the override
19
30
  * decides whether the cssVar lands in `changed` or `unchanged`.
20
31
  * - Idempotent: applying the same overrides twice in a row reports every key
21
- * as `unchanged` on the second call and produces byte-identical output.
32
+ * as `unchanged` on the second call and produces byte-identical output. The
33
+ * two non-overlapping regions are spliced back in DESCENDING start order so
34
+ * the earlier region's offsets stay valid through the later region's splice.
35
+ * - `unknownOutsideBlock` (#508): a whole-file scan (ignoring the "first
36
+ * block of each kind" and "no nesting" restrictions above) reclassifies any
37
+ * `unknown` key that IS declared somewhere in the file, so callers can tell
38
+ * "declared, but outside a scanned block" apart from "genuinely absent".
22
39
  *
23
40
  * Known limitations (if a richer transform is ever needed, swap this module
24
41
  * for a real CSS parser such as postcss):
25
42
  * - Only `:root` as a bare selector is recognized. Grouped selectors such as
26
43
  * `:root, html { ... }` are NOT treated as a `:root` block.
27
- * - Nested `:root` under `@media` / `@layer` / `@supports` is intentionally
28
- * skipped; see the test matrix in the companion `__tests__` file.
44
+ * - Only the FIRST top-level block of each kind is scanned; a second top-level
45
+ * `:root` or `@theme` block is ignored.
46
+ * - Nested `:root` / `@theme` under `@media` / `@layer` / `@supports` is
47
+ * intentionally skipped; see the test matrix in the companion `__tests__`
48
+ * file.
29
49
  * - Values containing a literal `;` inside a string or comment would confuse
30
50
  * the non-greedy regex. Not supported.
31
- * - If the same cssVar is declared multiple times in the top-level `:root`
51
+ * - If the same cssVar is declared multiple times inside a single scanned
32
52
  * block, only the FIRST occurrence is rewritten.
33
53
  */
34
54
  export interface ApplyResult {
35
55
  /** The new CSS file contents (byte-identical to `source` when nothing
36
- * changed, including the "no :root block" case). */
56
+ * changed, including the "no scannable block" case). */
37
57
  updated: string;
38
58
  /** cssVar names actually rewritten (trimmed new value differs from old). */
39
59
  changed: string[];
40
- /** cssVar names present in the file whose trimmed value already matched
41
- * the override. */
60
+ /** cssVar names present in a scanned block whose trimmed value already
61
+ * matched the override. */
42
62
  unchanged: string[];
43
- /** cssVar names in `overrides` that were not found in the first top-level
44
- * `:root` block (either because the name is absent, or because no
45
- * top-level `:root` block exists). */
63
+ /** cssVar names in `overrides` that were not found in EITHER the first
64
+ * top-level `:root` block or the first top-level `@theme` block (the name
65
+ * is absent, or neither block exists). */
46
66
  unknown: string[];
67
+ /**
68
+ * Subset of `unknown` (#508): cssVar names that ARE declared somewhere in
69
+ * `source` — found by a whole-file scan, not restricted to the two scanned
70
+ * blocks — but outside both of them, so nothing was rewritten. Typical
71
+ * causes: the declaration sits in a nested block (`@media`/`@layer`/
72
+ * `@supports`), a grouped selector (`:root, html { ... }`), or a SECOND
73
+ * top-level `:root`/`@theme` block (the scanner only reads the first of
74
+ * each kind). Lets callers distinguish "declared, but Apply doesn't rewrite
75
+ * this location" from "genuinely absent / typo'd name" — the remaining
76
+ * `unknown` entries not in this list.
77
+ */
78
+ unknownOutsideBlock: string[];
47
79
  }
48
80
  /**
49
- * Thrown by {@link applyTokenOverridesOrThrow} when the source has no
50
- * top-level `:root { ... }` block. Lets the dev-API handler surface a
51
- * diagnostic message without having to probe the result shape.
81
+ * Thrown by {@link applyTokenOverridesOrThrow} when the source has neither a
82
+ * top-level `:root { ... }` block nor a top-level `@theme { ... }` block. Lets
83
+ * the dev-API handler surface a diagnostic message without having to probe the
84
+ * result shape.
52
85
  */
53
- export declare class NoRootBlockError extends Error {
86
+ export declare class NoTokenBlockError extends Error {
54
87
  constructor(message?: string);
55
88
  }
56
89
  /**
57
- * Rewrite `overrides` into the first top-level `:root { ... }` block of
58
- * `source` and return an {@link ApplyResult} describing the outcome.
90
+ * @deprecated Retained as an alias for {@link NoTokenBlockError} so existing
91
+ * `instanceof` / import sites keep working. The scan now covers `@theme`
92
+ * blocks too, so "NoRootBlock" is no longer literally accurate.
93
+ */
94
+ export declare const NoRootBlockError: typeof NoTokenBlockError;
95
+ export interface BlockBounds {
96
+ /** Index of the first character INSIDE the block (just after the opening
97
+ * `{`). */
98
+ contentStart: number;
99
+ /** Index of the matching `}` (exclusive end of block content). */
100
+ contentEnd: number;
101
+ }
102
+ /**
103
+ * Bounds of the first top-level `:root { ... }` block, or `null`. Grouped
104
+ * selectors (`:root, html { ... }`) are intentionally NOT matched.
105
+ */
106
+ export declare function findFirstTopLevelRootBlock(source: string): BlockBounds | null;
107
+ /**
108
+ * Bounds of the first top-level `@theme { ... }` block, or `null`. Tolerates an
109
+ * optional modifier (`@theme inline`, `@theme static`, ...) between `@theme`
110
+ * and the opening `{`.
111
+ */
112
+ export declare function findFirstTopLevelThemeBlock(source: string): BlockBounds | null;
113
+ export type SingleReplaceStatus = 'changed' | 'unchanged' | 'unknown';
114
+ export interface SingleReplaceResult {
115
+ content: string;
116
+ status: SingleReplaceStatus;
117
+ }
118
+ /**
119
+ * Replace every CSS block comment (`/​* ... *​/`) with same-length whitespace
120
+ * so the per-declaration regex below cannot match a fake declaration that
121
+ * lives INSIDE a comment. Same-length is critical: the masked string is used
122
+ * only as a search space, but the resulting `match.index` is then sliced
123
+ * straight back into the original (un-masked) `content`, so masked and
124
+ * original must agree byte-for-byte on offsets.
125
+ *
126
+ * Pre-fix behaviour: a `:root` containing
127
+ *
128
+ * /* --zd-foo: red; *​/
129
+ * --zd-foo: blue;
59
130
  *
60
- * If `source` has no top-level `:root` block, the function returns
61
- * `source` unchanged with every key routed to `unknown` — the companion
62
- * {@link applyTokenOverridesOrThrow} raises {@link NoRootBlockError} in
63
- * that case so callers that need a hard failure can distinguish it from a
64
- * block where merely none of the overrides matched.
131
+ * would have its commented-out value rewritten by the regex (it matches the
132
+ * FIRST occurrence), leaving the live declaration untouched and silently
133
+ * corrupting the user-authored comment.
134
+ */
135
+ export declare function maskComments(input: string): string;
136
+ /**
137
+ * Replace the value of a single custom property inside `content`. The name
138
+ * may be supplied with or without the leading `--`. Returns the updated
139
+ * content along with a status flag used to populate `changed` / `unchanged`
140
+ * / `unknown` in the top-level {@link ApplyResult}.
141
+ *
142
+ * The match is run against a comment-masked copy of `content` so a
143
+ * commented-out declaration cannot shadow a live one (B4 fix). Edits are
144
+ * still spliced back into the original `content` so trailing inline comments
145
+ * survive byte-for-byte.
146
+ */
147
+ export declare function replaceOne(content: string, cssVarName: string, newValue: string): SingleReplaceResult;
148
+ /**
149
+ * Rewrite `overrides` into the first top-level `:root { ... }` block AND the
150
+ * first top-level `@theme { ... }` block of `source`, returning an
151
+ * {@link ApplyResult} describing the outcome.
152
+ *
153
+ * Per override var, `:root` is tried first and `@theme` is the fallback, so a
154
+ * var present in both blocks is rewritten only in `:root` (`:root`-first-match-
155
+ * wins). The two non-overlapping block regions are spliced back into `source`
156
+ * in DESCENDING start order so the earlier region's offsets stay valid.
157
+ *
158
+ * If `source` has neither block, the function returns `source` unchanged with
159
+ * every key routed to `unknown` — the companion {@link applyTokenOverridesOrThrow}
160
+ * raises {@link NoTokenBlockError} in that case so callers that need a hard
161
+ * failure can distinguish it from a file where merely none of the overrides
162
+ * matched.
65
163
  */
66
164
  export declare function applyTokenOverrides(source: string, overrides: Record<string, string>): ApplyResult;
67
165
  /**
68
166
  * Throwing variant. Same shape as {@link applyTokenOverrides} except it
69
- * raises {@link NoRootBlockError} when no top-level `:root { ... }` block
70
- * exists, instead of returning every override under `unknown`. Use this from
71
- * server-side handlers where "no :root block" is a fatal configuration
72
- * problem rather than an expected state.
167
+ * raises {@link NoTokenBlockError} when the source has neither a top-level
168
+ * `:root { ... }` block nor a top-level `@theme { ... }` block, instead of
169
+ * returning every override under `unknown`. Use this from server-side handlers
170
+ * where "no writable token block" is a fatal configuration problem rather than
171
+ * an expected state.
73
172
  */
74
173
  export declare function applyTokenOverridesOrThrow(source: string, overrides: Record<string, string>): ApplyResult;
75
174
  /**
76
175
  * Low-level predicate exposed primarily for callers that want to report a
77
176
  * precise diagnostic without running a full rewrite. Returns `true` iff the
78
- * source contains at least one top-level `:root { ... }` block.
177
+ * source contains at least one scannable top-level token block — a `:root`
178
+ * block OR a `@theme` block. Consistent with the {@link applyTokenOverridesOrThrow}
179
+ * throw condition (which fires only when this returns `false`).
180
+ */
181
+ export declare function hasApplicableTokenBlock(source: string): boolean;
182
+ /**
183
+ * @deprecated Renamed to {@link hasApplicableTokenBlock} now that the scan
184
+ * covers `@theme` blocks too. Retained as an alias so existing call sites keep
185
+ * working; it returns `true` for a `@theme`-only file (no `:root`).
79
186
  */
80
- export declare function hasTopLevelRootBlock(source: string): boolean;
187
+ export declare const hasTopLevelRootBlock: typeof hasApplicableTokenBlock;
@@ -1,56 +1,56 @@
1
- import { c as h, g as u, a as g, b as w, d as y, e as P } from "../panel-config-CXTCcYQs.js";
2
- import { g as d, Z as b } from "../tweak-state-BeXkzoj8.js";
3
- import { s as _, l as A, a as c, c as k, b as I } from "../autoload-state-CmhI7q9j.js";
4
- const l = "tokenpanel-config";
5
- function E() {
1
+ import { c as w, g as u, a as g, b as y, d as P, e as b, f as _ } from "../panel-config-CipeKMTz.js";
2
+ import { g as d, Z as A } from "../tweak-state-xt-tSQ_t.js";
3
+ import { s as k, l as I, a as c, c as E, b as C } from "../autoload-state-DF7TsTgY.js";
4
+ const i = "tokenpanel-config";
5
+ function S() {
6
6
  if (typeof document > "u")
7
7
  throw new Error(
8
8
  "[design-token-panel] host-adapter loaded without a document; expected to run in a browser context."
9
9
  );
10
- const n = document.getElementById(l);
10
+ const n = document.getElementById(i);
11
11
  if (!n)
12
12
  throw new Error(
13
- `[design-token-panel] Inline config script #${l} not found. Ensure <DesignTokenPanelHost config={...} /> is rendered on this page before the host script runs.`
13
+ `[design-token-panel] Inline config script #${i} not found. Ensure <DesignTokenPanelHost config={...} /> is rendered on this page before the host script runs.`
14
14
  );
15
- const o = n.textContent ?? "";
15
+ const a = n.textContent ?? "";
16
16
  let e;
17
17
  try {
18
- e = JSON.parse(o);
19
- } catch (a) {
18
+ e = JSON.parse(a);
19
+ } catch (o) {
20
20
  throw new Error(
21
- `[design-token-panel] Failed to parse inline config from #${l}: ${a.message}`
21
+ `[design-token-panel] Failed to parse inline config from #${i}: ${o.message}`
22
22
  );
23
23
  }
24
- return P(e), e;
24
+ return _(e), e;
25
25
  }
26
- function C(n) {
26
+ function T(n) {
27
27
  return n.__zudoDesignTokenPanelAdapter || (n.__zudoDesignTokenPanelAdapter = {}), n.__zudoDesignTokenPanelAdapter;
28
28
  }
29
- function S(n, o) {
30
- const e = C(n);
31
- let a = e[o];
32
- return a || (a = { bound: !1, modulePromise: null }, e[o] = a), a;
29
+ function V(n, a) {
30
+ const e = T(n);
31
+ let o = e[a];
32
+ return o || (o = { bound: !1, modulePromise: null }, e[a] = o), o;
33
33
  }
34
- function T(n) {
34
+ function v(n) {
35
35
  try {
36
36
  return window.localStorage.getItem(n) === "1";
37
37
  } catch {
38
38
  return !1;
39
39
  }
40
40
  }
41
- function v(n, o) {
41
+ function D(n, a, e) {
42
42
  try {
43
- const e = window.localStorage;
44
- return e.getItem(o) !== null || e.getItem(n) !== null;
43
+ const o = window.localStorage;
44
+ return o.getItem(e) !== null || o.getItem(a) !== null || o.getItem(n) !== null;
45
45
  } catch {
46
46
  return !1;
47
47
  }
48
48
  }
49
- async function i(n) {
50
- return n.modulePromise === null && (n.modulePromise = import("@takazudo/zdtp").then((o) => {
49
+ async function l(n) {
50
+ return n.modulePromise === null && (n.modulePromise = import("@takazudo/zdtp").then((a) => {
51
51
  try {
52
- const e = u(), a = o.__panelConfigForTest();
53
- e !== a && console.warn(
52
+ const e = u(), o = a.__panelConfigForTest();
53
+ e !== o && console.warn(
54
54
  "[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."
55
55
  );
56
56
  } catch (e) {
@@ -58,17 +58,17 @@ async function i(n) {
58
58
  "[design-token-panel] Singleton-sharing check could not run (likely an older dist without the __panelConfigForTest accessor): " + e.message
59
59
  );
60
60
  }
61
- return o;
61
+ return a;
62
62
  })), n.modulePromise;
63
63
  }
64
- function D(n, o, e, a, t) {
65
- const s = n[o] ?? {};
64
+ function K(n, a, e, o, t) {
65
+ const s = n[a] ?? {};
66
66
  s.showDesignPanel = async () => {
67
- c(t, !0), await i(e), a.open();
67
+ c(t, !0), await l(e), o.open();
68
68
  }, s.hideDesignPanel = async () => {
69
- await i(e), a.close();
69
+ await l(e), o.close();
70
70
  }, s.toggleDesignPanel = async () => {
71
- await i(e), a.toggle();
71
+ await l(e), o.toggle();
72
72
  let r = !1;
73
73
  try {
74
74
  r = window.localStorage.getItem(d(t)) === "1";
@@ -76,13 +76,13 @@ function D(n, o, e, a, t) {
76
76
  }
77
77
  r && c(t, !0);
78
78
  }, s.enableAutoload = async () => {
79
- (await i(e)).enableAutoload(t);
79
+ (await l(e)).enableAutoload(t);
80
80
  }, s.disableAutoload = async () => {
81
81
  if (e.modulePromise !== null) {
82
- (await i(e)).disableAutoload(t);
82
+ (await l(e)).disableAutoload(t);
83
83
  return;
84
84
  }
85
- k(t), I(!1, t);
85
+ E(t), C(!1, t);
86
86
  try {
87
87
  window.localStorage.setItem(g(t), "0");
88
88
  } catch {
@@ -91,12 +91,12 @@ function D(n, o, e, a, t) {
91
91
  window.localStorage.removeItem(d(t));
92
92
  } catch {
93
93
  }
94
- }, n[o] = s;
94
+ }, n[a] = s;
95
95
  }
96
96
  (function() {
97
- const o = E(), e = o.legacyIdRenameMap ? o : { ...o, legacyIdRenameMap: { ...b } }, a = h(e), t = u(), s = window, r = S(s, t.storagePrefix);
98
- if (D(s, t.consoleNamespace, r, a, t), r.bound) return;
97
+ const a = S(), e = a.legacyIdRenameMap ? a : { ...a, legacyIdRenameMap: { ...A } }, o = w(e), t = u(), s = window, r = V(s, t.storagePrefix);
98
+ if (K(s, t.consoleNamespace, r, o, t), r.bound) return;
99
99
  r.bound = !0;
100
- const f = g(t), p = w(t), m = y(t);
101
- (T(f) || v(p, m) || _(t) || A(t)) && i(r);
100
+ const f = g(t), m = y(t), p = P(t), h = b(t);
101
+ (v(f) || D(m, p, h) || k(t) || I(t)) && l(r);
102
102
  })();
@@ -1,4 +1,4 @@
1
- import { s as t } from "../panel-config-CXTCcYQs.js";
1
+ import { s as t } from "../panel-config-CipeKMTz.js";
2
2
  import { c as d } from "../color-schemes-CgzOBqGO.js";
3
3
  import { F as r, G as m, a as g, S as z } from "../manifest-DCReQE0k.js";
4
4
  const e = {
@@ -1,4 +1,4 @@
1
- import { g as a, j as o } from "./panel-config-CXTCcYQs.js";
1
+ import { g as a, k as o } from "./panel-config-CipeKMTz.js";
2
2
  function n(e = a()) {
3
3
  return `${e.storagePrefix}-elpath-enabled`;
4
4
  }