@tenphi/glaze 0.0.0-snapshot.7e2a1da → 0.0.0-snapshot.7f7cab2

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.d.cts CHANGED
@@ -133,6 +133,11 @@ interface RegularColorDef {
133
133
  * should not be combined (a console.warn is emitted).
134
134
  */
135
135
  opacity?: number;
136
+ /**
137
+ * Whether this color is inherited by child themes created via `extend()`.
138
+ * Default: true. Set to false to make this color local to the current theme.
139
+ */
140
+ inherit?: boolean;
136
141
  }
137
142
  /** Shadow tuning knobs. All values use the 0–1 scale (OKHSL). */
138
143
  interface ShadowTuning {
@@ -177,6 +182,11 @@ interface ShadowColorDef {
177
182
  intensity: HCPair<number>;
178
183
  /** Override default tuning. Merged field-by-field with global `shadowTuning`. */
179
184
  tuning?: ShadowTuning;
185
+ /**
186
+ * Whether this color is inherited by child themes created via `extend()`.
187
+ * Default: true. Set to false to make this color local to the current theme.
188
+ */
189
+ inherit?: boolean;
180
190
  }
181
191
  interface MixColorDef {
182
192
  type: 'mix';
@@ -211,6 +221,11 @@ interface MixColorDef {
211
221
  * Supports [normal, highContrast] pair.
212
222
  */
213
223
  contrast?: HCPair<MinContrast>;
224
+ /**
225
+ * Whether this color is inherited by child themes created via `extend()`.
226
+ * Default: true. Set to false to make this color local to the current theme.
227
+ */
228
+ inherit?: boolean;
214
229
  }
215
230
  type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
216
231
  type ColorMap = Record<string, ColorDef>;
@@ -243,11 +258,12 @@ interface GlazeConfig {
243
258
  /** Saturation reduction factor for dark scheme (0–1). Default: 0.1. */
244
259
  darkDesaturation?: number;
245
260
  /**
246
- * Power-curve exponent for dark auto-inversion (0–1).
261
+ * Möbius beta for dark auto-inversion (0–1).
247
262
  * Lower values expand subtle near-white distinctions in dark mode.
248
263
  * Set to 1 for linear (legacy) behavior. Default: 0.5.
264
+ * Accepts [normal, highContrast] pair for separate HC tuning.
249
265
  */
250
- darkCurve?: number;
266
+ darkCurve?: HCPair<number>;
251
267
  /** State alias names for token export. */
252
268
  states?: {
253
269
  dark?: string;
@@ -262,7 +278,7 @@ interface GlazeConfigResolved {
262
278
  lightLightness: [number, number];
263
279
  darkLightness: [number, number];
264
280
  darkDesaturation: number;
265
- darkCurve: number;
281
+ darkCurve: HCPair<number>;
266
282
  states: {
267
283
  dark: string;
268
284
  highContrast: string;
@@ -392,35 +408,47 @@ interface GlazeCssResult {
392
408
  lightContrast: string;
393
409
  darkContrast: string;
394
410
  }
395
- /** Options shared by palette `tokens()`, `tasty()`, and `css()` exports. */
396
- interface GlazePaletteExportOptions {
397
- /**
398
- * Prefix mode. `true` uses `"<themeName>-"`, or provide a custom map.
399
- * Defaults to `true` for palette export methods.
400
- * Set to `false` explicitly to disable prefixing (last-write-wins on collisions).
401
- */
402
- prefix?: boolean | Record<string, string>;
411
+ /** Options for `glaze.palette()` creation. */
412
+ interface GlazePaletteOptions {
403
413
  /**
404
414
  * Name of the primary theme. The primary theme's tokens are duplicated
405
- * without prefix, providing convenient short aliases alongside the
406
- * prefixed versions.
415
+ * without prefix in all exports, providing convenient short aliases
416
+ * alongside the prefixed versions. Can be overridden per-export.
407
417
  *
408
418
  * @example
409
419
  * ```ts
410
- * palette.tokens({ primary: 'brand' })
420
+ * const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
421
+ * palette.tokens()
411
422
  * // → { light: { 'brand-surface': '...', 'surface': '...', 'accent-surface': '...' } }
412
423
  * ```
413
424
  */
414
425
  primary?: string;
415
426
  }
427
+ /** Options shared by palette `tokens()`, `tasty()`, and `css()` exports. */
428
+ interface GlazePaletteExportOptions {
429
+ /**
430
+ * Prefix mode. `true` uses `"<themeName>-"`, or provide a custom map.
431
+ * Defaults to `true` for palette export methods.
432
+ * Set to `false` explicitly to disable prefixing. Colliding keys
433
+ * produce a console.warn and the first-written value wins.
434
+ */
435
+ prefix?: boolean | Record<string, string>;
436
+ /**
437
+ * Override the palette-level primary theme for this export.
438
+ * Pass a theme name to set/change the primary, or `false` to disable it.
439
+ * When omitted, inherits the palette-level `primary`.
440
+ */
441
+ primary?: string | false;
442
+ }
416
443
  interface GlazePalette {
417
444
  /**
418
445
  * Export all themes as a flat token map grouped by scheme variant.
419
446
  * Prefix defaults to `true` — all tokens are prefixed with the theme name.
420
- * Use `primary` to duplicate one theme's tokens without prefix.
447
+ * Inherits the palette-level `primary`; override per-call or pass `false` to disable.
421
448
  *
422
449
  * ```ts
423
- * palette.tokens({ primary: 'brand' })
450
+ * const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
451
+ * palette.tokens()
424
452
  * // → { light: { 'brand-surface': '...', 'surface': '...', 'accent-surface': '...' } }
425
453
  * ```
426
454
  */
@@ -428,12 +456,10 @@ interface GlazePalette {
428
456
  /**
429
457
  * Export all themes as tasty style-to-state bindings.
430
458
  * Uses `#name` color token keys and state aliases (`''`, `@dark`, etc.).
431
- * Prefix defaults to `true`. Use `primary` to duplicate one theme without prefix.
459
+ * Prefix defaults to `true`. Inherits the palette-level `primary`.
432
460
  * @see https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs
433
461
  */
434
- tasty(options?: GlazeTokenOptions & {
435
- primary?: string;
436
- }): Record<string, Record<string, string>>;
462
+ tasty(options?: GlazeTokenOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
437
463
  /** Export all themes as plain JSON grouped by theme name. */
438
464
  json(options?: GlazeJsonOptions & {
439
465
  prefix?: boolean | Record<string, string>;
@@ -460,11 +486,9 @@ declare function glaze(hueOrOptions: number | {
460
486
  }, saturation?: number): GlazeTheme;
461
487
  declare namespace glaze {
462
488
  var configure: (config: GlazeConfig) => void;
463
- var palette: (themes: PaletteInput) => {
489
+ var palette: (themes: PaletteInput, options?: GlazePaletteOptions) => {
464
490
  tokens(options?: GlazeJsonOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
465
- tasty(options?: GlazeTokenOptions & {
466
- primary?: string;
467
- }): Record<string, Record<string, string>>;
491
+ tasty(options?: GlazeTokenOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
468
492
  json(options?: GlazeJsonOptions & {
469
493
  prefix?: boolean | Record<string, string>;
470
494
  }): Record<string, Record<string, Record<string, string>>>;
@@ -548,5 +572,5 @@ declare function formatHsl(h: number, s: number, l: number): string;
548
572
  */
549
573
  declare function formatOklch(h: number, s: number, l: number): string;
550
574
  //#endregion
551
- export { type AdaptationMode, type ColorDef, type ColorMap, type ContrastPreset, type FindLightnessForContrastOptions, type FindLightnessForContrastResult, type FindValueForMixContrastOptions, type FindValueForMixContrastResult, type GlazeColorFormat, type GlazeColorInput, type GlazeColorToken, type GlazeConfig, type GlazeCssOptions, type GlazeCssResult, type GlazeExtendOptions, type GlazeJsonOptions, type GlazeOutputModes, type GlazePalette, type GlazePaletteExportOptions, type GlazeShadowInput, type GlazeTheme, type GlazeThemeExport, type GlazeTokenOptions, type HCPair, type HexColor, type MinContrast, type MixColorDef, type OkhslColor, type RegularColorDef, type RelativeValue, type ResolvedColor, type ResolvedColorVariant, type ShadowColorDef, type ShadowTuning, contrastRatioFromLuminance, findLightnessForContrast, findValueForMixContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, gamutClampedLuminance, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
575
+ export { type AdaptationMode, type ColorDef, type ColorMap, type ContrastPreset, type FindLightnessForContrastOptions, type FindLightnessForContrastResult, type FindValueForMixContrastOptions, type FindValueForMixContrastResult, type GlazeColorFormat, type GlazeColorInput, type GlazeColorToken, type GlazeConfig, type GlazeCssOptions, type GlazeCssResult, type GlazeExtendOptions, type GlazeJsonOptions, type GlazeOutputModes, type GlazePalette, type GlazePaletteExportOptions, type GlazePaletteOptions, type GlazeShadowInput, type GlazeTheme, type GlazeThemeExport, type GlazeTokenOptions, type HCPair, type HexColor, type MinContrast, type MixColorDef, type OkhslColor, type RegularColorDef, type RelativeValue, type ResolvedColor, type ResolvedColorVariant, type ShadowColorDef, type ShadowTuning, contrastRatioFromLuminance, findLightnessForContrast, findValueForMixContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, gamutClampedLuminance, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
552
576
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.mts CHANGED
@@ -133,6 +133,11 @@ interface RegularColorDef {
133
133
  * should not be combined (a console.warn is emitted).
134
134
  */
135
135
  opacity?: number;
136
+ /**
137
+ * Whether this color is inherited by child themes created via `extend()`.
138
+ * Default: true. Set to false to make this color local to the current theme.
139
+ */
140
+ inherit?: boolean;
136
141
  }
137
142
  /** Shadow tuning knobs. All values use the 0–1 scale (OKHSL). */
138
143
  interface ShadowTuning {
@@ -177,6 +182,11 @@ interface ShadowColorDef {
177
182
  intensity: HCPair<number>;
178
183
  /** Override default tuning. Merged field-by-field with global `shadowTuning`. */
179
184
  tuning?: ShadowTuning;
185
+ /**
186
+ * Whether this color is inherited by child themes created via `extend()`.
187
+ * Default: true. Set to false to make this color local to the current theme.
188
+ */
189
+ inherit?: boolean;
180
190
  }
181
191
  interface MixColorDef {
182
192
  type: 'mix';
@@ -211,6 +221,11 @@ interface MixColorDef {
211
221
  * Supports [normal, highContrast] pair.
212
222
  */
213
223
  contrast?: HCPair<MinContrast>;
224
+ /**
225
+ * Whether this color is inherited by child themes created via `extend()`.
226
+ * Default: true. Set to false to make this color local to the current theme.
227
+ */
228
+ inherit?: boolean;
214
229
  }
215
230
  type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
216
231
  type ColorMap = Record<string, ColorDef>;
@@ -243,11 +258,12 @@ interface GlazeConfig {
243
258
  /** Saturation reduction factor for dark scheme (0–1). Default: 0.1. */
244
259
  darkDesaturation?: number;
245
260
  /**
246
- * Power-curve exponent for dark auto-inversion (0–1).
261
+ * Möbius beta for dark auto-inversion (0–1).
247
262
  * Lower values expand subtle near-white distinctions in dark mode.
248
263
  * Set to 1 for linear (legacy) behavior. Default: 0.5.
264
+ * Accepts [normal, highContrast] pair for separate HC tuning.
249
265
  */
250
- darkCurve?: number;
266
+ darkCurve?: HCPair<number>;
251
267
  /** State alias names for token export. */
252
268
  states?: {
253
269
  dark?: string;
@@ -262,7 +278,7 @@ interface GlazeConfigResolved {
262
278
  lightLightness: [number, number];
263
279
  darkLightness: [number, number];
264
280
  darkDesaturation: number;
265
- darkCurve: number;
281
+ darkCurve: HCPair<number>;
266
282
  states: {
267
283
  dark: string;
268
284
  highContrast: string;
@@ -392,35 +408,47 @@ interface GlazeCssResult {
392
408
  lightContrast: string;
393
409
  darkContrast: string;
394
410
  }
395
- /** Options shared by palette `tokens()`, `tasty()`, and `css()` exports. */
396
- interface GlazePaletteExportOptions {
397
- /**
398
- * Prefix mode. `true` uses `"<themeName>-"`, or provide a custom map.
399
- * Defaults to `true` for palette export methods.
400
- * Set to `false` explicitly to disable prefixing (last-write-wins on collisions).
401
- */
402
- prefix?: boolean | Record<string, string>;
411
+ /** Options for `glaze.palette()` creation. */
412
+ interface GlazePaletteOptions {
403
413
  /**
404
414
  * Name of the primary theme. The primary theme's tokens are duplicated
405
- * without prefix, providing convenient short aliases alongside the
406
- * prefixed versions.
415
+ * without prefix in all exports, providing convenient short aliases
416
+ * alongside the prefixed versions. Can be overridden per-export.
407
417
  *
408
418
  * @example
409
419
  * ```ts
410
- * palette.tokens({ primary: 'brand' })
420
+ * const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
421
+ * palette.tokens()
411
422
  * // → { light: { 'brand-surface': '...', 'surface': '...', 'accent-surface': '...' } }
412
423
  * ```
413
424
  */
414
425
  primary?: string;
415
426
  }
427
+ /** Options shared by palette `tokens()`, `tasty()`, and `css()` exports. */
428
+ interface GlazePaletteExportOptions {
429
+ /**
430
+ * Prefix mode. `true` uses `"<themeName>-"`, or provide a custom map.
431
+ * Defaults to `true` for palette export methods.
432
+ * Set to `false` explicitly to disable prefixing. Colliding keys
433
+ * produce a console.warn and the first-written value wins.
434
+ */
435
+ prefix?: boolean | Record<string, string>;
436
+ /**
437
+ * Override the palette-level primary theme for this export.
438
+ * Pass a theme name to set/change the primary, or `false` to disable it.
439
+ * When omitted, inherits the palette-level `primary`.
440
+ */
441
+ primary?: string | false;
442
+ }
416
443
  interface GlazePalette {
417
444
  /**
418
445
  * Export all themes as a flat token map grouped by scheme variant.
419
446
  * Prefix defaults to `true` — all tokens are prefixed with the theme name.
420
- * Use `primary` to duplicate one theme's tokens without prefix.
447
+ * Inherits the palette-level `primary`; override per-call or pass `false` to disable.
421
448
  *
422
449
  * ```ts
423
- * palette.tokens({ primary: 'brand' })
450
+ * const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
451
+ * palette.tokens()
424
452
  * // → { light: { 'brand-surface': '...', 'surface': '...', 'accent-surface': '...' } }
425
453
  * ```
426
454
  */
@@ -428,12 +456,10 @@ interface GlazePalette {
428
456
  /**
429
457
  * Export all themes as tasty style-to-state bindings.
430
458
  * Uses `#name` color token keys and state aliases (`''`, `@dark`, etc.).
431
- * Prefix defaults to `true`. Use `primary` to duplicate one theme without prefix.
459
+ * Prefix defaults to `true`. Inherits the palette-level `primary`.
432
460
  * @see https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs
433
461
  */
434
- tasty(options?: GlazeTokenOptions & {
435
- primary?: string;
436
- }): Record<string, Record<string, string>>;
462
+ tasty(options?: GlazeTokenOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
437
463
  /** Export all themes as plain JSON grouped by theme name. */
438
464
  json(options?: GlazeJsonOptions & {
439
465
  prefix?: boolean | Record<string, string>;
@@ -460,11 +486,9 @@ declare function glaze(hueOrOptions: number | {
460
486
  }, saturation?: number): GlazeTheme;
461
487
  declare namespace glaze {
462
488
  var configure: (config: GlazeConfig) => void;
463
- var palette: (themes: PaletteInput) => {
489
+ var palette: (themes: PaletteInput, options?: GlazePaletteOptions) => {
464
490
  tokens(options?: GlazeJsonOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
465
- tasty(options?: GlazeTokenOptions & {
466
- primary?: string;
467
- }): Record<string, Record<string, string>>;
491
+ tasty(options?: GlazeTokenOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
468
492
  json(options?: GlazeJsonOptions & {
469
493
  prefix?: boolean | Record<string, string>;
470
494
  }): Record<string, Record<string, Record<string, string>>>;
@@ -548,5 +572,5 @@ declare function formatHsl(h: number, s: number, l: number): string;
548
572
  */
549
573
  declare function formatOklch(h: number, s: number, l: number): string;
550
574
  //#endregion
551
- export { type AdaptationMode, type ColorDef, type ColorMap, type ContrastPreset, type FindLightnessForContrastOptions, type FindLightnessForContrastResult, type FindValueForMixContrastOptions, type FindValueForMixContrastResult, type GlazeColorFormat, type GlazeColorInput, type GlazeColorToken, type GlazeConfig, type GlazeCssOptions, type GlazeCssResult, type GlazeExtendOptions, type GlazeJsonOptions, type GlazeOutputModes, type GlazePalette, type GlazePaletteExportOptions, type GlazeShadowInput, type GlazeTheme, type GlazeThemeExport, type GlazeTokenOptions, type HCPair, type HexColor, type MinContrast, type MixColorDef, type OkhslColor, type RegularColorDef, type RelativeValue, type ResolvedColor, type ResolvedColorVariant, type ShadowColorDef, type ShadowTuning, contrastRatioFromLuminance, findLightnessForContrast, findValueForMixContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, gamutClampedLuminance, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
575
+ export { type AdaptationMode, type ColorDef, type ColorMap, type ContrastPreset, type FindLightnessForContrastOptions, type FindLightnessForContrastResult, type FindValueForMixContrastOptions, type FindValueForMixContrastResult, type GlazeColorFormat, type GlazeColorInput, type GlazeColorToken, type GlazeConfig, type GlazeCssOptions, type GlazeCssResult, type GlazeExtendOptions, type GlazeJsonOptions, type GlazeOutputModes, type GlazePalette, type GlazePaletteExportOptions, type GlazePaletteOptions, type GlazeShadowInput, type GlazeTheme, type GlazeThemeExport, type GlazeTokenOptions, type HCPair, type HexColor, type MinContrast, type MixColorDef, type OkhslColor, type RegularColorDef, type RelativeValue, type ResolvedColor, type ResolvedColorVariant, type ShadowColorDef, type ShadowTuning, contrastRatioFromLuminance, findLightnessForContrast, findValueForMixContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, gamutClampedLuminance, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
552
576
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -960,31 +960,42 @@ function topoSort(defs) {
960
960
  for (const name of Object.keys(defs)) visit(name);
961
961
  return result;
962
962
  }
963
+ function lightnessWindow(isHighContrast, kind) {
964
+ if (isHighContrast) return [0, 100];
965
+ return kind === "dark" ? globalConfig.darkLightness : globalConfig.lightLightness;
966
+ }
963
967
  function mapLightnessLight(l, mode, isHighContrast) {
964
- if (mode === "static" || isHighContrast) return l;
965
- const [lo, hi] = globalConfig.lightLightness;
968
+ if (mode === "static") return l;
969
+ const [lo, hi] = lightnessWindow(isHighContrast, "light");
966
970
  return l * (hi - lo) / 100 + lo;
967
971
  }
972
+ function mobiusCurve(t, beta) {
973
+ if (beta >= 1) return t;
974
+ return t / (t + beta * (1 - t));
975
+ }
968
976
  function mapLightnessDark(l, mode, isHighContrast) {
969
977
  if (mode === "static") return l;
970
- if (isHighContrast) {
971
- if (mode === "fixed") return l;
972
- const t = (100 - l) / 100;
973
- return 100 * Math.pow(t, globalConfig.darkCurve);
974
- }
975
- const [darkLo, darkHi] = globalConfig.darkLightness;
978
+ const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
979
+ const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark");
976
980
  if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
977
- const [lightLo, lightHi] = globalConfig.lightLightness;
981
+ const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light");
978
982
  const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
979
- return darkLo + (darkHi - darkLo) * Math.pow(t, globalConfig.darkCurve);
983
+ return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
984
+ }
985
+ function lightMappedToDark(lightL, isHighContrast) {
986
+ const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
987
+ const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light");
988
+ const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark");
989
+ const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
990
+ return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
980
991
  }
981
992
  function mapSaturationDark(s, mode) {
982
993
  if (mode === "static") return s;
983
994
  return s * (1 - globalConfig.darkDesaturation);
984
995
  }
985
996
  function schemeLightnessRange(isDark, mode, isHighContrast) {
986
- if (mode === "static" || isHighContrast) return [0, 1];
987
- const [lo, hi] = isDark ? globalConfig.darkLightness : globalConfig.lightLightness;
997
+ if (mode === "static") return [0, 1];
998
+ const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light");
988
999
  return [lo / 100, hi / 100];
989
1000
  }
990
1001
  function clamp(v, min, max) {
@@ -1043,9 +1054,9 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
1043
1054
  else {
1044
1055
  const parsed = parseRelativeOrAbsolute(isHighContrast ? pairHC(rawLightness) : pairNormal(rawLightness));
1045
1056
  if (parsed.relative) {
1046
- let delta = parsed.value;
1047
- if (isDark && mode === "auto") delta = -delta;
1048
- preferredL = clamp(baseL + delta, 0, 100);
1057
+ const delta = parsed.value;
1058
+ if (isDark && mode === "auto") preferredL = lightMappedToDark(clamp(getSchemeVariant(baseResolved, false, isHighContrast).l * 100 + delta, 0, 100), isHighContrast);
1059
+ else preferredL = clamp(baseL + delta, 0, 100);
1049
1060
  } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast);
1050
1061
  else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast);
1051
1062
  }
@@ -1402,10 +1413,14 @@ function createTheme(hue, saturation, initialColors) {
1402
1413
  };
1403
1414
  },
1404
1415
  extend(options) {
1405
- return createTheme(options.hue ?? hue, options.saturation ?? saturation, options.colors ? {
1406
- ...colorDefs,
1416
+ const newHue = options.hue ?? hue;
1417
+ const newSat = options.saturation ?? saturation;
1418
+ const inheritedColors = {};
1419
+ for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
1420
+ return createTheme(newHue, newSat, options.colors ? {
1421
+ ...inheritedColors,
1407
1422
  ...options.colors
1408
- } : { ...colorDefs });
1423
+ } : { ...inheritedColors });
1409
1424
  },
1410
1425
  resolve() {
1411
1426
  return resolveAllColors(hue, saturation, colorDefs);
@@ -1439,40 +1454,74 @@ function validatePrimaryTheme(primary, themes) {
1439
1454
  throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
1440
1455
  }
1441
1456
  }
1442
- function createPalette(themes) {
1457
+ /**
1458
+ * Resolve the effective primary for an export call.
1459
+ * `false` disables, a string overrides, `undefined` inherits from palette.
1460
+ */
1461
+ function resolveEffectivePrimary(exportPrimary, palettePrimary) {
1462
+ if (exportPrimary === false) return void 0;
1463
+ return exportPrimary ?? palettePrimary;
1464
+ }
1465
+ /**
1466
+ * Filter a resolved color map, skipping keys already in `seen`.
1467
+ * Warns on collision and keeps the first-written value (first-write-wins).
1468
+ * Returns a new map containing only non-colliding entries.
1469
+ */
1470
+ function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
1471
+ const filtered = /* @__PURE__ */ new Map();
1472
+ const label = isPrimary ? `${themeName} (primary)` : themeName;
1473
+ for (const [name, color] of resolved) {
1474
+ const key = `${prefix}${name}`;
1475
+ if (seen.has(key)) {
1476
+ console.warn(`glaze: token "${key}" from theme "${label}" collides with theme "${seen.get(key)}" — skipping.`);
1477
+ continue;
1478
+ }
1479
+ seen.set(key, label);
1480
+ filtered.set(name, color);
1481
+ }
1482
+ return filtered;
1483
+ }
1484
+ function createPalette(themes, paletteOptions) {
1485
+ validatePrimaryTheme(paletteOptions?.primary, themes);
1443
1486
  return {
1444
1487
  tokens(options) {
1445
- validatePrimaryTheme(options?.primary, themes);
1488
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1489
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1446
1490
  const modes = resolveModes(options?.modes);
1447
1491
  const allTokens = {};
1492
+ const seen = /* @__PURE__ */ new Map();
1448
1493
  for (const [themeName, theme] of Object.entries(themes)) {
1449
1494
  const resolved = theme.resolve();
1450
- const tokens = buildFlatTokenMap(resolved, resolvePrefix(options, themeName, true), modes, options?.format);
1495
+ const prefix = resolvePrefix(options, themeName, true);
1496
+ const tokens = buildFlatTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, modes, options?.format);
1451
1497
  for (const variant of Object.keys(tokens)) {
1452
1498
  if (!allTokens[variant]) allTokens[variant] = {};
1453
1499
  Object.assign(allTokens[variant], tokens[variant]);
1454
1500
  }
1455
- if (themeName === options?.primary) {
1456
- const unprefixed = buildFlatTokenMap(resolved, "", modes, options?.format);
1501
+ if (themeName === effectivePrimary) {
1502
+ const unprefixed = buildFlatTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", modes, options?.format);
1457
1503
  for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
1458
1504
  }
1459
1505
  }
1460
1506
  return allTokens;
1461
1507
  },
1462
1508
  tasty(options) {
1463
- validatePrimaryTheme(options?.primary, themes);
1509
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1510
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1464
1511
  const states = {
1465
1512
  dark: options?.states?.dark ?? globalConfig.states.dark,
1466
1513
  highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1467
1514
  };
1468
1515
  const modes = resolveModes(options?.modes);
1469
1516
  const allTokens = {};
1517
+ const seen = /* @__PURE__ */ new Map();
1470
1518
  for (const [themeName, theme] of Object.entries(themes)) {
1471
1519
  const resolved = theme.resolve();
1472
- const tokens = buildTokenMap(resolved, resolvePrefix(options, themeName, true), states, modes, options?.format);
1520
+ const prefix = resolvePrefix(options, themeName, true);
1521
+ const tokens = buildTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, states, modes, options?.format);
1473
1522
  Object.assign(allTokens, tokens);
1474
- if (themeName === options?.primary) {
1475
- const unprefixed = buildTokenMap(resolved, "", states, modes, options?.format);
1523
+ if (themeName === effectivePrimary) {
1524
+ const unprefixed = buildTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", states, modes, options?.format);
1476
1525
  Object.assign(allTokens, unprefixed);
1477
1526
  }
1478
1527
  }
@@ -1485,7 +1534,8 @@ function createPalette(themes) {
1485
1534
  return result;
1486
1535
  },
1487
1536
  css(options) {
1488
- validatePrimaryTheme(options?.primary, themes);
1537
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1538
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1489
1539
  const suffix = options?.suffix ?? "-color";
1490
1540
  const format = options?.format ?? "rgb";
1491
1541
  const allLines = {
@@ -1494,17 +1544,19 @@ function createPalette(themes) {
1494
1544
  lightContrast: [],
1495
1545
  darkContrast: []
1496
1546
  };
1547
+ const seen = /* @__PURE__ */ new Map();
1497
1548
  for (const [themeName, theme] of Object.entries(themes)) {
1498
1549
  const resolved = theme.resolve();
1499
- const css = buildCssMap(resolved, resolvePrefix(options, themeName, true), suffix, format);
1550
+ const prefix = resolvePrefix(options, themeName, true);
1551
+ const css = buildCssMap(filterCollisions(resolved, prefix, seen, themeName), prefix, suffix, format);
1500
1552
  for (const key of [
1501
1553
  "light",
1502
1554
  "dark",
1503
1555
  "lightContrast",
1504
1556
  "darkContrast"
1505
1557
  ]) if (css[key]) allLines[key].push(css[key]);
1506
- if (themeName === options?.primary) {
1507
- const unprefixed = buildCssMap(resolved, "", suffix, format);
1558
+ if (themeName === effectivePrimary) {
1559
+ const unprefixed = buildCssMap(filterCollisions(resolved, "", seen, themeName, true), "", suffix, format);
1508
1560
  for (const key of [
1509
1561
  "light",
1510
1562
  "dark",
@@ -1586,8 +1638,8 @@ glaze.configure = function configure(config) {
1586
1638
  /**
1587
1639
  * Compose multiple themes into a palette.
1588
1640
  */
1589
- glaze.palette = function palette(themes) {
1590
- return createPalette(themes);
1641
+ glaze.palette = function palette(themes, options) {
1642
+ return createPalette(themes, options);
1591
1643
  };
1592
1644
  /**
1593
1645
  * Create a theme from a serialized export.