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