@tenphi/glaze 0.0.0-snapshot.c84faa6 → 0.0.0-snapshot.d38eee5

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/docs/api.md ADDED
@@ -0,0 +1,1287 @@
1
+ # API Reference
2
+
3
+ Full reference for every public method, option, and type exported by `@tenphi/glaze`. Organized for lookup, not for reading top-to-bottom — see [methodology.md](methodology.md) for a guided walkthrough of how to use these primitives to build a real palette.
4
+
5
+ ## Contents
6
+
7
+ - [Theme creation](#theme-creation)
8
+ - [Theme methods](#theme-methods)
9
+ - [Color definitions](#color-definitions)
10
+ - [Standalone color tokens](#standalone-color-tokens)
11
+ - [Shadows](#shadows)
12
+ - [Mix colors](#mix-colors)
13
+ - [Palette](#palette)
14
+ - [Output formats](#output-formats)
15
+ - [Adaptation modes](#adaptation-modes)
16
+ - [Light / dark scheme mapping](#light--dark-scheme-mapping)
17
+ - [Configuration](#configuration)
18
+ - [Output modes](#output-modes)
19
+ - [Validation](#validation)
20
+ - [Color math utilities](#color-math-utilities)
21
+
22
+ ---
23
+
24
+ ## Theme creation
25
+
26
+ | Method | Description |
27
+ |---|---|
28
+ | `glaze(hue, saturation?, config?)` | Create a theme from hue (0–360) and saturation (0–100). Optional `config` overrides the global config for this theme. |
29
+ | `glaze({ hue, saturation }, config?)` | Create a theme from an options object, with optional per-theme config override. |
30
+ | `glaze.from(data)` | Create a theme from an exported configuration (`theme.export()` snapshot). |
31
+ | `glaze.fromHex(hex)` | Create a theme from a hex color (`#rgb` or `#rrggbb`). Extracts hue and saturation. |
32
+ | `glaze.fromRgb(r, g, b)` | Create a theme from RGB values (0–255). Extracts hue and saturation. |
33
+
34
+ ```ts
35
+ const a = glaze(280, 80);
36
+ const b = glaze({ hue: 280, saturation: 80 });
37
+ const c = glaze.fromHex('#7a4dbf');
38
+ const d = glaze.fromRgb(122, 77, 191);
39
+ const e = glaze.from(a.export());
40
+
41
+ // Per-theme config override:
42
+ const rawTheme = glaze(280, 80, { lightTone: false, darkTone: false });
43
+ ```
44
+
45
+ The optional `config` parameter is a `GlazeConfigOverride` — see [Per-instance config override](#per-instance-config-override).
46
+
47
+ ---
48
+
49
+ ## Theme methods
50
+
51
+ A `GlazeTheme` exposes:
52
+
53
+ | Method | Description |
54
+ |---|---|
55
+ | `theme.hue` (readonly) | The hue seed (0–360). |
56
+ | `theme.saturation` (readonly) | The saturation seed (0–100). |
57
+ | `theme.colors(defs)` | Add/replace colors (additive merge — adds new, overwrites existing by name, doesn't remove others). |
58
+ | `theme.color(name)` | Get a color definition by name. |
59
+ | `theme.color(name, def)` | Set a single color definition. |
60
+ | `theme.remove(name \| names[])` | Remove one or more color definitions. |
61
+ | `theme.has(name)` | Check if a color is defined. |
62
+ | `theme.list()` | List all defined color names. |
63
+ | `theme.reset()` | Clear all color definitions. |
64
+ | `theme.export()` | Export the theme configuration as a JSON-safe object. |
65
+ | `theme.extend(options)` | Create a child theme inheriting all color definitions (see [`extend`](#themeextendoptions) below). |
66
+ | `theme.resolve()` | Resolve all colors and return a `Map<string, ResolvedColor>`. |
67
+ | `theme.tokens(options?)` | Export as a flat token map grouped by scheme variant. |
68
+ | `theme.tasty(options?)` | Export as Tasty style-to-state bindings. |
69
+ | `theme.json(options?)` | Export as plain JSON. |
70
+ | `theme.css(options?)` | Export as CSS custom property declarations. |
71
+
72
+ ### `theme.colors(defs)`
73
+
74
+ ```ts
75
+ theme.colors({ surface: { tone: 97 } });
76
+ theme.colors({ text: { tone: 30 } });
77
+ // Both 'surface' and 'text' are now defined.
78
+ ```
79
+
80
+ ### `theme.color(name) / theme.color(name, def)`
81
+
82
+ ```ts
83
+ theme.color('surface', { tone: 97, saturation: 0.75 }); // set
84
+ const def = theme.color('surface'); // get
85
+ ```
86
+
87
+ ### `theme.extend(options)`
88
+
89
+ Creates a new theme inheriting all color definitions, optionally replacing the hue / saturation seed, color overrides, and config:
90
+
91
+ ```ts
92
+ const danger = primary.extend({
93
+ hue: 23,
94
+ colors: { 'accent-fill': { tone: 48, mode: 'fixed' } },
95
+ });
96
+
97
+ // Inherit parent's config override and widen the dark window further:
98
+ const highSat = base.extend({ config: { darkTone: [10, 100] } });
99
+ ```
100
+
101
+ `GlazeExtendOptions`:
102
+
103
+ | Field | Type | Description |
104
+ |---|---|---|
105
+ | `hue` | `number` | Replace the hue seed. Defaults to the parent's hue. |
106
+ | `saturation` | `number` | Replace the saturation seed. Defaults to the parent's saturation. |
107
+ | `colors` | `ColorMap` | Per-theme overrides (additive merge over the inherited map). |
108
+ | `config` | `GlazeConfigOverride` | Config override for the child. Shallow-merged with the parent's override — child fields win. |
109
+
110
+ Colors marked with `inherit: false` on the parent are **not** copied into the child.
111
+
112
+ ### `theme.tokens(options?)`
113
+
114
+ Flat token map grouped by scheme variant.
115
+
116
+ ```ts
117
+ theme.tokens()
118
+ // → { light: { surface: 'okhsl(...)' }, dark: { surface: 'okhsl(...)' } }
119
+ ```
120
+
121
+ `GlazeJsonOptions`:
122
+
123
+ | Option | Default | Description |
124
+ |---|---|---|
125
+ | `format` | `'okhsl'` | Output color format. One of `'okhsl' \| 'rgb' \| 'hsl' \| 'oklch'`. |
126
+ | `modes` | `{ dark: true, highContrast: false }` (or global config) | Which scheme variants to include. |
127
+
128
+ ### `theme.tasty(options?)`
129
+
130
+ Tasty style-to-state bindings for the [Tasty style system](https://tasty.style/docs). Uses `#name` color token keys and state aliases (`''`, `@dark`, etc.).
131
+
132
+ ```ts
133
+ theme.tasty()
134
+ // → {
135
+ // '#surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
136
+ // ...
137
+ // }
138
+ ```
139
+
140
+ `GlazeTokenOptions`:
141
+
142
+ | Option | Default | Description |
143
+ |---|---|---|
144
+ | `format` | `'okhsl'` | Output color format. |
145
+ | `modes` | global config | Which scheme variants to include. |
146
+ | `states.dark` | `'@dark'` (or global config) | State alias for dark mode tokens. |
147
+ | `states.highContrast` | `'@high-contrast'` (or global config) | State alias for high-contrast tokens. |
148
+ | `prefix` | (palette only) | See [Palette](#palette). |
149
+
150
+ When both `dark` and `highContrast` modes are enabled, dark high-contrast variants are emitted under the combined key `<dark> & <highContrast>` (e.g. `'@dark & @high-contrast'`).
151
+
152
+ ### `theme.json(options?)`
153
+
154
+ Per-color JSON map.
155
+
156
+ ```ts
157
+ theme.json()
158
+ // → {
159
+ // surface: { light: 'okhsl(...)', dark: 'okhsl(...)' },
160
+ // text: { light: 'okhsl(...)', dark: 'okhsl(...)' },
161
+ // }
162
+ ```
163
+
164
+ Same options as `tokens()`.
165
+
166
+ ### `theme.css(options?)`
167
+
168
+ CSS custom property declaration strings, grouped by scheme variant.
169
+
170
+ ```ts
171
+ theme.css();
172
+ // → {
173
+ // light: '--surface-color: rgb(...);\n--text-color: rgb(...);',
174
+ // dark: '--surface-color: rgb(...);\n--text-color: rgb(...);',
175
+ // lightContrast: '...',
176
+ // darkContrast: '...',
177
+ // }
178
+ ```
179
+
180
+ `GlazeCssOptions`:
181
+
182
+ | Option | Default | Description |
183
+ |---|---|---|
184
+ | `format` | `'rgb'` | Output color format. |
185
+ | `suffix` | `'-color'` | Suffix appended to each CSS property name. Pass `''` for bare property names. |
186
+
187
+ `GlazeCssResult` always contains all four keys (`light`, `dark`, `lightContrast`, `darkContrast`); empty if no colors are defined for that variant.
188
+
189
+ ### `theme.export()`
190
+
191
+ ```ts
192
+ const snapshot = theme.export();
193
+ // → { hue: 280, saturation: 80, colors: { surface: { ... }, ... } }
194
+
195
+ const restored = glaze.from(snapshot);
196
+ ```
197
+
198
+ The export contains only the configuration — not resolved color values. Resolved values are recomputed on demand.
199
+
200
+ ---
201
+
202
+ ## Color definitions
203
+
204
+ `ColorDef` is a discriminated union:
205
+
206
+ ```ts
207
+ type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
208
+ ```
209
+
210
+ ### `RegularColorDef`
211
+
212
+ | Field | Type | Description |
213
+ |---|---|---|
214
+ | `tone` | `HCPair<ToneValue>` | Number = absolute (0–100, contrast-uniform). `'+N'`/`'-N'` = relative to base's tone (requires `base`). `'max'`/`'min'` = forced to the scheme's tone extreme (no `base`). Optional HC pair `[normal, hc]`. |
215
+ | `saturation` | `number` | Saturation factor applied to the seed saturation (0–1). Default: `1`. |
216
+ | `hue` | `number \| RelativeValue` | Number = absolute (0–360). String (`'+N'`/`'-N'`) = relative to the **theme seed hue** (never to a base color). |
217
+ | `base` | `string` | Name of another color in the same theme — makes this a *dependent* color. |
218
+ | `contrast` | `HCPair<ContrastSpec>` | Contrast floor against `base`. Requires `base`. See [`contrast`](#contrast-floor). |
219
+ | `mode` | `'auto' \| 'fixed' \| 'static'` | Adaptation mode. Default: `'auto'`. See [Adaptation modes](#adaptation-modes). |
220
+ | `flip` | `boolean` | Flip out-of-bounds results (relative `tone` overshoot / unmet `contrast`) to the opposite side instead of clamping. Default: the global `autoFlip` (`true`). See [`flip`](#flip). |
221
+ | `opacity` | `number` | Fixed alpha 0–1. Output includes alpha in the CSS value. Combining with `contrast` is not recommended (a `console.warn` is emitted). |
222
+ | `pastel` | `boolean` | Per-color override for the hue-independent "safe" chroma limit used in OKHSL↔sRGB conversions (luminance, contrast solving, output formatting). Falls through to the global / per-theme `pastel` config when omitted. Default: unset. See [Per-color `pastel`](#per-color-pastel). |
223
+ | `role` | `RoleInput` | Semantic role against `base` (`'text'` / `'surface'` / `'border'` or an alias). Fixes APCA contrast polarity. Resolved via: explicit `role` → name inference → opposite of the base's role → `'text'`. See [Roles](#roles). |
224
+ | `inherit` | `boolean` | Whether this color is inherited by child themes via `extend()`. Default: `true`. Set to `false` to make the color local to the current theme. |
225
+
226
+ #### Tone values
227
+
228
+ `tone` (0–100) replaces OKHSL lightness with a contrast-uniform axis — equal tone steps give equal WCAG contrast. See [`docs/okhst.md`](okhst.md) for the math. (To port old `lightness` values, see [migration.md](migration.md).)
229
+
230
+ | Form | Example | Meaning |
231
+ |---|---|---|
232
+ | Number (absolute) | `tone: 45` | Absolute tone 0–100. |
233
+ | String (relative) | `tone: '-52'` | Relative to base color's tone (requires `base`). |
234
+ | Extreme | `tone: 'max'` / `'min'` | Force to the scheme's highest (`'max'` = 100) or lowest (`'min'` = 0) tone. No `base` needed. |
235
+ | HC pair | `tone: ['-7', '-20']` | `[normal, high-contrast]`. A single value applies to both. |
236
+
237
+ **Absolute tone** on a dependent color (`base` set) positions the color independently. In dark mode it is tone-mapped (inverted + windowed) on its own. The `contrast` solver acts as a safety net.
238
+
239
+ **Relative tone** applies a signed delta to the base color's resolved tone. Because tone is contrast-uniform, a fixed delta yields a fixed contrast step. In dark mode with `mode: 'auto'`, the offset is anchored to the base's per-scheme tone. If `base + delta` falls outside `[0, 100]`, the result is clamped to the boundary, or — with `flip` (default on) — mirrored to the other side of the base.
240
+
241
+ **Extreme tone** (`'max'` / `'min'`) forces the color to the scheme's tone extreme without a contrast hack or a magic number. `'max'` resolves to author tone 100 and `'min'` to 0; both flow through scheme mapping like an absolute tone, so under `mode: 'auto'` they invert in dark (`'max'` is lightest in light, darkest in dark). Use `mode: 'static'` to pin the same extreme across schemes, or `mode: 'fixed'` to keep the same end without inverting. No `base` required.
242
+
243
+ A dependent color with `base` but no `tone` inherits the base's tone (equivalent to a delta of 0).
244
+
245
+ #### `flip`
246
+
247
+ `flip` governs what happens when a result would fall outside its valid range:
248
+
249
+ - **Relative `tone` overshoot:** when `base ± delta` exceeds `[0, 100]`, `flip` mirrors the delta to the other side of the base (e.g. `'+30'` becomes `'-30'`) instead of clamping to the boundary.
250
+ - **`contrast` direction:** when the requested tone direction can't meet the floor, `flip` lets the solver try the opposite side (the same behavior as the global `autoFlip`).
251
+
252
+ `flip` defaults to the global `autoFlip` (`true`). Set `flip: false` on a color to clamp instead of mirror — useful when you want a relative offset to stay on the authored side of the base, or to keep an unmet contrast pinned to one direction's extreme.
253
+
254
+ #### `contrast` (floor)
255
+
256
+ ```ts
257
+ type ContrastPreset = 'AA' | 'AAA' | 'AA-large' | 'AAA-large';
258
+ type ContrastSpec =
259
+ | number // bare WCAG ratio
260
+ | ContrastPreset // named WCAG preset
261
+ | { wcag: HCPair<number | ContrastPreset> }
262
+ | { apca: HCPair<number> }; // APCA Lc target
263
+ ```
264
+
265
+ | Preset | WCAG ratio |
266
+ |---|---|
267
+ | `'AA-large'` | 3 |
268
+ | `'AA'` | 4.5 |
269
+ | `'AAA-large'` | 4.5 |
270
+ | `'AAA'` | 7 |
271
+
272
+ A bare number or preset means **WCAG**. Use `{ wcag }` / `{ apca }` to pick the metric explicitly. The `[normal, highContrast]` pair may live at the outer level (`[4.5, 7]`, `[{ wcag: 4.5 }, { wcag: 7 }]`) or inside the metric (`{ wcag: [4.5, 7] }`, `{ apca: [45, 60] }`).
273
+
274
+ ```ts
275
+ contrast: 4.5 // WCAG 4.5
276
+ contrast: 'AAA' // WCAG 7
277
+ contrast: { wcag: 6 } // WCAG 6
278
+ contrast: { wcag: [4.5, 7] } // WCAG 4.5 normal / 7 high-contrast
279
+ contrast: { apca: 60 } // APCA Lc 60
280
+ contrast: { apca: [45, 60] } // APCA Lc 45 normal / 60 high-contrast
281
+ contrast: { apca: 'content' } // APCA preset -> Lc 60
282
+ contrast: { apca: ['content', 'body'] } // Lc 60 normal / 75 high-contrast
283
+ ```
284
+
285
+ APCA preset keywords (Bronze Simple Mode conformance levels, role-independent):
286
+ `'preferred'` (Lc 90), `'body'` (75), `'content'` (60, ~AA), `'large'` (45, ~3:1),
287
+ `'non-text'` (30), `'min'` (15, point of invisibility). See
288
+ [`docs/okhst.md`](okhst.md) §APCA.
289
+
290
+ The floor is applied independently per scheme — if the `tone` already satisfies it the tone is kept, otherwise the solver searches in tone (contrast-uniform → a closed-form WCAG seed and fast convergence) until the target is met.
291
+
292
+ By default, the solver crosses to the opposite side of the base color when the requested tone direction cannot satisfy the floor. This is controlled per-color by [`flip`](#flip) (which defaults to the global `autoFlip`). Set `glaze.configure({ autoFlip: false })` — or `flip: false` on a single color — to keep strict directionality: unmet colors pin to that direction's 0 or 100 tone extreme instead of falling back to the original requested value.
293
+
294
+ **Full tone spectrum in HC mode:** in high-contrast variants the `lightTone` and `darkTone` window constraints are bypassed entirely (the window is forced to `[0, 100]`). Colors can reach the full range, maximizing perceivable contrast.
295
+
296
+ **Chromatic drift (verification):** tone is contrast-uniform for grays. A chromatic swatch at a given tone shares its OKHSL lightness with the equivalent gray but drifts in real luminance, so a contrast-floored color may land slightly under its gray-tone expectation. Glaze measures the resolved result against the base and emits a deduped advisory `console.warn` when it drifts below the target. See [`docs/okhst.md`](okhst.md) §Verification.
297
+
298
+ #### Per-color hue override
299
+
300
+ ```ts
301
+ const theme = glaze(280, 80);
302
+ theme.colors({
303
+ surface: { tone: 97 },
304
+ gradientEnd: { tone: 90, hue: '+20' }, // 280 + 20 = 300
305
+ warning: { tone: 60, hue: 40 }, // absolute
306
+ });
307
+ ```
308
+
309
+ Relative hue is always relative to the **theme seed hue**, not to a base color.
310
+
311
+ #### Per-color `pastel`
312
+
313
+ `pastel: true` on a single color def overrides the global / per-theme `pastel` config for that color only. It toggles the hue-independent "safe" chroma limit used in every OKHSL↔sRGB conversion that touches this color: luminance calculations during contrast solving, gamut clamping during sRGB blend / mix edges, and output formatting. The effective flag is carried on the resolved variant (`ResolvedColorVariant.pastel`) so formatting matches the gamut mapping applied during resolution.
314
+
315
+ ```ts
316
+ const theme = glaze(280, 80);
317
+ theme.colors({
318
+ plain: { tone: 50, saturation: 1 },
319
+ soft: { tone: 50, saturation: 1, pastel: true },
320
+ });
321
+ // theme.resolve().get('soft')!.light.pastel === true
322
+ // theme.css().light contains different rgb() triples for `--plain` and `--soft`
323
+ ```
324
+
325
+ Omit the field to inherit the global / per-theme `pastel` config — useful for keeping the default behavior while opting a single accent into the pastel gamut.
326
+
327
+ The flag is part of the def object, so `extend()` copies it through to child themes alongside the rest of the def. Override it again on the child to flip a single color back:
328
+
329
+ ```ts
330
+ const parent = glaze(280, 80);
331
+ parent.colors({ soft: { tone: 50, saturation: 1, pastel: true } });
332
+
333
+ const child = parent.extend({
334
+ colors: { soft: { tone: 50, saturation: 1, pastel: false } },
335
+ });
336
+ // child.resolve().get('soft')!.light.pastel === false
337
+ ```
338
+
339
+ > **Note:** Per-color `pastel` is also supported on `ShadowColorDef` and `MixColorDef` (see the tables above). For shadows the math itself happens in OKHSL space, so the flag mainly controls the gamut-mapped output formatting and any luminance verification for that variant.
340
+ >
341
+ > Standalone `glaze.color()` tokens accept the same `pastel` field on both the structured (`GlazeColorInput`) and value-shorthand (`GlazeColorOverrides`) forms, and it survives the `export()` / `glaze.colorFrom()` round-trip.
342
+
343
+ #### Roles
344
+
345
+ A color's `role` describes how it is used against its `base` and fixes **APCA contrast polarity** — which side is the foreground vs the background. APCA is asymmetric (`|apca(a,b)| ≠ |apca(b,a)|`), so the role picks the correct argument order; WCAG is symmetric and unaffected.
346
+
347
+ | Role | Polarity | Use | Aliases (name inference) |
348
+ |---|---|---|---|
349
+ | `'text'` | fg | Text / icons / foreground content | `text`, `fg`, `foreground`, `content`, `ink`, `label`, `stroke` |
350
+ | `'border'` | fg | Non-text spot elements (borders, dividers, outlines) | `border`, `divider`, `outline`, `separator`, `hairline`, `rule` |
351
+ | `'surface'` | bg | Backgrounds / fills | `surface`, `bg`, `background`, `fill`, `canvas`, `paper`, `layer` |
352
+
353
+ Resolution chain (per color):
354
+
355
+ 1. Explicit `role` (normalized from an alias) wins.
356
+ 2. Else, when `inferRole` is enabled (default), infer from the color name — the **last** recognized token wins (`button-text` → `text`, `input-bg` → `surface`, `card-outline` → `border`).
357
+ 3. Else, the opposite of the base's role (a `surface` base ⇒ this is `text`).
358
+ 4. Else, `'text'` (foreground) — i.e. the base is treated as the background.
359
+
360
+ ```ts
361
+ const theme = glaze(280, 60);
362
+ theme.colors({
363
+ surface: { tone: 90 },
364
+ text: { base: 'surface', contrast: { apca: 'content' } }, // inferred text
365
+ border: { base: 'surface', tone: '-10' }, // inferred border
366
+ });
367
+ // role fixes APCA polarity; set `pastel: true` explicitly if a border
368
+ // needs the hue-independent safe chroma limit.
369
+ ```
370
+
371
+ Disable name inference with `glaze.configure({ inferRole: false })` (the base-opposite and foreground-default fallbacks still apply).
372
+
373
+ ### `ShadowColorDef`
374
+
375
+ | Field | Type | Description |
376
+ |---|---|---|
377
+ | `type` | `'shadow'` | Discriminator. |
378
+ | `bg` | `string` | Background color name — must reference a non-shadow color in the same theme. |
379
+ | `fg` | `string` | Optional foreground color name for tinting and intensity modulation. Must reference a non-shadow color. Omit for an achromatic shadow at full user-specified intensity. |
380
+ | `intensity` | `HCPair<number>` | Shadow intensity, 0–100. Supports HC pairs. |
381
+ | `tuning` | `ShadowTuning` | Per-color tuning overrides. Merged field-by-field with the global `shadowTuning`. |
382
+ | `pastel` | `boolean` | Per-color `pastel` override. See [Per-color `pastel`](#per-color-pastel). |
383
+ | `inherit` | `boolean` | Inheritance flag, default `true`. |
384
+
385
+ See [Shadows](#shadows) below for the algorithm and tuning details.
386
+
387
+ ### `MixColorDef`
388
+
389
+ | Field | Type | Description |
390
+ |---|---|---|
391
+ | `type` | `'mix'` | Discriminator. |
392
+ | `base` | `string` | "From" color name. |
393
+ | `target` | `string` | "To" color name. |
394
+ | `value` | `HCPair<number>` | Mix ratio 0–100 (0 = pure base, 100 = pure target). In `'transparent'` blend, this becomes the target's opacity. Supports HC pairs. |
395
+ | `blend` | `'opaque' \| 'transparent'` | Default `'opaque'`. |
396
+ | `space` | `'okhsl' \| 'srgb'` | Interpolation space for opaque blending. Default `'okhsl'`. Ignored for `'transparent'` (always composites in linear sRGB). |
397
+ | `contrast` | `HCPair<ContrastSpec>` | Optional contrast floor against `base` (WCAG or APCA — see [`contrast`](#contrast-floor)). The solver adjusts the mix ratio (opaque) or opacity (transparent). |
398
+ | `pastel` | `boolean` | Per-color `pastel` override. See [Per-color `pastel`](#per-color-pastel). |
399
+ | `role` | `RoleInput` | Semantic role of the mixed result against `base`. Same semantics as `RegularColorDef.role` (see [Roles](#roles)). |
400
+ | `inherit` | `boolean` | Inheritance flag, default `true`. |
401
+
402
+ See [Mix colors](#mix-colors) below.
403
+
404
+ ---
405
+
406
+ ## Standalone color tokens
407
+
408
+ `glaze.color()` creates a single color token without a full theme.
409
+
410
+ ```ts
411
+ // arg1: the color (four shapes — see below)
412
+ // arg2: optional config override (GlazeConfigOverride — see below)
413
+ glaze.color(color: GlazeFromInput | GlazeColorInput | GlazeColorValue, config?: GlazeConfigOverride): GlazeColorToken;
414
+ ```
415
+
416
+ ### Input forms
417
+
418
+ `glaze.color()` accepts **four input shapes**, discriminated by structure:
419
+
420
+ | Shape | Example | Notes |
421
+ |---|---|---|
422
+ | **Bare string** | `'#26fcb2'` | Hex or CSS color function (`rgb()`, `hsl()`, `okhsl()`, `okhst()`, `oklch()`). |
423
+ | **Value object** | `{ h: 152, s: 0.95, l: 0.74 }` | OKHSL, OKHST (`{ h, s, t }`), `{ r, g, b }` (sRGB 0–255), or `{ l, c, h }` (OKLCh). |
424
+ | **`{ from, ...overrides }`** | `{ from: '#1a1a2e', base: bg, contrast: 'AA' }` | Value + color overrides in one object. |
425
+ | **Structured** | `{ hue: 152, saturation: 95, tone: 74 }` | Full theme-style token (hue/saturation in 0–100, tone in 0–100). |
426
+
427
+ `GlazeColorValue` (bare string or value-object forms) accepts:
428
+
429
+ | Form | Example | Notes |
430
+ |---|---|---|
431
+ | Hex | `'#26fcb2'`, `'#26fcb2ff'`, `'#abc'` | 3, 6, or 8 digits. Alpha is dropped with a `console.warn` — use `opacity` instead. |
432
+ | `rgb()` | `'rgb(38 252 178)'`, `'rgb(38 252 178 / 0.8)'` | Modern space syntax. Alpha dropped with warning. |
433
+ | `hsl()` | `'hsl(152 97% 57%)'` | Modern space syntax. Alpha dropped with warning. |
434
+ | `okhsl()` | `'okhsl(152 95% 74%)'` | Glaze's own emit format. Alpha dropped with warning. |
435
+ | `okhst()` | `'okhst(152 95% 70%)'` | OKHST tone input (third value is tone 0–100). **Input only** — never emitted. Alpha dropped with warning. |
436
+ | `oklch()` | `'oklch(0.85 0.18 152)'` | Glaze's own emit format. Alpha dropped with warning. |
437
+ | `OkhslColor` object | `{ h: 152, s: 0.95, l: 0.74 }` | OKHSL shape (h: 0–360, s/l: 0–1). Passing 0–100 for `s`/`l` throws with a hint to use the structured form. |
438
+ | `OkhstColor` object | `{ h: 152, s: 0.95, t: 0.70 }` | OKHST shape (h: 0–360, s/t: 0–1). The `t` key disambiguates it from `{ h, s, l }`. **Input only.** |
439
+ | `RgbColor` object | `{ r: 38, g: 252, b: 178 }` | sRGB 0–255. RGB tuple `[r, g, b]` is not supported — use this object form. |
440
+ | `OklchColor` object | `{ l: 0.85, c: 0.18, h: 152 }` | OKLCh (L/C: 0–1, H: degrees), same semantics as `oklch()` strings. |
441
+
442
+ `GlazeColorInput` (structured form) is `{ hue, saturation, tone, ... }`:
443
+
444
+ | Field | Type | Description |
445
+ |---|---|---|
446
+ | `hue` | `number` | 0–360. |
447
+ | `saturation` | `number` | 0–100. |
448
+ | `tone` | `HCPair<number \| ExtremeValue>` | 0–100 (contrast-uniform) or `'max'`/`'min'`, optional HC pair. |
449
+ | `saturationFactor` | `number` | Multiplier on the seed (0–1). Default: `1`. |
450
+ | `mode` | `AdaptationMode` | Default: `'auto'`. |
451
+ | `flip` | `boolean` | Flip out-of-bounds results instead of clamping. Default: global `autoFlip`. |
452
+ | `opacity` | `number` | Fixed alpha 0–1. |
453
+ | `base` | `GlazeColorToken \| GlazeColorValue` | Optional dependency. See [Pairing colors](#pairing-colors). |
454
+ | `contrast` | `HCPair<ContrastSpec>` | Contrast floor against `base` (WCAG or APCA). Without `base`, anchored to the literal seed. |
455
+ | `pastel` | `boolean` | Per-color `pastel` override. Falls through to the global / per-theme `pastel` config when omitted. See [Per-color `pastel`](#per-color-pastel). |
456
+ | `role` | `RoleInput` | Semantic role against `base` / the seed (see [Roles](#roles)). Fixes APCA polarity. |
457
+ | `name` | `string` | Debug label for warnings; doesn't change output keys. Reserved names (`'value'`, `'seed'`, `'externalBase'`) are rejected. |
458
+
459
+ `GlazeFromInput` (from form) is `{ from: GlazeColorValue, ...colorOverrides }`:
460
+
461
+ | Field | Notes |
462
+ |---|---|
463
+ | `from` | **Required.** The source color value — same forms as `GlazeColorValue`. |
464
+ | `hue` | Number (absolute 0–360) or `'+N'`/`'-N'` (relative to seed, never to `base`). |
465
+ | `saturation` | Override seed saturation (0–100). |
466
+ | `tone` | Number (absolute 0–100), `'+N'`/`'-N'`, or `'max'`/`'min'`. Without `base`, relative anchors to the seed; with `base`, anchors to `base`'s tone per scheme. |
467
+ | `saturationFactor` | Multiplier on the seed (0–1). |
468
+ | `mode` | `'auto'` (default) / `'fixed'` / `'static'`. |
469
+ | `flip` | Flip out-of-bounds results instead of clamping. Default: global `autoFlip`. |
470
+ | `contrast` | Contrast floor (WCAG or APCA). Without `base`, anchored to the literal seed; with `base`, solved per scheme. |
471
+ | `base` | `GlazeColorToken` or raw `GlazeColorValue`. See [Pairing colors](#pairing-colors). |
472
+ | `opacity` | Fixed alpha 0–1. Combining with `contrast` is not recommended — `console.warn` is emitted. |
473
+ | `pastel` | Per-color `pastel` override. Falls through to the global / per-theme `pastel` config when omitted. See [Per-color `pastel`](#per-color-pastel). |
474
+ | `role` | Semantic role against `base` / the seed (see [Roles](#roles)). Fixes APCA polarity. |
475
+ | `name` | Debug label only — surfaces in warnings/errors. Does not change output keys. |
476
+
477
+ Named CSS colors (`'red'`, `'blueviolet'`) are not supported.
478
+
479
+ ### Defaults
480
+
481
+ Every input form defaults to `mode: 'auto'` so the resolved token adapts between light and dark like an ordinary theme color. The config snapshot taken at create time differs by input form:
482
+
483
+ - **Value-shorthand** (bare strings, value objects, and `{ from, ...overrides }`):
484
+ - Light variant preserves the input tone exactly (`lightTone: false`).
485
+ - All other config fields (`darkTone`, `darkDesaturation`, `autoFlip`) snapshot from `globalConfig` at create time.
486
+ - **Structured input** (`{ hue, saturation, tone, ... }`):
487
+ - Both tone windows snapshot from `globalConfig` at create time (same as a theme color).
488
+ - All fields are **snapshotted at color-creation time** — later `glaze.configure()` calls don't retroactively change existing tokens.
489
+
490
+ ```ts
491
+ // Bare string — adapts automatically
492
+ glaze.color('#26fcb2')
493
+
494
+ // Value-object — same behavior
495
+ glaze.color({ h: 152, s: 0.95, l: 0.74 })
496
+
497
+ // OKHST value-object — tone axis
498
+ glaze.color({ h: 152, s: 0.95, t: 0.70 })
499
+
500
+ // From form — value + color overrides
501
+ glaze.color({ from: '#1a1a2e', hue: '+20', contrast: 'AA' })
502
+
503
+ // Structured form — explicit hue/saturation/tone (0–100)
504
+ glaze.color({ hue: 152, saturation: 95, tone: 74 })
505
+ ```
506
+
507
+ ### Token methods
508
+
509
+ A `GlazeColorToken` exposes:
510
+
511
+ | Method | Description |
512
+ |---|---|
513
+ | `token.resolve()` | Resolve as a `ResolvedColor` (light/dark/lightContrast/darkContrast variants). |
514
+ | `token.token(options?)` | Flat token map (no color-name key). Options: `format`, `modes`, `states`. |
515
+ | `token.tasty(options?)` | Tasty state map (no color-name key). Same options as `token.token`. |
516
+ | `token.json(options?)` | JSON map (no color-name key). Options: `format`, `modes`. |
517
+ | `token.css({ name, format?, suffix? })` | CSS custom property declarations grouped by scheme variant. `name` is **required** and becomes the variable identifier (`'brand'` → `--brand-color`). Defaults: `format: 'rgb'`, `suffix: '-color'` (matches `theme.css`). |
518
+ | `token.export()` | JSON-safe snapshot — pass to `glaze.colorFrom(...)` to rehydrate. |
519
+
520
+ ### Per-instance config override
521
+
522
+ The optional `config` second argument (`GlazeConfigOverride`) overrides the resolve-relevant global config fields for a single token or theme. Fields that are omitted fall through to the live global config at create time (and are snapshotted). A tone window can be `[lo, hi]`, `{ lo, hi, eps }`, or `false` (disable clamping).
523
+
524
+ `GlazeConfigOverride`:
525
+
526
+ | Field | Default (from global) | Description |
527
+ |---|---|---|
528
+ | `lightTone` | `[10, 100]` | Light tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` (disable clamping). |
529
+ | `darkTone` | `[15, 95]` | Dark tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` (disable clamping). |
530
+ | `darkDesaturation` | `0.1` | Saturation reduction in dark scheme (0–1). |
531
+ | `autoFlip` | `true` | Default for each color's `flip`: when solving `contrast` (or applying a relative `tone` that overshoots), allow crossing to the opposite side instead of clamping. |
532
+ | `shadowTuning` | `undefined` | Default shadow tuning (meaningful for themes; harmless on color tokens). |
533
+
534
+ Config overrides apply to both `glaze.color()` tokens and `glaze()` themes:
535
+
536
+ ```ts
537
+ // Standalone color — preserve raw tone in both schemes
538
+ glaze.color('#26fcb2', { darkTone: false })
539
+
540
+ // Restore the #000 → white dark flip (full dark range)
541
+ glaze.color('#000000', {
542
+ lightTone: false,
543
+ darkTone: [15, 100],
544
+ })
545
+
546
+ // Structured form with config override
547
+ glaze.color({ hue: 152, saturation: 95, tone: 74 }, { darkTone: false })
548
+
549
+ // Theme with config override
550
+ const rawTheme = glaze(280, 80, { lightTone: false })
551
+ ```
552
+
553
+ The override is **snapshotted at create time** so later `glaze.configure()` calls don't change already-created tokens or themes (for non-overridden fields, the snapshot captured the global value at creation time; for themes, non-overridden fields are re-read from the live global at resolve time — see [Theme config override](#theme-config-override)).
554
+
555
+ ### Theme config override
556
+
557
+ When a theme is created with a `GlazeConfigOverride`, the override is **merged over the live global config at resolve time**. This means:
558
+
559
+ - Fields you overrode are fixed — `glaze.configure()` can't change them for this theme.
560
+ - Fields you didn't override still react to later `glaze.configure()` calls.
561
+
562
+ ```ts
563
+ const t = glaze(280, 80, { lightTone: [0, 50] });
564
+ t.colors({ text: { tone: 50, saturation: 1 } });
565
+ // text.light lands inside the [0, 50] window — always, regardless of
566
+ // global lightTone changes.
567
+ // text.dark.s reacts to glaze.configure({ darkDesaturation }) since it's not overridden.
568
+ ```
569
+
570
+ `extend` inherits the parent's override and shallow-merges the child's:
571
+
572
+ ```ts
573
+ const child = t.extend({ config: { darkTone: false } });
574
+ // child: lightTone { lo: 0, hi: 50 } (inherited) + darkTone: false (added)
575
+ ```
576
+
577
+ `theme.export()` includes `config`; `glaze.from(data)` restores it.
578
+
579
+ ### `glaze.colorFrom(data)`
580
+
581
+ Inverse of `token.export()`. The exported snapshot includes the original input, all overrides (with any `base` token recursively serialized), and the full effective config — so later `glaze.configure()` calls don't change rehydrated tokens.
582
+
583
+ ```ts
584
+ const text = glaze.color({ from: '#1a1a1a', contrast: 'AA' });
585
+ const data = text.export();
586
+ const restored = glaze.colorFrom(data);
587
+ // restored.resolve() === text.resolve() byte-for-byte
588
+ ```
589
+
590
+ Both value-form and structured-form tokens round-trip.
591
+
592
+ ### Pairing colors
593
+
594
+ Set `base` to anchor a standalone color to another standalone color or raw value. The contrast solver and relative `tone` offsets switch their anchor from the literal seed to the base's resolved variant per scheme — so the same text color automatically lands at AA against its background in light, dark, and high-contrast modes.
595
+
596
+ ```ts
597
+ const bg = glaze.color('#1a1a2e');
598
+
599
+ // Text guaranteed AA against `bg` in every scheme.
600
+ const text = glaze.color({ from: '#ffffff', base: bg, contrast: 'AA' });
601
+
602
+ // Border 8 tone units lighter than `bg` in each scheme.
603
+ const border = glaze.color({ from: '#000000',
604
+ base: bg,
605
+ tone: '+8',
606
+ mode: 'fixed',
607
+ });
608
+
609
+ // Raw-value base — Glaze auto-wraps it via `glaze.color(value)`.
610
+ const text2 = glaze.color({ from: '#ffffff', base: '#1a1a2e', contrast: 'AA' });
611
+ ```
612
+
613
+ Behavior with `base`:
614
+
615
+ - `contrast` is solved per scheme against `base`'s resolved variant (light / dark / lightContrast / darkContrast).
616
+ - Relative `tone: '+N'` / `'-N'` is anchored to `base`'s tone per scheme (matches theme behavior).
617
+ - Relative `hue: '+N'` / `'-N'` still anchors to the **seed** (the value passed to `glaze.color()`), not the base.
618
+ - `mode` works as a per-pair knob.
619
+ - The base token's `.resolve()` is called lazily on the first resolve of the dependent and the result is captured by reference; later mutations to the base don't apply.
620
+ - **Structured bases are resolved at full range for linking math**: when a value/`from` color links to a base created via the structured form, the contrast/tone anchor uses the raw input tone (not the windowed output). This ensures the anchor matches what you intended, not what the light window remapped it to. The base's own `.resolve()` output is unaffected.
621
+ - When the contrast target is physically unreachable, `glaze` emits a single `console.warn` per `(name, scheme, target)` triple and returns the closest passing variant. Use the `name` override to make the warning identifiable.
622
+
623
+ Chains compose:
624
+
625
+ ```ts
626
+ const bg = glaze.color('#000000');
627
+ const surface = glaze.color({ from: '#222222', base: bg, contrast: 'AAA' });
628
+ const text = glaze.color({ from: '#ffffff', base: surface, contrast: 'AA' });
629
+ ```
630
+
631
+ ### `name` is a debug label
632
+
633
+ The `name` override appears in `console.warn` / Error messages but **does not** change output keys (`.token()`, `.tasty()`, `.json()`, `.css()` still use `''`, `light`, etc.). The CSS variable name comes from `css({ name })`, not from the override.
634
+
635
+ ---
636
+
637
+ ## Shadows
638
+
639
+ ### Defining shadow colors in a theme
640
+
641
+ ```ts
642
+ theme.colors({
643
+ surface: { tone: 95 },
644
+ text: { base: 'surface', tone: '-52', contrast: 'AAA' },
645
+
646
+ 'shadow-sm': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 5 },
647
+ 'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
648
+ 'shadow-lg': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 20 },
649
+ });
650
+ ```
651
+
652
+ Shadow colors are included in all output methods (`tokens()`, `tasty()`, `css()`, `json()`) alongside regular colors and emit an alpha component:
653
+
654
+ ```
655
+ 'oklch(0.15 0.009 282 / 0.1)'
656
+ 'rgb(34 28 42 / 0.1)'
657
+ ```
658
+
659
+ ### How shadows work
660
+
661
+ 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.
662
+ 2. **Pigment color** — hue blended between fg and bg, low saturation, dark lightness.
663
+ 3. **Alpha** — computed via a `tanh` curve that saturates smoothly toward `alphaMax` (default `1.0`), ensuring well-separated shadow levels even on dark backgrounds.
664
+
665
+ Omit `fg` for an achromatic shadow at full user-specified intensity:
666
+
667
+ ```ts
668
+ theme.colors({
669
+ 'drop-shadow': { type: 'shadow', bg: 'surface', intensity: 12 },
670
+ });
671
+ ```
672
+
673
+ `intensity` supports `[normal, highContrast]` pairs:
674
+
675
+ ```ts
676
+ 'shadow-card': { type: 'shadow', bg: 'surface', fg: 'text', intensity: [10, 20] },
677
+ ```
678
+
679
+ ### `ShadowTuning`
680
+
681
+ Fine-tune behavior per-color or globally via `glaze.configure({ shadowTuning })`. Per-color `tuning` is merged field-by-field with the global one.
682
+
683
+ | Parameter | Default | Description |
684
+ |---|---|---|
685
+ | `saturationFactor` | `0.18` | Fraction of fg saturation kept in pigment. |
686
+ | `maxSaturation` | `0.25` | Upper clamp on pigment saturation. |
687
+ | `lightnessFactor` | `0.25` | Multiplier for bg lightness → pigment lightness. |
688
+ | `lightnessBounds` | `[0.05, 0.20]` | Clamp range for pigment lightness. |
689
+ | `minGapTarget` | `0.05` | Target minimum gap between pigment and bg lightness. |
690
+ | `alphaMax` | `1.0` | Asymptotic maximum alpha. |
691
+ | `bgHueBlend` | `0.2` | Blend weight pulling pigment hue toward bg hue. `0` = pure fg hue, `1` = pure bg hue. |
692
+
693
+ ```ts
694
+ theme.colors({
695
+ 'shadow-soft': {
696
+ type: 'shadow', bg: 'surface', intensity: 10,
697
+ tuning: { alphaMax: 0.3, saturationFactor: 0.1 },
698
+ },
699
+ });
700
+
701
+ glaze.configure({
702
+ shadowTuning: { alphaMax: 0.5, bgHueBlend: 0.3 },
703
+ });
704
+ ```
705
+
706
+ ### Standalone shadow computation
707
+
708
+ `glaze.shadow(input)` computes a shadow outside of a theme. `bg` and `fg` accept any `GlazeColorValue`:
709
+
710
+ ```ts
711
+ const v = glaze.shadow({
712
+ bg: '#f0eef5',
713
+ fg: '#1a1a2e',
714
+ intensity: 10,
715
+ });
716
+ // → { h: 280, s: 0.14, l: 0.2, alpha: 0.1 }
717
+
718
+ const css = glaze.format(v, 'oklch');
719
+ // → 'oklch(0.15 0.014 280 / 0.1)'
720
+ ```
721
+
722
+ `GlazeShadowInput`:
723
+
724
+ | Field | Type | Description |
725
+ |---|---|---|
726
+ | `bg` | `GlazeColorValue` | Background. Any `GlazeColorValue` form. Alpha components dropped with warning. |
727
+ | `fg` | `GlazeColorValue` | Optional foreground. Same forms as `bg`. |
728
+ | `intensity` | `number` | 0–100. |
729
+ | `tuning` | `ShadowTuning` | Optional. |
730
+
731
+ ### Fixed opacity (regular colors)
732
+
733
+ For a simple fixed-alpha color (no shadow algorithm), use `opacity` on a regular color:
734
+
735
+ ```ts
736
+ theme.colors({
737
+ overlay: { tone: 0, opacity: 0.5 },
738
+ });
739
+ // → 'oklch(0 0 0 / 0.5)'
740
+ ```
741
+
742
+ ---
743
+
744
+ ## Mix colors
745
+
746
+ ### Opaque mix
747
+
748
+ Produces a solid color by interpolating between `base` and `target`:
749
+
750
+ ```ts
751
+ theme.colors({
752
+ surface: { tone: 95 },
753
+ accent: { tone: 30 },
754
+ tint: { type: 'mix', base: 'surface', target: 'accent', value: 30 },
755
+ });
756
+ ```
757
+
758
+ - `value: 0` = pure base, `value: 100` = pure target.
759
+ - Result has alpha = 1.
760
+ - Adapts to light/dark/HC schemes automatically via the resolved base and target.
761
+
762
+ ### Transparent mix
763
+
764
+ Produces the target color with controlled opacity — useful for hover overlays:
765
+
766
+ ```ts
767
+ theme.colors({
768
+ surface: { tone: 95 },
769
+ black: { tone: 0, saturation: 0 },
770
+ hover: {
771
+ type: 'mix', base: 'surface', target: 'black',
772
+ value: 8, blend: 'transparent',
773
+ },
774
+ });
775
+ // hover → black with alpha = 0.08
776
+ ```
777
+
778
+ The output color has `h`, `s`, `l` from the target and `alpha = value / 100`.
779
+
780
+ ### Blend space (opaque only)
781
+
782
+ | `space` | Behavior | Best for |
783
+ |---|---|---|
784
+ | `'okhsl'` (default) | Perceptually uniform OKHSL interpolation. | Design token derivation. |
785
+ | `'srgb'` | Linear sRGB channel interpolation. | Matching browser compositing of CSS color-mix / overlay. |
786
+
787
+ Transparent blending always composites in linear sRGB (matches browser alpha compositing).
788
+
789
+ ### Contrast solving on mixes
790
+
791
+ Mix colors support the same `contrast` prop as regular colors. The solver adjusts the mix ratio (opaque) or opacity (transparent) to meet the WCAG target:
792
+
793
+ ```ts
794
+ 'tint': {
795
+ type: 'mix', base: 'surface', target: 'accent',
796
+ value: 10, contrast: 'AA',
797
+ },
798
+ 'overlay': {
799
+ type: 'mix', base: 'surface', target: 'accent',
800
+ value: 5, blend: 'transparent', contrast: 3,
801
+ },
802
+ ```
803
+
804
+ Both `value` and `contrast` support `[normal, highContrast]` pairs.
805
+
806
+ ### Achromatic colors
807
+
808
+ When mixing with achromatic colors (saturation near zero, e.g. white or black) in `okhsl` space, the hue comes from whichever color has saturation. Matches CSS `color-mix()` "missing component" behavior. For purely achromatic mixes prefer `space: 'srgb'` where hue is irrelevant.
809
+
810
+ ### Mix chaining
811
+
812
+ Mix colors can reference other mix colors:
813
+
814
+ ```ts
815
+ theme.colors({
816
+ white: { tone: 100, saturation: 0 },
817
+ black: { tone: 0, saturation: 0 },
818
+ gray: { type: 'mix', base: 'white', target: 'black', value: 50, space: 'srgb' },
819
+ lightGray: { type: 'mix', base: 'white', target: 'gray', value: 50, space: 'srgb' },
820
+ });
821
+ ```
822
+
823
+ Mix colors **cannot** reference shadow colors (same restriction as regular dependent colors).
824
+
825
+ ---
826
+
827
+ ## Palette
828
+
829
+ `glaze.palette(themes, options?)` composes multiple themes into a single token namespace.
830
+
831
+ ```ts
832
+ const palette = glaze.palette({ primary, danger, success, warning });
833
+ const palette = glaze.palette(
834
+ { primary, danger, success },
835
+ { primary: 'primary' },
836
+ );
837
+ ```
838
+
839
+ `GlazePaletteOptions`:
840
+
841
+ | Option | Description |
842
+ |---|---|
843
+ | `primary` | Name of the primary theme. The primary's tokens are duplicated **without** prefix in all exports, providing convenient short aliases alongside the prefixed versions. Throws if the name doesn't match any theme. |
844
+
845
+ A `GlazePalette` exposes:
846
+
847
+ | Method | Description |
848
+ |---|---|
849
+ | `palette.tokens(options?)` | Flat token map grouped by scheme variant. |
850
+ | `palette.tasty(options?)` | Tasty style-to-state bindings. |
851
+ | `palette.json(options?)` | Per-theme JSON map (no prefix needed — keyed by theme name). |
852
+ | `palette.css(options?)` | CSS custom property declaration strings. |
853
+
854
+ ### `GlazePaletteExportOptions`
855
+
856
+ Shared by `tokens`, `tasty`, and `css`:
857
+
858
+ | Option | Default | Description |
859
+ |---|---|---|
860
+ | `prefix` | `true` (= `"<themeName>-"`) | `false` disables prefixing. Or pass a custom map: `{ primary: 'brand-', danger: 'error-' }`. |
861
+ | `primary` | inherits from palette creation | `string` to override, `false` to disable for this call. |
862
+
863
+ Each export method also accepts its own format/options shape:
864
+
865
+ | Method | Additional options |
866
+ |---|---|
867
+ | `palette.tokens(options?)` | `format`, `modes` |
868
+ | `palette.tasty(options?)` | `format`, `modes`, `states` |
869
+ | `palette.css(options?)` | `format`, `suffix` |
870
+
871
+ `palette.css()` does not accept `modes`; it always returns all four CSS strings (`light`, `dark`, `lightContrast`, `darkContrast`).
872
+
873
+ ### Prefix behavior
874
+
875
+ By default all palette tokens are prefixed:
876
+
877
+ ```ts
878
+ palette.tokens();
879
+ // → {
880
+ // light: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
881
+ // dark: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
882
+ // }
883
+ ```
884
+
885
+ Custom map (any theme not listed falls back to `"<themeName>-"`):
886
+
887
+ ```ts
888
+ palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
889
+ ```
890
+
891
+ Disable prefixing:
892
+
893
+ ```ts
894
+ palette.tokens({ prefix: false });
895
+ ```
896
+
897
+ ### Collision detection
898
+
899
+ 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:
900
+
901
+ ```
902
+ glaze: token "surface" from theme "b" collides with theme "a" — skipping.
903
+ ```
904
+
905
+ ### Primary theme aliases
906
+
907
+ The primary theme's tokens are duplicated without prefix:
908
+
909
+ ```ts
910
+ const palette = glaze.palette(
911
+ { primary, danger, success },
912
+ { primary: 'primary' },
913
+ );
914
+ palette.tokens();
915
+ // → {
916
+ // light: {
917
+ // 'primary-surface': 'okhsl(...)',
918
+ // 'danger-surface': 'okhsl(...)',
919
+ // 'success-surface': 'okhsl(...)',
920
+ // 'surface': 'okhsl(...)', // unprefixed alias
921
+ // },
922
+ // }
923
+ ```
924
+
925
+ Override per-export:
926
+
927
+ ```ts
928
+ palette.tokens({ primary: 'danger' });
929
+ palette.tokens({ primary: false });
930
+ ```
931
+
932
+ The primary alias works alongside any prefix mode — when using a custom map, primary tokens are still duplicated without prefix:
933
+
934
+ ```ts
935
+ palette.tokens({ prefix: { primary: 'p-', danger: 'd-' } });
936
+ // → 'p-surface' + 'surface' (alias) + 'd-surface'
937
+ ```
938
+
939
+ ### `palette.json()`
940
+
941
+ JSON export groups by theme name (no prefix needed):
942
+
943
+ ```ts
944
+ palette.json();
945
+ // → {
946
+ // primary: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
947
+ // danger: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
948
+ // }
949
+ ```
950
+
951
+ ### `palette.css()`
952
+
953
+ ```ts
954
+ const css = palette.css();
955
+ const stylesheet = `
956
+ :root { ${css.light} }
957
+ @media (prefers-color-scheme: dark) {
958
+ :root { ${css.dark} }
959
+ }
960
+ `;
961
+ ```
962
+
963
+ `palette.css()` accepts the same `GlazeCssOptions` as `theme.css()` plus `GlazePaletteExportOptions`.
964
+ It does not accept `modes`; all four result fields are always returned.
965
+
966
+ ---
967
+
968
+ ## Output formats
969
+
970
+ Control the color format with the `format` option on any export method:
971
+
972
+ | Format | Output (alpha = 1) | Output (alpha < 1) | Notes |
973
+ |---|---|---|---|
974
+ | `'okhsl'` (default for tokens/tasty/json) | `okhsl(H S% L%)` | `okhsl(H S% L% / A)` | Glaze's native format, not a CSS function. |
975
+ | `'rgb'` (default for css) | `rgb(R G B)` | `rgb(R G B / A)` | Rounded integers, modern space syntax. |
976
+ | `'hsl'` | `hsl(H S% L%)` | `hsl(H S% L% / A)` | Modern space syntax. |
977
+ | `'oklch'` | `oklch(L C H)` | `oklch(L C H / A)` | OKLab-based LCH. |
978
+
979
+ ```ts
980
+ theme.tokens(); // 'okhsl(280 60% 97%)'
981
+ theme.tokens({ format: 'rgb' }); // 'rgb(244 240 250)'
982
+ theme.tokens({ format: 'hsl' }); // 'hsl(270.5 45.2% 95.8%)'
983
+ theme.tokens({ format: 'oklch' }); // 'oklch(0.965 0.0123 280)'
984
+ ```
985
+
986
+ All numeric output strips trailing zeros for cleaner CSS (e.g. `95` not `95.0`).
987
+
988
+ The `format` option works on every export: `theme.tokens()`, `theme.tasty()`, `theme.json()`, `theme.css()`, the same on `palette`, and on `token.token()` / `.tasty()` / `.json()` / `.css()`.
989
+
990
+ ---
991
+
992
+ ## Adaptation modes
993
+
994
+ `mode` controls how a color adapts across schemes:
995
+
996
+ | Mode | Behavior |
997
+ |---|---|
998
+ | `'auto'` (default) | Full adaptation. Dark is a single tone inversion (`100 − t`) remapped into the dark window. High-contrast uses the full range. |
999
+ | `'fixed'` | Color stays recognizable. Tone is *mapped* (not inverted) into the dark window. Use for brand buttons, CTAs, status banners. |
1000
+ | `'static'` | No adaptation. Same tone in every scheme. |
1001
+
1002
+ ### How relative tone adapts
1003
+
1004
+ **`auto`** — the offset is anchored to the base's per-scheme tone:
1005
+
1006
+ ```
1007
+ Light: surface tone=97, text tone='-52' → tone 45 (dark text on light bg)
1008
+ Dark: surface inverts to a low tone; the '-52' offset re-anchors to the
1009
+ base's light tone and maps into the dark window (light text on dark bg)
1010
+ ```
1011
+
1012
+ **`fixed`** — tone is mapped (not inverted), relative sign preserved:
1013
+
1014
+ ```
1015
+ Light: accent-fill tone=52, accent-text tone='+20' → lighter than the fill
1016
+ Dark: accent-fill maps into the dark window, sign preserved
1017
+ ```
1018
+
1019
+ Offsets that would push past `[0, 100]` clamp to the boundary, or — with `flip` (default on) — mirror to the other side of the base. Set `flip: false` to keep the authored side and clamp instead.
1020
+
1021
+ **`static`** — no adaptation, same tone in every scheme.
1022
+
1023
+ ---
1024
+
1025
+ ## Light / dark scheme mapping
1026
+
1027
+ The mapping is now a single tone pipeline; there is no Möbius curve. See [`docs/okhst.md`](okhst.md) for the full math and the calibrated default constants.
1028
+
1029
+ ### Light scheme
1030
+
1031
+ An authored tone (0–100) is remapped into the `lightTone` window. The window's `lo`/`hi` are OKHSL-lightness endpoints (0–100); the tone is positioned within the window's tone interval and converted to a final OKHSL lightness. `static` mode and HC variants use the full range.
1032
+
1033
+ ```
1034
+ window = lightTone // default [10, 100]
1035
+ finalTone = remap(authorTone, window)
1036
+ finalL = fromTone(finalTone) // OKHSL lightness
1037
+ ```
1038
+
1039
+ ### Dark scheme
1040
+
1041
+ **`auto`** — invert the tone, then remap into the dark window:
1042
+
1043
+ ```
1044
+ window = darkTone // default [15, 95]
1045
+ inverted = 100 - authorTone
1046
+ finalTone = remap(inverted, window)
1047
+ ```
1048
+
1049
+ Because tone is contrast-uniform, the inversion preserves contrast steps without a fitted curve — the asymmetry between light and dark lives entirely in the two windows' `(lo, hi, eps)` scalars (`eps` defaults to the reference `0.05`).
1050
+
1051
+ **`fixed`** — remap into the dark window without inversion:
1052
+
1053
+ ```
1054
+ finalTone = remap(authorTone, darkTone)
1055
+ ```
1056
+
1057
+ In high-contrast variants both windows are bypassed (forced to the full `[0, 100]` range): `auto` still inverts, `fixed`/`static` do not.
1058
+
1059
+ ### Dark scheme — saturation
1060
+
1061
+ `darkDesaturation` reduces saturation for all colors in dark scheme:
1062
+
1063
+ ```ts
1064
+ S_dark = S_light * (1 - darkDesaturation) // default: 0.1
1065
+ ```
1066
+
1067
+ `static` mode skips desaturation.
1068
+
1069
+ ---
1070
+
1071
+ ## Configuration
1072
+
1073
+ ```ts
1074
+ glaze.configure({
1075
+ lightTone: [10, 100], // [lo, hi]; or { lo, hi, eps } / false to disable clamping
1076
+ darkTone: [15, 95], // [lo, hi]; or { lo, hi, eps } / false to disable clamping
1077
+ darkDesaturation: 0.1,
1078
+ states: {
1079
+ dark: '@dark',
1080
+ highContrast: '@high-contrast',
1081
+ },
1082
+ modes: {
1083
+ dark: true,
1084
+ highContrast: false,
1085
+ },
1086
+ shadowTuning: {
1087
+ alphaMax: 0.6,
1088
+ bgHueBlend: 0.2,
1089
+ },
1090
+ });
1091
+ ```
1092
+
1093
+ A `ToneWindow` is `[lo, hi]` (OKHSL-lightness endpoints, reference eps — the common form), `{ lo, hi, eps }` (advanced: explicit per-mode render eps), or `false` to disable clamping (full range `[0, 100]` at the reference eps). `false` removes the *boundaries*, not the contrast-uniform tone curve.
1094
+
1095
+ `GlazeConfig`:
1096
+
1097
+ | Field | Default | Description |
1098
+ |---|---|---|
1099
+ | `lightTone` | `[10, 100]` | Light scheme tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` to disable clamping. Bypassed in HC. |
1100
+ | `darkTone` | `[15, 95]` | Dark scheme tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` to disable clamping. Bypassed in HC. |
1101
+ | `darkDesaturation` | `0.1` | Saturation reduction in dark scheme (0–1). |
1102
+ | `states.dark` | `'@dark'` | State alias for dark mode tokens (Tasty export). |
1103
+ | `states.highContrast` | `'@high-contrast'` | State alias for HC tokens. |
1104
+ | `modes.dark` | `true` | Include dark variants in exports. |
1105
+ | `modes.highContrast` | `false` | Include HC variants. |
1106
+ | `shadowTuning` | `undefined` | Default tuning for all shadow colors. Per-color tuning merges field-by-field. |
1107
+ | `autoFlip` | `true` | Default for each color's `flip`. When solving `contrast` (or applying a relative `tone` that overshoots `[0, 100]`), allow crossing to the opposite side instead of clamping. With `false`, only the requested direction is considered; unmet contrasts pin the tone to that direction's extreme (and emit a warning) and overshooting offsets clamp to the boundary. Override per color via [`flip`](#flip). |
1108
+ | `pastel` | `false` | Hue-independent "safe" chroma limit across all colors so scaling saturation never exceeds the sRGB boundary at any hue for the given lightness. Override per color via [`pastel`](#per-color-pastel). |
1109
+ | `inferRole` | `true` | Infer each color's [`role`](#roles) from its name when no explicit `role` is set. Set to `false` to opt out of name-based inference (the base-opposite and foreground-default fallbacks still apply). |
1110
+
1111
+ | Method | Description |
1112
+ |---|---|
1113
+ | `glaze.configure(config)` | Merge into the global config. Bumps a config version that invalidates theme caches. |
1114
+ | `glaze.getConfig()` | Snapshot the current resolved config (shallow copy). |
1115
+ | `glaze.resetConfig()` | Reset to defaults (also bumps the version counter). |
1116
+
1117
+ Standalone `glaze.color()` tokens snapshot the resolve-relevant fields at create time, so later `configure()` calls don't change already-created tokens. Themes merge the live global at resolve time for fields not overridden via `GlazeConfigOverride`.
1118
+
1119
+ ---
1120
+
1121
+ ## Output modes
1122
+
1123
+ Control which scheme variants appear in `tokens()` / `tasty()` / `json()` exports:
1124
+
1125
+ ```ts
1126
+ // Light only
1127
+ palette.tokens({ modes: { dark: false, highContrast: false } });
1128
+
1129
+ // Light + dark (default)
1130
+ palette.tokens({ modes: { highContrast: false } });
1131
+
1132
+ // All four variants
1133
+ palette.tokens({ modes: { dark: true, highContrast: true } });
1134
+ // → { light, dark, lightContrast, darkContrast }
1135
+ ```
1136
+
1137
+ Resolution priority (highest first):
1138
+
1139
+ 1. Per-call `modes` option on `tokens` / `tasty` / `json`.
1140
+ 2. `glaze.configure({ modes })` — global config.
1141
+ 3. Built-in default: `{ dark: true, highContrast: false }`.
1142
+
1143
+ ---
1144
+
1145
+ ## Validation
1146
+
1147
+ | Condition | Behavior |
1148
+ |---|---|
1149
+ | `contrast` without `base` in a **theme** color | Validation error |
1150
+ | Relative `tone` without `base` in a **theme** color | Validation error |
1151
+ | `contrast` without `base` in `glaze.color()` | Anchors against the literal seed (no error) |
1152
+ | Relative `tone` without `base` in `glaze.color()` | Anchors against the literal seed (no error) |
1153
+ | Relative `tone` overshoots `[0, 100]` | Mirror to the other side of the base (`flip` on, default), or clamp to the boundary (`flip` off) |
1154
+ | `tone` resolves outside 0–100 | Clamp silently |
1155
+ | `'max'` / `'min'` without `base` | Allowed — resolves to the scheme's tone extreme (root color) |
1156
+ | `saturation` outside 0–1 | Clamp silently |
1157
+ | Circular `base` references | Validation error |
1158
+ | `base` references non-existent name | Validation error |
1159
+ | Shadow `bg` references non-existent color | Validation error |
1160
+ | Shadow `fg` references non-existent color | Validation error |
1161
+ | Shadow `bg` references another shadow color | Validation error |
1162
+ | Shadow `fg` references another shadow color | Validation error |
1163
+ | Regular color `base` references a shadow color | Validation error |
1164
+ | Shadow `intensity` outside 0–100 | Clamp silently |
1165
+ | `contrast` + `opacity` combined | `console.warn` |
1166
+ | Mix `base` references non-existent color | Validation error |
1167
+ | Mix `target` references non-existent color | Validation error |
1168
+ | Mix `base` references a shadow color | Validation error |
1169
+ | Mix `target` references a shadow color | Validation error |
1170
+ | Mix `value` outside 0–100 | Clamp silently |
1171
+ | Circular references involving mix colors | Validation error |
1172
+ | Contrast target physically unreachable | `console.warn` (deduped per `(name, scheme, target)`); closest passing variant returned |
1173
+
1174
+ ---
1175
+
1176
+ ## Color math utilities
1177
+
1178
+ For advanced use, Glaze re-exports its internal color math.
1179
+
1180
+ ### Conversions
1181
+
1182
+ ```ts
1183
+ import {
1184
+ okhslToLinearSrgb,
1185
+ okhslToSrgb,
1186
+ okhslToOklab,
1187
+ oklabToOkhsl,
1188
+ srgbToOkhsl,
1189
+ hslToSrgb,
1190
+ parseHex,
1191
+ parseHexAlpha,
1192
+ relativeLuminanceFromLinearRgb,
1193
+ contrastRatioFromLuminance,
1194
+ gamutClampedLuminance,
1195
+ } from '@tenphi/glaze';
1196
+ ```
1197
+
1198
+ | Function | Description |
1199
+ |---|---|
1200
+ | `okhslToLinearSrgb(h, s, l)` | OKHSL (h: 0–360, s/l: 0–1) → linear sRGB tuple. |
1201
+ | `okhslToSrgb(h, s, l)` | OKHSL → gamma-encoded sRGB tuple (0–1 per channel). |
1202
+ | `okhslToOklab([h, s, l])` | OKHSL → OKLab `[L, a, b]`. |
1203
+ | `oklabToOkhsl([L, a, b])` | OKLab → OKHSL. |
1204
+ | `srgbToOkhsl([r, g, b])` | Gamma sRGB (0–1) → OKHSL. |
1205
+ | `hslToSrgb(h, s, l)` | CSS HSL → sRGB tuple. |
1206
+ | `parseHex(hex)` | Parse `#rgb` / `#rrggbb` to sRGB tuple. Returns `null` on invalid input. |
1207
+ | `parseHexAlpha(hex)` | Parse `#rgb` / `#rrggbb` / `#rrggbbaa`; returns `[r, g, b, a?]`. |
1208
+ | `relativeLuminanceFromLinearRgb(rgb)` | WCAG relative luminance from linear sRGB. |
1209
+ | `contrastRatioFromLuminance(yA, yB)` | WCAG contrast ratio from two luminances. |
1210
+ | `gamutClampedLuminance(linearRgb)` | Relative luminance with channel clamping for out-of-gamut colors. |
1211
+
1212
+ ### Format writers
1213
+
1214
+ ```ts
1215
+ import { formatOkhsl, formatRgb, formatHsl, formatOklch } from '@tenphi/glaze';
1216
+
1217
+ formatOkhsl(280, 60, 95); // 'okhsl(280 60% 95%)'
1218
+ formatRgb(280, 60, 95); // 'rgb(244 240 250)'
1219
+ formatHsl(280, 60, 95); // 'hsl(280 60% 95%)'
1220
+ formatOklch(280, 60, 95); // 'oklch(0.95 ... 280)'
1221
+ ```
1222
+
1223
+ To attach an alpha component, use `glaze.format(variant, format)` on a `ResolvedColorVariant` (which carries the `alpha` channel) instead of these raw writers.
1224
+
1225
+ ### OKHST tone utilities
1226
+
1227
+ ```ts
1228
+ import {
1229
+ toTone,
1230
+ fromTone,
1231
+ toneFromY,
1232
+ yFromTone,
1233
+ okhstToOkhsl,
1234
+ okhslToOkhst,
1235
+ variantToOkhsl,
1236
+ REF_EPS,
1237
+ } from '@tenphi/glaze';
1238
+ ```
1239
+
1240
+ | Function | Description |
1241
+ |---|---|
1242
+ | `toTone(l, eps?)` | OKHSL lightness (0–1) → tone (0–100). Defaults to `REF_EPS`. |
1243
+ | `fromTone(t, eps?)` | Tone (0–100) → OKHSL lightness (0–1). Inverse of `toTone`. |
1244
+ | `toneFromY(y, eps?)` / `yFromTone(t, eps?)` | Same transfer in luminance space (0–1). |
1245
+ | `okhstToOkhsl({ h, s, t })` | OKHST → OKHSL (`{ h, s, l }`). |
1246
+ | `okhslToOkhst({ h, s, l })` | OKHSL → OKHST (`{ h, s, t }`). |
1247
+ | `variantToOkhsl(variant)` | `ResolvedColorVariant` (stores `t`) → `{ h, s, l, alpha }` for rendering. |
1248
+ | `REF_EPS` | Reference epsilon (`0.05`) for the canonical tone axis. |
1249
+
1250
+ `ResolvedColorVariant` now stores `{ h, s, t, alpha }` (tone, not lightness). Use `variantToOkhsl(variant).l` to recover OKHSL lightness. See [`docs/okhst.md`](okhst.md) for the full model.
1251
+
1252
+ ### Contrast solver
1253
+
1254
+ ```ts
1255
+ import {
1256
+ findToneForContrast,
1257
+ findValueForMixContrast,
1258
+ resolveContrastForMode,
1259
+ resolveMinContrast,
1260
+ apcaContrast,
1261
+ } from '@tenphi/glaze';
1262
+ ```
1263
+
1264
+ | Function | Description |
1265
+ |---|---|
1266
+ | `findToneForContrast(opts)` | Binary-search for the tone (0–1) that meets a contrast floor (WCAG or APCA) against a base color. Returns `{ tone, contrast, met, branch, flipped? }`. |
1267
+ | `findValueForMixContrast(opts)` | Same, but searches for a mix `value` (0–1) that meets a contrast floor between a base and a target. |
1268
+ | `resolveContrastForMode(spec, isHC)` | Resolves a `ContrastSpec` to `{ metric: 'wcag' \| 'apca', target }` for the requested mode (picks the normal or HC entry of any pair). |
1269
+ | `resolveMinContrast(value)` | Resolves a `MinContrast` (WCAG preset or number) to a numeric ratio. |
1270
+ | `apcaContrast(yText, yBg)` | APCA Lc magnitude (0–106) for two relative luminances. |
1271
+
1272
+ `findToneForContrast` options:
1273
+
1274
+ | Option | Default | Description |
1275
+ |---|---|---|
1276
+ | `hue` | — | Candidate hue (0–360). |
1277
+ | `saturation` | — | Candidate saturation (0–1). |
1278
+ | `preferredTone` | — | Preferred candidate tone (0–1). Kept if it already meets the target. |
1279
+ | `baseLinearRgb` | — | Base color as linear sRGB tuple. |
1280
+ | `contrast` | — | `ResolvedContrast` (`{ metric, target }`). |
1281
+ | `toneRange` | `[0, 1]` | Search bounds in tone. |
1282
+ | `epsilon` | `1e-4` | Convergence threshold. |
1283
+ | `maxIterations` | `18` | Max binary-search iterations per branch. |
1284
+ | `initialDirection` | higher-contrast side | Direction to search first (`'lighter'` or `'darker'`). |
1285
+ | `flip` | `false` | When `true`, try the opposite direction if the initial one doesn't meet the target. When `false`, only the initial direction is searched — unmet contrasts pin the result to that direction's extreme. |
1286
+
1287
+ Result: `{ tone, contrast, met, branch: 'lighter' | 'darker' | 'preferred', flipped? }`. `flipped: true` indicates the initial direction failed and the opposite direction satisfied the target.