@tenphi/glaze 1.0.0 → 1.1.0

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/dist/index.cjs CHANGED
@@ -675,6 +675,8 @@ function okhslToOklch(h, s, l, pastel = false) {
675
675
  /**
676
676
  * Build a fresh defaults object. Called from module init and from
677
677
  * `resetConfig()` so the two paths can't drift.
678
+ *
679
+ * `pastel: false` is a fixed instance default — not globally configurable.
678
680
  */
679
681
  function defaultConfig() {
680
682
  return {
@@ -739,7 +741,7 @@ function configure(config) {
739
741
  },
740
742
  shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning,
741
743
  autoFlip: config.autoFlip ?? globalConfig.autoFlip,
742
- pastel: config.pastel ?? globalConfig.pastel,
744
+ pastel: false,
743
745
  inferRole: config.inferRole ?? globalConfig.inferRole
744
746
  };
745
747
  }
@@ -752,9 +754,15 @@ function resetConfig() {
752
754
  * Only fields present in `override` are replaced; others fall through
753
755
  * from `base`. `false` for tone windows passes through as-is
754
756
  * (treated as the full range by `activeWindow()` in okhst.ts).
757
+ *
758
+ * `pastel` is instance-only: `override.pastel ?? false`, never inherited
759
+ * from the global base.
755
760
  */
756
761
  function mergeConfig(base, override) {
757
- if (!override) return base;
762
+ if (!override) return base.pastel === false ? base : {
763
+ ...base,
764
+ pastel: false
765
+ };
758
766
  return {
759
767
  lightTone: override.lightTone !== void 0 ? override.lightTone : base.lightTone,
760
768
  darkTone: override.darkTone !== void 0 ? override.darkTone : base.darkTone,
@@ -763,10 +771,99 @@ function mergeConfig(base, override) {
763
771
  modes: base.modes,
764
772
  shadowTuning: override.shadowTuning ?? base.shadowTuning,
765
773
  autoFlip: override.autoFlip ?? base.autoFlip,
766
- pastel: override.pastel ?? base.pastel,
774
+ pastel: override.pastel ?? false,
767
775
  inferRole: override.inferRole ?? base.inferRole
768
776
  };
769
777
  }
778
+ /**
779
+ * Freeze `getConfig() ∪ instanceLocal ∪ exportArg` as a plain, resolve-relevant
780
+ * override for authoring export, so restored snapshots pin these fields. Later
781
+ * wins; nested objects (`shadowTuning`) replace wholesale. The final
782
+ * `structuredClone` detaches tone-window / shadow objects that `mergeConfig`
783
+ * may still share with the live global config.
784
+ */
785
+ function freezeConfigForExport(instanceLocal, exportArg) {
786
+ const effective = mergeConfig(getConfig(), {
787
+ ...instanceLocal,
788
+ ...exportArg
789
+ });
790
+ const out = {
791
+ lightTone: effective.lightTone,
792
+ darkTone: effective.darkTone,
793
+ darkDesaturation: effective.darkDesaturation,
794
+ autoFlip: effective.autoFlip,
795
+ pastel: effective.pastel,
796
+ inferRole: effective.inferRole
797
+ };
798
+ if (effective.shadowTuning !== void 0) out.shadowTuning = effective.shadowTuning;
799
+ return structuredClone(out);
800
+ }
801
+
802
+ //#endregion
803
+ //#region src/types.ts
804
+ /**
805
+ * Current authoring-export schema version. Bump when the export shape
806
+ * changes in a non-compatible way. Written on every `.export()` snapshot.
807
+ */
808
+ const GLAZE_EXPORT_VERSION = 1;
809
+
810
+ //#endregion
811
+ //#region src/serialize.ts
812
+ /**
813
+ * Authoring-export helpers: schema version, type guards, and restore
814
+ * validation shared by `themeFrom` / `colorFrom` / `paletteFrom`.
815
+ */
816
+ function isPlainObject(data) {
817
+ return typeof data === "object" && data !== null && !Array.isArray(data);
818
+ }
819
+ /**
820
+ * Reject unknown / invalid schema versions. Missing `version` is allowed
821
+ * (legacy snapshots). Present versions must be an integer in
822
+ * `1..=GLAZE_EXPORT_VERSION`.
823
+ */
824
+ function assertExportVersion(data, factory) {
825
+ if (data.version === void 0) return;
826
+ if (typeof data.version !== "number" || !Number.isInteger(data.version) || data.version < 1) throw new Error(`${factory}: invalid "version" field — expected an integer >= 1 (got ${JSON.stringify(data.version)}).`);
827
+ if (data.version > GLAZE_EXPORT_VERSION) throw new Error(`${factory}: unsupported export version ${data.version} (this library supports version ${GLAZE_EXPORT_VERSION}). Upgrade @tenphi/glaze to load this snapshot.`);
828
+ }
829
+ /**
830
+ * When `kind` is present, it must match the factory. Missing `kind` is
831
+ * allowed for legacy snapshots.
832
+ */
833
+ function assertExportKind(data, expected, factory) {
834
+ if (data.kind === void 0) return;
835
+ if (data.kind !== expected) throw new Error(`${factory}: expected kind "${expected}", got ${JSON.stringify(data.kind)}.`);
836
+ }
837
+ function hasThemeShape(data) {
838
+ return typeof data.hue === "number" && typeof data.saturation === "number" && !("form" in data) && !("themes" in data);
839
+ }
840
+ function hasColorTokenShape(data) {
841
+ return data.form === "value" || data.form === "structured";
842
+ }
843
+ function hasPaletteShape(data) {
844
+ return isPlainObject(data.themes) && !("form" in data) && typeof data.hue !== "number";
845
+ }
846
+ /** Type guard for theme authoring snapshots (prefers `kind`, falls back to shape). */
847
+ function isThemeExport(data) {
848
+ if (!isPlainObject(data)) return false;
849
+ if (data.kind === "theme") return hasThemeShape(data);
850
+ if (data.kind !== void 0) return false;
851
+ return hasThemeShape(data);
852
+ }
853
+ /** Type guard for color-token authoring snapshots. */
854
+ function isColorTokenExport(data) {
855
+ if (!isPlainObject(data)) return false;
856
+ if (data.kind === "color") return hasColorTokenShape(data);
857
+ if (data.kind !== void 0) return false;
858
+ return hasColorTokenShape(data);
859
+ }
860
+ /** Type guard for palette authoring snapshots. */
861
+ function isPaletteExport(data) {
862
+ if (!isPlainObject(data)) return false;
863
+ if (data.kind === "palette") return hasPaletteShape(data);
864
+ if (data.kind !== void 0) return false;
865
+ return hasPaletteShape(data);
866
+ }
770
867
 
771
868
  //#endregion
772
869
  //#region src/format-guard.ts
@@ -811,7 +908,7 @@ function assertAllPastel(resolved, modes) {
811
908
  }
812
909
  }
813
910
  if (nonPastel.length === 0) return;
814
- throw new Error(`glaze: splitHue requires every color to be pastel (hue rotation is only clip-free when chroma is bounded by the hue-independent safe chroma). Non-pastel: ${nonPastel.join(", ")}. Set pastel: true (global or per-color) or drop splitHue.`);
911
+ throw new Error(`glaze: splitHue requires every color to be pastel (hue rotation is only clip-free when chroma is bounded by the hue-independent safe chroma). Non-pastel: ${nonPastel.join(", ")}. Set pastel: true (per-theme, per-token, or per-color) or drop splitHue.`);
815
912
  }
816
913
 
817
914
  //#endregion
@@ -2652,12 +2749,10 @@ function buildTailwindMap(resolved, themePrefix, cssPrefix, modes, format, darkS
2652
2749
  * `{ l, c, h }`), the structured-input validator, the two factory paths
2653
2750
  * (value vs structured), and the JSON-safe export / rehydration round-trip.
2654
2751
  *
2655
- * Standalone tokens snapshot the full effective config at create time
2656
- * so later `configure()` calls do not retroactively change exported
2657
- * tokens. The snapshot is built eagerly in
2658
- * `buildValueFormConfigOverride()` / `buildStructuredConfigOverride()`.
2659
- * The token's resolved variants are then memoized on first
2660
- * `.resolve()` / `.token()` / ... call.
2752
+ * Tokens store a sparse local config override only. Resolve merges the
2753
+ * live global config with that local override (invalidated on
2754
+ * `configure()`). Authoring `.export(override?)` freezes
2755
+ * `getConfig() ∪ local ∪ override` at call time.
2661
2756
  */
2662
2757
  /** Internal name of the user-facing standalone color in the synthesized def map. */
2663
2758
  const STANDALONE_VALUE = "value";
@@ -2672,47 +2767,16 @@ const RESERVED_STANDALONE_NAMES = new Set([
2672
2767
  STANDALONE_BASE
2673
2768
  ]);
2674
2769
  /**
2675
- * Build the per-token effective config override for a value-form color.
2676
- *
2677
- * Light window defaults to `false` (preserve input tone exactly).
2678
- * All other fields snapshot from global at create time. User override
2679
- * fields win over all defaults.
2770
+ * Value-form local override: `lightTone` defaults to `false` (preserve
2771
+ * input tone). User override fields win.
2680
2772
  */
2681
- function buildValueFormConfigOverride(userOverride) {
2682
- const cfg = getConfig();
2773
+ function sparseValueFormLocal(userOverride) {
2683
2774
  return {
2684
- lightTone: userOverride?.lightTone !== void 0 ? userOverride.lightTone : false,
2685
- darkTone: userOverride?.darkTone !== void 0 ? userOverride.darkTone : cfg.darkTone,
2686
- darkDesaturation: userOverride?.darkDesaturation ?? cfg.darkDesaturation,
2687
- autoFlip: userOverride?.autoFlip ?? cfg.autoFlip,
2688
- shadowTuning: userOverride?.shadowTuning ?? cfg.shadowTuning
2775
+ ...userOverride,
2776
+ lightTone: userOverride?.lightTone !== void 0 ? userOverride.lightTone : false
2689
2777
  };
2690
2778
  }
2691
2779
  /**
2692
- * Build the per-token effective config override for a structured-form color.
2693
- *
2694
- * Both light and dark windows snapshot from global at create time.
2695
- * User override fields win.
2696
- */
2697
- function buildStructuredConfigOverride(userOverride) {
2698
- const cfg = getConfig();
2699
- return {
2700
- lightTone: userOverride?.lightTone !== void 0 ? userOverride.lightTone : cfg.lightTone,
2701
- darkTone: userOverride?.darkTone !== void 0 ? userOverride.darkTone : cfg.darkTone,
2702
- darkDesaturation: userOverride?.darkDesaturation ?? cfg.darkDesaturation,
2703
- autoFlip: userOverride?.autoFlip ?? cfg.autoFlip,
2704
- shadowTuning: userOverride?.shadowTuning ?? cfg.shadowTuning
2705
- };
2706
- }
2707
- /**
2708
- * Build the `GlazeConfigResolved` to pass to `resolveAllColors` from a
2709
- * snapshot override. Uses `defaultConfig()` as the base so all required
2710
- * fields are present; the snapshot fields win.
2711
- */
2712
- function resolvedConfigFromOverride(override) {
2713
- return mergeConfig(defaultConfig(), override);
2714
- }
2715
- /**
2716
2780
  * Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
2717
2781
  * `okhsl()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
2718
2782
  *
@@ -3002,12 +3066,30 @@ function buildStandaloneValueDefs(main, options) {
3002
3066
  primary
3003
3067
  };
3004
3068
  }
3005
- function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveConfig, baseToken, exportData) {
3006
- let cached;
3069
+ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, configOverride, baseToken, buildExport) {
3070
+ let cache = null;
3071
+ function getEffectiveConfig() {
3072
+ const version = getConfigVersion();
3073
+ if (cache && cache.version === version) return cache.effectiveConfig;
3074
+ const effectiveConfig = mergeConfig(getConfig(), configOverride);
3075
+ cache = {
3076
+ map: null,
3077
+ version,
3078
+ effectiveConfig
3079
+ };
3080
+ return effectiveConfig;
3081
+ }
3007
3082
  const resolveOnce = () => {
3008
- if (cached) return cached;
3009
- cached = resolveAllColors(seedHue, seedSaturation, defs, effectiveConfig, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
3010
- return cached;
3083
+ const version = getConfigVersion();
3084
+ if (cache && cache.version === version && cache.map) return cache.map;
3085
+ const effectiveConfig = getEffectiveConfig();
3086
+ const map = resolveAllColors(seedHue, seedSaturation, defs, effectiveConfig, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
3087
+ cache = {
3088
+ map,
3089
+ version,
3090
+ effectiveConfig
3091
+ };
3092
+ return map;
3011
3093
  };
3012
3094
  const resolveStates = (options) => {
3013
3095
  const cfg = getConfig();
@@ -3017,7 +3099,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
3017
3099
  };
3018
3100
  };
3019
3101
  const tokenLike = (options) => {
3020
- return buildTokenMap(resolveOnce(), "", resolveStates(options), resolveModes(options?.modes), options?.format ?? "oklch", effectiveConfig.pastel)[`#${primary}`];
3102
+ return buildTokenMap(resolveOnce(), "", resolveStates(options), resolveModes(options?.modes), options?.format ?? "oklch", getEffectiveConfig().pastel)[`#${primary}`];
3021
3103
  };
3022
3104
  return {
3023
3105
  resolve() {
@@ -3028,7 +3110,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
3028
3110
  json(options) {
3029
3111
  const format = options?.format ?? "oklch";
3030
3112
  assertNativeFormat(format, "json");
3031
- return buildJsonMap(resolveOnce(), resolveModes(options?.modes), format, effectiveConfig.pastel)[primary];
3113
+ return buildJsonMap(resolveOnce(), resolveModes(options?.modes), format, getEffectiveConfig().pastel)[primary];
3032
3114
  },
3033
3115
  css(options) {
3034
3116
  const format = options.format ?? "oklch";
@@ -3047,11 +3129,11 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
3047
3129
  resolvedHue: resolved.light.h
3048
3130
  };
3049
3131
  }
3050
- return buildCssMap(renamed, "", options.suffix ?? "-color", format, effectiveConfig.pastel, channelCtx);
3132
+ return buildCssMap(renamed, "", options.suffix ?? "-color", format, getEffectiveConfig().pastel, channelCtx);
3051
3133
  },
3052
3134
  dtcg(options) {
3053
3135
  const modes = resolveModes(options?.modes);
3054
- const doc = buildDtcgMap(resolveOnce(), "", modes, options?.colorSpace ?? "srgb", effectiveConfig.pastel);
3136
+ const doc = buildDtcgMap(resolveOnce(), "", modes, options?.colorSpace ?? "srgb", getEffectiveConfig().pastel);
3055
3137
  const result = { light: doc.light[primary] };
3056
3138
  if (doc.dark) result.dark = doc.dark[primary];
3057
3139
  if (doc.lightContrast) result.lightContrast = doc.lightContrast[primary];
@@ -3059,7 +3141,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
3059
3141
  return result;
3060
3142
  },
3061
3143
  dtcgResolver(options) {
3062
- const doc = buildDtcgMap(resolveOnce(), "", resolveModes(options?.modes), options?.colorSpace ?? "srgb", effectiveConfig.pastel);
3144
+ const doc = buildDtcgMap(resolveOnce(), "", resolveModes(options?.modes), options?.colorSpace ?? "srgb", getEffectiveConfig().pastel);
3063
3145
  const name = options.name;
3064
3146
  const result = { light: { [name]: doc.light[primary] } };
3065
3147
  if (doc.dark) result.dark = { [name]: doc.dark[primary] };
@@ -3070,9 +3152,11 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
3070
3152
  tailwind(options) {
3071
3153
  const format = options.format ?? "oklch";
3072
3154
  assertNativeFormat(format, "tailwind");
3073
- return buildTailwindMap(new Map([[options.name, resolveOnce().get(primary)]]), "", options.namespace ?? "color-", resolveModes(options?.modes), format, options.darkSelector ?? ".dark", options.highContrastSelector ?? ".high-contrast", effectiveConfig.pastel);
3155
+ return buildTailwindMap(new Map([[options.name, resolveOnce().get(primary)]]), "", options.namespace ?? "color-", resolveModes(options?.modes), format, options.darkSelector ?? ".dark", options.highContrastSelector ?? ".high-contrast", getEffectiveConfig().pastel);
3074
3156
  },
3075
- export: exportData
3157
+ export(override) {
3158
+ return buildExport(override);
3159
+ }
3076
3160
  };
3077
3161
  }
3078
3162
  /**
@@ -3139,35 +3223,35 @@ function createColorToken(input, configOverride) {
3139
3223
  mode: "static"
3140
3224
  };
3141
3225
  }
3142
- const effectiveConfigOverride = buildStructuredConfigOverride(configOverride);
3143
- const effectiveConfig = resolvedConfigFromOverride(effectiveConfigOverride);
3144
- const exportData = () => ({
3226
+ const localOverride = configOverride;
3227
+ return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, localOverride, baseToken, (exportArg) => ({
3228
+ kind: "color",
3229
+ version: GLAZE_EXPORT_VERSION,
3145
3230
  form: "structured",
3146
- input: buildStructuredInputExport(input),
3147
- config: effectiveConfigOverride
3148
- });
3149
- return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, effectiveConfig, baseToken, exportData);
3231
+ input: buildStructuredInputExport(input, exportArg),
3232
+ config: freezeConfigForExport(localOverride, exportArg)
3233
+ }));
3150
3234
  }
3151
3235
  function createColorTokenFromValue(value, options, configOverride) {
3152
3236
  const main = extractOkhslFromValue(value);
3153
3237
  const linkingBase = toLinkingBase(resolveBaseToken(options?.base));
3154
3238
  const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options);
3155
- const effectiveConfigOverride = buildValueFormConfigOverride(configOverride);
3156
- const effectiveConfig = resolvedConfigFromOverride(effectiveConfigOverride);
3157
- const exportData = () => ({
3239
+ const localOverride = sparseValueFormLocal(configOverride);
3240
+ return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, localOverride, linkingBase, (exportArg) => ({
3241
+ kind: "color",
3242
+ version: GLAZE_EXPORT_VERSION,
3158
3243
  form: "value",
3159
3244
  input: value,
3160
- ...options !== void 0 ? { overrides: buildOverridesExport(options) } : {},
3161
- config: effectiveConfigOverride
3162
- });
3163
- return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveConfig, linkingBase, exportData);
3245
+ ...options !== void 0 ? { overrides: buildOverridesExport(options, exportArg) } : {},
3246
+ config: freezeConfigForExport(localOverride, exportArg)
3247
+ }));
3164
3248
  }
3165
3249
  /**
3166
3250
  * Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is
3167
3251
  * recursively serialized when it was originally a token; raw values are
3168
3252
  * preserved as-is so `glaze.colorFrom(...)` round-trips them.
3169
3253
  */
3170
- function buildOverridesExport(options) {
3254
+ function buildOverridesExport(options, exportArg) {
3171
3255
  const out = {};
3172
3256
  if (options.hue !== void 0) out.hue = options.hue;
3173
3257
  if (options.saturation !== void 0) out.saturation = options.saturation;
@@ -3180,10 +3264,10 @@ function buildOverridesExport(options) {
3180
3264
  if (options.name !== void 0) out.name = options.name;
3181
3265
  if (options.pastel !== void 0) out.pastel = options.pastel;
3182
3266
  if (options.role !== void 0) out.role = options.role;
3183
- if (options.base !== void 0) out.base = isGlazeColorToken(options.base) ? options.base.export() : options.base;
3267
+ if (options.base !== void 0) out.base = isGlazeColorToken(options.base) ? options.base.export(exportArg) : options.base;
3184
3268
  return out;
3185
3269
  }
3186
- function buildStructuredInputExport(input) {
3270
+ function buildStructuredInputExport(input, exportArg) {
3187
3271
  const out = {
3188
3272
  hue: input.hue,
3189
3273
  saturation: input.saturation,
@@ -3197,14 +3281,14 @@ function buildStructuredInputExport(input) {
3197
3281
  if (input.name !== void 0) out.name = input.name;
3198
3282
  if (input.pastel !== void 0) out.pastel = input.pastel;
3199
3283
  if (input.role !== void 0) out.role = input.role;
3200
- if (input.base !== void 0) out.base = isGlazeColorToken(input.base) ? input.base.export() : input.base;
3284
+ if (input.base !== void 0) out.base = isGlazeColorToken(input.base) ? input.base.export(exportArg) : input.base;
3201
3285
  return out;
3202
3286
  }
3203
3287
  /**
3204
3288
  * Discriminate a `GlazeColorTokenExport` from a raw `GlazeColorValue`.
3205
3289
  */
3206
3290
  function isExportedToken(candidate) {
3207
- return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "form" in candidate && (candidate.form === "value" || candidate.form === "structured");
3291
+ return isColorTokenExport(candidate);
3208
3292
  }
3209
3293
  function rehydrateOverrides(data) {
3210
3294
  const out = {};
@@ -3243,12 +3327,14 @@ function rehydrateStructuredInput(data) {
3243
3327
  * Rehydrate a token from its `.export()` snapshot. Recursively rebuilds
3244
3328
  * any base dependency. Inverse of `GlazeColorToken.export()`.
3245
3329
  *
3246
- * The stored `config` field contains the full effective config override
3247
- * snapshotted at creation time, so the rehydrated token is deterministic
3248
- * regardless of subsequent `glaze.configure()` calls.
3330
+ * The stored `config` field is the freeze from export time — passed as
3331
+ * the instance local override so the rehydrated token stays pinned
3332
+ * against later `glaze.configure()` calls.
3249
3333
  */
3250
3334
  function colorFromExport(data) {
3251
3335
  if (data === null || typeof data !== "object") throw new Error(`glaze.colorFrom: expected an object from token.export(), got ${data === null ? "null" : typeof data}.`);
3336
+ assertExportKind(data, "color", "glaze.colorFrom");
3337
+ assertExportVersion(data, "glaze.colorFrom");
3252
3338
  if (data.form !== "value" && data.form !== "structured") throw new Error(`glaze.colorFrom: invalid "form" field — expected "value" or "structured" (got ${JSON.stringify(data.form)}).`);
3253
3339
  if (data.input === void 0) throw new Error(`glaze.colorFrom: missing "input" field — expected the original ${data.form === "value" ? "GlazeColorValue" : "GlazeColorInput"}.`);
3254
3340
  if (data.form === "value") {
@@ -3258,6 +3344,157 @@ function colorFromExport(data) {
3258
3344
  return createColorToken(rehydrateStructuredInput(data.input), data.config);
3259
3345
  }
3260
3346
 
3347
+ //#endregion
3348
+ //#region src/theme.ts
3349
+ function createTheme(hue, saturation, initialColors, configOverride) {
3350
+ let colorDefs = initialColors ? { ...initialColors } : {};
3351
+ let cache = null;
3352
+ function getEffectiveConfig() {
3353
+ const version = getConfigVersion();
3354
+ if (cache && cache.version === version) return cache.effectiveConfig;
3355
+ const effectiveConfig = mergeConfig(getConfig(), configOverride);
3356
+ cache = {
3357
+ map: null,
3358
+ version,
3359
+ effectiveConfig
3360
+ };
3361
+ return effectiveConfig;
3362
+ }
3363
+ function resolveCached() {
3364
+ const version = getConfigVersion();
3365
+ if (cache && cache.version === version && cache.map) return cache.map;
3366
+ const effectiveConfig = getEffectiveConfig();
3367
+ const map = resolveAllColors(hue, saturation, colorDefs, effectiveConfig);
3368
+ cache = {
3369
+ map,
3370
+ version,
3371
+ effectiveConfig
3372
+ };
3373
+ return map;
3374
+ }
3375
+ function invalidate() {
3376
+ cache = null;
3377
+ }
3378
+ function channelCtxFor(options, formatDefault, prefix) {
3379
+ const format = options?.format ?? formatDefault;
3380
+ if (!options?.splitHue || format !== "oklch") return void 0;
3381
+ assertAllPastel(resolveCached(), resolveModes(options?.modes));
3382
+ return {
3383
+ seedHue: hue,
3384
+ baseName: options.name ?? "theme",
3385
+ prefix,
3386
+ defs: colorDefs,
3387
+ mode: "theme"
3388
+ };
3389
+ }
3390
+ return {
3391
+ get hue() {
3392
+ return hue;
3393
+ },
3394
+ get saturation() {
3395
+ return saturation;
3396
+ },
3397
+ getConfig() {
3398
+ return getEffectiveConfig();
3399
+ },
3400
+ colors(defs) {
3401
+ colorDefs = {
3402
+ ...colorDefs,
3403
+ ...defs
3404
+ };
3405
+ invalidate();
3406
+ },
3407
+ color(name, def) {
3408
+ if (def === void 0) return colorDefs[name];
3409
+ colorDefs[name] = def;
3410
+ invalidate();
3411
+ },
3412
+ remove(names) {
3413
+ const list = Array.isArray(names) ? names : [names];
3414
+ for (const name of list) delete colorDefs[name];
3415
+ invalidate();
3416
+ },
3417
+ has(name) {
3418
+ return name in colorDefs;
3419
+ },
3420
+ list() {
3421
+ return Object.keys(colorDefs);
3422
+ },
3423
+ reset() {
3424
+ colorDefs = {};
3425
+ invalidate();
3426
+ },
3427
+ export(override) {
3428
+ return {
3429
+ kind: "theme",
3430
+ version: GLAZE_EXPORT_VERSION,
3431
+ hue,
3432
+ saturation,
3433
+ colors: structuredClone(colorDefs),
3434
+ config: freezeConfigForExport(configOverride, override)
3435
+ };
3436
+ },
3437
+ extend(options) {
3438
+ const newHue = options.hue ?? hue;
3439
+ const newSat = options.saturation ?? saturation;
3440
+ const inheritedColors = {};
3441
+ for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
3442
+ return createTheme(newHue, newSat, options.colors ? {
3443
+ ...inheritedColors,
3444
+ ...options.colors
3445
+ } : { ...inheritedColors }, configOverride || options.config ? {
3446
+ ...configOverride ?? {},
3447
+ ...options.config ?? {}
3448
+ } : void 0);
3449
+ },
3450
+ resolve() {
3451
+ return new Map(resolveCached());
3452
+ },
3453
+ tokens(options) {
3454
+ const format = options?.format ?? "oklch";
3455
+ assertNativeFormat(format, "tokens");
3456
+ const modes = resolveModes(options?.modes);
3457
+ return buildFlatTokenMap(resolveCached(), "", modes, format, getEffectiveConfig().pastel);
3458
+ },
3459
+ tasty(options) {
3460
+ const cfg = getEffectiveConfig();
3461
+ const states = {
3462
+ dark: options?.states?.dark ?? cfg.states.dark,
3463
+ highContrast: options?.states?.highContrast ?? cfg.states.highContrast
3464
+ };
3465
+ const modes = resolveModes(options?.modes);
3466
+ const format = options?.format ?? "oklch";
3467
+ const channelCtx = channelCtxFor(options, "oklch", "");
3468
+ return buildTokenMap(resolveCached(), "", states, modes, format, cfg.pastel, channelCtx);
3469
+ },
3470
+ json(options) {
3471
+ const format = options?.format ?? "oklch";
3472
+ assertNativeFormat(format, "json");
3473
+ const modes = resolveModes(options?.modes);
3474
+ return buildJsonMap(resolveCached(), modes, format, getEffectiveConfig().pastel);
3475
+ },
3476
+ css(options) {
3477
+ const format = options?.format ?? "oklch";
3478
+ assertNativeFormat(format, "css");
3479
+ const channelCtx = channelCtxFor(options, "oklch", "");
3480
+ return buildCssMap(resolveCached(), "", options?.suffix ?? "-color", format, getEffectiveConfig().pastel, channelCtx);
3481
+ },
3482
+ dtcg(options) {
3483
+ const modes = resolveModes(options?.modes);
3484
+ return buildDtcgMap(resolveCached(), "", modes, options?.colorSpace ?? "srgb", getEffectiveConfig().pastel);
3485
+ },
3486
+ dtcgResolver(options) {
3487
+ return buildDtcgResolver(buildDtcgMap(resolveCached(), "", resolveModes(options?.modes), options?.colorSpace ?? "srgb", getEffectiveConfig().pastel), options);
3488
+ },
3489
+ tailwind(options) {
3490
+ const format = options?.format ?? "oklch";
3491
+ assertNativeFormat(format, "tailwind");
3492
+ const modes = resolveModes(options?.modes);
3493
+ return buildTailwindMap(resolveCached(), "", options?.namespace ?? "color-", modes, format, options?.darkSelector ?? ".dark", options?.highContrastSelector ?? ".high-contrast", getEffectiveConfig().pastel);
3494
+ }
3495
+ };
3496
+ }
3497
+
3261
3498
  //#endregion
3262
3499
  //#region src/palette.ts
3263
3500
  function resolvePrefix(options, themeName, defaultPrefix = false) {
@@ -3337,6 +3574,26 @@ function buildPaletteOutput(themes, paletteOptions, options, buildOne, merge, em
3337
3574
  }
3338
3575
  return acc;
3339
3576
  }
3577
+ /** Rebuild a theme from a `theme.export()` snapshot. */
3578
+ function createThemeFromExport(data, factory = "glaze.themeFrom") {
3579
+ if (data === null || typeof data !== "object") throw new Error(`${factory}: expected an object from theme.export(), got ${data === null ? "null" : typeof data}.`);
3580
+ assertExportKind(data, "theme", factory);
3581
+ assertExportVersion(data, factory);
3582
+ if (typeof data.hue !== "number" || typeof data.saturation !== "number") throw new Error(`${factory}: expected numeric "hue" and "saturation" fields.`);
3583
+ return createTheme(data.hue, data.saturation, data.colors, data.config);
3584
+ }
3585
+ /**
3586
+ * Rebuild a palette from a `palette.export()` snapshot.
3587
+ */
3588
+ function createPaletteFromExport(data) {
3589
+ if (data === null || typeof data !== "object") throw new Error(`glaze.paletteFrom: expected an object from palette.export(), got ${data === null ? "null" : typeof data}.`);
3590
+ assertExportKind(data, "palette", "glaze.paletteFrom");
3591
+ assertExportVersion(data, "glaze.paletteFrom");
3592
+ if (data.themes === null || typeof data.themes !== "object") throw new Error(`glaze.paletteFrom: expected a "themes" object map of theme exports.`);
3593
+ const rebuilt = {};
3594
+ for (const [name, themeExport] of Object.entries(data.themes)) rebuilt[name] = createThemeFromExport(themeExport, `glaze.paletteFrom (theme "${name}")`);
3595
+ return createPalette(rebuilt, { primary: data.primary });
3596
+ }
3340
3597
  function createPalette(themes, paletteOptions) {
3341
3598
  validatePrimaryTheme(paletteOptions?.primary, themes);
3342
3599
  const buildDtcgResult = (options) => {
@@ -3350,6 +3607,29 @@ function createPalette(themes, paletteOptions) {
3350
3607
  }, () => ({ light: {} }));
3351
3608
  };
3352
3609
  return {
3610
+ list() {
3611
+ return Object.keys(themes);
3612
+ },
3613
+ get primary() {
3614
+ return paletteOptions?.primary;
3615
+ },
3616
+ theme(name) {
3617
+ return themes[name];
3618
+ },
3619
+ themes() {
3620
+ return { ...themes };
3621
+ },
3622
+ export(override) {
3623
+ const themesExport = {};
3624
+ for (const [name, theme] of Object.entries(themes)) themesExport[name] = theme.export(override);
3625
+ const out = {
3626
+ kind: "palette",
3627
+ version: GLAZE_EXPORT_VERSION,
3628
+ themes: themesExport
3629
+ };
3630
+ if (paletteOptions?.primary !== void 0) out.primary = paletteOptions.primary;
3631
+ return out;
3632
+ },
3353
3633
  tokens(options) {
3354
3634
  const format = options?.format ?? "oklch";
3355
3635
  assertNativeFormat(format, "tokens");
@@ -3438,150 +3718,6 @@ function createPalette(themes, paletteOptions) {
3438
3718
  };
3439
3719
  }
3440
3720
 
3441
- //#endregion
3442
- //#region src/theme.ts
3443
- function createTheme(hue, saturation, initialColors, configOverride) {
3444
- let colorDefs = initialColors ? { ...initialColors } : {};
3445
- let cache = null;
3446
- function getEffectiveConfig() {
3447
- const version = getConfigVersion();
3448
- if (cache && cache.version === version) return cache.effectiveConfig;
3449
- return mergeConfig(getConfig(), configOverride);
3450
- }
3451
- function resolveCached() {
3452
- const version = getConfigVersion();
3453
- if (cache && cache.version === version) return cache.map;
3454
- const effectiveConfig = mergeConfig(getConfig(), configOverride);
3455
- const map = resolveAllColors(hue, saturation, colorDefs, effectiveConfig);
3456
- cache = {
3457
- map,
3458
- version,
3459
- effectiveConfig
3460
- };
3461
- return map;
3462
- }
3463
- function invalidate() {
3464
- cache = null;
3465
- }
3466
- function channelCtxFor(options, formatDefault, prefix) {
3467
- const format = options?.format ?? formatDefault;
3468
- if (!options?.splitHue || format !== "oklch") return void 0;
3469
- assertAllPastel(resolveCached(), resolveModes(options?.modes));
3470
- return {
3471
- seedHue: hue,
3472
- baseName: options.name ?? "theme",
3473
- prefix,
3474
- defs: colorDefs,
3475
- mode: "theme"
3476
- };
3477
- }
3478
- return {
3479
- get hue() {
3480
- return hue;
3481
- },
3482
- get saturation() {
3483
- return saturation;
3484
- },
3485
- getConfig() {
3486
- return getEffectiveConfig();
3487
- },
3488
- colors(defs) {
3489
- colorDefs = {
3490
- ...colorDefs,
3491
- ...defs
3492
- };
3493
- invalidate();
3494
- },
3495
- color(name, def) {
3496
- if (def === void 0) return colorDefs[name];
3497
- colorDefs[name] = def;
3498
- invalidate();
3499
- },
3500
- remove(names) {
3501
- const list = Array.isArray(names) ? names : [names];
3502
- for (const name of list) delete colorDefs[name];
3503
- invalidate();
3504
- },
3505
- has(name) {
3506
- return name in colorDefs;
3507
- },
3508
- list() {
3509
- return Object.keys(colorDefs);
3510
- },
3511
- reset() {
3512
- colorDefs = {};
3513
- invalidate();
3514
- },
3515
- export() {
3516
- const out = {
3517
- hue,
3518
- saturation,
3519
- colors: { ...colorDefs }
3520
- };
3521
- if (configOverride !== void 0) out.config = configOverride;
3522
- return out;
3523
- },
3524
- extend(options) {
3525
- const newHue = options.hue ?? hue;
3526
- const newSat = options.saturation ?? saturation;
3527
- const inheritedColors = {};
3528
- for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
3529
- return createTheme(newHue, newSat, options.colors ? {
3530
- ...inheritedColors,
3531
- ...options.colors
3532
- } : { ...inheritedColors }, configOverride || options.config ? {
3533
- ...configOverride ?? {},
3534
- ...options.config ?? {}
3535
- } : void 0);
3536
- },
3537
- resolve() {
3538
- return new Map(resolveCached());
3539
- },
3540
- tokens(options) {
3541
- const format = options?.format ?? "oklch";
3542
- assertNativeFormat(format, "tokens");
3543
- const modes = resolveModes(options?.modes);
3544
- return buildFlatTokenMap(resolveCached(), "", modes, format, getEffectiveConfig().pastel);
3545
- },
3546
- tasty(options) {
3547
- const cfg = getEffectiveConfig();
3548
- const states = {
3549
- dark: options?.states?.dark ?? cfg.states.dark,
3550
- highContrast: options?.states?.highContrast ?? cfg.states.highContrast
3551
- };
3552
- const modes = resolveModes(options?.modes);
3553
- const format = options?.format ?? "oklch";
3554
- const channelCtx = channelCtxFor(options, "oklch", "");
3555
- return buildTokenMap(resolveCached(), "", states, modes, format, cfg.pastel, channelCtx);
3556
- },
3557
- json(options) {
3558
- const format = options?.format ?? "oklch";
3559
- assertNativeFormat(format, "json");
3560
- const modes = resolveModes(options?.modes);
3561
- return buildJsonMap(resolveCached(), modes, format, getEffectiveConfig().pastel);
3562
- },
3563
- css(options) {
3564
- const format = options?.format ?? "oklch";
3565
- assertNativeFormat(format, "css");
3566
- const channelCtx = channelCtxFor(options, "oklch", "");
3567
- return buildCssMap(resolveCached(), "", options?.suffix ?? "-color", format, getEffectiveConfig().pastel, channelCtx);
3568
- },
3569
- dtcg(options) {
3570
- const modes = resolveModes(options?.modes);
3571
- return buildDtcgMap(resolveCached(), "", modes, options?.colorSpace ?? "srgb", getEffectiveConfig().pastel);
3572
- },
3573
- dtcgResolver(options) {
3574
- return buildDtcgResolver(buildDtcgMap(resolveCached(), "", resolveModes(options?.modes), options?.colorSpace ?? "srgb", getEffectiveConfig().pastel), options);
3575
- },
3576
- tailwind(options) {
3577
- const format = options?.format ?? "oklch";
3578
- assertNativeFormat(format, "tailwind");
3579
- const modes = resolveModes(options?.modes);
3580
- return buildTailwindMap(resolveCached(), "", options?.namespace ?? "color-", modes, format, options?.darkSelector ?? ".dark", options?.highContrastSelector ?? ".high-contrast", getEffectiveConfig().pastel);
3581
- }
3582
- };
3583
- }
3584
-
3585
3721
  //#endregion
3586
3722
  //#region src/glaze.ts
3587
3723
  /**
@@ -3595,6 +3731,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3595
3731
  * - `shadow.ts` — standalone shadow factory (`glaze.shadow`)
3596
3732
  * - `formatters.ts` — variant → string (`glaze.format`)
3597
3733
  * - `config.ts` — global config singleton
3734
+ * - `serialize.ts` — authoring export type guards / version checks
3598
3735
  */
3599
3736
  /**
3600
3737
  * Create a single-hue glaze theme.
@@ -3626,10 +3763,15 @@ glaze.configure = function configure$1(config) {
3626
3763
  glaze.palette = function palette(themes, options) {
3627
3764
  return createPalette(themes, options);
3628
3765
  };
3629
- /** Create a theme from a serialized export. */
3630
- glaze.from = function from(data) {
3631
- return createTheme(data.hue, data.saturation, data.colors, data.config);
3766
+ /**
3767
+ * Create a theme from a serialized `theme.export()` snapshot.
3768
+ * Prefer this over the legacy `glaze.from` alias.
3769
+ */
3770
+ glaze.themeFrom = function themeFrom(data) {
3771
+ return createThemeFromExport(data);
3632
3772
  };
3773
+ /** Compat alias for `glaze.themeFrom`. */
3774
+ glaze.from = glaze.themeFrom;
3633
3775
  /**
3634
3776
  * Create a standalone single-color token.
3635
3777
  *
@@ -3645,8 +3787,8 @@ glaze.from = function from(data) {
3645
3787
  * **arg2 — config override** (optional, all shapes):
3646
3788
  * Overrides the resolve-relevant global config fields for this token.
3647
3789
  * Fields that are omitted fall through to the live global config at
3648
- * create time (and are snapshotted). Pass `false` for a tone window
3649
- * to disable clamping entirely.
3790
+ * resolve time. Pass `false` for a tone window to disable clamping
3791
+ * entirely. `pastel` is instance-only (not set via `configure()`).
3650
3792
  *
3651
3793
  * ```ts
3652
3794
  * // Bare string — no overrides
@@ -3665,8 +3807,8 @@ glaze.from = function from(data) {
3665
3807
  *
3666
3808
  * Defaults: every form defaults to `mode: 'auto'`. Value-shorthand forms
3667
3809
  * (bare strings and value objects) preserve light tone exactly
3668
- * (`lightTone: false` internally). Structured form snapshots both
3669
- * tone windows from `globalConfig` at create time.
3810
+ * (`lightTone: false` locally). Structured form falls through to the live
3811
+ * global tone windows for omitted fields.
3670
3812
  *
3671
3813
  * Relative `tone: '+N'` and `contrast` anchor to the literal seed by
3672
3814
  * default; when `base` is set they anchor to the base's resolved variant
@@ -3744,8 +3886,8 @@ glaze.fromRgb = function fromRgb(r, g, b) {
3744
3886
  *
3745
3887
  * The snapshot is a plain JSON-safe object containing the original
3746
3888
  * input value, overrides (with any `base` token recursively serialized),
3747
- * and the effective config snapshot. The reconstructed token is identical
3748
- * in behavior to the original at the time of export.
3889
+ * and the effective config freeze from export time. The reconstructed
3890
+ * token pins that freeze as its local override.
3749
3891
  *
3750
3892
  * @example
3751
3893
  * ```ts
@@ -3759,6 +3901,18 @@ glaze.fromRgb = function fromRgb(r, g, b) {
3759
3901
  glaze.colorFrom = function colorFrom(data) {
3760
3902
  return colorFromExport(data);
3761
3903
  };
3904
+ /**
3905
+ * Rehydrate a palette from a `palette.export()` snapshot.
3906
+ */
3907
+ glaze.paletteFrom = function paletteFrom(data) {
3908
+ return createPaletteFromExport(data);
3909
+ };
3910
+ /** Type guard for theme authoring snapshots. */
3911
+ glaze.isThemeExport = isThemeExport;
3912
+ /** Type guard for color-token authoring snapshots. */
3913
+ glaze.isColorTokenExport = isColorTokenExport;
3914
+ /** Type guard for palette authoring snapshots. */
3915
+ glaze.isPaletteExport = isPaletteExport;
3762
3916
  /** Get the current global configuration (for testing/debugging). */
3763
3917
  glaze.getConfig = function getConfig() {
3764
3918
  return snapshotConfig();
@@ -3772,6 +3926,7 @@ glaze.resetConfig = function resetConfig$1() {
3772
3926
  exports.APCA_HC_ENHANCEMENT = APCA_HC_ENHANCEMENT;
3773
3927
  exports.APCA_MAX_LC = APCA_MAX_LC;
3774
3928
  exports.APCA_PRESETS = APCA_PRESETS;
3929
+ exports.GLAZE_EXPORT_VERSION = GLAZE_EXPORT_VERSION;
3775
3930
  exports.REF_EPS = REF_EPS;
3776
3931
  exports.apcaContrast = apcaContrast;
3777
3932
  exports.assertAllPastel = assertAllPastel;
@@ -3790,6 +3945,9 @@ exports.gamutClampedLuminance = gamutClampedLuminance;
3790
3945
  exports.glaze = glaze;
3791
3946
  exports.hslToSrgb = hslToSrgb;
3792
3947
  exports.inferRoleFromName = inferRoleFromName;
3948
+ exports.isColorTokenExport = isColorTokenExport;
3949
+ exports.isPaletteExport = isPaletteExport;
3950
+ exports.isThemeExport = isThemeExport;
3793
3951
  exports.normalizeRole = normalizeRole;
3794
3952
  exports.okhslToLinearSrgb = okhslToLinearSrgb;
3795
3953
  exports.okhslToOkhst = okhslToOkhst;