@tenphi/glaze 0.0.0-snapshot.f01add5 → 0.0.0-snapshot.f3bb46b

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,33 +393,64 @@ interface GlazeCssResult {
385
393
  lightContrast: string;
386
394
  darkContrast: string;
387
395
  }
396
+ /** Options for `glaze.palette()` creation. */
397
+ interface GlazePaletteOptions {
398
+ /**
399
+ * Name of the primary theme. The primary theme's tokens are duplicated
400
+ * without prefix in all exports, providing convenient short aliases
401
+ * alongside the prefixed versions. Can be overridden per-export.
402
+ *
403
+ * @example
404
+ * ```ts
405
+ * const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
406
+ * palette.tokens()
407
+ * // → { light: { 'brand-surface': '...', 'surface': '...', 'accent-surface': '...' } }
408
+ * ```
409
+ */
410
+ primary?: string;
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
+ }
388
428
  interface GlazePalette {
389
429
  /**
390
430
  * Export all themes as a flat token map grouped by scheme variant.
431
+ * Prefix defaults to `true` — all tokens are prefixed with the theme name.
432
+ * Inherits the palette-level `primary`; override per-call or pass `false` to disable.
391
433
  *
392
434
  * ```ts
393
- * palette.tokens({ prefix: true })
394
- * // → { light: { 'primary-surface': 'okhsl(...)' }, dark: { 'primary-surface': 'okhsl(...)' } }
435
+ * const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
436
+ * palette.tokens()
437
+ * // → { light: { 'brand-surface': '...', 'surface': '...', 'accent-surface': '...' } }
395
438
  * ```
396
439
  */
397
- tokens(options?: GlazeJsonOptions & {
398
- prefix?: boolean | Record<string, string>;
399
- }): Record<string, Record<string, string>>;
440
+ tokens(options?: GlazeJsonOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
400
441
  /**
401
442
  * Export all themes as tasty style-to-state bindings.
402
443
  * Uses `#name` color token keys and state aliases (`''`, `@dark`, etc.).
403
- * Spread into component styles or register as a recipe via `configure({ recipes })`.
444
+ * Prefix defaults to `true`. Inherits the palette-level `primary`.
404
445
  * @see https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs
405
446
  */
406
- tasty(options?: GlazeTokenOptions): Record<string, Record<string, string>>;
407
- /** Export all themes as plain JSON. */
447
+ tasty(options?: GlazeTokenOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
448
+ /** Export all themes as plain JSON grouped by theme name. */
408
449
  json(options?: GlazeJsonOptions & {
409
450
  prefix?: boolean | Record<string, string>;
410
451
  }): Record<string, Record<string, Record<string, string>>>;
411
452
  /** Export all themes as CSS custom property declarations. */
412
- css(options?: GlazeCssOptions & {
413
- prefix?: boolean | Record<string, string>;
414
- }): GlazeCssResult;
453
+ css(options?: GlazeCssOptions & GlazePaletteExportOptions): GlazeCssResult;
415
454
  }
416
455
  //#endregion
417
456
  //#region src/glaze.d.ts
@@ -432,17 +471,13 @@ declare function glaze(hueOrOptions: number | {
432
471
  }, saturation?: number): GlazeTheme;
433
472
  declare namespace glaze {
434
473
  var configure: (config: GlazeConfig) => void;
435
- var palette: (themes: PaletteInput) => {
436
- tokens(options?: GlazeJsonOptions & {
437
- prefix?: boolean | Record<string, string>;
438
- }): Record<string, Record<string, string>>;
439
- tasty(options?: GlazeTokenOptions): Record<string, Record<string, string>>;
474
+ var palette: (themes: PaletteInput, options?: GlazePaletteOptions) => {
475
+ tokens(options?: GlazeJsonOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
476
+ tasty(options?: GlazeTokenOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
440
477
  json(options?: GlazeJsonOptions & {
441
478
  prefix?: boolean | Record<string, string>;
442
479
  }): Record<string, Record<string, Record<string, string>>>;
443
- css(options?: GlazeCssOptions & {
444
- prefix?: boolean | Record<string, string>;
445
- }): GlazeCssResult;
480
+ css(options?: GlazeCssOptions & GlazePaletteExportOptions): GlazeCssResult;
446
481
  };
447
482
  var from: (data: GlazeThemeExport) => GlazeTheme;
448
483
  var color: (input: GlazeColorInput) => GlazeColorToken;
@@ -522,5 +557,5 @@ declare function formatHsl(h: number, s: number, l: number): string;
522
557
  */
523
558
  declare function formatOklch(h: number, s: number, l: number): string;
524
559
  //#endregion
525
- 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 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 };
526
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,33 +393,64 @@ interface GlazeCssResult {
385
393
  lightContrast: string;
386
394
  darkContrast: string;
387
395
  }
396
+ /** Options for `glaze.palette()` creation. */
397
+ interface GlazePaletteOptions {
398
+ /**
399
+ * Name of the primary theme. The primary theme's tokens are duplicated
400
+ * without prefix in all exports, providing convenient short aliases
401
+ * alongside the prefixed versions. Can be overridden per-export.
402
+ *
403
+ * @example
404
+ * ```ts
405
+ * const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
406
+ * palette.tokens()
407
+ * // → { light: { 'brand-surface': '...', 'surface': '...', 'accent-surface': '...' } }
408
+ * ```
409
+ */
410
+ primary?: string;
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
+ }
388
428
  interface GlazePalette {
389
429
  /**
390
430
  * Export all themes as a flat token map grouped by scheme variant.
431
+ * Prefix defaults to `true` — all tokens are prefixed with the theme name.
432
+ * Inherits the palette-level `primary`; override per-call or pass `false` to disable.
391
433
  *
392
434
  * ```ts
393
- * palette.tokens({ prefix: true })
394
- * // → { light: { 'primary-surface': 'okhsl(...)' }, dark: { 'primary-surface': 'okhsl(...)' } }
435
+ * const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
436
+ * palette.tokens()
437
+ * // → { light: { 'brand-surface': '...', 'surface': '...', 'accent-surface': '...' } }
395
438
  * ```
396
439
  */
397
- tokens(options?: GlazeJsonOptions & {
398
- prefix?: boolean | Record<string, string>;
399
- }): Record<string, Record<string, string>>;
440
+ tokens(options?: GlazeJsonOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
400
441
  /**
401
442
  * Export all themes as tasty style-to-state bindings.
402
443
  * Uses `#name` color token keys and state aliases (`''`, `@dark`, etc.).
403
- * Spread into component styles or register as a recipe via `configure({ recipes })`.
444
+ * Prefix defaults to `true`. Inherits the palette-level `primary`.
404
445
  * @see https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs
405
446
  */
406
- tasty(options?: GlazeTokenOptions): Record<string, Record<string, string>>;
407
- /** Export all themes as plain JSON. */
447
+ tasty(options?: GlazeTokenOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
448
+ /** Export all themes as plain JSON grouped by theme name. */
408
449
  json(options?: GlazeJsonOptions & {
409
450
  prefix?: boolean | Record<string, string>;
410
451
  }): Record<string, Record<string, Record<string, string>>>;
411
452
  /** Export all themes as CSS custom property declarations. */
412
- css(options?: GlazeCssOptions & {
413
- prefix?: boolean | Record<string, string>;
414
- }): GlazeCssResult;
453
+ css(options?: GlazeCssOptions & GlazePaletteExportOptions): GlazeCssResult;
415
454
  }
416
455
  //#endregion
417
456
  //#region src/glaze.d.ts
@@ -432,17 +471,13 @@ declare function glaze(hueOrOptions: number | {
432
471
  }, saturation?: number): GlazeTheme;
433
472
  declare namespace glaze {
434
473
  var configure: (config: GlazeConfig) => void;
435
- var palette: (themes: PaletteInput) => {
436
- tokens(options?: GlazeJsonOptions & {
437
- prefix?: boolean | Record<string, string>;
438
- }): Record<string, Record<string, string>>;
439
- tasty(options?: GlazeTokenOptions): Record<string, Record<string, string>>;
474
+ var palette: (themes: PaletteInput, options?: GlazePaletteOptions) => {
475
+ tokens(options?: GlazeJsonOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
476
+ tasty(options?: GlazeTokenOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
440
477
  json(options?: GlazeJsonOptions & {
441
478
  prefix?: boolean | Record<string, string>;
442
479
  }): Record<string, Record<string, Record<string, string>>>;
443
- css(options?: GlazeCssOptions & {
444
- prefix?: boolean | Record<string, string>;
445
- }): GlazeCssResult;
480
+ css(options?: GlazeCssOptions & GlazePaletteExportOptions): GlazeCssResult;
446
481
  };
447
482
  var from: (data: GlazeThemeExport) => GlazeTheme;
448
483
  var color: (input: GlazeColorInput) => GlazeColorToken;
@@ -522,5 +557,5 @@ declare function formatHsl(h: number, s: number, l: number): string;
522
557
  */
523
558
  declare function formatOklch(h: number, s: number, l: number): string;
524
559
  //#endregion
525
- 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 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 };
526
561
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -468,7 +468,7 @@ function formatOklch(h, s, l) {
468
468
  const C = Math.sqrt(a * a + b * b);
469
469
  let hh = Math.atan2(b, a) * (180 / Math.PI);
470
470
  hh = constrainAngle(hh);
471
- return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 1)})`;
471
+ return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 2)})`;
472
472
  }
473
473
 
474
474
  //#endregion
@@ -618,7 +618,7 @@ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
618
618
  function findLightnessForContrast(options) {
619
619
  const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
620
620
  const target = resolveMinContrast(contrastInput);
621
- const searchTarget = target * 1.005;
621
+ const searchTarget = target * 1.007;
622
622
  const yBase = gamutClampedLuminance(baseLinearRgb);
623
623
  const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
624
624
  if (crPref >= searchTarget) return {
@@ -742,7 +742,7 @@ function searchMixBranch(lo, hi, yBase, target, epsilon, maxIter, preferred, lum
742
742
  function findValueForMixContrast(options) {
743
743
  const { preferredValue, baseLinearRgb, contrast: contrastInput, luminanceAtValue, epsilon = 1e-4, maxIterations = 20 } = options;
744
744
  const target = resolveMinContrast(contrastInput);
745
- const searchTarget = target * 1.005;
745
+ const searchTarget = target * 1.01;
746
746
  const yBase = gamutClampedLuminance(baseLinearRgb);
747
747
  const crPref = contrastRatioFromLuminance(luminanceAtValue(preferredValue), yBase);
748
748
  if (crPref >= searchTarget) return {
@@ -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,21 +960,44 @@ 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
  }
996
+ function schemeLightnessRange(isDark, mode, isHighContrast) {
997
+ if (mode === "static") return [0, 1];
998
+ const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light");
999
+ return [lo / 100, hi / 100];
1000
+ }
977
1001
  function clamp(v, min, max) {
978
1002
  return Math.max(min, Math.min(max, v));
979
1003
  }
@@ -1030,24 +1054,26 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
1030
1054
  else {
1031
1055
  const parsed = parseRelativeOrAbsolute(isHighContrast ? pairHC(rawLightness) : pairNormal(rawLightness));
1032
1056
  if (parsed.relative) {
1033
- let delta = parsed.value;
1034
- if (isDark && mode === "auto") delta = -delta;
1035
- preferredL = clamp(baseL + delta, 0, 100);
1036
- } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode);
1037
- else preferredL = clamp(parsed.value, 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);
1060
+ } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast);
1061
+ else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast);
1038
1062
  }
1039
1063
  const rawContrast = def.contrast;
1040
1064
  if (rawContrast !== void 0) {
1041
1065
  const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
1042
1066
  const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
1043
1067
  const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
1068
+ const windowRange = schemeLightnessRange(isDark, mode, isHighContrast);
1044
1069
  return {
1045
1070
  l: findLightnessForContrast({
1046
1071
  hue: effectiveHue,
1047
1072
  saturation: effectiveSat,
1048
- preferredLightness: preferredL / 100,
1073
+ preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
1049
1074
  baseLinearRgb,
1050
- contrast: minCr
1075
+ contrast: minCr,
1076
+ lightnessRange: [0, 1]
1051
1077
  }).lightness * 100,
1052
1078
  satFactor
1053
1079
  };
@@ -1084,13 +1110,13 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
1084
1110
  let finalL;
1085
1111
  let finalSat;
1086
1112
  if (isDark && isRoot) {
1087
- finalL = mapLightnessDark(lightL, mode);
1113
+ finalL = mapLightnessDark(lightL, mode, isHighContrast);
1088
1114
  finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
1089
1115
  } else if (isDark && !isRoot) {
1090
1116
  finalL = lightL;
1091
1117
  finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
1092
1118
  } else if (isRoot) {
1093
- finalL = mapLightnessLight(lightL, mode);
1119
+ finalL = mapLightnessLight(lightL, mode, isHighContrast);
1094
1120
  finalSat = satFactor * ctx.saturation / 100;
1095
1121
  } else {
1096
1122
  finalL = lightL;
@@ -1412,35 +1438,88 @@ function createTheme(hue, saturation, initialColors) {
1412
1438
  }
1413
1439
  };
1414
1440
  }
1415
- function resolvePrefix(options, themeName) {
1416
- if (options?.prefix === true) return `${themeName}-`;
1417
- if (typeof options?.prefix === "object" && options.prefix !== null) return options.prefix[themeName] ?? `${themeName}-`;
1441
+ function resolvePrefix(options, themeName, defaultPrefix = false) {
1442
+ const prefix = options?.prefix ?? defaultPrefix;
1443
+ if (prefix === true) return `${themeName}-`;
1444
+ if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
1418
1445
  return "";
1419
1446
  }
1420
- function createPalette(themes) {
1447
+ function validatePrimaryTheme(primary, themes) {
1448
+ if (primary !== void 0 && !(primary in themes)) {
1449
+ const available = Object.keys(themes).join(", ");
1450
+ throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
1451
+ }
1452
+ }
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);
1421
1482
  return {
1422
1483
  tokens(options) {
1484
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1485
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1423
1486
  const modes = resolveModes(options?.modes);
1424
1487
  const allTokens = {};
1488
+ const seen = /* @__PURE__ */ new Map();
1425
1489
  for (const [themeName, theme] of Object.entries(themes)) {
1426
- const tokens = buildFlatTokenMap(theme.resolve(), resolvePrefix(options, themeName), modes, options?.format);
1490
+ const resolved = theme.resolve();
1491
+ const prefix = resolvePrefix(options, themeName, true);
1492
+ const tokens = buildFlatTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, modes, options?.format);
1427
1493
  for (const variant of Object.keys(tokens)) {
1428
1494
  if (!allTokens[variant]) allTokens[variant] = {};
1429
1495
  Object.assign(allTokens[variant], tokens[variant]);
1430
1496
  }
1497
+ if (themeName === effectivePrimary) {
1498
+ const unprefixed = buildFlatTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", modes, options?.format);
1499
+ for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
1500
+ }
1431
1501
  }
1432
1502
  return allTokens;
1433
1503
  },
1434
1504
  tasty(options) {
1505
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1506
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1435
1507
  const states = {
1436
1508
  dark: options?.states?.dark ?? globalConfig.states.dark,
1437
1509
  highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1438
1510
  };
1439
1511
  const modes = resolveModes(options?.modes);
1440
1512
  const allTokens = {};
1513
+ const seen = /* @__PURE__ */ new Map();
1441
1514
  for (const [themeName, theme] of Object.entries(themes)) {
1442
- const tokens = buildTokenMap(theme.resolve(), resolvePrefix(options, themeName), states, modes, options?.format);
1515
+ const resolved = theme.resolve();
1516
+ const prefix = resolvePrefix(options, themeName, true);
1517
+ const tokens = buildTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, states, modes, options?.format);
1443
1518
  Object.assign(allTokens, tokens);
1519
+ if (themeName === effectivePrimary) {
1520
+ const unprefixed = buildTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", states, modes, options?.format);
1521
+ Object.assign(allTokens, unprefixed);
1522
+ }
1444
1523
  }
1445
1524
  return allTokens;
1446
1525
  },
@@ -1451,6 +1530,8 @@ function createPalette(themes) {
1451
1530
  return result;
1452
1531
  },
1453
1532
  css(options) {
1533
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1534
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1454
1535
  const suffix = options?.suffix ?? "-color";
1455
1536
  const format = options?.format ?? "rgb";
1456
1537
  const allLines = {
@@ -1459,14 +1540,26 @@ function createPalette(themes) {
1459
1540
  lightContrast: [],
1460
1541
  darkContrast: []
1461
1542
  };
1543
+ const seen = /* @__PURE__ */ new Map();
1462
1544
  for (const [themeName, theme] of Object.entries(themes)) {
1463
- const css = buildCssMap(theme.resolve(), resolvePrefix(options, themeName), suffix, format);
1545
+ const resolved = theme.resolve();
1546
+ const prefix = resolvePrefix(options, themeName, true);
1547
+ const css = buildCssMap(filterCollisions(resolved, prefix, seen, themeName), prefix, suffix, format);
1464
1548
  for (const key of [
1465
1549
  "light",
1466
1550
  "dark",
1467
1551
  "lightContrast",
1468
1552
  "darkContrast"
1469
1553
  ]) if (css[key]) allLines[key].push(css[key]);
1554
+ if (themeName === effectivePrimary) {
1555
+ const unprefixed = buildCssMap(filterCollisions(resolved, "", seen, themeName, true), "", suffix, format);
1556
+ for (const key of [
1557
+ "light",
1558
+ "dark",
1559
+ "lightContrast",
1560
+ "darkContrast"
1561
+ ]) if (unprefixed[key]) allLines[key].push(unprefixed[key]);
1562
+ }
1470
1563
  }
1471
1564
  return {
1472
1565
  light: allLines.light.join("\n"),
@@ -1526,6 +1619,7 @@ glaze.configure = function configure(config) {
1526
1619
  lightLightness: config.lightLightness ?? globalConfig.lightLightness,
1527
1620
  darkLightness: config.darkLightness ?? globalConfig.darkLightness,
1528
1621
  darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
1622
+ darkCurve: config.darkCurve ?? globalConfig.darkCurve,
1529
1623
  states: {
1530
1624
  dark: config.states?.dark ?? globalConfig.states.dark,
1531
1625
  highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
@@ -1540,8 +1634,8 @@ glaze.configure = function configure(config) {
1540
1634
  /**
1541
1635
  * Compose multiple themes into a palette.
1542
1636
  */
1543
- glaze.palette = function palette(themes) {
1544
- return createPalette(themes);
1637
+ glaze.palette = function palette(themes, options) {
1638
+ return createPalette(themes, options);
1545
1639
  };
1546
1640
  /**
1547
1641
  * Create a theme from a serialized export.
@@ -1625,6 +1719,7 @@ glaze.resetConfig = function resetConfig() {
1625
1719
  lightLightness: [10, 100],
1626
1720
  darkLightness: [15, 95],
1627
1721
  darkDesaturation: .1,
1722
+ darkCurve: .5,
1628
1723
  states: {
1629
1724
  dark: "@dark",
1630
1725
  highContrast: "@high-contrast"