@tenphi/glaze 0.0.0-snapshot.99c649d → 0.0.0-snapshot.9dc79fc

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
@@ -5,7 +5,7 @@
5
5
  <h1 align="center">Glaze</h1>
6
6
 
7
7
  <p align="center">
8
- OKHSL-based color theme generator with WCAG contrast solving
8
+ OKHST-based color theme generator with WCAG and APCA contrast solving
9
9
  </p>
10
10
 
11
11
  <p align="center">
@@ -16,18 +16,32 @@
16
16
 
17
17
  ---
18
18
 
19
- Glaze generates robust **light**, **dark**, and **high-contrast** color schemes from a single hue/saturation seed. It preserves WCAG contrast ratios for UI color pairs via explicit dependency declarations — no hidden role math, no magic multipliers.
19
+ Glaze generates **light**, **dark**, and **high-contrast** color schemes from a
20
+ single hue/saturation seed. Relationships stay explicit: colors either define
21
+ an absolute tone or depend on a named base through a tone delta and an optional
22
+ contrast floor.
20
23
 
21
24
  ## Features
22
25
 
23
- - **OKHSL color space** — perceptually uniform hue and saturation
24
- - **WCAG 2 contrast solving** — automatic lightness adjustment to meet AA/AAA targets
25
- - **Shadow colors** — OKHSL-native shadow computation with automatic alpha, fg/bg tinting, and per-scheme adaptation
26
+ - **OKHST color space** — OKHSL with a contrast-shaped **tone** axis. Equal
27
+ steps give equal WCAG contrast for neutrals and a useful approximation for
28
+ chromatic colors. See [Tone and OKHST](docs/okhst.mdx) and the
29
+ [full OKHST specification](https://github.com/tenphi/okhst).
30
+ - **WCAG 2 + APCA contrast solving** — automatic tone adjustment to meet a WCAG ratio or APCA Lc floor
31
+ - **Unified dark mode** — one tone space for light, dark, and high-contrast; dark is a single `100 − t` inversion, no fitted curve
32
+ - **Mix colors** — blend two colors with OKHSL or sRGB interpolation, opaque or transparent, with optional contrast solving
33
+ - **Shadows and glows** — translucent overlays whose opacity is solved per surface, so `intensity` is an exact tone step wherever the surface has room for it (a step past black or white is pinned and warns), with object-aware strength
34
+ - **HDR glows** — set `glowTuning.headroom` and a glow whose step passes white also gets an exact HDR value: `color(srgb-linear …)` in CSS, an exposure in DTCG for native apps, exported when you turn on `modes.hdr`
26
35
  - **Light + Dark + High-Contrast** — all schemes from one definition
36
+ - **Manual contrast level** — put your normal palette on a `0–100` slider between itself and the high-contrast one; every level in between is solved, not approximated, and the high-contrast tier stays available on top
27
37
  - **Per-color hue override** — absolute or relative hue shifts within a theme
28
- - **Multi-format output** — `okhsl`, `rgb`, `hsl`, `oklch` with modern CSS space syntax
38
+ - **Multi-format output** — native `rgb`, `hsl`, and `oklch`, plus
39
+ [Tasty](https://tasty.style)-compatible `okhsl` and `okhst`
29
40
  - **CSS custom properties export** — ready-to-use `--var: value;` declarations per scheme
30
- - **Import/Export** — serialize and restore theme configurations
41
+ - **W3C DTCG export** — spec-conformant `.tokens.json` (2025.10) for Figma, Tokens Studio, Style Dictionary, and every DTCG tool
42
+ - **W3C DTCG Resolver-Module export** — opt-in single-document `dtcgResolver()` (sets + a `scheme` modifier with a context per variant) for resolver tools such as Dispersa
43
+ - **Tailwind CSS v4 export** — `@theme` block + dark / high-contrast overrides
44
+ - **Import/Export** — serialize and restore themes, color tokens, and entire palettes as JSON
31
45
  - **Create from hex/RGB** — start from an existing brand color
32
46
  - **Zero dependencies** — pure math, runs anywhere (Node.js, browser, edge)
33
47
  - **Tree-shakeable ESM + CJS** — dual-format package
@@ -52,879 +66,112 @@ yarn add @tenphi/glaze
52
66
  ```ts
53
67
  import { glaze } from '@tenphi/glaze';
54
68
 
55
- // Create a theme from a hue (0–360) and saturation (0–100)
56
- const primary = glaze(280, 80);
57
-
58
- // Define colors with explicit lightness and contrast relationships
59
- primary.colors({
60
- surface: { lightness: 97, saturation: 0.75 },
61
- text: { base: 'surface', lightness: '-52', contrast: 'AAA' },
62
- border: { base: 'surface', lightness: ['-7', '-20'], contrast: 'AA-large' },
63
- 'accent-fill': { lightness: 52, mode: 'fixed' },
64
- 'accent-text': { base: 'accent-fill', lightness: '+48', contrast: 'AA', mode: 'fixed' },
65
- });
66
-
67
- // Create status themes by rotating the hue
68
- const danger = primary.extend({ hue: 23 });
69
- const success = primary.extend({ hue: 157 });
70
-
71
- // Compose into a palette and export
72
- const palette = glaze.palette({ primary, danger, success });
73
- const tokens = palette.tokens({ prefix: true });
74
- // → { light: { 'primary-surface': 'okhsl(...)', ... }, dark: { 'primary-surface': 'okhsl(...)', ... } }
75
- ```
76
-
77
- ## Core Concepts
78
-
79
- ### One Theme = One Hue Family
80
-
81
- A single `glaze` theme is tied to one hue/saturation seed. Status colors (danger, success, warning) are derived via `extend`, which inherits all color definitions and replaces the seed.
82
-
83
- Individual colors can override the hue via the `hue` prop (see [Per-Color Hue Override](#per-color-hue-override)), but the primary purpose of a theme is to scope colors with the same hue.
84
-
85
- ### Color Definitions
86
-
87
- Every color is defined explicitly. No implicit roles — every value is stated.
88
-
89
- #### Root Colors (explicit position)
90
-
91
- ```ts
92
- primary.colors({
93
- surface: { lightness: 97, saturation: 0.75 },
94
- border: { lightness: 90, saturation: 0.20 },
95
- });
96
- ```
97
-
98
- - `lightness` — lightness in the light scheme (0–100)
99
- - `saturation` — saturation factor applied to the seed saturation (0–1, default: `1`)
100
-
101
- #### Dependent Colors (relative to base)
102
-
103
- ```ts
104
- primary.colors({
105
- surface: { lightness: 97, saturation: 0.75 },
106
- text: { base: 'surface', lightness: '-52', contrast: 'AAA' },
107
- });
108
- ```
109
-
110
- - `base` — name of another color in the same theme
111
- - `lightness` — position of this color (see [Lightness Values](#lightness-values))
112
- - `contrast` — ensures the WCAG contrast ratio meets a target floor against the base
113
-
114
- ### Lightness Values
115
-
116
- The `lightness` prop accepts two forms:
117
-
118
- | Form | Example | Meaning |
119
- |---|---|---|
120
- | Number (absolute) | `lightness: 45` | Absolute lightness 0–100 |
121
- | String (relative) | `lightness: '-52'` | Relative to base color's lightness |
122
-
123
- **Absolute lightness** on a dependent color (with `base`) positions the color independently. In dark mode, it is dark-mapped on its own. The `contrast` WCAG solver acts as a safety net.
124
-
125
- **Relative lightness** applies a signed delta to the base color's resolved lightness. In dark mode with `auto` adaptation, the sign flips automatically.
126
-
127
- ```ts
128
- // Relative: 97 - 52 = 45 in light mode
129
- 'text': { base: 'surface', lightness: '-52' }
130
-
131
- // Absolute: lightness 45 in light mode, dark-mapped independently
132
- 'text': { base: 'surface', lightness: 45 }
133
- ```
134
-
135
- A dependent color with `base` but no `lightness` inherits the base's lightness (equivalent to a delta of 0).
136
-
137
- ### Per-Color Hue Override
138
-
139
- Individual colors can override the theme's hue. The `hue` prop accepts:
140
-
141
- | Form | Example | Meaning |
142
- |---|---|---|
143
- | Number (absolute) | `hue: 120` | Absolute hue 0–360 |
144
- | String (relative) | `hue: '+20'` | Relative to the **theme seed** hue |
145
-
146
- **Important:** Relative hue is always relative to the **theme seed hue**, not to a base color's hue.
147
-
148
- ```ts
149
- const theme = glaze(280, 80);
150
- theme.colors({
151
- surface: { lightness: 97 },
152
- // Gradient end — slight hue shift from seed (280 + 20 = 300)
153
- gradientEnd: { lightness: 90, hue: '+20' },
154
- // Entirely different hue
155
- warning: { lightness: 60, hue: 40 },
69
+ glaze.configure({
70
+ modes: { dark: true, highContrast: true },
156
71
  });
157
- ```
158
-
159
- ### contrast (WCAG Floor)
160
-
161
- Ensures the WCAG contrast ratio meets a target floor. Accepts a numeric ratio or a preset string:
162
72
 
163
- ```ts
164
- type MinContrast = number | 'AA' | 'AAA' | 'AA-large' | 'AAA-large';
165
- ```
166
-
167
- | Preset | Ratio |
168
- |---|---|
169
- | `'AA'` | 4.5 |
170
- | `'AAA'` | 7 |
171
- | `'AA-large'` | 3 |
172
- | `'AAA-large'` | 4.5 |
173
-
174
- You can also pass any numeric ratio directly (e.g., `contrast: 4.5`, `contrast: 7`, `contrast: 11`).
175
-
176
- The constraint is applied independently for each scheme. If the `lightness` already satisfies the floor, it's kept. Otherwise, the solver adjusts lightness until the target is met.
177
-
178
- ### High-Contrast via Array Values
179
-
180
- `lightness` and `contrast` accept a `[normal, high-contrast]` pair:
181
-
182
- ```ts
183
- 'border': { base: 'surface', lightness: ['-7', '-20'], contrast: 'AA-large' }
184
- // ↑ ↑
185
- // normal high-contrast
186
- ```
187
-
188
- A single value applies to both modes. All control is local and explicit.
189
-
190
- ```ts
191
- 'text': { base: 'surface', lightness: '-52', contrast: 'AAA' }
192
- 'border': { base: 'surface', lightness: ['-7', '-20'], contrast: 'AA-large' }
193
- 'muted': { base: 'surface', lightness: ['-35', '-50'], contrast: ['AA-large', 'AA'] }
194
- ```
195
-
196
- ## Theme Color Management
197
-
198
- ### Adding Colors
199
-
200
- `.colors(defs)` performs an **additive merge** — it adds new colors and overwrites existing ones by name, but does not remove other colors:
201
-
202
- ```ts
203
73
  const theme = glaze(280, 80);
204
- theme.colors({ surface: { lightness: 97 } });
205
- theme.colors({ text: { lightness: 30 } });
206
- // Both 'surface' and 'text' are now defined
207
- ```
208
-
209
- ### Single Color Getter/Setter
210
-
211
- `.color(name)` returns the definition, `.color(name, def)` sets it:
212
-
213
- ```ts
214
- theme.color('surface', { lightness: 97, saturation: 0.75 }); // set
215
- const def = theme.color('surface'); // get → { lightness: 97, saturation: 0.75 }
216
- ```
217
-
218
- ### Removing Colors
219
-
220
- `.remove(name)` or `.remove([name1, name2])` deletes color definitions:
221
-
222
- ```ts
223
- theme.remove('surface');
224
- theme.remove(['text', 'border']);
225
- ```
226
-
227
- ### Introspection
228
-
229
- ```ts
230
- theme.has('surface'); // → true/false
231
- theme.list(); // → ['surface', 'text', 'border', ...]
232
- ```
233
-
234
- ### Clearing All Colors
235
-
236
- ```ts
237
- theme.reset(); // removes all color definitions
238
- ```
239
-
240
- ## Import / Export
241
-
242
- Serialize a theme's configuration (hue, saturation, color definitions) to a plain JSON-safe object, and restore it later:
243
-
244
- ```ts
245
- // Export
246
- const snapshot = theme.export();
247
- // → { hue: 280, saturation: 80, colors: { surface: { lightness: 97, saturation: 0.75 }, ... } }
248
-
249
- const jsonString = JSON.stringify(snapshot);
250
-
251
- // Import
252
- const restored = glaze.from(JSON.parse(jsonString));
253
- // restored is a fully functional GlazeTheme
254
- ```
255
74
 
256
- The export contains only the configuration — not resolved color values. Resolved values are recomputed on demand.
257
-
258
- ## Standalone Color Token
259
-
260
- Create a single color token without a full theme:
261
-
262
- ```ts
263
- const accent = glaze.color({ hue: 280, saturation: 80, lightness: 52, mode: 'fixed' });
264
-
265
- accent.resolve(); // → ResolvedColor with light/dark/lightContrast/darkContrast
266
- accent.token(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' } (tasty format)
267
- accent.tasty(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' } (same as token)
268
- accent.json(); // → { light: 'okhsl(...)', dark: 'okhsl(...)' }
269
- ```
270
-
271
- Standalone colors are always root colors (no `base`/`contrast`).
272
-
273
- ## From Existing Colors
274
-
275
- Create a theme from an existing brand color by extracting its OKHSL hue and saturation:
276
-
277
- ```ts
278
- // From hex
279
- const brand = glaze.fromHex('#7a4dbf');
280
-
281
- // From RGB (0–255)
282
- const brand = glaze.fromRgb(122, 77, 191);
283
- ```
284
-
285
- The resulting theme has the extracted hue and saturation. Add colors as usual:
286
-
287
- ```ts
288
- brand.colors({
289
- surface: { lightness: 97, saturation: 0.75 },
290
- text: { base: 'surface', lightness: '-52', contrast: 'AAA' },
291
- });
292
- ```
293
-
294
- ## Shadow Colors
295
-
296
- Shadow colors are colors with computed alpha. Instead of a parallel shadow system, they extend the existing color pipeline. All math is done natively in OKHSL.
297
-
298
- ### Defining Shadow Colors
299
-
300
- Shadow colors use `type: 'shadow'` and reference a `bg` (background) color and optionally an `fg` (foreground) color for tinting and intensity modulation:
301
-
302
- ```ts
303
- theme.colors({
304
- surface: { lightness: 95 },
305
- text: { base: 'surface', lightness: '-52', contrast: 'AAA' },
306
-
307
- 'shadow-sm': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 5 },
308
- 'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
309
- 'shadow-lg': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 20 },
310
- });
311
- ```
312
-
313
- Shadow colors are included in all output methods (`tokens()`, `tasty()`, `css()`, `json()`) alongside regular colors:
314
-
315
- ```ts
316
- theme.tokens({ format: 'oklch' });
317
- // light: { 'shadow-md': 'oklch(0.15 0.009 282 / 0.1)', ... }
318
- // dark: { 'shadow-md': 'oklch(0.06 0.004 0 / 0.49)', ... }
319
- ```
320
-
321
- ### How Shadows Work
322
-
323
- The shadow algorithm computes a dark, low-saturation pigment color and an alpha value that produces the desired visual intensity:
324
-
325
- 1. **Contrast weight** — when `fg` is provided, shadow strength scales with `|l_bg - l_fg|`. Dark text on a light background produces a strong shadow; near-background-lightness elements produce barely visible shadows.
326
- 2. **Pigment color** — hue blended between fg and bg, low saturation, dark lightness.
327
- 3. **Alpha** — computed via a `tanh` curve that saturates smoothly toward `alphaMax` (default 0.6), ensuring well-separated shadow levels even on dark backgrounds.
328
-
329
- ### Achromatic Shadows
330
-
331
- Omit `fg` for a pure achromatic shadow at full user-specified intensity:
332
-
333
- ```ts
334
- theme.colors({
335
- 'drop-shadow': { type: 'shadow', bg: 'surface', intensity: 12 },
336
- });
337
- ```
338
-
339
- ### High-Contrast Intensity
340
-
341
- `intensity` supports `[normal, highContrast]` pairs:
342
-
343
- ```ts
344
- theme.colors({
345
- 'shadow-card': { type: 'shadow', bg: 'surface', fg: 'text', intensity: [10, 20] },
346
- });
347
- ```
348
-
349
- ### Fixed Opacity (Regular Colors)
350
-
351
- For a simple fixed-alpha color (no shadow algorithm), use `opacity` on a regular color:
352
-
353
- ```ts
354
75
  theme.colors({
355
- overlay: { lightness: 0, opacity: 0.5 },
356
- });
357
- // → 'oklch(0 0 0 / 0.5)'
358
- ```
359
-
360
- ### Shadow Tuning
361
-
362
- Fine-tune shadow behavior per-color or globally:
363
-
364
- ```ts
365
- // Per-color tuning
366
- theme.colors({
367
- 'shadow-soft': {
368
- type: 'shadow', bg: 'surface', intensity: 10,
369
- tuning: { alphaMax: 0.3, saturationFactor: 0.1 },
76
+ surface: { tone: 97, saturation: 0.15 },
77
+ 'surface-text': {
78
+ base: 'surface',
79
+ tone: '-1',
80
+ saturation: 0.4,
81
+ contrast: { apca: ['content', 'body'] },
370
82
  },
371
- });
372
-
373
- // Global tuning
374
- glaze.configure({
375
- shadowTuning: { alphaMax: 0.5, bgHueBlend: 0.3 },
376
- });
377
- ```
378
-
379
- Available tuning parameters:
380
-
381
- | Parameter | Default | Description |
382
- |---|---|---|
383
- | `saturationFactor` | 0.18 | Fraction of fg saturation kept in pigment |
384
- | `maxSaturation` | 0.25 | Upper clamp on pigment saturation |
385
- | `lightnessFactor` | 0.25 | Multiplier for bg lightness to pigment lightness |
386
- | `lightnessBounds` | [0.05, 0.20] | Clamp range for pigment lightness |
387
- | `minGapTarget` | 0.05 | Target minimum gap between pigment and bg lightness |
388
- | `alphaMax` | 0.6 | Asymptotic maximum alpha |
389
- | `bgHueBlend` | 0.2 | Blend weight pulling pigment hue toward bg hue |
390
-
391
- ### Standalone Shadow Computation
392
-
393
- Compute a shadow outside of a theme:
394
-
395
- ```ts
396
- const v = glaze.shadow({
397
- bg: '#f0eef5',
398
- fg: '#1a1a2e',
399
- intensity: 10,
400
- });
401
- // → { h: 280, s: 0.14, l: 0.2, alpha: 0.1 }
402
-
403
- const css = glaze.format(v, 'oklch');
404
- // → 'oklch(0.15 0.014 280 / 0.1)'
405
- ```
406
-
407
- ### Consuming in CSS
408
-
409
- ```css
410
- .card {
411
- box-shadow: 0 2px 6px var(--shadow-sm-color),
412
- 0 8px 24px var(--shadow-md-color);
413
- }
414
- ```
415
-
416
- ## Output Formats
417
-
418
- Control the color format in exports with the `format` option:
419
-
420
- ```ts
421
- // Default: OKHSL
422
- theme.tokens(); // → 'okhsl(280 60% 97%)'
423
-
424
- // RGB (modern space syntax, rounded integers)
425
- theme.tokens({ format: 'rgb' }); // → 'rgb(244 240 250)'
426
-
427
- // HSL (modern space syntax)
428
- theme.tokens({ format: 'hsl' }); // → 'hsl(270.5 45.2% 95.8%)'
429
-
430
- // OKLCH
431
- theme.tokens({ format: 'oklch' }); // → 'oklch(0.965 0.0123 280)'
432
- ```
433
-
434
- 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()`.
435
-
436
- Colors with `alpha < 1` (shadow colors, or regular colors with `opacity`) include an alpha component:
437
-
438
- ```ts
439
- // → 'oklch(0.15 0.009 282 / 0.1)'
440
- // → 'rgb(34 28 42 / 0.1)'
441
- ```
442
-
443
- Available formats:
444
-
445
- | Format | Output (alpha = 1) | Output (alpha < 1) | Notes |
446
- |---|---|---|---|
447
- | `'okhsl'` (default) | `okhsl(H S% L%)` | `okhsl(H S% L% / A)` | Native format, not a CSS function |
448
- | `'rgb'` | `rgb(R G B)` | `rgb(R G B / A)` | Rounded integers, space syntax |
449
- | `'hsl'` | `hsl(H S% L%)` | `hsl(H S% L% / A)` | Modern space syntax |
450
- | `'oklch'` | `oklch(L C H)` | `oklch(L C H / A)` | OKLab-based LCH |
451
-
452
- All numeric output strips trailing zeros for cleaner CSS (e.g., `95` not `95.0`).
453
-
454
- ## Adaptation Modes
455
-
456
- Modes control how colors adapt across schemes:
457
-
458
- | Mode | Behavior |
459
- |---|---|
460
- | `'auto'` (default) | Full adaptation. Light ↔ dark inversion. High-contrast boost. |
461
- | `'fixed'` | Color stays recognizable. Only safety corrections. For brand buttons, CTAs. |
462
- | `'static'` | No adaptation. Same value in every scheme. |
463
-
464
- ### How Relative Lightness Adapts
465
-
466
- **`auto` mode** — relative lightness sign flips in dark scheme:
467
-
468
- ```ts
469
- // Light: surface L=97, text lightness='-52' → L=45 (dark text on light bg)
470
- // Dark: surface inverts to L≈14, sign flips → L=14+52=66
471
- // contrast solver may push further (light text on dark bg)
472
- ```
473
-
474
- **`fixed` mode** — lightness is mapped (not inverted), relative sign preserved:
475
-
476
- ```ts
477
- // Light: accent-fill L=52, accent-text lightness='+48' → L=100 (white on brand)
478
- // Dark: accent-fill maps to L≈51.6, sign preserved → L≈99.6
479
- ```
480
-
481
- **`static` mode** — no adaptation, same value in every scheme.
482
-
483
- ## Light Scheme Mapping
484
-
485
- ### Lightness
486
-
487
- Root color lightness is mapped linearly within the configured `lightLightness` window:
488
-
489
- ```ts
490
- const [lo, hi] = lightLightness; // default: [10, 100]
491
- const mappedL = (lightness * (hi - lo)) / 100 + lo;
492
- ```
493
-
494
- Both `auto` and `fixed` modes use the same linear formula. `static` mode bypasses the mapping entirely.
495
-
496
- | Color | Raw L | Mapped L (default [10, 100]) |
497
- |---|---|---|
498
- | surface (L=97) | 97 | 97.3 |
499
- | accent-fill (L=52) | 52 | 56.8 |
500
- | near-black (L=0) | 0 | 10 |
501
-
502
- ## Dark Scheme Mapping
503
-
504
- ### Lightness
505
-
506
- **`auto`** — inverted within the configured window:
507
-
508
- ```ts
509
- const [lo, hi] = darkLightness; // default: [15, 95]
510
- const invertedL = ((100 - lightness) * (hi - lo)) / 100 + lo;
511
- ```
512
-
513
- **`fixed`** — mapped without inversion:
514
-
515
- ```ts
516
- const mappedL = (lightness * (hi - lo)) / 100 + lo;
517
- ```
518
-
519
- | Color | Light L | Auto (inverted) | Fixed (mapped) |
520
- |---|---|---|---|
521
- | surface (L=97) | 97 | 17.4 | 92.6 |
522
- | accent-fill (L=52) | 52 | 53.4 | 56.6 |
523
- | accent-text (L=100) | 100 | 15 | 95 |
524
-
525
- ### Saturation
526
-
527
- `darkDesaturation` reduces saturation for all colors in dark scheme:
528
-
529
- ```ts
530
- S_dark = S_light * (1 - darkDesaturation) // default: 0.1
531
- ```
532
-
533
- ## Inherited Themes (`extend`)
534
-
535
- `extend` creates a new theme inheriting all color definitions, replacing the hue and/or saturation seed:
536
-
537
- ```ts
538
- const primary = glaze(280, 80);
539
- primary.colors({ /* ... */ });
540
-
541
- const danger = primary.extend({ hue: 23 });
542
- const success = primary.extend({ hue: 157 });
543
- const warning = primary.extend({ hue: 84 });
544
- ```
545
-
546
- Override individual colors (additive merge):
547
-
548
- ```ts
549
- const danger = primary.extend({
550
- hue: 23,
551
- colors: { 'accent-fill': { lightness: 48, mode: 'fixed' } },
552
- });
553
- ```
554
-
555
- ## Palette Composition
556
-
557
- Combine multiple themes into a single palette:
558
-
559
- ```ts
560
- const palette = glaze.palette({ primary, danger, success, warning });
561
- ```
562
-
563
- ### Token Export
564
-
565
- Tokens are grouped by scheme variant, with plain color names as keys:
566
-
567
- ```ts
568
- const tokens = palette.tokens({ prefix: true });
569
- // → {
570
- // light: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
571
- // dark: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
572
- // }
573
- ```
574
-
575
- Custom prefix mapping:
576
-
577
- ```ts
578
- palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
579
- ```
580
-
581
- ### Tasty Export (for [Tasty](https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs) style system)
582
-
583
- 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.):
584
-
585
- ```ts
586
- const tastyTokens = palette.tasty({ prefix: true });
587
- // → {
588
- // '#primary-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
589
- // '#danger-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
590
- // }
591
- ```
592
-
593
- Apply as global styles to make color tokens available app-wide:
594
-
595
- ```ts
596
- import { useGlobalStyles } from '@cube-dev/ui-kit';
597
-
598
- // In your root component
599
- useGlobalStyles('body', tastyTokens);
600
- ```
601
-
602
- For zero-runtime builds, use `tastyStatic` to generate the CSS at build time:
603
-
604
- ```ts
605
- import { tastyStatic } from '@cube-dev/ui-kit';
606
-
607
- tastyStatic('body', tastyTokens);
608
- ```
609
-
610
- Alternatively, register as a recipe via `configure()`:
611
-
612
- ```ts
613
- import { configure, tasty } from '@cube-dev/ui-kit';
614
-
615
- configure({
616
- recipes: {
617
- 'all-themes': tastyTokens,
83
+ border: {
84
+ base: 'surface',
85
+ tone: ['-8', '-16'],
86
+ saturation: 0.2,
87
+ inherit: false,
618
88
  },
619
- });
620
-
621
- const Page = tasty({
622
- styles: {
623
- recipe: 'all-themes',
624
- fill: '#primary-surface',
625
- color: '#primary-text',
89
+ 'accent-surface-text': { tone: 100, mode: 'fixed' },
90
+ 'accent-surface': {
91
+ base: 'accent-surface-text',
92
+ tone: '-1',
93
+ contrast: 'AA',
94
+ mode: 'fixed',
626
95
  },
627
96
  });
628
- ```
629
97
 
630
- Or spread directly into component styles:
98
+ const danger = theme.extend({ hue: 23 });
99
+ const palette = glaze.palette({ default: theme, danger });
631
100
 
632
- ```ts
633
- const Card = tasty({
634
- styles: {
635
- ...tastyTokens,
636
- fill: '#primary-surface',
637
- color: '#primary-text',
638
- },
101
+ const tokens = palette.tokens({
102
+ prefix: { default: '', danger: 'danger-' },
639
103
  });
640
- ```
641
-
642
- Custom prefix mapping:
643
-
644
- ```ts
645
- palette.tasty({ prefix: { primary: 'brand-', danger: 'error-' } });
646
- ```
647
-
648
- Custom state aliases:
649
-
650
- ```ts
651
- palette.tasty({ states: { dark: '@dark', highContrast: '@hc' } });
652
- ```
653
-
654
- ### JSON Export (Framework-Agnostic)
655
-
656
- ```ts
657
- const data = palette.json({ prefix: true });
658
104
  // → {
659
- // primary: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
660
- // danger: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
105
+ // light: { surface: 'oklch(...)', 'danger-surface': 'oklch(...)', ... },
106
+ // dark: { ... },
107
+ // lightContrast: { ... },
108
+ // darkContrast: { ... },
661
109
  // }
662
110
  ```
663
111
 
664
- ### CSS Export
665
-
666
- Export as CSS custom property declarations, grouped by scheme variant. Each variant is a string of `--name-color: value;` lines that you can wrap in your own selectors and media queries.
667
-
668
- ```ts
669
- const css = theme.css();
670
- // css.light → "--surface-color: rgb(...);\n--text-color: rgb(...);"
671
- // css.dark → "--surface-color: rgb(...);\n--text-color: rgb(...);"
672
- // css.lightContrast → "--surface-color: rgb(...);\n--text-color: rgb(...);"
673
- // css.darkContrast → "--surface-color: rgb(...);\n--text-color: rgb(...);"
674
- ```
675
-
676
- Use in a stylesheet:
677
-
678
- ```ts
679
- const css = palette.css({ prefix: true });
680
-
681
- const stylesheet = `
682
- :root { ${css.light} }
683
- @media (prefers-color-scheme: dark) {
684
- :root { ${css.dark} }
685
- }
686
- `;
687
- ```
688
-
689
- Options:
690
-
691
- | Option | Default | Description |
692
- |---|---|---|
693
- | `format` | `'rgb'` | Color format (`'rgb'`, `'hsl'`, `'okhsl'`, `'oklch'`) |
694
- | `suffix` | `'-color'` | Suffix appended to each CSS property name |
695
- | `prefix` | — | (palette only) Same prefix behavior as `tokens()` |
696
-
697
- ```ts
698
- // Custom suffix
699
- theme.css({ suffix: '' });
700
- // → "--surface: rgb(...);"
701
-
702
- // Custom format
703
- theme.css({ format: 'hsl' });
704
- // → "--surface-color: hsl(...);"
705
-
706
- // Palette with prefix
707
- palette.css({ prefix: true });
708
- // → "--primary-surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
709
- ```
710
-
711
- ## Output Modes
712
-
713
- Control which scheme variants appear in exports:
714
-
715
- ```ts
716
- // Light only
717
- palette.tokens({ modes: { dark: false, highContrast: false } });
718
- // → { light: { ... } }
719
-
720
- // Light + dark (default)
721
- palette.tokens({ modes: { highContrast: false } });
722
- // → { light: { ... }, dark: { ... } }
723
-
724
- // All four variants
725
- palette.tokens({ modes: { dark: true, highContrast: true } });
726
- // → { light: { ... }, dark: { ... }, lightContrast: { ... }, darkContrast: { ... } }
727
- ```
728
-
729
- The `modes` option works the same way on `tokens()`, `tasty()`, `json()`, and `css()`.
730
-
731
- Resolution priority (highest first):
732
-
733
- 1. `tokens({ modes })` / `tasty({ modes })` / `json({ modes })` / `css({ ... })` — per-call override
734
- 2. `glaze.configure({ modes })` — global config
735
- 3. Built-in default: `{ dark: true, highContrast: false }`
736
-
737
- ## Configuration
738
-
739
- ```ts
740
- glaze.configure({
741
- lightLightness: [10, 100], // Light scheme lightness window [lo, hi]
742
- darkLightness: [15, 95], // Dark scheme lightness window [lo, hi]
743
- darkDesaturation: 0.1, // Saturation reduction in dark scheme (0–1)
744
- states: {
745
- dark: '@dark', // State alias for dark mode tokens
746
- highContrast: '@high-contrast',
747
- },
748
- modes: {
749
- dark: true, // Include dark variants in exports
750
- highContrast: false, // Include high-contrast variants
751
- },
752
- shadowTuning: { // Default tuning for all shadow colors
753
- alphaMax: 0.6,
754
- bgHueBlend: 0.2,
755
- },
756
- });
757
- ```
758
-
759
- ## Color Definition Shape
760
-
761
- `ColorDef` is a discriminated union of regular colors and shadow colors:
762
-
763
- ```ts
764
- type ColorDef = RegularColorDef | ShadowColorDef;
765
-
766
- interface RegularColorDef {
767
- lightness?: HCPair<number | RelativeValue>;
768
- saturation?: number;
769
- hue?: number | RelativeValue;
770
- base?: string;
771
- contrast?: HCPair<MinContrast>;
772
- mode?: 'auto' | 'fixed' | 'static';
773
- opacity?: number; // fixed alpha (0–1)
774
- }
775
-
776
- interface ShadowColorDef {
777
- type: 'shadow';
778
- bg: string; // background color name (non-shadow)
779
- fg?: string; // foreground color name (non-shadow)
780
- intensity: HCPair<number>; // 0–100
781
- tuning?: ShadowTuning;
782
- }
783
- ```
784
-
785
- A root color must have absolute `lightness` (a number). A dependent color must have `base`. Relative `lightness` (a string) requires `base`. Shadow colors use `type: 'shadow'` and must reference a non-shadow `bg` color.
786
-
787
- ## Validation
788
-
789
- | Condition | Behavior |
790
- |---|---|
791
- | `contrast` without `base` | Validation error |
792
- | Relative `lightness` without `base` | Validation error |
793
- | `lightness` resolves outside 0–100 | Clamp silently |
794
- | `saturation` outside 0–1 | Clamp silently |
795
- | Circular `base` references | Validation error |
796
- | `base` references non-existent name | Validation error |
797
- | Shadow `bg` references non-existent color | Validation error |
798
- | Shadow `fg` references non-existent color | Validation error |
799
- | Shadow `bg` references another shadow color | Validation error |
800
- | Shadow `fg` references another shadow color | Validation error |
801
- | Regular color `base` references a shadow color | Validation error |
802
- | Shadow `intensity` outside 0–100 | Clamp silently |
803
- | `contrast` + `opacity` combined | Warning |
804
-
805
- ## Advanced: Color Math Utilities
806
-
807
- Glaze re-exports its internal color math for advanced use:
808
-
809
- ```ts
810
- import {
811
- okhslToLinearSrgb,
812
- okhslToSrgb,
813
- okhslToOklab,
814
- srgbToOkhsl,
815
- parseHex,
816
- relativeLuminanceFromLinearRgb,
817
- contrastRatioFromLuminance,
818
- formatOkhsl,
819
- formatRgb,
820
- formatHsl,
821
- formatOklch,
822
- findLightnessForContrast,
823
- resolveMinContrast,
824
- } from '@tenphi/glaze';
825
- ```
826
-
827
- ## Full Example
828
-
829
- ```ts
830
- import { glaze } from '@tenphi/glaze';
831
-
832
- const primary = glaze(280, 80);
833
-
834
- primary.colors({
835
- surface: { lightness: 97, saturation: 0.75 },
836
- text: { base: 'surface', lightness: '-52', contrast: 'AAA' },
837
- border: { base: 'surface', lightness: ['-7', '-20'], contrast: 'AA-large' },
838
- bg: { lightness: 97, saturation: 0.75 },
839
- icon: { lightness: 60, saturation: 0.94 },
840
- 'accent-fill': { lightness: 52, mode: 'fixed' },
841
- 'accent-text': { base: 'accent-fill', lightness: '+48', contrast: 'AA', mode: 'fixed' },
842
- disabled: { lightness: 81, saturation: 0.4 },
843
-
844
- // Shadow colors — computed alpha, automatic dark-mode adaptation
845
- 'shadow-sm': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 5 },
846
- 'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
847
- 'shadow-lg': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 20 },
848
-
849
- // Fixed-alpha overlay
850
- overlay: { lightness: 0, opacity: 0.5 },
851
- });
852
-
853
- const danger = primary.extend({ hue: 23 });
854
- const success = primary.extend({ hue: 157 });
855
- const warning = primary.extend({ hue: 84 });
856
- const note = primary.extend({ hue: 302 });
857
-
858
- const palette = glaze.palette({ primary, danger, success, warning, note });
859
-
860
- // Export as flat token map grouped by variant
861
- const tokens = palette.tokens({ prefix: true });
862
- // tokens.light → { 'primary-surface': 'okhsl(...)', 'primary-shadow-md': 'okhsl(... / 0.1)' }
863
-
864
- // Export as tasty style-to-state bindings (for Tasty style system)
865
- const tastyTokens = palette.tasty({ prefix: true });
866
-
867
- // Export as CSS custom properties (rgb format by default)
868
- const css = palette.css({ prefix: true });
869
- // css.light → "--primary-surface-color: rgb(...);\n--primary-shadow-md-color: rgb(... / 0.1);"
870
-
871
- // Standalone shadow computation
872
- const v = glaze.shadow({ bg: '#f0eef5', fg: '#1a1a2e', intensity: 10 });
873
- const shadowCss = glaze.format(v, 'oklch');
874
- // → 'oklch(0.15 0.014 280 / 0.1)'
875
-
876
- // Save and restore a theme
877
- const snapshot = primary.export();
878
- const restored = glaze.from(snapshot);
879
-
880
- // Create from an existing brand color
881
- const brand = glaze.fromHex('#7a4dbf');
882
- brand.colors({ surface: { lightness: 97 }, text: { base: 'surface', lightness: '-52' } });
883
- ```
884
-
885
- ## API Reference
886
-
887
- ### Theme Creation
888
-
889
- | Method | Description |
890
- |---|---|
891
- | `glaze(hue, saturation?)` | Create a theme from hue (0–360) and saturation (0–100) |
892
- | `glaze({ hue, saturation })` | Create a theme from an options object |
893
- | `glaze.from(data)` | Create a theme from an exported configuration |
894
- | `glaze.fromHex(hex)` | Create a theme from a hex color (`#rgb` or `#rrggbb`) |
895
- | `glaze.fromRgb(r, g, b)` | Create a theme from RGB values (0–255) |
896
- | `glaze.color(input)` | Create a standalone color token |
897
- | `glaze.shadow(input)` | Compute a standalone shadow color (returns `ResolvedColorVariant`) |
898
- | `glaze.format(variant, format?)` | Format any `ResolvedColorVariant` as a CSS string |
899
-
900
- ### Theme Methods
901
-
902
- | Method | Description |
903
- |---|---|
904
- | `theme.colors(defs)` | Add/replace colors (additive merge) |
905
- | `theme.color(name)` | Get a color definition |
906
- | `theme.color(name, def)` | Set a single color definition |
907
- | `theme.remove(names)` | Remove one or more colors |
908
- | `theme.has(name)` | Check if a color is defined |
909
- | `theme.list()` | List all defined color names |
910
- | `theme.reset()` | Clear all color definitions |
911
- | `theme.export()` | Export configuration as JSON-safe object |
912
- | `theme.extend(options)` | Create a child theme |
913
- | `theme.resolve()` | Resolve all colors |
914
- | `theme.tokens(options?)` | Export as flat token map grouped by variant |
915
- | `theme.tasty(options?)` | Export as [Tasty](https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs) style-to-state bindings |
916
- | `theme.json(options?)` | Export as plain JSON |
917
- | `theme.css(options?)` | Export as CSS custom property declarations |
918
-
919
- ### Global Configuration
920
-
921
- | Method | Description |
922
- |---|---|
923
- | `glaze.configure(config)` | Set global configuration |
924
- | `glaze.palette(themes)` | Compose themes into a palette |
925
- | `glaze.getConfig()` | Get current global config |
926
- | `glaze.resetConfig()` | Reset to defaults |
112
+ The accent fill is solved against its white label, not the reverse: the fill
113
+ darkens until the pair meets `'AA'`, or `'AAA'` in high contrast, where a bare
114
+ `'AA'` rises a tier.
115
+
116
+ ## Concepts at a glance
117
+
118
+ 1. **A theme has one hue/saturation seed.** Create status themes with
119
+ `extend()` so they inherit the same relationships while changing hue.
120
+ 2. **A color is a root or a dependent.** Roots use an absolute `tone` or
121
+ `'max'`/`'min'`. Dependent colors name a `base` and may use a signed
122
+ **tone delta** such as `'-8'` plus a `contrast` floor.
123
+ 3. **Tone is an authoring axis, not a contrast guarantee.** Equal tone deltas
124
+ are exact WCAG steps for neutrals. Chromatic colors can drift in rendered
125
+ luminance, so Glaze measures contrast floors against the resolved colors.
126
+ 4. **`contrast` is a floor.** A bare number or preset means WCAG;
127
+ `{ wcag }` and `{ apca }` select the metric explicitly. The solver keeps
128
+ the requested tone whenever it already clears the floor with a small
129
+ margin (1% over a WCAG ratio, 0.5 Lc over an APCA target).
130
+ 5. **Each color chooses its dark adaptation.** `mode: 'auto'` uses dark tone
131
+ inversion (`100 - t`), `fixed` keeps the authored side of the tone scale,
132
+ and `static` skips scheme adaptation.
133
+ 6. **Tone windows bound ordinary schemes.** `lightTone` and `darkTone`
134
+ configure the light and dark tone windows. High-contrast variants use the
135
+ full range. Values that accept `[normal, highContrast]` pairs can tighten
136
+ deliberately in HC.
137
+
138
+ In Glaze, a **scheme variant** is one of `light`, `dark`, `lightContrast`, or
139
+ `darkContrast`. Export `modes` choose which variants are included; [Tasty](https://tasty.style)
140
+ `states` choose how those variants are activated in an application.
141
+
142
+ ## Choosing an output
143
+
144
+ - `tasty()` returns [Tasty](https://tasty.style) `#token` bindings, including its custom `okhsl()` or
145
+ `okhst()` serialization.
146
+ - `tokens()` and `json()` return JavaScript data and default to native
147
+ `oklch()` values.
148
+ - `css()` returns custom-property declarations.
149
+ - `dtcg()` / `dtcgResolver()` target design-token tooling.
150
+ - `tailwind()` emits a Tailwind CSS v4 theme.
151
+
152
+ ## Documentation
153
+
154
+ The full documentation, with live examples that follow your light / dark and
155
+ contrast preferences, is at [glaze.tenphi.me](https://glaze.tenphi.me). The
156
+ same pages live in [`docs/`](docs):
157
+
158
+ - [Getting started](docs/getting-started.mdx) — install, build a first theme,
159
+ and export it.
160
+ - Concepts: [light, dark, and high contrast](docs/schemes.mdx),
161
+ [contrast solving](docs/contrast.mdx),
162
+ [shadows and glows](docs/shadows-and-glows.mdx),
163
+ [mixes and transparency](docs/mixes.mdx), and the color model under them,
164
+ [tone and OKHST](docs/okhst.mdx).
165
+ - Guides: [designing a palette](docs/methodology.mdx),
166
+ [exporting and integration](docs/exporting.mdx), and
167
+ [migration and upgrading](docs/migration.md) (including Glaze 2 → 3).
168
+ - The [API reference](docs/reference/index.md) for every method, option, and
169
+ type.
170
+ - Explore the tone axis in the [interactive playground](https://glaze.tenphi.me/playground/),
171
+ or read the [full OKHST specification](https://github.com/tenphi/okhst) for
172
+ its math.
173
+ - [`AGENTS.md`](https://github.com/tenphi/glaze/blob/main/AGENTS.md) — source-tree orientation for contributors.
927
174
 
928
175
  ## License
929
176
 
930
- [MIT](LICENSE)
177
+ [MIT](https://github.com/tenphi/glaze/blob/main/LICENSE)