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