@tenphi/glaze 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -242,6 +242,13 @@ interface GlazeConfig {
242
242
  darkLightness?: [number, number];
243
243
  /** Saturation reduction factor for dark scheme (0–1). Default: 0.1. */
244
244
  darkDesaturation?: number;
245
+ /**
246
+ * Möbius beta for dark auto-inversion (0–1).
247
+ * Lower values expand subtle near-white distinctions in dark mode.
248
+ * Set to 1 for linear (legacy) behavior. Default: 0.5.
249
+ * Accepts [normal, highContrast] pair for separate HC tuning.
250
+ */
251
+ darkCurve?: HCPair<number>;
245
252
  /** State alias names for token export. */
246
253
  states?: {
247
254
  dark?: string;
@@ -256,6 +263,7 @@ interface GlazeConfigResolved {
256
263
  lightLightness: [number, number];
257
264
  darkLightness: [number, number];
258
265
  darkDesaturation: number;
266
+ darkCurve: HCPair<number>;
259
267
  states: {
260
268
  dark: string;
261
269
  highContrast: string;
@@ -385,35 +393,47 @@ interface GlazeCssResult {
385
393
  lightContrast: string;
386
394
  darkContrast: string;
387
395
  }
388
- /** Options shared by palette `tokens()`, `tasty()`, and `css()` exports. */
389
- interface GlazePaletteExportOptions {
390
- /**
391
- * Prefix mode. `true` uses `"<themeName>-"`, or provide a custom map.
392
- * Defaults to `true` for palette export methods.
393
- * Set to `false` explicitly to disable prefixing (last-write-wins on collisions).
394
- */
395
- prefix?: boolean | Record<string, string>;
396
+ /** Options for `glaze.palette()` creation. */
397
+ interface GlazePaletteOptions {
396
398
  /**
397
399
  * Name of the primary theme. The primary theme's tokens are duplicated
398
- * without prefix, providing convenient short aliases alongside the
399
- * prefixed versions.
400
+ * without prefix in all exports, providing convenient short aliases
401
+ * alongside the prefixed versions. Can be overridden per-export.
400
402
  *
401
403
  * @example
402
404
  * ```ts
403
- * palette.tokens({ primary: 'brand' })
405
+ * const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
406
+ * palette.tokens()
404
407
  * // → { light: { 'brand-surface': '...', 'surface': '...', 'accent-surface': '...' } }
405
408
  * ```
406
409
  */
407
410
  primary?: string;
408
411
  }
412
+ /** Options shared by palette `tokens()`, `tasty()`, and `css()` exports. */
413
+ interface GlazePaletteExportOptions {
414
+ /**
415
+ * Prefix mode. `true` uses `"<themeName>-"`, or provide a custom map.
416
+ * Defaults to `true` for palette export methods.
417
+ * Set to `false` explicitly to disable prefixing. Colliding keys
418
+ * produce a console.warn and the first-written value wins.
419
+ */
420
+ prefix?: boolean | Record<string, string>;
421
+ /**
422
+ * Override the palette-level primary theme for this export.
423
+ * Pass a theme name to set/change the primary, or `false` to disable it.
424
+ * When omitted, inherits the palette-level `primary`.
425
+ */
426
+ primary?: string | false;
427
+ }
409
428
  interface GlazePalette {
410
429
  /**
411
430
  * Export all themes as a flat token map grouped by scheme variant.
412
431
  * Prefix defaults to `true` — all tokens are prefixed with the theme name.
413
- * Use `primary` to duplicate one theme's tokens without prefix.
432
+ * Inherits the palette-level `primary`; override per-call or pass `false` to disable.
414
433
  *
415
434
  * ```ts
416
- * palette.tokens({ primary: 'brand' })
435
+ * const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
436
+ * palette.tokens()
417
437
  * // → { light: { 'brand-surface': '...', 'surface': '...', 'accent-surface': '...' } }
418
438
  * ```
419
439
  */
@@ -421,12 +441,10 @@ interface GlazePalette {
421
441
  /**
422
442
  * Export all themes as tasty style-to-state bindings.
423
443
  * Uses `#name` color token keys and state aliases (`''`, `@dark`, etc.).
424
- * Prefix defaults to `true`. Use `primary` to duplicate one theme without prefix.
444
+ * Prefix defaults to `true`. Inherits the palette-level `primary`.
425
445
  * @see https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs
426
446
  */
427
- tasty(options?: GlazeTokenOptions & {
428
- primary?: string;
429
- }): Record<string, Record<string, string>>;
447
+ tasty(options?: GlazeTokenOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
430
448
  /** Export all themes as plain JSON grouped by theme name. */
431
449
  json(options?: GlazeJsonOptions & {
432
450
  prefix?: boolean | Record<string, string>;
@@ -453,11 +471,9 @@ declare function glaze(hueOrOptions: number | {
453
471
  }, saturation?: number): GlazeTheme;
454
472
  declare namespace glaze {
455
473
  var configure: (config: GlazeConfig) => void;
456
- var palette: (themes: PaletteInput) => {
474
+ var palette: (themes: PaletteInput, options?: GlazePaletteOptions) => {
457
475
  tokens(options?: GlazeJsonOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
458
- tasty(options?: GlazeTokenOptions & {
459
- primary?: string;
460
- }): Record<string, Record<string, string>>;
476
+ tasty(options?: GlazeTokenOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
461
477
  json(options?: GlazeJsonOptions & {
462
478
  prefix?: boolean | Record<string, string>;
463
479
  }): Record<string, Record<string, Record<string, string>>>;
@@ -541,5 +557,5 @@ declare function formatHsl(h: number, s: number, l: number): string;
541
557
  */
542
558
  declare function formatOklch(h: number, s: number, l: number): string;
543
559
  //#endregion
544
- 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 };
560
+ 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 };
545
561
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.mts CHANGED
@@ -242,6 +242,13 @@ interface GlazeConfig {
242
242
  darkLightness?: [number, number];
243
243
  /** Saturation reduction factor for dark scheme (0–1). Default: 0.1. */
244
244
  darkDesaturation?: number;
245
+ /**
246
+ * Möbius beta for dark auto-inversion (0–1).
247
+ * Lower values expand subtle near-white distinctions in dark mode.
248
+ * Set to 1 for linear (legacy) behavior. Default: 0.5.
249
+ * Accepts [normal, highContrast] pair for separate HC tuning.
250
+ */
251
+ darkCurve?: HCPair<number>;
245
252
  /** State alias names for token export. */
246
253
  states?: {
247
254
  dark?: string;
@@ -256,6 +263,7 @@ interface GlazeConfigResolved {
256
263
  lightLightness: [number, number];
257
264
  darkLightness: [number, number];
258
265
  darkDesaturation: number;
266
+ darkCurve: HCPair<number>;
259
267
  states: {
260
268
  dark: string;
261
269
  highContrast: string;
@@ -385,35 +393,47 @@ interface GlazeCssResult {
385
393
  lightContrast: string;
386
394
  darkContrast: string;
387
395
  }
388
- /** Options shared by palette `tokens()`, `tasty()`, and `css()` exports. */
389
- interface GlazePaletteExportOptions {
390
- /**
391
- * Prefix mode. `true` uses `"<themeName>-"`, or provide a custom map.
392
- * Defaults to `true` for palette export methods.
393
- * Set to `false` explicitly to disable prefixing (last-write-wins on collisions).
394
- */
395
- prefix?: boolean | Record<string, string>;
396
+ /** Options for `glaze.palette()` creation. */
397
+ interface GlazePaletteOptions {
396
398
  /**
397
399
  * Name of the primary theme. The primary theme's tokens are duplicated
398
- * without prefix, providing convenient short aliases alongside the
399
- * prefixed versions.
400
+ * without prefix in all exports, providing convenient short aliases
401
+ * alongside the prefixed versions. Can be overridden per-export.
400
402
  *
401
403
  * @example
402
404
  * ```ts
403
- * palette.tokens({ primary: 'brand' })
405
+ * const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
406
+ * palette.tokens()
404
407
  * // → { light: { 'brand-surface': '...', 'surface': '...', 'accent-surface': '...' } }
405
408
  * ```
406
409
  */
407
410
  primary?: string;
408
411
  }
412
+ /** Options shared by palette `tokens()`, `tasty()`, and `css()` exports. */
413
+ interface GlazePaletteExportOptions {
414
+ /**
415
+ * Prefix mode. `true` uses `"<themeName>-"`, or provide a custom map.
416
+ * Defaults to `true` for palette export methods.
417
+ * Set to `false` explicitly to disable prefixing. Colliding keys
418
+ * produce a console.warn and the first-written value wins.
419
+ */
420
+ prefix?: boolean | Record<string, string>;
421
+ /**
422
+ * Override the palette-level primary theme for this export.
423
+ * Pass a theme name to set/change the primary, or `false` to disable it.
424
+ * When omitted, inherits the palette-level `primary`.
425
+ */
426
+ primary?: string | false;
427
+ }
409
428
  interface GlazePalette {
410
429
  /**
411
430
  * Export all themes as a flat token map grouped by scheme variant.
412
431
  * Prefix defaults to `true` — all tokens are prefixed with the theme name.
413
- * Use `primary` to duplicate one theme's tokens without prefix.
432
+ * Inherits the palette-level `primary`; override per-call or pass `false` to disable.
414
433
  *
415
434
  * ```ts
416
- * palette.tokens({ primary: 'brand' })
435
+ * const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
436
+ * palette.tokens()
417
437
  * // → { light: { 'brand-surface': '...', 'surface': '...', 'accent-surface': '...' } }
418
438
  * ```
419
439
  */
@@ -421,12 +441,10 @@ interface GlazePalette {
421
441
  /**
422
442
  * Export all themes as tasty style-to-state bindings.
423
443
  * Uses `#name` color token keys and state aliases (`''`, `@dark`, etc.).
424
- * Prefix defaults to `true`. Use `primary` to duplicate one theme without prefix.
444
+ * Prefix defaults to `true`. Inherits the palette-level `primary`.
425
445
  * @see https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs
426
446
  */
427
- tasty(options?: GlazeTokenOptions & {
428
- primary?: string;
429
- }): Record<string, Record<string, string>>;
447
+ tasty(options?: GlazeTokenOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
430
448
  /** Export all themes as plain JSON grouped by theme name. */
431
449
  json(options?: GlazeJsonOptions & {
432
450
  prefix?: boolean | Record<string, string>;
@@ -453,11 +471,9 @@ declare function glaze(hueOrOptions: number | {
453
471
  }, saturation?: number): GlazeTheme;
454
472
  declare namespace glaze {
455
473
  var configure: (config: GlazeConfig) => void;
456
- var palette: (themes: PaletteInput) => {
474
+ var palette: (themes: PaletteInput, options?: GlazePaletteOptions) => {
457
475
  tokens(options?: GlazeJsonOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
458
- tasty(options?: GlazeTokenOptions & {
459
- primary?: string;
460
- }): Record<string, Record<string, string>>;
476
+ tasty(options?: GlazeTokenOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
461
477
  json(options?: GlazeJsonOptions & {
462
478
  prefix?: boolean | Record<string, string>;
463
479
  }): Record<string, Record<string, Record<string, string>>>;
@@ -541,5 +557,5 @@ declare function formatHsl(h: number, s: number, l: number): string;
541
557
  */
542
558
  declare function formatOklch(h: number, s: number, l: number): string;
543
559
  //#endregion
544
- 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 };
560
+ 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 };
545
561
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -812,6 +812,7 @@ let globalConfig = {
812
812
  lightLightness: [10, 100],
813
813
  darkLightness: [15, 95],
814
814
  darkDesaturation: .1,
815
+ darkCurve: .5,
815
816
  states: {
816
817
  dark: "@dark",
817
818
  highContrast: "@high-contrast"
@@ -959,24 +960,42 @@ function topoSort(defs) {
959
960
  for (const name of Object.keys(defs)) visit(name);
960
961
  return result;
961
962
  }
962
- function mapLightnessLight(l, mode) {
963
+ function lightnessWindow(isHighContrast, kind) {
964
+ if (isHighContrast) return [0, 100];
965
+ return kind === "dark" ? globalConfig.darkLightness : globalConfig.lightLightness;
966
+ }
967
+ function mapLightnessLight(l, mode, isHighContrast) {
963
968
  if (mode === "static") return l;
964
- const [lo, hi] = globalConfig.lightLightness;
969
+ const [lo, hi] = lightnessWindow(isHighContrast, "light");
965
970
  return l * (hi - lo) / 100 + lo;
966
971
  }
967
- function mapLightnessDark(l, mode) {
972
+ function mobiusCurve(t, beta) {
973
+ if (beta >= 1) return t;
974
+ return t / (t + beta * (1 - t));
975
+ }
976
+ function mapLightnessDark(l, mode, isHighContrast) {
968
977
  if (mode === "static") return l;
969
- const [lo, hi] = globalConfig.darkLightness;
970
- if (mode === "fixed") return l * (hi - lo) / 100 + lo;
971
- return (100 - l) * (hi - lo) / 100 + lo;
978
+ const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
979
+ const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark");
980
+ if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
981
+ const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light");
982
+ const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
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);
972
991
  }
973
992
  function mapSaturationDark(s, mode) {
974
993
  if (mode === "static") return s;
975
994
  return s * (1 - globalConfig.darkDesaturation);
976
995
  }
977
- function schemeLightnessRange(isDark, mode) {
996
+ function schemeLightnessRange(isDark, mode, isHighContrast) {
978
997
  if (mode === "static") return [0, 1];
979
- const [lo, hi] = isDark ? globalConfig.darkLightness : globalConfig.lightLightness;
998
+ const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light");
980
999
  return [lo / 100, hi / 100];
981
1000
  }
982
1001
  function clamp(v, min, max) {
@@ -1035,26 +1054,26 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
1035
1054
  else {
1036
1055
  const parsed = parseRelativeOrAbsolute(isHighContrast ? pairHC(rawLightness) : pairNormal(rawLightness));
1037
1056
  if (parsed.relative) {
1038
- let delta = parsed.value;
1039
- if (isDark && mode === "auto") delta = -delta;
1040
- preferredL = clamp(baseL + delta, 0, 100);
1041
- } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode);
1042
- else preferredL = mapLightnessLight(parsed.value, mode);
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);
1060
+ } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast);
1061
+ else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast);
1043
1062
  }
1044
1063
  const rawContrast = def.contrast;
1045
1064
  if (rawContrast !== void 0) {
1046
1065
  const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
1047
1066
  const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
1048
1067
  const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
1049
- const lightnessRange = schemeLightnessRange(isDark, mode);
1068
+ const windowRange = schemeLightnessRange(isDark, mode, isHighContrast);
1050
1069
  return {
1051
1070
  l: findLightnessForContrast({
1052
1071
  hue: effectiveHue,
1053
1072
  saturation: effectiveSat,
1054
- preferredLightness: clamp(preferredL / 100, lightnessRange[0], lightnessRange[1]),
1073
+ preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
1055
1074
  baseLinearRgb,
1056
1075
  contrast: minCr,
1057
- lightnessRange
1076
+ lightnessRange: [0, 1]
1058
1077
  }).lightness * 100,
1059
1078
  satFactor
1060
1079
  };
@@ -1091,13 +1110,13 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
1091
1110
  let finalL;
1092
1111
  let finalSat;
1093
1112
  if (isDark && isRoot) {
1094
- finalL = mapLightnessDark(lightL, mode);
1113
+ finalL = mapLightnessDark(lightL, mode, isHighContrast);
1095
1114
  finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
1096
1115
  } else if (isDark && !isRoot) {
1097
1116
  finalL = lightL;
1098
1117
  finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
1099
1118
  } else if (isRoot) {
1100
- finalL = mapLightnessLight(lightL, mode);
1119
+ finalL = mapLightnessLight(lightL, mode, isHighContrast);
1101
1120
  finalSat = satFactor * ctx.saturation / 100;
1102
1121
  } else {
1103
1122
  finalL = lightL;
@@ -1431,40 +1450,74 @@ function validatePrimaryTheme(primary, themes) {
1431
1450
  throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
1432
1451
  }
1433
1452
  }
1434
- function createPalette(themes) {
1453
+ /**
1454
+ * Resolve the effective primary for an export call.
1455
+ * `false` disables, a string overrides, `undefined` inherits from palette.
1456
+ */
1457
+ function resolveEffectivePrimary(exportPrimary, palettePrimary) {
1458
+ if (exportPrimary === false) return void 0;
1459
+ return exportPrimary ?? palettePrimary;
1460
+ }
1461
+ /**
1462
+ * Filter a resolved color map, skipping keys already in `seen`.
1463
+ * Warns on collision and keeps the first-written value (first-write-wins).
1464
+ * Returns a new map containing only non-colliding entries.
1465
+ */
1466
+ function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
1467
+ const filtered = /* @__PURE__ */ new Map();
1468
+ const label = isPrimary ? `${themeName} (primary)` : themeName;
1469
+ for (const [name, color] of resolved) {
1470
+ const key = `${prefix}${name}`;
1471
+ if (seen.has(key)) {
1472
+ console.warn(`glaze: token "${key}" from theme "${label}" collides with theme "${seen.get(key)}" — skipping.`);
1473
+ continue;
1474
+ }
1475
+ seen.set(key, label);
1476
+ filtered.set(name, color);
1477
+ }
1478
+ return filtered;
1479
+ }
1480
+ function createPalette(themes, paletteOptions) {
1481
+ validatePrimaryTheme(paletteOptions?.primary, themes);
1435
1482
  return {
1436
1483
  tokens(options) {
1437
- validatePrimaryTheme(options?.primary, themes);
1484
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1485
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1438
1486
  const modes = resolveModes(options?.modes);
1439
1487
  const allTokens = {};
1488
+ const seen = /* @__PURE__ */ new Map();
1440
1489
  for (const [themeName, theme] of Object.entries(themes)) {
1441
1490
  const resolved = theme.resolve();
1442
- const tokens = buildFlatTokenMap(resolved, resolvePrefix(options, themeName, true), modes, options?.format);
1491
+ const prefix = resolvePrefix(options, themeName, true);
1492
+ const tokens = buildFlatTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, modes, options?.format);
1443
1493
  for (const variant of Object.keys(tokens)) {
1444
1494
  if (!allTokens[variant]) allTokens[variant] = {};
1445
1495
  Object.assign(allTokens[variant], tokens[variant]);
1446
1496
  }
1447
- if (themeName === options?.primary) {
1448
- const unprefixed = buildFlatTokenMap(resolved, "", modes, options?.format);
1497
+ if (themeName === effectivePrimary) {
1498
+ const unprefixed = buildFlatTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", modes, options?.format);
1449
1499
  for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
1450
1500
  }
1451
1501
  }
1452
1502
  return allTokens;
1453
1503
  },
1454
1504
  tasty(options) {
1455
- validatePrimaryTheme(options?.primary, themes);
1505
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1506
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1456
1507
  const states = {
1457
1508
  dark: options?.states?.dark ?? globalConfig.states.dark,
1458
1509
  highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1459
1510
  };
1460
1511
  const modes = resolveModes(options?.modes);
1461
1512
  const allTokens = {};
1513
+ const seen = /* @__PURE__ */ new Map();
1462
1514
  for (const [themeName, theme] of Object.entries(themes)) {
1463
1515
  const resolved = theme.resolve();
1464
- const tokens = buildTokenMap(resolved, resolvePrefix(options, themeName, true), states, modes, options?.format);
1516
+ const prefix = resolvePrefix(options, themeName, true);
1517
+ const tokens = buildTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, states, modes, options?.format);
1465
1518
  Object.assign(allTokens, tokens);
1466
- if (themeName === options?.primary) {
1467
- const unprefixed = buildTokenMap(resolved, "", states, modes, options?.format);
1519
+ if (themeName === effectivePrimary) {
1520
+ const unprefixed = buildTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", states, modes, options?.format);
1468
1521
  Object.assign(allTokens, unprefixed);
1469
1522
  }
1470
1523
  }
@@ -1477,7 +1530,8 @@ function createPalette(themes) {
1477
1530
  return result;
1478
1531
  },
1479
1532
  css(options) {
1480
- validatePrimaryTheme(options?.primary, themes);
1533
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1534
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1481
1535
  const suffix = options?.suffix ?? "-color";
1482
1536
  const format = options?.format ?? "rgb";
1483
1537
  const allLines = {
@@ -1486,17 +1540,19 @@ function createPalette(themes) {
1486
1540
  lightContrast: [],
1487
1541
  darkContrast: []
1488
1542
  };
1543
+ const seen = /* @__PURE__ */ new Map();
1489
1544
  for (const [themeName, theme] of Object.entries(themes)) {
1490
1545
  const resolved = theme.resolve();
1491
- const css = buildCssMap(resolved, resolvePrefix(options, themeName, true), suffix, format);
1546
+ const prefix = resolvePrefix(options, themeName, true);
1547
+ const css = buildCssMap(filterCollisions(resolved, prefix, seen, themeName), prefix, suffix, format);
1492
1548
  for (const key of [
1493
1549
  "light",
1494
1550
  "dark",
1495
1551
  "lightContrast",
1496
1552
  "darkContrast"
1497
1553
  ]) if (css[key]) allLines[key].push(css[key]);
1498
- if (themeName === options?.primary) {
1499
- const unprefixed = buildCssMap(resolved, "", suffix, format);
1554
+ if (themeName === effectivePrimary) {
1555
+ const unprefixed = buildCssMap(filterCollisions(resolved, "", seen, themeName, true), "", suffix, format);
1500
1556
  for (const key of [
1501
1557
  "light",
1502
1558
  "dark",
@@ -1563,6 +1619,7 @@ glaze.configure = function configure(config) {
1563
1619
  lightLightness: config.lightLightness ?? globalConfig.lightLightness,
1564
1620
  darkLightness: config.darkLightness ?? globalConfig.darkLightness,
1565
1621
  darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
1622
+ darkCurve: config.darkCurve ?? globalConfig.darkCurve,
1566
1623
  states: {
1567
1624
  dark: config.states?.dark ?? globalConfig.states.dark,
1568
1625
  highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
@@ -1577,8 +1634,8 @@ glaze.configure = function configure(config) {
1577
1634
  /**
1578
1635
  * Compose multiple themes into a palette.
1579
1636
  */
1580
- glaze.palette = function palette(themes) {
1581
- return createPalette(themes);
1637
+ glaze.palette = function palette(themes, options) {
1638
+ return createPalette(themes, options);
1582
1639
  };
1583
1640
  /**
1584
1641
  * Create a theme from a serialized export.
@@ -1662,6 +1719,7 @@ glaze.resetConfig = function resetConfig() {
1662
1719
  lightLightness: [10, 100],
1663
1720
  darkLightness: [15, 95],
1664
1721
  darkDesaturation: .1,
1722
+ darkCurve: .5,
1665
1723
  states: {
1666
1724
  dark: "@dark",
1667
1725
  highContrast: "@high-contrast"