@tenphi/glaze 0.0.0-snapshot.07e5d83 → 0.0.0-snapshot.0821a67

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/README.md CHANGED
@@ -70,9 +70,9 @@ const danger = primary.extend({ hue: 23 });
70
70
  const success = primary.extend({ hue: 157 });
71
71
 
72
72
  // Compose into a palette and export
73
- const palette = glaze.palette({ primary, danger, success });
74
- const tokens = palette.tokens({ prefix: true });
75
- // → { light: { 'primary-surface': 'okhsl(...)', ... }, dark: { 'primary-surface': 'okhsl(...)', ... } }
73
+ const palette = glaze.palette({ primary, danger, success }, { primary: 'primary' });
74
+ const tokens = palette.tokens();
75
+ // → { light: { 'primary-surface': 'okhsl(...)', 'surface': 'okhsl(...)', ... }, dark: { ... } }
76
76
  ```
77
77
 
78
78
  ## Core Concepts
@@ -194,6 +194,8 @@ A single value applies to both modes. All control is local and explicit.
194
194
  'muted': { base: 'surface', lightness: ['-35', '-50'], contrast: ['AA-large', 'AA'] }
195
195
  ```
196
196
 
197
+ **Full lightness spectrum in HC mode:** In high-contrast variants, the `lightLightness` and `darkLightness` window constraints are bypassed entirely. Colors can reach the full 0–100 lightness range, maximizing perceivable contrast. Normal (non-HC) variants continue to use the configured windows.
198
+
197
199
  ## Theme Color Management
198
200
 
199
201
  ### Adding Colors
@@ -261,15 +263,300 @@ The export contains only the configuration — not resolved color values. Resolv
261
263
  Create a single color token without a full theme:
262
264
 
263
265
  ```ts
264
- const accent = glaze.color({ hue: 280, saturation: 80, lightness: 52, mode: 'fixed' });
266
+ const accent = glaze.color({ hue: 280, saturation: 80, lightness: 52 });
267
+
268
+ accent.resolve(); // → ResolvedColor with light/dark/lightContrast/darkContrast
269
+ accent.token(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' } (tasty format)
270
+ accent.tasty(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' } (same as token)
271
+ accent.json(); // → { light: 'okhsl(...)', dark: 'okhsl(...)' }
272
+ accent.css({ name: 'accent' });
273
+ // → { light: '--accent-color: rgb(...);', dark: '--accent-color: rgb(...);', ... }
274
+ accent.export(); // → JSON-safe snapshot — pass to `glaze.colorFrom(...)` to rehydrate
275
+ ```
276
+
277
+ ### Defaults
278
+
279
+ `glaze.color()` is tuned for "render this exact color, but adapt the
280
+ dark variant" — different from theme colors, which are seeds that
281
+ adapt to both lightness windows. The defaults vary by input form,
282
+ because string inputs are typically end-user values (color pickers,
283
+ theme settings) where natural light/dark inversion is the expectation:
284
+
285
+ - **String value-shorthand** (hex, `rgb()`, `hsl()`, `okhsl()`,
286
+ `oklch()`):
287
+ - Light variant preserves the input lightness exactly.
288
+ - Dark variant is **Möbius-inverted** into `[globalConfig.darkLightness[0], 100]`,
289
+ so `glaze.color('#000')` renders as `#fff` in dark mode and
290
+ `glaze.color('#fff')` falls to the dark `lo` floor (default `0.15`).
291
+ - Adaptation mode defaults to `'auto'`.
292
+ - The dark `lo` is snapshotted from `globalConfig` at color-creation
293
+ time, matching how an explicit `scaling.darkLightness: [lo, hi]`
294
+ behaves.
295
+
296
+ - **Object / tuple value-shorthand** (`{ h, s, l }`, `[r, g, b]`) and
297
+ the **structured form** (`{ hue, saturation, lightness, ... }`):
298
+ - Light variant preserves the input lightness exactly.
299
+ - Dark variant is linearly mapped into `globalConfig.darkLightness`
300
+ (default `[15, 95]`), snapshotted at color-creation time so later
301
+ `glaze.configure()` calls don't retroactively change exported tokens.
302
+ - Adaptation mode defaults to `'fixed'` (linear, no Möbius curve).
303
+
304
+ To opt back into the old fixed-linear default for string inputs, pass
305
+ either `{ mode: 'fixed' }` as the second arg, or supply an explicit
306
+ `scaling` as the third arg (see [Lightness scaling](#lightness-scaling)).
307
+
308
+ ```ts
309
+ // Default: pure black inverts to pure white in dark mode.
310
+ glaze.color('#000000').tasty();
311
+ // → { '': 'okhsl(0 0% 0%)', '@dark': 'okhsl(... 100%)' }
312
+
313
+ // Opt back into the fixed-linear behavior:
314
+ glaze.color('#000000', { mode: 'fixed' }).tasty();
315
+ // → { '': 'okhsl(0 0% 0%)', '@dark': 'okhsl(... 15%)' }
316
+ ```
317
+
318
+ ### Value Shorthand
319
+
320
+ The first argument can also be a color value — Glaze extracts the seed
321
+ hue/saturation/lightness for you. All forms support the same exports
322
+ (`resolve / token / tasty / json / css`):
323
+
324
+ ```ts
325
+ // Hex (3, 6, or 8 digits — alpha dropped with warning)
326
+ glaze.color('#26fcb2').tasty();
327
+ glaze.color('#26fcb2ff').tasty(); // alpha dropped
328
+
329
+ // CSS color functions Glaze itself emits (`rgb()`, `hsl()`, `okhsl()`, `oklch()`)
330
+ // — anything from theme.tasty()/json()/css() round-trips back in.
331
+ glaze.color('rgb(38 252 178)').tasty();
332
+ glaze.color('hsl(152 97% 57%)').tasty();
333
+ glaze.color('okhsl(152 95% 74%)').tasty();
334
+ glaze.color('oklch(0.85 0.18 152)').tasty();
335
+
336
+ // OKHSL object — Glaze's native shape (h: 0–360, s/l: 0–1).
337
+ // Passing 0–100 values for s/l throws with a hint to use the
338
+ // structured form { hue, saturation, lightness }.
339
+ glaze.color({ h: 152, s: 0.95, l: 0.74 }).tasty();
340
+
341
+ // RGB tuple, 0–255 (same range as glaze.fromRgb).
342
+ glaze.color([38, 252, 178]).tasty();
343
+ ```
344
+
345
+ The optional second argument supplies overrides — the WCAG `contrast`
346
+ solver, relative `hue` / `lightness`, plus the usual seed knobs:
347
+
348
+ ```ts
349
+ // Brand color seeded from a hex, with saturation/mode overrides
350
+ glaze.color('#26fcb2', { saturation: 80, mode: 'fixed' }).tasty();
351
+
352
+ // Brand text guaranteed AAA against the seed itself.
353
+ // Relative `lightness: '+48'` is anchored to the literal seed value.
354
+ glaze.color('#1a1a2e', {
355
+ lightness: '+48',
356
+ contrast: 'AAA',
357
+ }).tasty();
358
+ ```
359
+
360
+ By default, relative `lightness: '+N'` and `contrast: <ratio>` are
361
+ anchored to the literal seed (the value passed to `glaze.color()`).
362
+ Internally Glaze synthesizes a hidden `mode: 'static'` reference of
363
+ the seed so the contrast solver compares against the unmapped color
364
+ across every variant. Pass `base` (another `glaze.color()` token) to
365
+ anchor against another color's resolved variant per scheme instead —
366
+ see [Pairing Colors](#pairing-colors).
367
+
368
+ All overrides:
369
+
370
+ | Option | Notes |
371
+ |---|---|
372
+ | `hue` | Number (absolute 0–360) or `'+N'`/`'-N'` (relative to seed — never to `base`) |
373
+ | `saturation` | Override seed saturation (0–100) |
374
+ | `lightness` | Number (absolute 0–100) or `'+N'`/`'-N'`. Without `base`, relative is anchored to the literal seed; with `base`, anchored to `base`'s lightness per scheme. Supports `[normal, hc]` pairs |
375
+ | `saturationFactor` | Multiplier on seed (0–1, default 1) |
376
+ | `mode` | `'auto'` (default for string inputs) / `'fixed'` (default for object / tuple / structured inputs) / `'static'` — see [Adaptation Modes](#adaptation-modes) |
377
+ | `contrast` | WCAG floor. Without `base`, anchored to the literal seed; with `base`, solved per scheme against `base`'s resolved variant. Same shape as `RegularColorDef.contrast`. When the target can't be physically met, `glaze` emits a `console.warn` and returns the closest passing variant |
378
+ | `base` | Another `glaze.color()` token **or** a raw `GlazeColorValue` (hex / `rgb()` / `OkhslColor` / `[r, g, b]`). Raw values are auto-wrapped via `glaze.color(value)` so they pick up the same auto-invert defaults as an explicit wrap. When set, `contrast` and relative `lightness` anchor to it per scheme; relative `hue` still anchors to the seed |
379
+ | `opacity` | Fixed alpha 0–1 applied to every variant. Surfaces in `rgb(... / A)`, `okhsl(... / A)`, etc. Combining with `contrast` is not recommended (perceived lightness becomes unpredictable) — `glaze` emits a `console.warn` |
380
+ | `name` | **Debug label only** — surfaces in error and `console.warn` messages instead of the internal `"value"` sentinel. Does **not** change `.token()` / `.tasty()` / `.json()` / `.css()` output keys (those still use `''`, `light`, etc.). Reserved names (`"value"`, `"seed"`, `"externalBase"`) are rejected |
381
+
382
+ Alpha components in `rgb(... / A)` / `hsl(... / A)` / `rgba(...)` /
383
+ `hsla(...)` and 8-digit hex (`#rrggbbaa` / `#rgba`) are parsed but the
384
+ alpha channel is dropped with a `console.warn`. To set a fixed alpha
385
+ on a standalone color, use the `opacity` override (or `opacity` on a
386
+ theme color). Named CSS colors (`'red'`, `'blueviolet'`) are not
387
+ supported.
388
+
389
+ ### Lightness Scaling
390
+
391
+ The optional third positional argument lets you override the lightness
392
+ windows used by `glaze.color()`. Both keys mirror the field names from
393
+ `GlazeConfig`:
394
+
395
+ ```ts
396
+ // Preserve raw lightness in dark mode too:
397
+ glaze.color('#26fcb2', undefined, { darkLightness: false }).tasty();
398
+
399
+ // Or opt back into a theme-style window:
400
+ glaze.color('#26fcb2', undefined, {
401
+ lightLightness: [10, 100],
402
+ darkLightness: [15, 95],
403
+ }).tasty();
404
+
405
+ // Structured form takes scaling as the second positional arg:
406
+ glaze
407
+ .color({ hue: 152, saturation: 95, lightness: 74 }, { darkLightness: false })
408
+ .tasty();
409
+ ```
410
+
411
+ | Key | Default for `glaze.color()` (string input) | Default for `glaze.color()` (object / tuple / structured) | Effect |
412
+ |---|---|---|---|
413
+ | `lightLightness` | `false` | `false` | `false` = preserve input. Pass `[lo, hi]` to opt into a remap window. |
414
+ | `darkLightness` | `[globalConfig.darkLightness[0], 100]` (snapshotted; default `[15, 100]`) | `globalConfig.darkLightness` (snapshotted; default `[15, 95]`) | `false` = preserve input in dark too. Pass `[lo, hi]` to override the window. |
415
+
416
+ > Note: `scaling` is all-or-nothing — passing it replaces both fields
417
+ > at once. To keep one field's default, restate it explicitly. The
418
+ > default windows are snapshotted from `globalConfig` at color-creation
419
+ > time, so later `glaze.configure()` calls don't retroactively change
420
+ > already-created tokens (and `token.export()` round-trips
421
+ > byte-for-byte across `configure()` changes).
422
+
423
+ ### Pairing Colors
424
+
425
+ `glaze.color()` accepts an optional `base` override that ties one
426
+ standalone color to another. When you set `base`, the WCAG contrast
427
+ solver and relative `lightness` offsets switch their anchor from the
428
+ literal seed to the base's resolved variant per scheme — so the same
429
+ text color automatically lands at AA against its background in light,
430
+ dark, and high-contrast modes.
431
+
432
+ ```ts
433
+ const bg = glaze.color('#1a1a2e');
434
+
435
+ // Text guaranteed AA against `bg` in every scheme.
436
+ const text = glaze.color('#ffffff', { base: bg, contrast: 'AA' });
437
+
438
+ // Border 8 lightness units lighter than `bg` in each scheme.
439
+ const border = glaze.color('#000000', {
440
+ base: bg,
441
+ lightness: '+8',
442
+ mode: 'fixed',
443
+ });
444
+ ```
445
+
446
+ `base` also accepts a raw `GlazeColorValue` for one-off pairs without
447
+ a separate token binding:
448
+
449
+ ```ts
450
+ // Equivalent to `base: glaze.color('#1a1a2e')` — `glaze` auto-wraps it.
451
+ const text = glaze.color('#ffffff', { base: '#1a1a2e', contrast: 'AA' });
452
+ ```
453
+
454
+ Behavior with `base`:
455
+
456
+ - `contrast` is solved per scheme against `base`'s resolved variant
457
+ (light / dark / lightContrast / darkContrast).
458
+ - Relative `lightness: '+N'` / `'-N'` is anchored to `base`'s lightness
459
+ per scheme (matches theme behavior).
460
+ - Relative `hue: '+N'` still anchors to the **seed** (the value passed
461
+ to `glaze.color()`), not the base. Absolute hue overrides take
462
+ precedence as usual.
463
+ - `mode` works as a per-pair knob — pass `mode: 'fixed'` to disable
464
+ Möbius inversion for the dependent color, or `mode: 'auto'` to keep
465
+ it (defaults follow the same string-vs-object rules as standalone).
466
+ - The base token's `.resolve()` is called lazily on the first resolve
467
+ of the dependent and the result is captured by reference; later
468
+ mutations to the base don't apply (matches existing snapshot
469
+ semantics for `scaling.darkLightness`).
470
+ - Raw value bases (`base: '#fff'`, `base: { h, s, l }`, `base: [r, g, b]`)
471
+ are auto-wrapped via `glaze.color(value)` and inherit the same
472
+ string-vs-object defaults. To skip auto-invert on the base, wrap it
473
+ yourself: `base: glaze.color(value, undefined, { darkLightness: false })`.
474
+ - When the contrast target is physically unreachable (e.g. AAA against
475
+ a mid-grey base), `glaze` emits a single `console.warn` per
476
+ `(name, scheme, target)` triple and returns the closest passing
477
+ variant. Use the `name` override to make the warning more
478
+ identifiable in your logs.
479
+
480
+ Chains compose:
481
+
482
+ ```ts
483
+ const bg = glaze.color('#000000');
484
+ const surface = glaze.color('#222222', { base: bg, contrast: 'AAA' });
485
+ const text = glaze.color('#ffffff', { base: surface, contrast: 'AA' });
486
+ // Each level meets its contrast budget against its base in every scheme.
487
+ ```
488
+
489
+ ### Naming Standalone Colors
490
+
491
+ The `name` override is a **debug label**, not an output key:
492
+
493
+ ```ts
494
+ const cardBg = glaze.color('#1a1a2e', {
495
+ name: 'card-bg', // surfaces in `console.warn` / Error messages
496
+ });
497
+
498
+ cardBg.token(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' }
499
+ cardBg.json(); // → { light: 'okhsl(...)', dark: 'okhsl(...)' }
500
+ cardBg.css({ name: 'card' }); // CSS variable name comes from `css({ name })`,
501
+ // NOT from the override above
502
+ ```
503
+
504
+ Use it to make warnings traceable when you have many `glaze.color()`
505
+ calls in a project — without it, `glaze` falls back to the internal
506
+ sentinel `"value"`:
507
+
508
+ ```ts
509
+ // With name:
510
+ // > glaze: color "card-bg" cannot meet contrast "AAA" (7.00) in dark scheme...
265
511
 
266
- accent.resolve(); // ResolvedColor with light/dark/lightContrast/darkContrast
267
- accent.token(); // { '': 'okhsl(...)', '@dark': 'okhsl(...)' } (tasty format)
268
- accent.tasty(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' } (same as token)
269
- accent.json(); // → { light: 'okhsl(...)', dark: 'okhsl(...)' }
512
+ // Without name:
513
+ // > glaze: color "value" cannot meet contrast "AAA" (7.00) in dark scheme...
270
514
  ```
271
515
 
272
- Standalone colors are always root colors (no `base`/`contrast`).
516
+ The reserved internal sentinels (`"value"`, `"seed"`, `"externalBase"`)
517
+ are rejected with a clear error pointing at the conflict.
518
+
519
+ ### Persisting Standalone Colors
520
+
521
+ `glaze.color()` tokens can be serialized to JSON-safe data and
522
+ rehydrated later — useful for color pickers, theme settings UIs, and
523
+ URL state.
524
+
525
+ ```ts
526
+ const text = glaze.color('#1a1a1a', {
527
+ contrast: 'AA',
528
+ opacity: 0.9,
529
+ name: 'profile-text',
530
+ });
531
+
532
+ const data = text.export(); // JSON-safe snapshot
533
+ const json = JSON.stringify(data); // ship to localStorage / API / URL
534
+ const restored = glaze.colorFrom(JSON.parse(json));
535
+ // `restored.resolve()` matches `text.resolve()` byte-for-byte.
536
+ ```
537
+
538
+ The export captures the original `value`, all overrides, and the
539
+ effective `scaling` (snapshotted from `globalConfig` at create time so
540
+ later `glaze.configure()` calls don't change exported tokens).
541
+ Token-typed `base` is recursively serialized, value-typed `base` is
542
+ preserved as the raw value.
543
+
544
+ Both forms round-trip:
545
+
546
+ ```ts
547
+ // Value form
548
+ const a = glaze.color('#26fcb2', { contrast: 'AA' });
549
+ const aBack = glaze.colorFrom(a.export());
550
+
551
+ // Structured form
552
+ const b = glaze.color({
553
+ hue: 280,
554
+ saturation: 50,
555
+ lightness: 50,
556
+ opacity: 0.5,
557
+ });
558
+ const bBack = glaze.colorFrom(b.export());
559
+ ```
273
560
 
274
561
  ## From Existing Colors
275
562
 
@@ -391,7 +678,10 @@ Available tuning parameters:
391
678
 
392
679
  ### Standalone Shadow Computation
393
680
 
394
- Compute a shadow outside of a theme:
681
+ Compute a shadow outside of a theme. `bg` and `fg` accept any
682
+ `GlazeColorValue`: hex (`#rgb` / `#rrggbb` / `#rrggbbaa`), `rgb()` /
683
+ `hsl()` / `okhsl()` / `oklch()` strings, OKHSL objects, or `[r, g, b]`
684
+ (0–255) tuples.
395
685
 
396
686
  ```ts
397
687
  const v = glaze.shadow({
@@ -401,6 +691,13 @@ const v = glaze.shadow({
401
691
  });
402
692
  // → { h: 280, s: 0.14, l: 0.2, alpha: 0.1 }
403
693
 
694
+ // Equivalent with non-hex inputs:
695
+ glaze.shadow({
696
+ bg: 'rgb(240 238 245)',
697
+ fg: { h: 280, s: 0.06, l: 0.13 },
698
+ intensity: 10,
699
+ });
700
+
404
701
  const css = glaze.format(v, 'oklch');
405
702
  // → 'oklch(0.15 0.014 280 / 0.1)'
406
703
  ```
@@ -565,7 +862,7 @@ theme.tokens({ format: 'hsl' }); // → 'hsl(270.5 45.2% 95.8%)'
565
862
  theme.tokens({ format: 'oklch' }); // → 'oklch(0.965 0.0123 280)'
566
863
  ```
567
864
 
568
- The `format` option works on all export methods: `theme.tokens()`, `theme.tasty()`, `theme.json()`, `theme.css()`, `palette.tokens()`, `palette.tasty()`, `palette.json()`, `palette.css()`, and standalone `glaze.color().token()` / `.tasty()` / `.json()`.
865
+ The `format` option works on all export methods: `theme.tokens()`, `theme.tasty()`, `theme.json()`, `theme.css()`, `palette.tokens()`, `palette.tasty()`, `palette.json()`, `palette.css()`, and standalone `glaze.color().token()` / `.tasty()` / `.json()` / `.css()`.
569
866
 
570
867
  Colors with `alpha < 1` (shadow colors, or regular colors with `opacity`) include an alpha component:
571
868
 
@@ -601,7 +898,7 @@ Modes control how colors adapt across schemes:
601
898
 
602
899
  ```ts
603
900
  // Light: surface L=97, text lightness='-52' → L=45 (dark text on light bg)
604
- // Dark: surface inverts to L≈14, sign flips → L=14+52=66
901
+ // Dark: surface inverts to L≈20 (Möbius curve), sign flips → L=20+52=72
605
902
  // contrast solver may push further (light text on dark bg)
606
903
  ```
607
904
 
@@ -618,14 +915,14 @@ Modes control how colors adapt across schemes:
618
915
 
619
916
  ### Lightness
620
917
 
621
- Root color lightness is mapped linearly within the configured `lightLightness` window:
918
+ Absolute lightness values (both root colors and dependent colors with absolute lightness) are mapped linearly within the configured `lightLightness` window:
622
919
 
623
920
  ```ts
624
921
  const [lo, hi] = lightLightness; // default: [10, 100]
625
922
  const mappedL = (lightness * (hi - lo)) / 100 + lo;
626
923
  ```
627
924
 
628
- Both `auto` and `fixed` modes use the same linear formula. `static` mode bypasses the mapping entirely.
925
+ Both `auto` and `fixed` modes use the same linear formula. `static` mode and high-contrast variants bypass the mapping entirely (identity: `mappedL = l`).
629
926
 
630
927
  | Color | Raw L | Mapped L (default [10, 100]) |
631
928
  |---|---|---|
@@ -637,24 +934,29 @@ Both `auto` and `fixed` modes use the same linear formula. `static` mode bypasse
637
934
 
638
935
  ### Lightness
639
936
 
640
- **`auto`** — inverted within the configured window:
937
+ **`auto`** — inverted with a Möbius transformation within the configured window:
641
938
 
642
939
  ```ts
643
940
  const [lo, hi] = darkLightness; // default: [15, 95]
644
- const invertedL = ((100 - lightness) * (hi - lo)) / 100 + lo;
941
+ const t = (100 - lightness) / 100;
942
+ const invertedL = lo + (hi - lo) * t / (t + darkCurve * (1 - t)); // darkCurve default: 0.5
645
943
  ```
646
944
 
647
- **`fixed`** mapped without inversion:
945
+ The `darkCurve` parameter (default `0.5`, range 0–1) controls how much the dark-mode inversion expands lightness deltas. Lower values produce stronger expansion; `1` gives linear (legacy) behavior. Accepts a `[normal, highContrast]` pair for separate HC tuning (e.g. `darkCurve: [0.5, 0.3]`); a single number applies to both. Unlike a power curve, the Möbius transformation provides **proportional expansion** — small and large deltas are scaled by similar ratios, preserving the visual hierarchy of the light theme.
946
+
947
+ **`fixed`** — mapped without inversion (not affected by `darkCurve`):
648
948
 
649
949
  ```ts
650
950
  const mappedL = (lightness * (hi - lo)) / 100 + lo;
651
951
  ```
652
952
 
653
- | Color | Light L | Auto (inverted) | Fixed (mapped) |
654
- |---|---|---|---|
655
- | surface (L=97) | 97 | 17.4 | 92.6 |
656
- | accent-fill (L=52) | 52 | 53.4 | 56.6 |
657
- | accent-text (L=100) | 100 | 15 | 95 |
953
+ | Color | Light L | Auto (curve=0.5) | Auto (curve=1, linear) | Fixed (mapped) |
954
+ |---|---|---|---|---|
955
+ | surface (L=97) | 97 | 19.7 | 17.4 | 92.6 |
956
+ | accent-fill (L=52) | 52 | 66.9 | 53.4 | 56.6 |
957
+ | accent-text (L=100) | 100 | 15 | 15 | 95 |
958
+
959
+ In high-contrast variants, the `darkLightness` window is bypassed — auto uses the Möbius curve over the full [0, 100] range, and fixed uses identity (`L`). To use a different curve shape for HC, pass a `[normal, hc]` pair to `darkCurve` (e.g. `darkCurve: [0.5, 0.3]`).
658
960
 
659
961
  ### Saturation
660
962
 
@@ -694,12 +996,21 @@ Combine multiple themes into a single palette:
694
996
  const palette = glaze.palette({ primary, danger, success, warning });
695
997
  ```
696
998
 
697
- ### Token Export
999
+ Optionally designate a primary theme at creation time:
1000
+
1001
+ ```ts
1002
+ const palette = glaze.palette(
1003
+ { primary, danger, success, warning },
1004
+ { primary: 'primary' },
1005
+ );
1006
+ ```
698
1007
 
699
- Tokens are grouped by scheme variant, with plain color names as keys:
1008
+ ### Prefix Behavior
1009
+
1010
+ Palette export methods (`tokens()`, `tasty()`, `css()`) default to `prefix: true` — all tokens are automatically prefixed with the theme name to avoid collisions:
700
1011
 
701
1012
  ```ts
702
- const tokens = palette.tokens({ prefix: true });
1013
+ const tokens = palette.tokens();
703
1014
  // → {
704
1015
  // light: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
705
1016
  // dark: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
@@ -712,15 +1023,68 @@ Custom prefix mapping:
712
1023
  palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
713
1024
  ```
714
1025
 
715
- ### Tasty Export (for [Tasty](https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs) style system)
1026
+ To disable prefixing entirely, pass `prefix: false` explicitly.
1027
+
1028
+ ### Collision Detection
1029
+
1030
+ When two themes produce the same output key (via `prefix: false`, custom prefix maps, or primary unprefixed aliases), the first-written value wins and a `console.warn` is emitted:
1031
+
1032
+ ```ts
1033
+ const palette = glaze.palette({ a, b });
1034
+ palette.tokens({ prefix: false });
1035
+ // ⚠ glaze: token "surface" from theme "b" collides with theme "a" — skipping.
1036
+ ```
1037
+
1038
+ ### Primary Theme
1039
+
1040
+ The primary theme's tokens are duplicated without prefix, providing convenient short aliases alongside the prefixed versions. Set at palette creation to apply to all exports automatically:
1041
+
1042
+ ```ts
1043
+ const palette = glaze.palette(
1044
+ { primary, danger, success },
1045
+ { primary: 'primary' },
1046
+ );
1047
+ const tokens = palette.tokens();
1048
+ // → {
1049
+ // light: {
1050
+ // 'primary-surface': 'okhsl(...)', // prefixed (all themes)
1051
+ // 'danger-surface': 'okhsl(...)',
1052
+ // 'success-surface': 'okhsl(...)',
1053
+ // 'surface': 'okhsl(...)', // unprefixed alias (primary only)
1054
+ // },
1055
+ // }
1056
+ ```
1057
+
1058
+ Override or disable per-export:
1059
+
1060
+ ```ts
1061
+ palette.tokens({ primary: 'danger' }); // use danger as primary for this call
1062
+ palette.tokens({ primary: false }); // no primary for this call
1063
+ ```
1064
+
1065
+ The `primary` option works on `tokens()`, `tasty()`, and `css()`. It combines with any prefix mode — when using a custom prefix map, primary tokens are still duplicated without prefix:
716
1066
 
717
- The `tasty()` method exports tokens in the [Tasty](https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs) style-to-state binding format — `#name` color token keys with state aliases (`''`, `@dark`, etc.):
1067
+ ```ts
1068
+ palette.tokens({ prefix: { primary: 'p-', danger: 'd-' } });
1069
+ // → 'p-surface' + 'surface' (alias from palette-level primary) + 'd-surface'
1070
+ ```
1071
+
1072
+ An error is thrown if the primary name doesn't match any theme in the palette.
1073
+
1074
+ ### Tasty Export (for [Tasty](https://tasty.style) style system)
1075
+
1076
+ The `tasty()` method exports tokens in the [Tasty](https://tasty.style/docs) style-to-state binding format — `#name` color token keys with state aliases (`''`, `@dark`, etc.). See the [Playground](https://tasty.style/playground) for live examples of Glaze integration:
718
1077
 
719
1078
  ```ts
720
- const tastyTokens = palette.tasty({ prefix: true });
1079
+ const palette = glaze.palette(
1080
+ { primary, danger, success },
1081
+ { primary: 'primary' },
1082
+ );
1083
+ const tastyTokens = palette.tasty();
721
1084
  // → {
722
1085
  // '#primary-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
723
1086
  // '#danger-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
1087
+ // '#surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' }, // alias
724
1088
  // }
725
1089
  ```
726
1090
 
@@ -787,8 +1151,10 @@ palette.tasty({ states: { dark: '@dark', highContrast: '@hc' } });
787
1151
 
788
1152
  ### JSON Export (Framework-Agnostic)
789
1153
 
1154
+ JSON export groups by theme name (no prefix needed):
1155
+
790
1156
  ```ts
791
- const data = palette.json({ prefix: true });
1157
+ const data = palette.json();
792
1158
  // → {
793
1159
  // primary: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
794
1160
  // danger: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
@@ -810,7 +1176,11 @@ const css = theme.css();
810
1176
  Use in a stylesheet:
811
1177
 
812
1178
  ```ts
813
- const css = palette.css({ prefix: true });
1179
+ const palette = glaze.palette(
1180
+ { primary, danger, success },
1181
+ { primary: 'primary' },
1182
+ );
1183
+ const css = palette.css();
814
1184
 
815
1185
  const stylesheet = `
816
1186
  :root { ${css.light} }
@@ -826,7 +1196,8 @@ Options:
826
1196
  |---|---|---|
827
1197
  | `format` | `'rgb'` | Color format (`'rgb'`, `'hsl'`, `'okhsl'`, `'oklch'`) |
828
1198
  | `suffix` | `'-color'` | Suffix appended to each CSS property name |
829
- | `prefix` | | (palette only) Same prefix behavior as `tokens()` |
1199
+ | `prefix` | `true` (palette) | (palette only) `true` uses `"<themeName>-"`, or provide a custom map |
1200
+ | `primary` | inherited | (palette only) Override or disable (`false`) the palette-level primary for this call |
830
1201
 
831
1202
  ```ts
832
1203
  // Custom suffix
@@ -837,9 +1208,9 @@ theme.css({ suffix: '' });
837
1208
  theme.css({ format: 'hsl' });
838
1209
  // → "--surface-color: hsl(...);"
839
1210
 
840
- // Palette with prefix
841
- palette.css({ prefix: true });
842
- // → "--primary-surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
1211
+ // Palette with primary (inherited from palette creation)
1212
+ palette.css();
1213
+ // → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
843
1214
  ```
844
1215
 
845
1216
  ## Output Modes
@@ -872,9 +1243,10 @@ Resolution priority (highest first):
872
1243
 
873
1244
  ```ts
874
1245
  glaze.configure({
875
- lightLightness: [10, 100], // Light scheme lightness window [lo, hi]
876
- darkLightness: [15, 95], // Dark scheme lightness window [lo, hi]
1246
+ lightLightness: [10, 100], // Light scheme lightness window [lo, hi] (bypassed in HC)
1247
+ darkLightness: [15, 95], // Dark scheme lightness window [lo, hi] (bypassed in HC)
877
1248
  darkDesaturation: 0.1, // Saturation reduction in dark scheme (0–1)
1249
+ darkCurve: 0.5, // Möbius beta for dark auto-inversion (0–1); or [normal, hc] pair
878
1250
  states: {
879
1251
  dark: '@dark', // State alias for dark mode tokens
880
1252
  highContrast: '@high-contrast',
@@ -1009,18 +1381,21 @@ const success = primary.extend({ hue: 157 });
1009
1381
  const warning = primary.extend({ hue: 84 });
1010
1382
  const note = primary.extend({ hue: 302 });
1011
1383
 
1012
- const palette = glaze.palette({ primary, danger, success, warning, note });
1384
+ const palette = glaze.palette(
1385
+ { primary, danger, success, warning, note },
1386
+ { primary: 'primary' },
1387
+ );
1013
1388
 
1014
- // Export as flat token map grouped by variant
1015
- const tokens = palette.tokens({ prefix: true });
1016
- // tokens.light → { 'primary-surface': 'okhsl(...)', 'primary-shadow-md': 'okhsl(... / 0.1)' }
1389
+ // Export as flat token map grouped by variant (prefix defaults to true)
1390
+ const tokens = palette.tokens();
1391
+ // tokens.light → { 'primary-surface': '...', 'surface': '...', 'danger-surface': '...' }
1017
1392
 
1018
1393
  // Export as tasty style-to-state bindings (for Tasty style system)
1019
- const tastyTokens = palette.tasty({ prefix: true });
1394
+ const tastyTokens = palette.tasty();
1020
1395
 
1021
1396
  // Export as CSS custom properties (rgb format by default)
1022
- const css = palette.css({ prefix: true });
1023
- // css.light → "--primary-surface-color: rgb(...);\n--primary-shadow-md-color: rgb(... / 0.1);"
1397
+ const css = palette.css();
1398
+ // css.light → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
1024
1399
 
1025
1400
  // Standalone shadow computation
1026
1401
  const v = glaze.shadow({ bg: '#f0eef5', fg: '#1a1a2e', intensity: 10 });
@@ -1047,8 +1422,10 @@ brand.colors({ surface: { lightness: 97 }, text: { base: 'surface', lightness: '
1047
1422
  | `glaze.from(data)` | Create a theme from an exported configuration |
1048
1423
  | `glaze.fromHex(hex)` | Create a theme from a hex color (`#rgb` or `#rrggbb`) |
1049
1424
  | `glaze.fromRgb(r, g, b)` | Create a theme from RGB values (0–255) |
1050
- | `glaze.color(input)` | Create a standalone color token |
1051
- | `glaze.shadow(input)` | Compute a standalone shadow color (returns `ResolvedColorVariant`) |
1425
+ | `glaze.color(input, scaling?)` | Create a standalone color token from `{ hue, saturation, lightness, opacity?, contrast?, base?, name?, ... }`. Optional `scaling` overrides the lightness windows |
1426
+ | `glaze.color(value, overrides?, scaling?)` | Create a standalone color token from a hex string (3/6/8 digits), an `rgb()` / `hsl()` / `okhsl()` / `oklch()` string, an `{ h, s, l }` OKHSL object, or an `[r, g, b]` (0–255) tuple. Overrides accept absolute or relative `hue` / `lightness`, `saturation`, `mode`, `contrast`, `opacity`, `name`, and `base` (a `GlazeColorToken` or any `GlazeColorValue`; raw values are auto-wrapped). When `base` is set, `contrast` and relative `lightness` are anchored to the base per scheme — see [Pairing Colors](#pairing-colors). String inputs default to `mode: 'auto'` with the dark window extended to upper `100`; object / tuple inputs default to `mode: 'fixed'`. |
1427
+ | `glaze.colorFrom(data)` | Rehydrate a `glaze.color()` token from a `.export()` snapshot. Inverse of `token.export()` — see [Persisting Standalone Colors](#persisting-standalone-colors) |
1428
+ | `glaze.shadow(input)` | Compute a standalone shadow color (returns `ResolvedColorVariant`). `bg` / `fg` accept any `GlazeColorValue` form |
1052
1429
  | `glaze.format(variant, format?)` | Format any `ResolvedColorVariant` as a CSS string |
1053
1430
 
1054
1431
  ### Theme Methods
@@ -1066,7 +1443,7 @@ brand.colors({ surface: { lightness: 97 }, text: { base: 'surface', lightness: '
1066
1443
  | `theme.extend(options)` | Create a child theme |
1067
1444
  | `theme.resolve()` | Resolve all colors |
1068
1445
  | `theme.tokens(options?)` | Export as flat token map grouped by variant |
1069
- | `theme.tasty(options?)` | Export as [Tasty](https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs) style-to-state bindings |
1446
+ | `theme.tasty(options?)` | Export as [Tasty](https://tasty.style/docs) style-to-state bindings |
1070
1447
  | `theme.json(options?)` | Export as plain JSON |
1071
1448
  | `theme.css(options?)` | Export as CSS custom property declarations |
1072
1449
 
@@ -1075,7 +1452,7 @@ brand.colors({ surface: { lightness: 97 }, text: { base: 'surface', lightness: '
1075
1452
  | Method | Description |
1076
1453
  |---|---|
1077
1454
  | `glaze.configure(config)` | Set global configuration |
1078
- | `glaze.palette(themes)` | Compose themes into a palette |
1455
+ | `glaze.palette(themes, options?)` | Compose themes into a palette (options: `{ primary? }`) |
1079
1456
  | `glaze.getConfig()` | Get current global config |
1080
1457
  | `glaze.resetConfig()` | Reset to defaults |
1081
1458