@tenphi/glaze 0.0.0-snapshot.7f3fb7f → 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>;
@@ -242,6 +257,13 @@ interface GlazeConfig {
242
257
  darkLightness?: [number, number];
243
258
  /** Saturation reduction factor for dark scheme (0–1). Default: 0.1. */
244
259
  darkDesaturation?: number;
260
+ /**
261
+ * Möbius beta for dark auto-inversion (0–1).
262
+ * Lower values expand subtle near-white distinctions in dark mode.
263
+ * Set to 1 for linear (legacy) behavior. Default: 0.5.
264
+ * Accepts [normal, highContrast] pair for separate HC tuning.
265
+ */
266
+ darkCurve?: HCPair<number>;
245
267
  /** State alias names for token export. */
246
268
  states?: {
247
269
  dark?: string;
@@ -256,6 +278,7 @@ interface GlazeConfigResolved {
256
278
  lightLightness: [number, number];
257
279
  darkLightness: [number, number];
258
280
  darkDesaturation: number;
281
+ darkCurve: HCPair<number>;
259
282
  states: {
260
283
  dark: string;
261
284
  highContrast: string;
@@ -385,33 +408,64 @@ interface GlazeCssResult {
385
408
  lightContrast: string;
386
409
  darkContrast: string;
387
410
  }
411
+ /** Options for `glaze.palette()` creation. */
412
+ interface GlazePaletteOptions {
413
+ /**
414
+ * Name of the primary theme. The primary theme's tokens are duplicated
415
+ * without prefix in all exports, providing convenient short aliases
416
+ * alongside the prefixed versions. Can be overridden per-export.
417
+ *
418
+ * @example
419
+ * ```ts
420
+ * const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
421
+ * palette.tokens()
422
+ * // → { light: { 'brand-surface': '...', 'surface': '...', 'accent-surface': '...' } }
423
+ * ```
424
+ */
425
+ primary?: string;
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
+ }
388
443
  interface GlazePalette {
389
444
  /**
390
445
  * Export all themes as a flat token map grouped by scheme variant.
446
+ * Prefix defaults to `true` — all tokens are prefixed with the theme name.
447
+ * Inherits the palette-level `primary`; override per-call or pass `false` to disable.
391
448
  *
392
449
  * ```ts
393
- * palette.tokens({ prefix: true })
394
- * // → { light: { 'primary-surface': 'okhsl(...)' }, dark: { 'primary-surface': 'okhsl(...)' } }
450
+ * const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
451
+ * palette.tokens()
452
+ * // → { light: { 'brand-surface': '...', 'surface': '...', 'accent-surface': '...' } }
395
453
  * ```
396
454
  */
397
- tokens(options?: GlazeJsonOptions & {
398
- prefix?: boolean | Record<string, string>;
399
- }): Record<string, Record<string, string>>;
455
+ tokens(options?: GlazeJsonOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
400
456
  /**
401
457
  * Export all themes as tasty style-to-state bindings.
402
458
  * Uses `#name` color token keys and state aliases (`''`, `@dark`, etc.).
403
- * Spread into component styles or register as a recipe via `configure({ recipes })`.
459
+ * Prefix defaults to `true`. Inherits the palette-level `primary`.
404
460
  * @see https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs
405
461
  */
406
- tasty(options?: GlazeTokenOptions): Record<string, Record<string, string>>;
407
- /** Export all themes as plain JSON. */
462
+ tasty(options?: GlazeTokenOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
463
+ /** Export all themes as plain JSON grouped by theme name. */
408
464
  json(options?: GlazeJsonOptions & {
409
465
  prefix?: boolean | Record<string, string>;
410
466
  }): Record<string, Record<string, Record<string, string>>>;
411
467
  /** Export all themes as CSS custom property declarations. */
412
- css(options?: GlazeCssOptions & {
413
- prefix?: boolean | Record<string, string>;
414
- }): GlazeCssResult;
468
+ css(options?: GlazeCssOptions & GlazePaletteExportOptions): GlazeCssResult;
415
469
  }
416
470
  //#endregion
417
471
  //#region src/glaze.d.ts
@@ -432,17 +486,13 @@ declare function glaze(hueOrOptions: number | {
432
486
  }, saturation?: number): GlazeTheme;
433
487
  declare namespace glaze {
434
488
  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>>;
489
+ var palette: (themes: PaletteInput, options?: GlazePaletteOptions) => {
490
+ tokens(options?: GlazeJsonOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
491
+ tasty(options?: GlazeTokenOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
440
492
  json(options?: GlazeJsonOptions & {
441
493
  prefix?: boolean | Record<string, string>;
442
494
  }): Record<string, Record<string, Record<string, string>>>;
443
- css(options?: GlazeCssOptions & {
444
- prefix?: boolean | Record<string, string>;
445
- }): GlazeCssResult;
495
+ css(options?: GlazeCssOptions & GlazePaletteExportOptions): GlazeCssResult;
446
496
  };
447
497
  var from: (data: GlazeThemeExport) => GlazeTheme;
448
498
  var color: (input: GlazeColorInput) => GlazeColorToken;
@@ -522,5 +572,5 @@ declare function formatHsl(h: number, s: number, l: number): string;
522
572
  */
523
573
  declare function formatOklch(h: number, s: number, l: number): string;
524
574
  //#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 };
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 };
526
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>;
@@ -242,6 +257,13 @@ interface GlazeConfig {
242
257
  darkLightness?: [number, number];
243
258
  /** Saturation reduction factor for dark scheme (0–1). Default: 0.1. */
244
259
  darkDesaturation?: number;
260
+ /**
261
+ * Möbius beta for dark auto-inversion (0–1).
262
+ * Lower values expand subtle near-white distinctions in dark mode.
263
+ * Set to 1 for linear (legacy) behavior. Default: 0.5.
264
+ * Accepts [normal, highContrast] pair for separate HC tuning.
265
+ */
266
+ darkCurve?: HCPair<number>;
245
267
  /** State alias names for token export. */
246
268
  states?: {
247
269
  dark?: string;
@@ -256,6 +278,7 @@ interface GlazeConfigResolved {
256
278
  lightLightness: [number, number];
257
279
  darkLightness: [number, number];
258
280
  darkDesaturation: number;
281
+ darkCurve: HCPair<number>;
259
282
  states: {
260
283
  dark: string;
261
284
  highContrast: string;
@@ -385,33 +408,64 @@ interface GlazeCssResult {
385
408
  lightContrast: string;
386
409
  darkContrast: string;
387
410
  }
411
+ /** Options for `glaze.palette()` creation. */
412
+ interface GlazePaletteOptions {
413
+ /**
414
+ * Name of the primary theme. The primary theme's tokens are duplicated
415
+ * without prefix in all exports, providing convenient short aliases
416
+ * alongside the prefixed versions. Can be overridden per-export.
417
+ *
418
+ * @example
419
+ * ```ts
420
+ * const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
421
+ * palette.tokens()
422
+ * // → { light: { 'brand-surface': '...', 'surface': '...', 'accent-surface': '...' } }
423
+ * ```
424
+ */
425
+ primary?: string;
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
+ }
388
443
  interface GlazePalette {
389
444
  /**
390
445
  * Export all themes as a flat token map grouped by scheme variant.
446
+ * Prefix defaults to `true` — all tokens are prefixed with the theme name.
447
+ * Inherits the palette-level `primary`; override per-call or pass `false` to disable.
391
448
  *
392
449
  * ```ts
393
- * palette.tokens({ prefix: true })
394
- * // → { light: { 'primary-surface': 'okhsl(...)' }, dark: { 'primary-surface': 'okhsl(...)' } }
450
+ * const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
451
+ * palette.tokens()
452
+ * // → { light: { 'brand-surface': '...', 'surface': '...', 'accent-surface': '...' } }
395
453
  * ```
396
454
  */
397
- tokens(options?: GlazeJsonOptions & {
398
- prefix?: boolean | Record<string, string>;
399
- }): Record<string, Record<string, string>>;
455
+ tokens(options?: GlazeJsonOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
400
456
  /**
401
457
  * Export all themes as tasty style-to-state bindings.
402
458
  * Uses `#name` color token keys and state aliases (`''`, `@dark`, etc.).
403
- * Spread into component styles or register as a recipe via `configure({ recipes })`.
459
+ * Prefix defaults to `true`. Inherits the palette-level `primary`.
404
460
  * @see https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs
405
461
  */
406
- tasty(options?: GlazeTokenOptions): Record<string, Record<string, string>>;
407
- /** Export all themes as plain JSON. */
462
+ tasty(options?: GlazeTokenOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
463
+ /** Export all themes as plain JSON grouped by theme name. */
408
464
  json(options?: GlazeJsonOptions & {
409
465
  prefix?: boolean | Record<string, string>;
410
466
  }): Record<string, Record<string, Record<string, string>>>;
411
467
  /** Export all themes as CSS custom property declarations. */
412
- css(options?: GlazeCssOptions & {
413
- prefix?: boolean | Record<string, string>;
414
- }): GlazeCssResult;
468
+ css(options?: GlazeCssOptions & GlazePaletteExportOptions): GlazeCssResult;
415
469
  }
416
470
  //#endregion
417
471
  //#region src/glaze.d.ts
@@ -432,17 +486,13 @@ declare function glaze(hueOrOptions: number | {
432
486
  }, saturation?: number): GlazeTheme;
433
487
  declare namespace glaze {
434
488
  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>>;
489
+ var palette: (themes: PaletteInput, options?: GlazePaletteOptions) => {
490
+ tokens(options?: GlazeJsonOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
491
+ tasty(options?: GlazeTokenOptions & GlazePaletteExportOptions): Record<string, Record<string, string>>;
440
492
  json(options?: GlazeJsonOptions & {
441
493
  prefix?: boolean | Record<string, string>;
442
494
  }): Record<string, Record<string, Record<string, string>>>;
443
- css(options?: GlazeCssOptions & {
444
- prefix?: boolean | Record<string, string>;
445
- }): GlazeCssResult;
495
+ css(options?: GlazeCssOptions & GlazePaletteExportOptions): GlazeCssResult;
446
496
  };
447
497
  var from: (data: GlazeThemeExport) => GlazeTheme;
448
498
  var color: (input: GlazeColorInput) => GlazeColorToken;
@@ -522,5 +572,5 @@ declare function formatHsl(h: number, s: number, l: number): string;
522
572
  */
523
573
  declare function formatOklch(h: number, s: number, l: number): string;
524
574
  //#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 };
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 };
526
576
  //# 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,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 = 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);
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;
@@ -1394,10 +1413,14 @@ function createTheme(hue, saturation, initialColors) {
1394
1413
  };
1395
1414
  },
1396
1415
  extend(options) {
1397
- return createTheme(options.hue ?? hue, options.saturation ?? saturation, options.colors ? {
1398
- ...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,
1399
1422
  ...options.colors
1400
- } : { ...colorDefs });
1423
+ } : { ...inheritedColors });
1401
1424
  },
1402
1425
  resolve() {
1403
1426
  return resolveAllColors(hue, saturation, colorDefs);
@@ -1419,35 +1442,88 @@ function createTheme(hue, saturation, initialColors) {
1419
1442
  }
1420
1443
  };
1421
1444
  }
1422
- function resolvePrefix(options, themeName) {
1423
- if (options?.prefix === true) return `${themeName}-`;
1424
- if (typeof options?.prefix === "object" && options.prefix !== null) return options.prefix[themeName] ?? `${themeName}-`;
1445
+ function resolvePrefix(options, themeName, defaultPrefix = false) {
1446
+ const prefix = options?.prefix ?? defaultPrefix;
1447
+ if (prefix === true) return `${themeName}-`;
1448
+ if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
1425
1449
  return "";
1426
1450
  }
1427
- function createPalette(themes) {
1451
+ function validatePrimaryTheme(primary, themes) {
1452
+ if (primary !== void 0 && !(primary in themes)) {
1453
+ const available = Object.keys(themes).join(", ");
1454
+ throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
1455
+ }
1456
+ }
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);
1428
1486
  return {
1429
1487
  tokens(options) {
1488
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1489
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1430
1490
  const modes = resolveModes(options?.modes);
1431
1491
  const allTokens = {};
1492
+ const seen = /* @__PURE__ */ new Map();
1432
1493
  for (const [themeName, theme] of Object.entries(themes)) {
1433
- const tokens = buildFlatTokenMap(theme.resolve(), resolvePrefix(options, themeName), modes, options?.format);
1494
+ const resolved = theme.resolve();
1495
+ const prefix = resolvePrefix(options, themeName, true);
1496
+ const tokens = buildFlatTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, modes, options?.format);
1434
1497
  for (const variant of Object.keys(tokens)) {
1435
1498
  if (!allTokens[variant]) allTokens[variant] = {};
1436
1499
  Object.assign(allTokens[variant], tokens[variant]);
1437
1500
  }
1501
+ if (themeName === effectivePrimary) {
1502
+ const unprefixed = buildFlatTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", modes, options?.format);
1503
+ for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
1504
+ }
1438
1505
  }
1439
1506
  return allTokens;
1440
1507
  },
1441
1508
  tasty(options) {
1509
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1510
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1442
1511
  const states = {
1443
1512
  dark: options?.states?.dark ?? globalConfig.states.dark,
1444
1513
  highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1445
1514
  };
1446
1515
  const modes = resolveModes(options?.modes);
1447
1516
  const allTokens = {};
1517
+ const seen = /* @__PURE__ */ new Map();
1448
1518
  for (const [themeName, theme] of Object.entries(themes)) {
1449
- const tokens = buildTokenMap(theme.resolve(), resolvePrefix(options, themeName), states, modes, options?.format);
1519
+ const resolved = theme.resolve();
1520
+ const prefix = resolvePrefix(options, themeName, true);
1521
+ const tokens = buildTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, states, modes, options?.format);
1450
1522
  Object.assign(allTokens, tokens);
1523
+ if (themeName === effectivePrimary) {
1524
+ const unprefixed = buildTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", states, modes, options?.format);
1525
+ Object.assign(allTokens, unprefixed);
1526
+ }
1451
1527
  }
1452
1528
  return allTokens;
1453
1529
  },
@@ -1458,6 +1534,8 @@ function createPalette(themes) {
1458
1534
  return result;
1459
1535
  },
1460
1536
  css(options) {
1537
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1538
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1461
1539
  const suffix = options?.suffix ?? "-color";
1462
1540
  const format = options?.format ?? "rgb";
1463
1541
  const allLines = {
@@ -1466,14 +1544,26 @@ function createPalette(themes) {
1466
1544
  lightContrast: [],
1467
1545
  darkContrast: []
1468
1546
  };
1547
+ const seen = /* @__PURE__ */ new Map();
1469
1548
  for (const [themeName, theme] of Object.entries(themes)) {
1470
- const css = buildCssMap(theme.resolve(), resolvePrefix(options, themeName), suffix, format);
1549
+ const resolved = theme.resolve();
1550
+ const prefix = resolvePrefix(options, themeName, true);
1551
+ const css = buildCssMap(filterCollisions(resolved, prefix, seen, themeName), prefix, suffix, format);
1471
1552
  for (const key of [
1472
1553
  "light",
1473
1554
  "dark",
1474
1555
  "lightContrast",
1475
1556
  "darkContrast"
1476
1557
  ]) if (css[key]) allLines[key].push(css[key]);
1558
+ if (themeName === effectivePrimary) {
1559
+ const unprefixed = buildCssMap(filterCollisions(resolved, "", seen, themeName, true), "", suffix, format);
1560
+ for (const key of [
1561
+ "light",
1562
+ "dark",
1563
+ "lightContrast",
1564
+ "darkContrast"
1565
+ ]) if (unprefixed[key]) allLines[key].push(unprefixed[key]);
1566
+ }
1477
1567
  }
1478
1568
  return {
1479
1569
  light: allLines.light.join("\n"),
@@ -1533,6 +1623,7 @@ glaze.configure = function configure(config) {
1533
1623
  lightLightness: config.lightLightness ?? globalConfig.lightLightness,
1534
1624
  darkLightness: config.darkLightness ?? globalConfig.darkLightness,
1535
1625
  darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
1626
+ darkCurve: config.darkCurve ?? globalConfig.darkCurve,
1536
1627
  states: {
1537
1628
  dark: config.states?.dark ?? globalConfig.states.dark,
1538
1629
  highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
@@ -1547,8 +1638,8 @@ glaze.configure = function configure(config) {
1547
1638
  /**
1548
1639
  * Compose multiple themes into a palette.
1549
1640
  */
1550
- glaze.palette = function palette(themes) {
1551
- return createPalette(themes);
1641
+ glaze.palette = function palette(themes, options) {
1642
+ return createPalette(themes, options);
1552
1643
  };
1553
1644
  /**
1554
1645
  * Create a theme from a serialized export.
@@ -1632,6 +1723,7 @@ glaze.resetConfig = function resetConfig() {
1632
1723
  lightLightness: [10, 100],
1633
1724
  darkLightness: [15, 95],
1634
1725
  darkDesaturation: .1,
1726
+ darkCurve: .5,
1635
1727
  states: {
1636
1728
  dark: "@dark",
1637
1729
  highContrast: "@high-contrast"