@takazudo/zdtp 0.4.3 → 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.
@@ -22,6 +22,12 @@ export interface PerFileResult {
22
22
  changed: string[];
23
23
  unchanged: string[];
24
24
  unknown: string[];
25
+ /**
26
+ * Subset of `unknown` (#508): declared somewhere in the file but outside
27
+ * the scanned `:root`/`@theme` blocks — see `ApplyResult.unknownOutsideBlock`
28
+ * in `apply-token-overrides.ts` for the full classification rules.
29
+ */
30
+ unknownOutsideBlock: string[];
25
31
  }
26
32
  /**
27
33
  * Create a framework-agnostic Fetch API handler for the design-token apply
@@ -1,4 +1,4 @@
1
- import { C as s, c as i, i as l, a as r, l as t, s as o, v as d } from "../load-routing-DtNTKoLW.js";
1
+ import { C as s, c as i, i as l, a as r, l as t, s as o, v as d } from "../load-routing-LJ72261l.js";
2
2
  export {
3
3
  s as CSS_VAR_NAME_RE,
4
4
  i as createApplyHandler,
@@ -11,6 +11,13 @@
11
11
  * Framework-agnostic wrt. the setState function: pass any
12
12
  * `(updater) => void` that propagates `updater(prev)` to the caller's state.
13
13
  * In practice the panel passes Preact's `setState` from `useState`.
14
+ *
15
+ * `persistColor`'s updater is `(prev: ColorTweakState) => ColorTweakState`, so
16
+ * the widened `ColorTweakState.semanticMappings: Record<string, SemanticValue>`
17
+ * (#459) — including the `{ literal }` / `{ ref }` object variants added for
18
+ * #462 — already flows through this hook's generic pass-through unchanged; the
19
+ * actual validating hydrate/round-trip logic for those variants lives in
20
+ * `tweak-state.ts` (`hydrateSemanticMappings` / `savePersistedState`), not here.
14
21
  */
15
22
  import { type ColorTweakState, type TabOverrides, type TokenOverrides, type TweakState } from './tweak-state';
16
23
  import type { PanelConfig } from '../config/panel-config';
@@ -42,8 +42,11 @@
42
42
  import type { ColorRef, ColorScheme } from '../config/color-schemes';
43
43
  import type { TokenDef } from '../tokens/manifest';
44
44
  import { type ColorClusterDataConfig } from '../config/cluster-config';
45
+ import type { SemanticValue } from '../tokens/tier-model';
46
+ export type { SemanticValue } from '../tokens/tier-model';
45
47
  import { type ApplySink, type PanelConfig } from '../config/panel-config';
46
48
  export type { ApplySink } from '../config/panel-config';
49
+ import type { TabConfig } from '../tokens/tier-model';
47
50
  import { type TabOverrides } from '../apply/tier-resolver';
48
51
  export type { BaseRoleKey, ColorClusterDataConfig } from '../config/cluster-config';
49
52
  export { resolvePaletteCssVar } from '../config/cluster-config';
@@ -77,6 +80,12 @@ export type { TabOverrides } from '../apply/tier-resolver';
77
80
  export declare function getStorageKeyV1(cfg?: PanelConfig): string;
78
81
  export declare function getStorageKeyV2(cfg?: PanelConfig): string;
79
82
  export declare function getStorageKeyV3(cfg?: PanelConfig): string;
83
+ /**
84
+ * v4 unified envelope key — the per-scheme-keyed color format (#500/#509).
85
+ * See `storageKey_stateV4` for the shape rationale. Read first by
86
+ * `loadPersistedState`; v1/v2/v3 migrate into it on first load.
87
+ */
88
+ export declare function getStorageKeyV4(cfg?: PanelConfig): string;
80
89
  export declare function getOpenKey(cfg?: PanelConfig): string;
81
90
  export declare function getPositionKey(cfg?: PanelConfig): string;
82
91
  export declare function getSizeKey(cfg?: PanelConfig): string;
@@ -182,9 +191,47 @@ export interface ColorTweakState {
182
191
  cursor: number;
183
192
  selectionBg: number;
184
193
  selectionFg: number;
185
- semanticMappings: Record<string, number | 'bg' | 'fg'>;
194
+ /**
195
+ * Semantic token name → mapping. Widened from `Record<string, number | 'bg'
196
+ * | 'fg'>` to `Record<string, SemanticValue>` (#459 S1). This is additive
197
+ * for readers of the type (every legacy value is still a valid
198
+ * `SemanticValue`), but it BREAKS any exhaustive `switch`/equality check
199
+ * written against the old narrow union — callers that pattern-match on a
200
+ * mapping value must now also handle the new `{ literal }` / `{ ref }`
201
+ * object variants (see `isIndexMapping` / `isLiteralMapping` /
202
+ * `isPerModeLiteral` / `isRefMapping` below). `resolveMapping` (exported)
203
+ * still only resolves the legacy `number | 'bg' | 'fg'` shape — resolving
204
+ * the new variants is downstream work (#467/#469).
205
+ */
206
+ semanticMappings: Record<string, SemanticValue>;
186
207
  shikiTheme: string;
187
208
  }
209
+ /** True when `v` is a legacy index-style mapping (palette index or bg/fg alias). */
210
+ export declare function isIndexMapping(v: SemanticValue): v is number | 'bg' | 'fg';
211
+ /** True when `v` is either literal-color variant (plain string or light/dark pair). */
212
+ export declare function isLiteralMapping(v: SemanticValue): v is {
213
+ literal: string;
214
+ } | {
215
+ literal: {
216
+ light: string;
217
+ dark: string;
218
+ };
219
+ };
220
+ /** True when `v` is specifically the light/dark literal-color variant. */
221
+ export declare function isPerModeLiteral(v: SemanticValue): v is {
222
+ literal: {
223
+ light: string;
224
+ dark: string;
225
+ };
226
+ };
227
+ /** True when `v` is a cross-tab/tier ramp-item reference. */
228
+ export declare function isRefMapping(v: SemanticValue): v is {
229
+ ref: {
230
+ tab?: string;
231
+ tier: string;
232
+ item: string;
233
+ };
234
+ };
188
235
  /**
189
236
  * Per-token override map. Keys are `TokenDef.id` (e.g. `hsp-sm`); values are
190
237
  * raw CSS length strings (e.g. `0.75rem`). Only overridden tokens appear in
@@ -217,6 +264,32 @@ export interface TweakState {
217
264
  /** Generic tab overrides keyed by tab id. Added in v3 envelope. */
218
265
  tabs?: Record<string, TabOverrides>;
219
266
  }
267
+ /**
268
+ * On-disk v4 persist envelope (#500/#509). Same top-level slices as the runtime
269
+ * `TweakState`, EXCEPT the `color` and `secondary` slices are identity-keyed
270
+ * sub-objects: one `ColorTweakState` slot per active-color IDENTITY (see
271
+ * `getActiveColorIdentity`). This is what lets a per-scheme color tweak survive
272
+ * a scheme toggle — editing under scheme A never touches scheme B's slot.
273
+ *
274
+ * The non-color slices (`spacing` / `typography` / `size` / `tabs`) stay global
275
+ * and unkeyed, exactly as in v1–v3: a light/dark toggle does not change them.
276
+ *
277
+ * `secondary` slots are keyed by the SAME primary identity — the secondary
278
+ * cluster has no scheme of its own but reseeds on the same `color-scheme-changed`
279
+ * event, so it follows the primary's identity.
280
+ *
281
+ * `loadPersistedState` selects the active identity's slots into a runtime
282
+ * `TweakState`; `savePersistedState` merge-saves the active slots while
283
+ * preserving every inactive identity's slot.
284
+ */
285
+ export interface PersistedEnvelopeV4 {
286
+ color: Record<string, ColorTweakState>;
287
+ secondary?: Record<string, ColorTweakState>;
288
+ spacing: TokenOverrides;
289
+ typography: TokenOverrides;
290
+ size: TokenOverrides;
291
+ tabs?: Record<string, TabOverrides>;
292
+ }
220
293
  /** Produce an empty overrides map — `TweakState` default for new tabs. */
221
294
  export declare function emptyOverrides(): TokenOverrides;
222
295
  /**
@@ -238,6 +311,10 @@ export declare function getActivePrimaryCluster(cfg?: PanelConfig): ColorCluster
238
311
  * slots interpolate. Functional but visually flat; hosts wanting a
239
312
  * designed seed should ship a scheme registry on the cluster and call
240
313
  * `initColorFromScheme(cluster)` instead.
314
+ * - A cluster with `paletteSize: 0` (a lone `semantic: true` tier with no
315
+ * palette sibling, #458/#466) seeds an EMPTY palette — there is nothing
316
+ * to ramp. Forcing a 1-slot grayscale floor here produced a phantom
317
+ * `--zudo-stub-p0` swatch/token with no backing tier; see #466.
241
318
  *
242
319
  * The base-role indices are kept on the state shape for envelope-round-trip
243
320
  * compatibility but are inert — `applyColorState` only writes a base role
@@ -315,6 +392,24 @@ export declare function getActiveSchemeName(cluster?: ColorClusterDataConfig, cf
315
392
  * is the correct behavior for non-sink instances but wrong for sinks.
316
393
  */
317
394
  export declare function initColorFromScheme(cluster?: ColorClusterDataConfig, cfg?: PanelConfig): ColorTweakState;
395
+ /**
396
+ * Derive the active-color IDENTITY — the key under which the v4 envelope files
397
+ * this instance's `color` / `secondary` slots (#500/#509).
398
+ *
399
+ * The identity is EXACTLY the active scheme name `initColorFromScheme` resolves
400
+ * for the primary cluster via `getActiveSchemeName` (the active scheme, with the
401
+ * light/dark mode already folded into the resolved name where the cluster
402
+ * `panelSettings.colorMode` distinguishes modes). This is deliberate: the same
403
+ * inputs whose change triggers a reseed today produce the identity, so the
404
+ * invariant holds — *the same identity that causes a reseed selects its own
405
+ * persisted slot*. A colorMode-less cluster resolves a constant identity (one
406
+ * slot); a light/dark cluster resolves distinct identities per side.
407
+ *
408
+ * The secondary cluster has no scheme concept of its own but reseeds on the same
409
+ * `color-scheme-changed` event, so its slots are keyed by this SAME primary
410
+ * identity — callers pass the PRIMARY cluster here for both slices.
411
+ */
412
+ export declare function getActiveColorIdentity(cluster?: ColorClusterDataConfig, cfg?: PanelConfig): string;
318
413
  /**
319
414
  * Palette entry sanitiser. Raw `oklch(...)` values are preserved verbatim so
320
415
  * wide-gamut chroma survives (and they never touch the canvas → no '#000000'
@@ -331,11 +426,79 @@ export declare function initColorFromScheme(cluster?: ColorClusterDataConfig, cf
331
426
  */
332
427
  export declare function normalizeSchemePaletteEntry(value: string): string;
333
428
  export declare function initColorFromSchemeData(scheme: ColorScheme, cluster?: ColorClusterDataConfig): ColorTweakState;
334
- /** Resolve a semantic mapping to an actual color (bounds-checked). */
429
+ /**
430
+ * Resolve a semantic mapping to an actual color (bounds-checked).
431
+ *
432
+ * Only handles the legacy `number | 'bg' | 'fg'` index shape — the narrower
433
+ * type this function accepted before `ColorTweakState.semanticMappings` /
434
+ * `ColorClusterDataConfig.semanticDefaults` were widened to `SemanticValue`
435
+ * (#459 S1). Callers holding a `SemanticValue` must narrow with
436
+ * `isIndexMapping()` first; resolving the `{ literal }` / `{ ref }` variants
437
+ * is downstream work (#467/#469), not this function's job today.
438
+ */
335
439
  export declare function resolveMapping(mapping: number | 'bg' | 'fg', palette: string[], bgIndex: number, fgIndex: number): string;
336
440
  export declare function safeIndex(index: number, len: number): number;
337
- /** Apply a single `ColorTweakState` to the DOM using the given cluster config. */
338
- export declare function applyColorState(state: ColorTweakState, cluster?: ColorClusterDataConfig, sink?: ApplySink): void;
441
+ /**
442
+ * The cluster's configured default light/dark mode, or `'light'` when the
443
+ * cluster declares no `colorMode` (`colorMode === false`).
444
+ *
445
+ * This is the single runtime reader of `ClusterPanelSettings.colorMode.defaultMode`
446
+ * (#472). The field validates but was previously never read at runtime; this
447
+ * helper (consumed by `resolveSemanticPreviewColor`) gives it a real effect —
448
+ * it selects which side of a per-mode `{ literal: { light, dark } }` pair is the
449
+ * fallback when `light-dark()` cannot resolve.
450
+ */
451
+ export declare function getClusterDefaultMode(cluster: ColorClusterDataConfig): 'light' | 'dark';
452
+ /**
453
+ * Resolve a per-mode literal to a single concrete CSS color for `mode`.
454
+ *
455
+ * Used wherever CSS `light-dark()` cannot be used — a preview swatch, an SSR
456
+ * seed, or any non-browser consumer that needs one flat value rather than a
457
+ * `light-dark(...)` function. The emitters (`applyColorState`,
458
+ * `buildApplyOverrides`) deliberately do NOT call this; they emit
459
+ * `light-dark()` and let the browser choose.
460
+ */
461
+ export declare function resolvePerModeLiteral(value: {
462
+ literal: {
463
+ light: string;
464
+ dark: string;
465
+ };
466
+ }, mode: 'light' | 'dark'): string;
467
+ /**
468
+ * Resolve a semantic mapping to a single concrete CSS color suitable for a
469
+ * PREVIEW swatch — a flat value, never a `light-dark()` function. Mirrors the
470
+ * internal apply-path resolver, except a per-mode `{ literal: { light, dark } }`
471
+ * value collapses to `getClusterDefaultMode(cluster)`'s side (#472).
472
+ *
473
+ * This is the "preview/seed" consumer referenced by #472/#473: the per-mode
474
+ * editor UI (#473) renders its swatch from this so the user sees the cluster's
475
+ * default-mode color, while the applied CSS var still emits `light-dark()`.
476
+ */
477
+ export declare function resolveSemanticPreviewColor(mapping: SemanticValue, state: ColorTweakState, cluster?: ColorClusterDataConfig, currentTab?: TabConfig, tabs?: readonly TabConfig[]): string;
478
+ /**
479
+ * Apply a single `ColorTweakState` to the DOM using the given cluster config.
480
+ *
481
+ * `currentTab` / `tabs` are optional and exist so a cross-tab/tier `{ ref }`
482
+ * semantic mapping (#468) can resolve: `currentTab` should be the color
483
+ * TabConfig this cluster was derived from (id `'color'` or
484
+ * `'color-secondary'`), and `tabs` the panel's full tabs array so a ref
485
+ * pointing into another tab (e.g. the grouped Palette tab) resolves. Callers
486
+ * that never hold `{ ref }` mappings (or have no tab context, e.g. most
487
+ * existing tests) can omit both — `resolveSemanticCssValue` returns `null`
488
+ * for an unresolvable ref rather than throwing, and this function skips a
489
+ * `null` (leaving the token at its stylesheet default) rather than painting
490
+ * a fallback color.
491
+ *
492
+ * `ownerPrefix` (#501) is the instance `storagePrefix` this apply belongs to.
493
+ * When supplied AND this cluster emits a per-mode `light-dark()` literal (so
494
+ * `color-scheme: light dark` is written to the apply target — either
495
+ * `document.documentElement` or the sink's root), the instance is recorded as
496
+ * the owner of that inline `color-scheme` so the clear paths can later remove
497
+ * it without ever wiping a host-owned `<html style="color-scheme">`. Omitting
498
+ * it (direct callers with no instance context, e.g. some tests) skips the
499
+ * bookkeeping only — the actual CSS write is unchanged.
500
+ */
501
+ export declare function applyColorState(state: ColorTweakState, cluster?: ColorClusterDataConfig, sink?: ApplySink, currentTab?: TabConfig, tabs?: readonly TabConfig[], ownerPrefix?: string): void;
339
502
  /**
340
503
  * Apply a `TokenOverrides` map for a given manifest — writes inline
341
504
  * `--css-var: value` on `:root` for every overridden token, and removes the
@@ -346,6 +509,31 @@ export declare function applyColorState(state: ColorTweakState, cluster?: ColorC
346
509
  * in the UI and are never written to storage.
347
510
  */
348
511
  export declare function applyTokenOverrides(tokens: readonly TokenDef[], overrides: TokenOverrides, sink?: ApplySink): void;
512
+ /**
513
+ * Apply ONLY the color slices — primary color cluster + optional secondary
514
+ * cluster + the aggregate `color-scheme` decision — leaving spacing / font /
515
+ * size / generic-tab vars untouched.
516
+ *
517
+ * Extracted from `applyFullState` (#500/#509) so the scheme-change handler can
518
+ * re-paint the new identity's persisted color overrides after
519
+ * `clearAppliedColorStyles` WITHOUT re-writing (or clearing) the scheme-
520
+ * independent non-color vars that must survive a light/dark toggle (#347).
521
+ *
522
+ * `color-scheme` is managed as an AGGREGATE across the primary + secondary
523
+ * clusters, not per-cluster (#482 D3): `applyColorState` only ever SETS
524
+ * `color-scheme: light dark` when its OWN cluster needs it, it never clears
525
+ * it, so demoting the last per-mode-literal row (in either cluster) would
526
+ * otherwise leave the previous apply's value stale. A naive per-cluster
527
+ * clear inside `applyColorState` would let a per-mode-free secondary undo
528
+ * what the primary just required (or vice versa), so the clear-when-neither-
529
+ * needs-it decision is made here, once, after both clusters have applied.
530
+ *
531
+ * #501 — the `document.documentElement` clear is scoped to a color-scheme THIS
532
+ * instance actually wrote (`clearOwnedColorSchemeFromDocument`) so a host that
533
+ * owns `<html style="color-scheme">` is never clobbered; the sink path stays
534
+ * unconditional (sink targets are panel-owned) but drops the ownership claim.
535
+ */
536
+ export declare function applyColorSlices(color: ColorTweakState, secondary: ColorTweakState | undefined, cfg?: PanelConfig): void;
349
537
  /**
350
538
  * Apply the full unified `TweakState` — primary color cluster + token
351
539
  * overrides + optional secondary cluster.
@@ -357,8 +545,27 @@ export declare function applyTokenOverrides(tokens: readonly TokenDef[], overrid
357
545
  * When `cfg` is supplied its `applySink` (if any) is used to route all
358
546
  * CSS-var writes for this instance. Omitting `cfg` uses the default active
359
547
  * config (single-panel path, unchanged behavior).
548
+ *
549
+ * The color slices + aggregate `color-scheme` decision are delegated to
550
+ * `applyColorSlices`; this function adds the non-color (spacing / font / size /
551
+ * generic-tab) writes.
360
552
  */
361
553
  export declare function applyFullState(state: TweakState, cfg?: PanelConfig): void;
554
+ /**
555
+ * Apply ONLY the non-color slices (spacing / typography / size / generic-tab
556
+ * overrides), leaving the color clusters and aggregate `color-scheme` decision
557
+ * untouched. Complement of `applyColorSlices`; together they compose
558
+ * `applyFullState`.
559
+ *
560
+ * Extracted (#509 audit) so the persisted-overrides reapply paths
561
+ * (`reapplyPersistedOverrides`, the panel first-open effect) can, when a v4
562
+ * envelope exists but the ACTIVE (scheme, mode) identity has no color slot,
563
+ * still apply the scheme-INDEPENDENT non-color tweaks while leaving the active
564
+ * scheme's own stylesheet colors to show through — instead of painting the
565
+ * synthesized config-default colors inline. This mirrors the scheme-change
566
+ * handler's missing-slot behavior (gated on `hasActiveColorSlot`).
567
+ */
568
+ export declare function applyNonColorSlices(state: TweakState, cfg?: PanelConfig): void;
362
569
  /**
363
570
  * Strip the inline CSS variables written by the color cluster(s) only —
364
571
  * palette slots, base roles, and semantic vars. Spacing / typography / size
@@ -382,6 +589,11 @@ export declare function applyFullState(state: TweakState, cfg?: PanelConfig): vo
382
589
  * historical `(clusters, sink)` call shape (e.g. the scheme-change path before
383
590
  * #357, and existing tests) is unchanged. Omitting all three preserves the
384
591
  * single-panel default-instance behaviour.
592
+ *
593
+ * The color-scheme removal on the `document.documentElement` path is gated on
594
+ * this instance owning the inline value (#501) — see
595
+ * `clearOwnedColorSchemeFromDocument`. The owner is `cfg.storagePrefix` when a
596
+ * `cfg` is given, else the default instance's prefix.
385
597
  */
386
598
  export declare function clearAppliedColorStyles(clusters?: readonly ColorClusterDataConfig[], sink?: ApplySink, cfg?: PanelConfig): void;
387
599
  /**
@@ -447,17 +659,41 @@ export interface StorageLike {
447
659
  * labels), so this migration only applies to the typography slice.
448
660
  */
449
661
  export declare const ZDTP_LEGACY_TYPOGRAPHY_RENAME_MAP: Readonly<Record<string, string | null>>;
662
+ /**
663
+ * True when the v4 envelope holds a color slot for the CURRENTLY active
664
+ * identity. The scheme-change handler uses this to decide whether to hydrate +
665
+ * apply persisted overrides for the new scheme (slot present) or reseed from
666
+ * config defaults WITHOUT applying (slot absent — the pre-#500 behavior that
667
+ * lets the new scheme's stylesheet show through).
668
+ */
669
+ export declare function hasActiveColorSlot(cfg?: PanelConfig, cluster?: ColorClusterDataConfig, storage?: StorageLike): boolean;
670
+ /**
671
+ * Load the active `TweakState`, preferring the v4 per-scheme envelope
672
+ * (#500/#509).
673
+ *
674
+ * Order:
675
+ * 1. v4 present → select the active identity's color/secondary slots.
676
+ * 2. else run the legacy v1/v2/v3 chain (unchanged) → migrate that state into
677
+ * a v4 envelope filed under the identity active at load, WITHOUT deleting
678
+ * the legacy key (v4 wins on every future load via step 1). This is why the
679
+ * pre-existing v1/v2/v3 migration tests keep passing: the legacy chain still
680
+ * writes/keeps the v3 key exactly as before; v4 is layered on top.
681
+ * 3. else null (caller cold-seeds).
682
+ *
683
+ * A present-but-malformed v4 blob falls through to steps 2/3, which overwrite it.
684
+ */
450
685
  export declare function loadPersistedState(storage?: StorageLike, colorDefaults?: ColorTweakState, cluster?: ColorClusterDataConfig, cfg?: PanelConfig): TweakState | null;
451
686
  /**
452
- * Persist the full `TweakState` to v3.
687
+ * Merge-save the full `TweakState` into the v4 per-scheme envelope (#500/#509).
453
688
  *
454
- * `cfg` scopes the storage key to a specific panel instance (multi-instance,
455
- * #357). Omitting it resolves the default instance via `getPanelConfig()`,
456
- * preserving the single-panel path.
689
+ * The active identity's color/secondary slots are overwritten; every inactive
690
+ * identity's slot is preserved (read → merge → write). Global non-color slices
691
+ * are replaced wholesale. `cfg` scopes the storage key to a specific panel
692
+ * instance (multi-instance, #357).
457
693
  */
458
694
  export declare function savePersistedState(state: TweakState, storage?: StorageLike, cfg?: PanelConfig): void;
459
695
  /**
460
- * Remove v3 (and lingering v2/v1) keys.
696
+ * Remove the v4 key (and lingering v3/v2/v1 keys).
461
697
  *
462
698
  * `cfg` scopes the cleared keys to a specific panel instance (multi-instance,
463
699
  * #357). Omitting it resolves the default instance via `getPanelConfig()`,
@@ -35,6 +35,7 @@
35
35
  * in the panel.
36
36
  */
37
37
  import { type ColorTweakState } from '../state/tweak-state';
38
+ import { type PanelConfig } from '../config/panel-config';
38
39
  import type { TabConfig } from '../tokens/tier-model';
39
40
  import type { PersistColor, PersistSecondary } from '../state/persist';
40
41
  interface ColorTabProps {
@@ -52,6 +53,17 @@ interface ColorTabProps {
52
53
  secondaryTab: TabConfig | null;
53
54
  secondaryState: ColorTweakState | null;
54
55
  persistSecondary: PersistSecondary;
56
+ /**
57
+ * The mounted panel instance's config (multi-instance, #353/#357). When
58
+ * supplied, cross-tab cluster/ref resolution (cluster derivation, the
59
+ * grouped ref-or-literal picker's ramp groups, preview resolution, and the
60
+ * host preset list) reads THIS instance's `tabs` / `colorPresets` rather
61
+ * than the active default instance — matching the apply path, which
62
+ * already resolves against `cfg.tabs` via `usePersist` (`applyFullState`,
63
+ * `state/persist.ts`). Omitted (e.g. a direct test render) →
64
+ * `getPanelConfig()`, preserving the single-instance path.
65
+ */
66
+ instanceConfig?: PanelConfig;
55
67
  }
56
- export default function ColorTab({ tab, state, persistColor, secondaryTab, secondaryState, persistSecondary, }: ColorTabProps): import("preact").JSX.Element;
68
+ export default function ColorTab({ tab, state, persistColor, secondaryTab, secondaryState, persistSecondary, instanceConfig, }: ColorTabProps): import("preact").JSX.Element;
57
69
  export {};
@@ -28,8 +28,10 @@ export interface GenericTabProps {
28
28
  * When an item has no override, its `TierItem.default` is used.
29
29
  */
30
30
  overrides: TabOverrides;
31
- /** Called when the user commits a change to any item. */
32
- onChange: (tierId: string, itemId: string, next: string) => void;
31
+ /** Called when the user commits a change to any item. `next: undefined`
32
+ * (only reachable for ref-tier items) means "drop the stored override for
33
+ * this item" rather than writing a literal value. */
34
+ onChange: (tierId: string, itemId: string, next: string | undefined) => void;
33
35
  }
34
36
  /**
35
37
  * Renders a non-reserved tab (any TabConfig whose id is not color/font/
package/dist/testing.js CHANGED
@@ -1,5 +1,5 @@
1
- import { _ as r, c as i, f as _, h as n, i as g, b as R, a as p } from "./panel-config-3tBEHjvZ.js";
2
- import { a as l, b as y, l as O } from "./tweak-state-BEx1SHu7.js";
1
+ import { _ as r, c as i, h as _, i as n, j as g, b as R, a as p } from "./panel-config-CipeKMTz.js";
2
+ import { a as y, b as O, l as P } from "./tweak-state-xt-tSQ_t.js";
3
3
  import { F as K, G as E, a as S, S as u } from "./manifest-DCReQE0k.js";
4
4
  async function s(a, e) {
5
5
  await a.fill(e), await a.dispatchEvent("input");
@@ -11,9 +11,9 @@ export {
11
11
  u as SIZE_GROUP_ORDER,
12
12
  r as __resetPanelConfigForTests,
13
13
  i as configurePanel,
14
- l as getStorageKeyV1,
15
- y as getStorageKeyV2,
16
- O as loadPersistedState,
14
+ y as getStorageKeyV1,
15
+ O as getStorageKeyV2,
16
+ P as loadPersistedState,
17
17
  s as setPanelInputValue,
18
18
  _ as storageKey_open,
19
19
  n as storageKey_position,
@@ -56,6 +56,33 @@ export interface PillSpec {
56
56
  value: string;
57
57
  customDefault: string;
58
58
  }
59
+ /**
60
+ * A semantic token's mapping value.
61
+ *
62
+ * Legacy shape stays valid: a palette index (`number`) or the `'bg'` / `'fg'`
63
+ * sentinel aliases. New variants (added for #459) let a semantic tier point at
64
+ * an arbitrary literal color (optionally split light/dark) or reference a ramp
65
+ * item living in another tab/tier — both resolved downstream by #467/#469.
66
+ *
67
+ * Defined here (not in `state/tweak-state.ts`) because `config/cluster-config.ts`
68
+ * needs it for `ColorClusterDataConfig.semanticDefaults`, and cluster-config.ts
69
+ * already imports types from this module — keeping the definition here avoids
70
+ * a new type-only import cycle. `state/tweak-state.ts` re-exports it.
71
+ */
72
+ export type SemanticValue = number | 'bg' | 'fg' | {
73
+ literal: string;
74
+ } | {
75
+ literal: {
76
+ light: string;
77
+ dark: string;
78
+ };
79
+ } | {
80
+ ref: {
81
+ tab?: string;
82
+ tier: string;
83
+ item: string;
84
+ };
85
+ };
59
86
  export interface TierItem {
60
87
  id: string;
61
88
  cssVar: string;
@@ -73,6 +100,29 @@ export interface TierConfig {
73
100
  * an item in the tier whose id matches referencesTier. The apply pipeline
74
101
  * emits var(--tier1-cssvar). */
75
102
  referencesTier?: string;
103
+ /**
104
+ * Marks this tier as a SEMANTIC tier (its items hold `SemanticValue`
105
+ * mappings, not raw palette entries) so the panel never mistakes it for
106
+ * the palette tier. Optional and additive — omitting it preserves today's
107
+ * behavior (palette-tier detection stays structural, via
108
+ * `resolveColorClusterFromTab`'s `kind: 'color'` check).
109
+ */
110
+ semantic?: true;
111
+ /**
112
+ * Cross-tab ramp-source declaration for a semantic tier: the ramp tier(s)
113
+ * this tier's `{ ref }` mappings are allowed to point into. Each entry names
114
+ * a tier id and an optional tab id (omitted `tab` means "this tab").
115
+ * `assertValidPanelConfig` validates every declared source up front (the
116
+ * tab/tier must exist and share this tier's kind); `resolveColorClusterFromTab`
117
+ * (`config/cluster-config.ts`) uses the allow-list to derive each row's
118
+ * `SemanticValue`; the grouped `TierRefSelector` picker
119
+ * (`controls/tier-ref-selector.tsx`) renders one `<optgroup>` per declared
120
+ * source.
121
+ */
122
+ referencesRamps?: readonly {
123
+ tab?: string;
124
+ tier: string;
125
+ }[];
76
126
  }
77
127
  export interface ColorClusterExtras {
78
128
  id: string;
@@ -82,6 +132,18 @@ export interface ColorClusterExtras {
82
132
  defaultShikiTheme: string;
83
133
  colorSchemes: Record<string, ColorScheme>;
84
134
  panelSettings: ClusterPanelSettings;
135
+ /**
136
+ * Config-time override map for a semantic tier's derived defaults (#499).
137
+ * Keyed by semantic item id; when a key is present here,
138
+ * `resolveColorClusterFromTab` uses its `SemanticValue` verbatim instead of
139
+ * running `deriveSemanticValue` on the item's plain-string `default` — this
140
+ * is the only way a host can ship a `{ literal: { light, dark } }` (or
141
+ * `{ ref }`) config-time default, since `TierItem.default` itself stays a
142
+ * plain string (Option B from #499, chosen over widening `TierItem.default`
143
+ * to `SemanticValue` — that field is consumed generically as a plain string
144
+ * by every tab kind, not just Color).
145
+ */
146
+ semanticDefaults?: Record<string, SemanticValue>;
85
147
  }
86
148
  export interface TabConfig {
87
149
  id: string;