@takazudo/zdtp 0.4.3 → 0.4.4

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.
@@ -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';
@@ -182,9 +185,47 @@ export interface ColorTweakState {
182
185
  cursor: number;
183
186
  selectionBg: number;
184
187
  selectionFg: number;
185
- semanticMappings: Record<string, number | 'bg' | 'fg'>;
188
+ /**
189
+ * Semantic token name → mapping. Widened from `Record<string, number | 'bg'
190
+ * | 'fg'>` to `Record<string, SemanticValue>` (#459 S1). This is additive
191
+ * for readers of the type (every legacy value is still a valid
192
+ * `SemanticValue`), but it BREAKS any exhaustive `switch`/equality check
193
+ * written against the old narrow union — callers that pattern-match on a
194
+ * mapping value must now also handle the new `{ literal }` / `{ ref }`
195
+ * object variants (see `isIndexMapping` / `isLiteralMapping` /
196
+ * `isPerModeLiteral` / `isRefMapping` below). `resolveMapping` (exported)
197
+ * still only resolves the legacy `number | 'bg' | 'fg'` shape — resolving
198
+ * the new variants is downstream work (#467/#469).
199
+ */
200
+ semanticMappings: Record<string, SemanticValue>;
186
201
  shikiTheme: string;
187
202
  }
203
+ /** True when `v` is a legacy index-style mapping (palette index or bg/fg alias). */
204
+ export declare function isIndexMapping(v: SemanticValue): v is number | 'bg' | 'fg';
205
+ /** True when `v` is either literal-color variant (plain string or light/dark pair). */
206
+ export declare function isLiteralMapping(v: SemanticValue): v is {
207
+ literal: string;
208
+ } | {
209
+ literal: {
210
+ light: string;
211
+ dark: string;
212
+ };
213
+ };
214
+ /** True when `v` is specifically the light/dark literal-color variant. */
215
+ export declare function isPerModeLiteral(v: SemanticValue): v is {
216
+ literal: {
217
+ light: string;
218
+ dark: string;
219
+ };
220
+ };
221
+ /** True when `v` is a cross-tab/tier ramp-item reference. */
222
+ export declare function isRefMapping(v: SemanticValue): v is {
223
+ ref: {
224
+ tab?: string;
225
+ tier: string;
226
+ item: string;
227
+ };
228
+ };
188
229
  /**
189
230
  * Per-token override map. Keys are `TokenDef.id` (e.g. `hsp-sm`); values are
190
231
  * raw CSS length strings (e.g. `0.75rem`). Only overridden tokens appear in
@@ -238,6 +279,10 @@ export declare function getActivePrimaryCluster(cfg?: PanelConfig): ColorCluster
238
279
  * slots interpolate. Functional but visually flat; hosts wanting a
239
280
  * designed seed should ship a scheme registry on the cluster and call
240
281
  * `initColorFromScheme(cluster)` instead.
282
+ * - A cluster with `paletteSize: 0` (a lone `semantic: true` tier with no
283
+ * palette sibling, #458/#466) seeds an EMPTY palette — there is nothing
284
+ * to ramp. Forcing a 1-slot grayscale floor here produced a phantom
285
+ * `--zudo-stub-p0` swatch/token with no backing tier; see #466.
241
286
  *
242
287
  * The base-role indices are kept on the state shape for envelope-round-trip
243
288
  * compatibility but are inert — `applyColorState` only writes a base role
@@ -331,11 +376,70 @@ export declare function initColorFromScheme(cluster?: ColorClusterDataConfig, cf
331
376
  */
332
377
  export declare function normalizeSchemePaletteEntry(value: string): string;
333
378
  export declare function initColorFromSchemeData(scheme: ColorScheme, cluster?: ColorClusterDataConfig): ColorTweakState;
334
- /** Resolve a semantic mapping to an actual color (bounds-checked). */
379
+ /**
380
+ * Resolve a semantic mapping to an actual color (bounds-checked).
381
+ *
382
+ * Only handles the legacy `number | 'bg' | 'fg'` index shape — the narrower
383
+ * type this function accepted before `ColorTweakState.semanticMappings` /
384
+ * `ColorClusterDataConfig.semanticDefaults` were widened to `SemanticValue`
385
+ * (#459 S1). Callers holding a `SemanticValue` must narrow with
386
+ * `isIndexMapping()` first; resolving the `{ literal }` / `{ ref }` variants
387
+ * is downstream work (#467/#469), not this function's job today.
388
+ */
335
389
  export declare function resolveMapping(mapping: number | 'bg' | 'fg', palette: string[], bgIndex: number, fgIndex: number): string;
336
390
  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;
391
+ /**
392
+ * The cluster's configured default light/dark mode, or `'light'` when the
393
+ * cluster declares no `colorMode` (`colorMode === false`).
394
+ *
395
+ * This is the single runtime reader of `ClusterPanelSettings.colorMode.defaultMode`
396
+ * (#472). The field validates but was previously never read at runtime; this
397
+ * helper (consumed by `resolveSemanticPreviewColor`) gives it a real effect —
398
+ * it selects which side of a per-mode `{ literal: { light, dark } }` pair is the
399
+ * fallback when `light-dark()` cannot resolve.
400
+ */
401
+ export declare function getClusterDefaultMode(cluster: ColorClusterDataConfig): 'light' | 'dark';
402
+ /**
403
+ * Resolve a per-mode literal to a single concrete CSS color for `mode`.
404
+ *
405
+ * Used wherever CSS `light-dark()` cannot be used — a preview swatch, an SSR
406
+ * seed, or any non-browser consumer that needs one flat value rather than a
407
+ * `light-dark(...)` function. The emitters (`applyColorState`,
408
+ * `buildApplyOverrides`) deliberately do NOT call this; they emit
409
+ * `light-dark()` and let the browser choose.
410
+ */
411
+ export declare function resolvePerModeLiteral(value: {
412
+ literal: {
413
+ light: string;
414
+ dark: string;
415
+ };
416
+ }, mode: 'light' | 'dark'): string;
417
+ /**
418
+ * Resolve a semantic mapping to a single concrete CSS color suitable for a
419
+ * PREVIEW swatch — a flat value, never a `light-dark()` function. Mirrors the
420
+ * internal apply-path resolver, except a per-mode `{ literal: { light, dark } }`
421
+ * value collapses to `getClusterDefaultMode(cluster)`'s side (#472).
422
+ *
423
+ * This is the "preview/seed" consumer referenced by #472/#473: the per-mode
424
+ * editor UI (#473) renders its swatch from this so the user sees the cluster's
425
+ * default-mode color, while the applied CSS var still emits `light-dark()`.
426
+ */
427
+ export declare function resolveSemanticPreviewColor(mapping: SemanticValue, state: ColorTweakState, cluster?: ColorClusterDataConfig, currentTab?: TabConfig, tabs?: readonly TabConfig[]): string;
428
+ /**
429
+ * Apply a single `ColorTweakState` to the DOM using the given cluster config.
430
+ *
431
+ * `currentTab` / `tabs` are optional and exist so a cross-tab/tier `{ ref }`
432
+ * semantic mapping (#468) can resolve: `currentTab` should be the color
433
+ * TabConfig this cluster was derived from (id `'color'` or
434
+ * `'color-secondary'`), and `tabs` the panel's full tabs array so a ref
435
+ * pointing into another tab (e.g. the grouped Palette tab) resolves. Callers
436
+ * that never hold `{ ref }` mappings (or have no tab context, e.g. most
437
+ * existing tests) can omit both — `resolveSemanticCssValue` returns `null`
438
+ * for an unresolvable ref rather than throwing, and this function skips a
439
+ * `null` (leaving the token at its stylesheet default) rather than painting
440
+ * a fallback color.
441
+ */
442
+ export declare function applyColorState(state: ColorTweakState, cluster?: ColorClusterDataConfig, sink?: ApplySink, currentTab?: TabConfig, tabs?: readonly TabConfig[]): void;
339
443
  /**
340
444
  * Apply a `TokenOverrides` map for a given manifest — writes inline
341
445
  * `--css-var: value` on `:root` for every overridden token, and removes the
@@ -357,6 +461,15 @@ export declare function applyTokenOverrides(tokens: readonly TokenDef[], overrid
357
461
  * When `cfg` is supplied its `applySink` (if any) is used to route all
358
462
  * CSS-var writes for this instance. Omitting `cfg` uses the default active
359
463
  * config (single-panel path, unchanged behavior).
464
+ *
465
+ * `color-scheme` is managed as an AGGREGATE across the primary + secondary
466
+ * clusters, not per-cluster (#482 D3): `applyColorState` only ever SETS
467
+ * `color-scheme: light dark` when its OWN cluster needs it, it never clears
468
+ * it, so demoting the last per-mode-literal row (in either cluster) would
469
+ * otherwise leave the previous apply's value stale. A naive per-cluster
470
+ * clear inside `applyColorState` would let a per-mode-free secondary undo
471
+ * what the primary just required (or vice versa), so the clear-when-neither-
472
+ * needs-it decision is made here, once, after both clusters have applied.
360
473
  */
361
474
  export declare function applyFullState(state: TweakState, cfg?: PanelConfig): void;
362
475
  /**
@@ -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, f as _, h as n, i as g, b as R, a as p } from "./panel-config-CXTCcYQs.js";
2
+ import { a as l, b as y, l as O } from "./tweak-state-BeXkzoj8.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");
@@ -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;