@tenphi/glaze 1.0.0 → 1.1.1

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
@@ -1882,15 +1979,18 @@ function resolveContrastSpec(spec, isHighContrast, polarity) {
1882
1979
  /**
1883
1980
  * Apply the relative-tone delta against a base, honoring `flip`.
1884
1981
  *
1885
- * When `flip` is on and `base + delta` falls outside `[0, 100]`, mirror the
1886
- * delta to the other side of the base (so an offset that would clamp instead
1887
- * reflects back into range). When off, the caller clamps as usual.
1982
+ * When `flip` is on and `base + delta` falls outside `[0, 100]`, try mirroring
1983
+ * the delta to the other side of the base. If the mirrored target is also out
1984
+ * of range, keep the original delta so the caller clamps on the authored side.
1985
+ * When off, the caller clamps as usual.
1888
1986
  */
1889
1987
  function applyToneFlip(delta, baseTone, flip) {
1890
1988
  if (!flip) return delta;
1891
1989
  const target = baseTone + delta;
1892
1990
  if (target >= 0 && target <= 100) return delta;
1893
- return -delta;
1991
+ const mirrored = baseTone - delta;
1992
+ if (mirrored >= 0 && mirrored <= 100) return -delta;
1993
+ return delta;
1894
1994
  }
1895
1995
  function resolveRootColor(def, isHighContrast) {
1896
1996
  const rawT = def.tone;
@@ -2652,12 +2752,10 @@ function buildTailwindMap(resolved, themePrefix, cssPrefix, modes, format, darkS
2652
2752
  * `{ l, c, h }`), the structured-input validator, the two factory paths
2653
2753
  * (value vs structured), and the JSON-safe export / rehydration round-trip.
2654
2754
  *
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.
2755
+ * Tokens store a sparse local config override only. Resolve merges the
2756
+ * live global config with that local override (invalidated on
2757
+ * `configure()`). Authoring `.export(override?)` freezes
2758
+ * `getConfig() ∪ local ∪ override` at call time.
2661
2759
  */
2662
2760
  /** Internal name of the user-facing standalone color in the synthesized def map. */
2663
2761
  const STANDALONE_VALUE = "value";
@@ -2672,47 +2770,16 @@ const RESERVED_STANDALONE_NAMES = new Set([
2672
2770
  STANDALONE_BASE
2673
2771
  ]);
2674
2772
  /**
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.
2773
+ * Value-form local override: `lightTone` defaults to `false` (preserve
2774
+ * input tone). User override fields win.
2680
2775
  */
2681
- function buildValueFormConfigOverride(userOverride) {
2682
- const cfg = getConfig();
2776
+ function sparseValueFormLocal(userOverride) {
2683
2777
  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
2778
+ ...userOverride,
2779
+ lightTone: userOverride?.lightTone !== void 0 ? userOverride.lightTone : false
2689
2780
  };
2690
2781
  }
2691
2782
  /**
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
2783
  * Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
2717
2784
  * `okhsl()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
2718
2785
  *
@@ -3002,12 +3069,30 @@ function buildStandaloneValueDefs(main, options) {
3002
3069
  primary
3003
3070
  };
3004
3071
  }
3005
- function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveConfig, baseToken, exportData) {
3006
- let cached;
3072
+ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, configOverride, baseToken, buildExport) {
3073
+ let cache = null;
3074
+ function getEffectiveConfig() {
3075
+ const version = getConfigVersion();
3076
+ if (cache && cache.version === version) return cache.effectiveConfig;
3077
+ const effectiveConfig = mergeConfig(getConfig(), configOverride);
3078
+ cache = {
3079
+ map: null,
3080
+ version,
3081
+ effectiveConfig
3082
+ };
3083
+ return effectiveConfig;
3084
+ }
3007
3085
  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;
3086
+ const version = getConfigVersion();
3087
+ if (cache && cache.version === version && cache.map) return cache.map;
3088
+ const effectiveConfig = getEffectiveConfig();
3089
+ const map = resolveAllColors(seedHue, seedSaturation, defs, effectiveConfig, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
3090
+ cache = {
3091
+ map,
3092
+ version,
3093
+ effectiveConfig
3094
+ };
3095
+ return map;
3011
3096
  };
3012
3097
  const resolveStates = (options) => {
3013
3098
  const cfg = getConfig();
@@ -3017,7 +3102,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
3017
3102
  };
3018
3103
  };
3019
3104
  const tokenLike = (options) => {
3020
- return buildTokenMap(resolveOnce(), "", resolveStates(options), resolveModes(options?.modes), options?.format ?? "oklch", effectiveConfig.pastel)[`#${primary}`];
3105
+ return buildTokenMap(resolveOnce(), "", resolveStates(options), resolveModes(options?.modes), options?.format ?? "oklch", getEffectiveConfig().pastel)[`#${primary}`];
3021
3106
  };
3022
3107
  return {
3023
3108
  resolve() {
@@ -3028,7 +3113,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
3028
3113
  json(options) {
3029
3114
  const format = options?.format ?? "oklch";
3030
3115
  assertNativeFormat(format, "json");
3031
- return buildJsonMap(resolveOnce(), resolveModes(options?.modes), format, effectiveConfig.pastel)[primary];
3116
+ return buildJsonMap(resolveOnce(), resolveModes(options?.modes), format, getEffectiveConfig().pastel)[primary];
3032
3117
  },
3033
3118
  css(options) {
3034
3119
  const format = options.format ?? "oklch";
@@ -3047,11 +3132,11 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
3047
3132
  resolvedHue: resolved.light.h
3048
3133
  };
3049
3134
  }
3050
- return buildCssMap(renamed, "", options.suffix ?? "-color", format, effectiveConfig.pastel, channelCtx);
3135
+ return buildCssMap(renamed, "", options.suffix ?? "-color", format, getEffectiveConfig().pastel, channelCtx);
3051
3136
  },
3052
3137
  dtcg(options) {
3053
3138
  const modes = resolveModes(options?.modes);
3054
- const doc = buildDtcgMap(resolveOnce(), "", modes, options?.colorSpace ?? "srgb", effectiveConfig.pastel);
3139
+ const doc = buildDtcgMap(resolveOnce(), "", modes, options?.colorSpace ?? "srgb", getEffectiveConfig().pastel);
3055
3140
  const result = { light: doc.light[primary] };
3056
3141
  if (doc.dark) result.dark = doc.dark[primary];
3057
3142
  if (doc.lightContrast) result.lightContrast = doc.lightContrast[primary];
@@ -3059,7 +3144,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
3059
3144
  return result;
3060
3145
  },
3061
3146
  dtcgResolver(options) {
3062
- const doc = buildDtcgMap(resolveOnce(), "", resolveModes(options?.modes), options?.colorSpace ?? "srgb", effectiveConfig.pastel);
3147
+ const doc = buildDtcgMap(resolveOnce(), "", resolveModes(options?.modes), options?.colorSpace ?? "srgb", getEffectiveConfig().pastel);
3063
3148
  const name = options.name;
3064
3149
  const result = { light: { [name]: doc.light[primary] } };
3065
3150
  if (doc.dark) result.dark = { [name]: doc.dark[primary] };
@@ -3070,9 +3155,11 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
3070
3155
  tailwind(options) {
3071
3156
  const format = options.format ?? "oklch";
3072
3157
  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);
3158
+ 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
3159
  },
3075
- export: exportData
3160
+ export(override) {
3161
+ return buildExport(override);
3162
+ }
3076
3163
  };
3077
3164
  }
3078
3165
  /**
@@ -3139,35 +3226,35 @@ function createColorToken(input, configOverride) {
3139
3226
  mode: "static"
3140
3227
  };
3141
3228
  }
3142
- const effectiveConfigOverride = buildStructuredConfigOverride(configOverride);
3143
- const effectiveConfig = resolvedConfigFromOverride(effectiveConfigOverride);
3144
- const exportData = () => ({
3229
+ const localOverride = configOverride;
3230
+ return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, localOverride, baseToken, (exportArg) => ({
3231
+ kind: "color",
3232
+ version: GLAZE_EXPORT_VERSION,
3145
3233
  form: "structured",
3146
- input: buildStructuredInputExport(input),
3147
- config: effectiveConfigOverride
3148
- });
3149
- return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, effectiveConfig, baseToken, exportData);
3234
+ input: buildStructuredInputExport(input, exportArg),
3235
+ config: freezeConfigForExport(localOverride, exportArg)
3236
+ }));
3150
3237
  }
3151
3238
  function createColorTokenFromValue(value, options, configOverride) {
3152
3239
  const main = extractOkhslFromValue(value);
3153
3240
  const linkingBase = toLinkingBase(resolveBaseToken(options?.base));
3154
3241
  const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options);
3155
- const effectiveConfigOverride = buildValueFormConfigOverride(configOverride);
3156
- const effectiveConfig = resolvedConfigFromOverride(effectiveConfigOverride);
3157
- const exportData = () => ({
3242
+ const localOverride = sparseValueFormLocal(configOverride);
3243
+ return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, localOverride, linkingBase, (exportArg) => ({
3244
+ kind: "color",
3245
+ version: GLAZE_EXPORT_VERSION,
3158
3246
  form: "value",
3159
3247
  input: value,
3160
- ...options !== void 0 ? { overrides: buildOverridesExport(options) } : {},
3161
- config: effectiveConfigOverride
3162
- });
3163
- return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveConfig, linkingBase, exportData);
3248
+ ...options !== void 0 ? { overrides: buildOverridesExport(options, exportArg) } : {},
3249
+ config: freezeConfigForExport(localOverride, exportArg)
3250
+ }));
3164
3251
  }
3165
3252
  /**
3166
3253
  * Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is
3167
3254
  * recursively serialized when it was originally a token; raw values are
3168
3255
  * preserved as-is so `glaze.colorFrom(...)` round-trips them.
3169
3256
  */
3170
- function buildOverridesExport(options) {
3257
+ function buildOverridesExport(options, exportArg) {
3171
3258
  const out = {};
3172
3259
  if (options.hue !== void 0) out.hue = options.hue;
3173
3260
  if (options.saturation !== void 0) out.saturation = options.saturation;
@@ -3180,10 +3267,10 @@ function buildOverridesExport(options) {
3180
3267
  if (options.name !== void 0) out.name = options.name;
3181
3268
  if (options.pastel !== void 0) out.pastel = options.pastel;
3182
3269
  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;
3270
+ if (options.base !== void 0) out.base = isGlazeColorToken(options.base) ? options.base.export(exportArg) : options.base;
3184
3271
  return out;
3185
3272
  }
3186
- function buildStructuredInputExport(input) {
3273
+ function buildStructuredInputExport(input, exportArg) {
3187
3274
  const out = {
3188
3275
  hue: input.hue,
3189
3276
  saturation: input.saturation,
@@ -3197,14 +3284,14 @@ function buildStructuredInputExport(input) {
3197
3284
  if (input.name !== void 0) out.name = input.name;
3198
3285
  if (input.pastel !== void 0) out.pastel = input.pastel;
3199
3286
  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;
3287
+ if (input.base !== void 0) out.base = isGlazeColorToken(input.base) ? input.base.export(exportArg) : input.base;
3201
3288
  return out;
3202
3289
  }
3203
3290
  /**
3204
3291
  * Discriminate a `GlazeColorTokenExport` from a raw `GlazeColorValue`.
3205
3292
  */
3206
3293
  function isExportedToken(candidate) {
3207
- return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "form" in candidate && (candidate.form === "value" || candidate.form === "structured");
3294
+ return isColorTokenExport(candidate);
3208
3295
  }
3209
3296
  function rehydrateOverrides(data) {
3210
3297
  const out = {};
@@ -3243,12 +3330,14 @@ function rehydrateStructuredInput(data) {
3243
3330
  * Rehydrate a token from its `.export()` snapshot. Recursively rebuilds
3244
3331
  * any base dependency. Inverse of `GlazeColorToken.export()`.
3245
3332
  *
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.
3333
+ * The stored `config` field is the freeze from export time — passed as
3334
+ * the instance local override so the rehydrated token stays pinned
3335
+ * against later `glaze.configure()` calls.
3249
3336
  */
3250
3337
  function colorFromExport(data) {
3251
3338
  if (data === null || typeof data !== "object") throw new Error(`glaze.colorFrom: expected an object from token.export(), got ${data === null ? "null" : typeof data}.`);
3339
+ assertExportKind(data, "color", "glaze.colorFrom");
3340
+ assertExportVersion(data, "glaze.colorFrom");
3252
3341
  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
3342
  if (data.input === void 0) throw new Error(`glaze.colorFrom: missing "input" field — expected the original ${data.form === "value" ? "GlazeColorValue" : "GlazeColorInput"}.`);
3254
3343
  if (data.form === "value") {
@@ -3258,6 +3347,157 @@ function colorFromExport(data) {
3258
3347
  return createColorToken(rehydrateStructuredInput(data.input), data.config);
3259
3348
  }
3260
3349
 
3350
+ //#endregion
3351
+ //#region src/theme.ts
3352
+ function createTheme(hue, saturation, initialColors, configOverride) {
3353
+ let colorDefs = initialColors ? { ...initialColors } : {};
3354
+ let cache = null;
3355
+ function getEffectiveConfig() {
3356
+ const version = getConfigVersion();
3357
+ if (cache && cache.version === version) return cache.effectiveConfig;
3358
+ const effectiveConfig = mergeConfig(getConfig(), configOverride);
3359
+ cache = {
3360
+ map: null,
3361
+ version,
3362
+ effectiveConfig
3363
+ };
3364
+ return effectiveConfig;
3365
+ }
3366
+ function resolveCached() {
3367
+ const version = getConfigVersion();
3368
+ if (cache && cache.version === version && cache.map) return cache.map;
3369
+ const effectiveConfig = getEffectiveConfig();
3370
+ const map = resolveAllColors(hue, saturation, colorDefs, effectiveConfig);
3371
+ cache = {
3372
+ map,
3373
+ version,
3374
+ effectiveConfig
3375
+ };
3376
+ return map;
3377
+ }
3378
+ function invalidate() {
3379
+ cache = null;
3380
+ }
3381
+ function channelCtxFor(options, formatDefault, prefix) {
3382
+ const format = options?.format ?? formatDefault;
3383
+ if (!options?.splitHue || format !== "oklch") return void 0;
3384
+ assertAllPastel(resolveCached(), resolveModes(options?.modes));
3385
+ return {
3386
+ seedHue: hue,
3387
+ baseName: options.name ?? "theme",
3388
+ prefix,
3389
+ defs: colorDefs,
3390
+ mode: "theme"
3391
+ };
3392
+ }
3393
+ return {
3394
+ get hue() {
3395
+ return hue;
3396
+ },
3397
+ get saturation() {
3398
+ return saturation;
3399
+ },
3400
+ getConfig() {
3401
+ return getEffectiveConfig();
3402
+ },
3403
+ colors(defs) {
3404
+ colorDefs = {
3405
+ ...colorDefs,
3406
+ ...defs
3407
+ };
3408
+ invalidate();
3409
+ },
3410
+ color(name, def) {
3411
+ if (def === void 0) return colorDefs[name];
3412
+ colorDefs[name] = def;
3413
+ invalidate();
3414
+ },
3415
+ remove(names) {
3416
+ const list = Array.isArray(names) ? names : [names];
3417
+ for (const name of list) delete colorDefs[name];
3418
+ invalidate();
3419
+ },
3420
+ has(name) {
3421
+ return name in colorDefs;
3422
+ },
3423
+ list() {
3424
+ return Object.keys(colorDefs);
3425
+ },
3426
+ reset() {
3427
+ colorDefs = {};
3428
+ invalidate();
3429
+ },
3430
+ export(override) {
3431
+ return {
3432
+ kind: "theme",
3433
+ version: GLAZE_EXPORT_VERSION,
3434
+ hue,
3435
+ saturation,
3436
+ colors: structuredClone(colorDefs),
3437
+ config: freezeConfigForExport(configOverride, override)
3438
+ };
3439
+ },
3440
+ extend(options) {
3441
+ const newHue = options.hue ?? hue;
3442
+ const newSat = options.saturation ?? saturation;
3443
+ const inheritedColors = {};
3444
+ for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
3445
+ return createTheme(newHue, newSat, options.colors ? {
3446
+ ...inheritedColors,
3447
+ ...options.colors
3448
+ } : { ...inheritedColors }, configOverride || options.config ? {
3449
+ ...configOverride ?? {},
3450
+ ...options.config ?? {}
3451
+ } : void 0);
3452
+ },
3453
+ resolve() {
3454
+ return new Map(resolveCached());
3455
+ },
3456
+ tokens(options) {
3457
+ const format = options?.format ?? "oklch";
3458
+ assertNativeFormat(format, "tokens");
3459
+ const modes = resolveModes(options?.modes);
3460
+ return buildFlatTokenMap(resolveCached(), "", modes, format, getEffectiveConfig().pastel);
3461
+ },
3462
+ tasty(options) {
3463
+ const cfg = getEffectiveConfig();
3464
+ const states = {
3465
+ dark: options?.states?.dark ?? cfg.states.dark,
3466
+ highContrast: options?.states?.highContrast ?? cfg.states.highContrast
3467
+ };
3468
+ const modes = resolveModes(options?.modes);
3469
+ const format = options?.format ?? "oklch";
3470
+ const channelCtx = channelCtxFor(options, "oklch", "");
3471
+ return buildTokenMap(resolveCached(), "", states, modes, format, cfg.pastel, channelCtx);
3472
+ },
3473
+ json(options) {
3474
+ const format = options?.format ?? "oklch";
3475
+ assertNativeFormat(format, "json");
3476
+ const modes = resolveModes(options?.modes);
3477
+ return buildJsonMap(resolveCached(), modes, format, getEffectiveConfig().pastel);
3478
+ },
3479
+ css(options) {
3480
+ const format = options?.format ?? "oklch";
3481
+ assertNativeFormat(format, "css");
3482
+ const channelCtx = channelCtxFor(options, "oklch", "");
3483
+ return buildCssMap(resolveCached(), "", options?.suffix ?? "-color", format, getEffectiveConfig().pastel, channelCtx);
3484
+ },
3485
+ dtcg(options) {
3486
+ const modes = resolveModes(options?.modes);
3487
+ return buildDtcgMap(resolveCached(), "", modes, options?.colorSpace ?? "srgb", getEffectiveConfig().pastel);
3488
+ },
3489
+ dtcgResolver(options) {
3490
+ return buildDtcgResolver(buildDtcgMap(resolveCached(), "", resolveModes(options?.modes), options?.colorSpace ?? "srgb", getEffectiveConfig().pastel), options);
3491
+ },
3492
+ tailwind(options) {
3493
+ const format = options?.format ?? "oklch";
3494
+ assertNativeFormat(format, "tailwind");
3495
+ const modes = resolveModes(options?.modes);
3496
+ return buildTailwindMap(resolveCached(), "", options?.namespace ?? "color-", modes, format, options?.darkSelector ?? ".dark", options?.highContrastSelector ?? ".high-contrast", getEffectiveConfig().pastel);
3497
+ }
3498
+ };
3499
+ }
3500
+
3261
3501
  //#endregion
3262
3502
  //#region src/palette.ts
3263
3503
  function resolvePrefix(options, themeName, defaultPrefix = false) {
@@ -3337,6 +3577,26 @@ function buildPaletteOutput(themes, paletteOptions, options, buildOne, merge, em
3337
3577
  }
3338
3578
  return acc;
3339
3579
  }
3580
+ /** Rebuild a theme from a `theme.export()` snapshot. */
3581
+ function createThemeFromExport(data, factory = "glaze.themeFrom") {
3582
+ if (data === null || typeof data !== "object") throw new Error(`${factory}: expected an object from theme.export(), got ${data === null ? "null" : typeof data}.`);
3583
+ assertExportKind(data, "theme", factory);
3584
+ assertExportVersion(data, factory);
3585
+ if (typeof data.hue !== "number" || typeof data.saturation !== "number") throw new Error(`${factory}: expected numeric "hue" and "saturation" fields.`);
3586
+ return createTheme(data.hue, data.saturation, data.colors, data.config);
3587
+ }
3588
+ /**
3589
+ * Rebuild a palette from a `palette.export()` snapshot.
3590
+ */
3591
+ function createPaletteFromExport(data) {
3592
+ if (data === null || typeof data !== "object") throw new Error(`glaze.paletteFrom: expected an object from palette.export(), got ${data === null ? "null" : typeof data}.`);
3593
+ assertExportKind(data, "palette", "glaze.paletteFrom");
3594
+ assertExportVersion(data, "glaze.paletteFrom");
3595
+ if (data.themes === null || typeof data.themes !== "object") throw new Error(`glaze.paletteFrom: expected a "themes" object map of theme exports.`);
3596
+ const rebuilt = {};
3597
+ for (const [name, themeExport] of Object.entries(data.themes)) rebuilt[name] = createThemeFromExport(themeExport, `glaze.paletteFrom (theme "${name}")`);
3598
+ return createPalette(rebuilt, { primary: data.primary });
3599
+ }
3340
3600
  function createPalette(themes, paletteOptions) {
3341
3601
  validatePrimaryTheme(paletteOptions?.primary, themes);
3342
3602
  const buildDtcgResult = (options) => {
@@ -3350,6 +3610,29 @@ function createPalette(themes, paletteOptions) {
3350
3610
  }, () => ({ light: {} }));
3351
3611
  };
3352
3612
  return {
3613
+ list() {
3614
+ return Object.keys(themes);
3615
+ },
3616
+ get primary() {
3617
+ return paletteOptions?.primary;
3618
+ },
3619
+ theme(name) {
3620
+ return themes[name];
3621
+ },
3622
+ themes() {
3623
+ return { ...themes };
3624
+ },
3625
+ export(override) {
3626
+ const themesExport = {};
3627
+ for (const [name, theme] of Object.entries(themes)) themesExport[name] = theme.export(override);
3628
+ const out = {
3629
+ kind: "palette",
3630
+ version: GLAZE_EXPORT_VERSION,
3631
+ themes: themesExport
3632
+ };
3633
+ if (paletteOptions?.primary !== void 0) out.primary = paletteOptions.primary;
3634
+ return out;
3635
+ },
3353
3636
  tokens(options) {
3354
3637
  const format = options?.format ?? "oklch";
3355
3638
  assertNativeFormat(format, "tokens");
@@ -3438,150 +3721,6 @@ function createPalette(themes, paletteOptions) {
3438
3721
  };
3439
3722
  }
3440
3723
 
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
3724
  //#endregion
3586
3725
  //#region src/glaze.ts
3587
3726
  /**
@@ -3595,6 +3734,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3595
3734
  * - `shadow.ts` — standalone shadow factory (`glaze.shadow`)
3596
3735
  * - `formatters.ts` — variant → string (`glaze.format`)
3597
3736
  * - `config.ts` — global config singleton
3737
+ * - `serialize.ts` — authoring export type guards / version checks
3598
3738
  */
3599
3739
  /**
3600
3740
  * Create a single-hue glaze theme.
@@ -3626,10 +3766,15 @@ glaze.configure = function configure$1(config) {
3626
3766
  glaze.palette = function palette(themes, options) {
3627
3767
  return createPalette(themes, options);
3628
3768
  };
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);
3769
+ /**
3770
+ * Create a theme from a serialized `theme.export()` snapshot.
3771
+ * Prefer this over the legacy `glaze.from` alias.
3772
+ */
3773
+ glaze.themeFrom = function themeFrom(data) {
3774
+ return createThemeFromExport(data);
3632
3775
  };
3776
+ /** Compat alias for `glaze.themeFrom`. */
3777
+ glaze.from = glaze.themeFrom;
3633
3778
  /**
3634
3779
  * Create a standalone single-color token.
3635
3780
  *
@@ -3645,8 +3790,8 @@ glaze.from = function from(data) {
3645
3790
  * **arg2 — config override** (optional, all shapes):
3646
3791
  * Overrides the resolve-relevant global config fields for this token.
3647
3792
  * 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.
3793
+ * resolve time. Pass `false` for a tone window to disable clamping
3794
+ * entirely. `pastel` is instance-only (not set via `configure()`).
3650
3795
  *
3651
3796
  * ```ts
3652
3797
  * // Bare string — no overrides
@@ -3665,8 +3810,8 @@ glaze.from = function from(data) {
3665
3810
  *
3666
3811
  * Defaults: every form defaults to `mode: 'auto'`. Value-shorthand forms
3667
3812
  * (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.
3813
+ * (`lightTone: false` locally). Structured form falls through to the live
3814
+ * global tone windows for omitted fields.
3670
3815
  *
3671
3816
  * Relative `tone: '+N'` and `contrast` anchor to the literal seed by
3672
3817
  * default; when `base` is set they anchor to the base's resolved variant
@@ -3744,8 +3889,8 @@ glaze.fromRgb = function fromRgb(r, g, b) {
3744
3889
  *
3745
3890
  * The snapshot is a plain JSON-safe object containing the original
3746
3891
  * 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.
3892
+ * and the effective config freeze from export time. The reconstructed
3893
+ * token pins that freeze as its local override.
3749
3894
  *
3750
3895
  * @example
3751
3896
  * ```ts
@@ -3759,6 +3904,18 @@ glaze.fromRgb = function fromRgb(r, g, b) {
3759
3904
  glaze.colorFrom = function colorFrom(data) {
3760
3905
  return colorFromExport(data);
3761
3906
  };
3907
+ /**
3908
+ * Rehydrate a palette from a `palette.export()` snapshot.
3909
+ */
3910
+ glaze.paletteFrom = function paletteFrom(data) {
3911
+ return createPaletteFromExport(data);
3912
+ };
3913
+ /** Type guard for theme authoring snapshots. */
3914
+ glaze.isThemeExport = isThemeExport;
3915
+ /** Type guard for color-token authoring snapshots. */
3916
+ glaze.isColorTokenExport = isColorTokenExport;
3917
+ /** Type guard for palette authoring snapshots. */
3918
+ glaze.isPaletteExport = isPaletteExport;
3762
3919
  /** Get the current global configuration (for testing/debugging). */
3763
3920
  glaze.getConfig = function getConfig() {
3764
3921
  return snapshotConfig();
@@ -3772,6 +3929,7 @@ glaze.resetConfig = function resetConfig$1() {
3772
3929
  exports.APCA_HC_ENHANCEMENT = APCA_HC_ENHANCEMENT;
3773
3930
  exports.APCA_MAX_LC = APCA_MAX_LC;
3774
3931
  exports.APCA_PRESETS = APCA_PRESETS;
3932
+ exports.GLAZE_EXPORT_VERSION = GLAZE_EXPORT_VERSION;
3775
3933
  exports.REF_EPS = REF_EPS;
3776
3934
  exports.apcaContrast = apcaContrast;
3777
3935
  exports.assertAllPastel = assertAllPastel;
@@ -3790,6 +3948,9 @@ exports.gamutClampedLuminance = gamutClampedLuminance;
3790
3948
  exports.glaze = glaze;
3791
3949
  exports.hslToSrgb = hslToSrgb;
3792
3950
  exports.inferRoleFromName = inferRoleFromName;
3951
+ exports.isColorTokenExport = isColorTokenExport;
3952
+ exports.isPaletteExport = isPaletteExport;
3953
+ exports.isThemeExport = isThemeExport;
3793
3954
  exports.normalizeRole = normalizeRole;
3794
3955
  exports.okhslToLinearSrgb = okhslToLinearSrgb;
3795
3956
  exports.okhslToOkhst = okhslToOkhst;