@tenphi/cookbook 0.32.0 → 0.33.0

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.
Files changed (28) hide show
  1. package/docs/customization-rules.md +7 -5
  2. package/docs/migration.md +52 -26
  3. package/docs/theme-and-components.md +32 -15
  4. package/docs/upstream/glaze/README.md +46 -26
  5. package/docs/upstream/glaze/docs/contrast.mdx +751 -0
  6. package/docs/upstream/glaze/docs/exporting.mdx +634 -0
  7. package/docs/upstream/glaze/docs/getting-started.mdx +489 -0
  8. package/docs/upstream/glaze/docs/hdr.mdx +523 -0
  9. package/docs/upstream/glaze/docs/methodology.mdx +922 -0
  10. package/docs/upstream/glaze/docs/migration.md +477 -286
  11. package/docs/upstream/glaze/docs/mixes.mdx +348 -0
  12. package/docs/upstream/glaze/docs/okhst.mdx +679 -0
  13. package/docs/upstream/glaze/docs/reference/color-definitions.md +552 -0
  14. package/docs/upstream/glaze/docs/reference/color-tokens.md +420 -0
  15. package/docs/upstream/glaze/docs/reference/configuration.md +527 -0
  16. package/docs/upstream/glaze/docs/reference/contrast.md +277 -0
  17. package/docs/upstream/glaze/docs/reference/index.md +225 -0
  18. package/docs/upstream/glaze/docs/reference/output.md +713 -0
  19. package/docs/upstream/glaze/docs/reference/palette.md +434 -0
  20. package/docs/upstream/glaze/docs/reference/theme.md +331 -0
  21. package/docs/upstream/glaze/docs/reference/utilities.md +646 -0
  22. package/docs/upstream/glaze/docs/schemes.mdx +635 -0
  23. package/docs/upstream/glaze/docs/shadows-and-glows.mdx +519 -0
  24. package/docs/upstream/manifest.json +2 -2
  25. package/package.json +3 -3
  26. package/docs/upstream/glaze/docs/api.md +0 -2021
  27. package/docs/upstream/glaze/docs/methodology.md +0 -686
  28. package/docs/upstream/glaze/docs/okhst.md +0 -207
@@ -0,0 +1,527 @@
1
+ ---
2
+ title: Configuration
3
+ description: The global config, per-instance overrides, tone windows, glow headroom, the scheme mapping formulas, validation rules, and every warning Glaze emits.
4
+ ---
5
+
6
+ Glaze has one global config that every theme, palette, and color token reads at resolve time. Themes and tokens can override part of it with a sparse per-instance `GlazeConfigOverride`. This page lists every field, how the scheme mapping uses them, what Glaze validates, and what it warns about. For the ideas behind tone windows and adaptation, see [Light, dark, and high contrast](../schemes.mdx) and [Tone and OKHST](../okhst.mdx).
7
+
8
+ ## Global config
9
+
10
+ ```ts
11
+ import { glaze } from '@tenphi/glaze';
12
+
13
+ glaze.configure({
14
+ lightTone: [10, 100],
15
+ darkTone: [15, 95],
16
+ darkDesaturation: 0.1,
17
+ states: {
18
+ dark: '@media(prefers-color-scheme: dark)',
19
+ highContrast: '@media(prefers-contrast: more)',
20
+ hdr: '@media(dynamic-range: high)',
21
+ },
22
+ modes: { dark: true, highContrast: false, hdr: false },
23
+ shadowTuning: { objectInfluence: 0.5 },
24
+ glowTuning: { objectInfluence: 0.5, headroom: 0 },
25
+ autoFlip: true,
26
+ inferRole: true,
27
+ contrastLevel: 'auto',
28
+ });
29
+ ```
30
+
31
+ | Function | Returns | Description |
32
+ | --- | --- | --- |
33
+ | `glaze.configure(config)` | `void` | Merges a `GlazeConfig` into the global config and invalidates every theme, palette, and token cache. |
34
+ | `glaze.getConfig()` | `GlazeConfigResolved` | A snapshot of the current global config. |
35
+ | `glaze.resetConfig()` | `void` | Restores the defaults and invalidates every cache. |
36
+
37
+ ### `glaze.configure(config)`
38
+
39
+ - Omitted fields keep their current value. `configure()` never clears a field by omission, and passing `undefined` is the same as omitting it. `null` is not: a `null` tone window throws. Use `resetConfig()` to start over, or `contrastLevel: 'auto'` to leave manual contrast mode.
40
+ - `states` and `modes` merge field by field: `configure({ modes: { highContrast: true } })` keeps `modes.dark`.
41
+ - `lightTone`, `darkTone`, `shadowTuning`, and `glowTuning` are replaced as a whole. `configure({ shadowTuning: { tint: 0.3 } })` followed by `configure({ shadowTuning: { reach: 0.9 } })` leaves `{ reach: 0.9 }`, and a `glowTuning` without `headroom` drops an earlier one. They are copied, so changing the array or object you passed afterwards has no effect.
42
+ - `contrastLevel`, `darkDesaturation`, the tone windows, and the tunings are validated on the spot. `contrastLevel` clamps to 0–100 and `darkDesaturation` to 0–1. A non-finite number throws, and so does any other value that isn't a number (besides the level's `'auto'`), such as the string `'50'` from an `<input>`: for example `glaze: contrastLevel must be a finite number 0–100 or 'auto' (got NaN).` Window rules are under [Tone windows](#tone-windows), `headroom` rules under [Glow headroom](#glow-headroom). A call that throws changes nothing, not even the fields that were valid, and prints no warning: a `headroom` clamp found on the way is reported only once the whole call has passed.
43
+ - Every call bumps an internal config version, so cached resolutions recompute on the next output call. Existing themes pick up the change for every field their own override doesn't set.
44
+
45
+ ### `glaze.getConfig()`
46
+
47
+ Returns a `GlazeConfigResolved` — every field present, defaults filled in:
48
+
49
+ ```ts
50
+ glaze.getConfig();
51
+ // → {
52
+ // lightTone: { lo: 10, hi: 100, eps: 0.05 },
53
+ // darkTone: { lo: 15, hi: 95, eps: 0.05 },
54
+ // darkDesaturation: 0.1,
55
+ // states: {
56
+ // dark: '@media(prefers-color-scheme: dark)',
57
+ // highContrast: '@media(prefers-contrast: more)',
58
+ // hdr: '@media(dynamic-range: high)',
59
+ // },
60
+ // modes: { dark: true, highContrast: false, hdr: false },
61
+ // autoFlip: true,
62
+ // pastel: false,
63
+ // inferRole: true,
64
+ // contrastLevel: 'auto',
65
+ // }
66
+ ```
67
+
68
+ The default windows come back in the `{ lo, hi, eps }` form; a window you set comes back in the form you passed (`[12, 98]` stays `[12, 98]`), with its endpoints clamped to 0–100. `shadowTuning` and `glowTuning` are absent until you set them. `pastel` is always `false` here because it is instance-only.
69
+
70
+ The snapshot is a deep copy: mutating it, nested `states`, `modes`, windows, and tunings included, changes nothing. Use `configure()` to change the config.
71
+
72
+ For the config a particular theme resolves with (global merged with its override), use [`theme.getConfig()`](./theme.md#themegetconfig).
73
+
74
+ ## `GlazeConfig`
75
+
76
+ The input to `glaze.configure()`. Every field is optional.
77
+
78
+ | Field | Type | Default | Description |
79
+ | --- | --- | --- | --- |
80
+ | `lightTone` | `ToneWindow` | `[10, 100]` | Light-scheme [tone window](#tone-windows). Bypassed by high-contrast variants and `mode: 'static'` colors; widened by `contrastLevel`. Endpoints clamp to 0–100; an inverted or non-finite window throws. |
81
+ | `darkTone` | `ToneWindow` | `[15, 95]` | Dark-scheme tone window. Same bypasses, widening, and validation. |
82
+ | `darkDesaturation` | `number` | `0.1` | Fraction (0–1) of saturation removed in `dark` and `darkContrast`. Clamps to 0–1; non-finite numbers throw. See [Saturation](#saturation). |
83
+ | `states.dark` | `string` | `'@media(prefers-color-scheme: dark)'` | State key for dark variants in [`tasty()`](./output.md#state-aliases) output. |
84
+ | `states.highContrast` | `string` | `'@media(prefers-contrast: more)'` | State key for high-contrast variants in `tasty()` output. |
85
+ | `states.hdr` | `string` | `'@media(dynamic-range: high)'` | State key for the HDR values of glows in `tasty()` output. Used only with `modes.hdr`. Experimental, like every CSS HDR tier. |
86
+ | `modes.dark` | `boolean` | `true` | Emit dark variants. See [Output modes](./output.md#output-modes). |
87
+ | `modes.highContrast` | `boolean` | `false` | Emit high-contrast variants. Independent of `contrastLevel`, except that a global level of `100` drops the tier (it would duplicate the normal one). |
88
+ | `modes.hdr` | `boolean` | `false` | Emit the HDR values of glows past SDR white next to their SDR values, in every exporter. Off, the output is the same as with no `headroom` set. `css()` reads its own `hdr` option, which falls back to this. The CSS tiers (`css()`, `tasty()`, `tailwind()`) are [experimental](../hdr.mdx#browsers), so prefer turning it on per call, such as `theme.dtcg({ modes: { hdr: true } })`. See [HDR output](./output.md#hdr-output). |
89
+ | `shadowTuning` | `ShadowTuning` | — | Default tuning for every `shadow` color. A per-color `tuning` merges over it field by field. Fields and defaults: [`LightingTuning`](./color-definitions.md#lightingtuning). A `headroom` here throws. |
90
+ | `glowTuning` | `GlowTuning` | — | Default tuning for every `glow` color. Same shape and merge rule, plus [`headroom`](#glow-headroom). |
91
+ | `autoFlip` | `boolean` | `true` | Default for each color's `autoFlip`. When on, a contrast floor that can't be met on the requested side of the base may be met on the other side, and a tone delta that overshoots `[0, 100]` mirrors to the other side of its base. When off, only the requested direction is used: an unmet floor pins the tone to that direction's extreme (with a warning) and an overshooting tone delta clamps. Mix colors have no per-color `autoFlip` and always use this value. |
92
+ | `inferRole` | `boolean` | `true` | Infer a color's `role` (APCA polarity) from its name when it has no explicit `role`. With `false`, the base-opposite and `'text'` fallbacks still apply. Theme colors only: a standalone token never infers from its `name`. See [How a role is chosen](../contrast.mdx#how-a-role-is-chosen). |
93
+ | `contrastLevel` | `number \| 'auto'` | `'auto'` | Manual contrast level, 0–100. Resolves the `light` / `dark` variants at that point between normal (`0`) and high contrast (`100`); `lightContrast` / `darkContrast` stay the true high-contrast resolution. Numbers clamp to 0–100; a non-finite number or any other non-number throws. Full rules: [Manual contrast level](./contrast.md#manual-contrast-level). |
94
+
95
+ `pastel` is not part of `GlazeConfig`: it can only be set per instance or per color.
96
+
97
+ ## `GlazeConfigResolved`
98
+
99
+ What `glaze.getConfig()` and `theme.getConfig()` return: the same fields with defaults applied, plus `pastel`.
100
+
101
+ | Field | Type |
102
+ | --- | --- |
103
+ | `lightTone`, `darkTone` | `ToneWindow` |
104
+ | `darkDesaturation` | `number` |
105
+ | `states` | `{ dark: string; highContrast: string; hdr: string }` |
106
+ | `modes` | `{ dark: boolean; highContrast: boolean; hdr: boolean }` |
107
+ | `shadowTuning` | `ShadowTuning \| undefined` |
108
+ | `glowTuning` | `GlowTuning \| undefined` — `headroom` already clamped to 0–2. |
109
+ | `autoFlip` | `boolean` |
110
+ | `inferRole` | `boolean` |
111
+ | `contrastLevel` | `number \| 'auto'` |
112
+ | `pastel` | `boolean` — always `false` globally; the override's value on `theme.getConfig()`. |
113
+
114
+ ## Tone windows
115
+
116
+ ```ts
117
+ type ToneWindow =
118
+ | [number, number] // [lo, hi]
119
+ | { lo: number; hi: number; eps: number }
120
+ | false;
121
+ ```
122
+
123
+ `lo` and `hi` are OKHSL lightness boundaries (0–100). The authored tone scale `0–100` is spread linearly across the tone interval between them.
124
+
125
+ | Form | Meaning |
126
+ | --- | --- |
127
+ | `[lo, hi]` | Boundaries at the reference eps `0.05`. The common form. |
128
+ | `{ lo, hi, eps }` | Boundaries plus a per-scheme render eps. Advanced; see [Reference and render epsilon](../okhst.mdx#reference-and-render-epsilon). |
129
+ | `false` | The full range `[0, 100]` at the reference eps. Removes the boundaries, not the tone transfer. |
130
+
131
+ | Window | Default | Stored tone range |
132
+ | --- | --- | --- |
133
+ | `lightTone` | `[10, 100]` | 4.95–100 |
134
+ | `darkTone` | `[15, 95]` | 9.25–95.9 |
135
+
136
+ - **High contrast** replaces both windows with `[0, 100]`, keeping the window's `eps`.
137
+ - **`contrastLevel`** moves each boundary toward `[0, 100]` (formula below). A `false` window is already the full range, so it doesn't change with the level.
138
+ - **`mode: 'static'`** colors skip the window entirely.
139
+ - **Colors seeded with `from`** skip the light window so the light variant reproduces the literal; dark and high contrast still adapt. See [`from`](./color-definitions.md#from).
140
+
141
+ Windows are validated where they enter: by `configure()` on the spot, and for a per-instance override when the instance resolves. Endpoints must be finite numbers with `lo ≤ hi`, and an object window's `eps` must be a finite number above 0 and at most `1e6` (past that the tone transfer loses precision, and from about `1e15` it renders `NaN`). Anything else throws, `null` included, in `configure()` as in an override (messages under [Errors](#errors)). Endpoints outside 0–100 then clamp into it, keeping the form you wrote: `[-10, 120]` is stored as `[0, 100]`. `lo === hi` is allowed and renders every tone at that one lightness.
142
+
143
+ ## Glow headroom
144
+
145
+ `glowTuning.headroom` lets a glow go past SDR white. It counts stops above white, doublings of white's luminance: `1` allows up to 2× white (tone 121.98), `2` up to 4× (tone 144.34). At the default `0` nothing changes and no variant carries an HDR value. What an HDR glow is for, and where browsers stand: [HDR glows](../hdr.mdx).
146
+
147
+ ```ts
148
+ glaze.configure({ glowTuning: { headroom: 1 } });
149
+ ```
150
+
151
+ - **Where it's set.** Globally (`glowTuning`), per theme (the override's `glowTuning`; a `glaze.color()` token is never a glow), per color (a glow's `tuning`), or per call (`glaze.glow({ tuning })`). A glow's `tuning.headroom` merges over the global `glowTuning` field by field. An override's `glowTuning` replaces the global object as a whole, so `glaze(280, 80, { glowTuning: { tint: 0.1 } })` drops a global `headroom`, and so does a child's `glowTuning` in `extend()`.
152
+ - **When it applies.** Only to a glow whose target tone passes 100 while its SDR value is pinned at its brightest; the exact conditions are in [When a glow gets an HDR value](../hdr.mdx#when-a-glow-gets-an-hdr-value). Such a variant gains `hdr`, an opaque base plus a gain in stops; see [`ResolvedHdr`](./color-definitions.md#hdr-values-resolvedhdr). Every scheme uses the same headroom, high contrast included.
153
+ - **Validation.** It must be a finite number ≥ 0; anything else throws. Above 2 it clamps to 2 with a [warning](#headroom-clamped), deduplicated per field and value and printed only once the whole `configure()` call, instance override, or color map has passed. A `headroom` on `shadowTuning`, a shadow's `tuning`, or `glaze.shadow()` throws: a shadow darkens toward black, and nothing goes below black.
154
+ - **Export.** `export()` freezes `glowTuning` with the clamped headroom, and a glow's own `tuning.headroom` travels with its definition. Snapshots stay at version 2.
155
+ - **Output.** The exporters emit HDR values only with `modes.hdr`; see [HDR output](./output.md#hdr-output).
156
+
157
+ ## Per-instance overrides
158
+
159
+ A `GlazeConfigOverride` overrides resolve-relevant fields for one theme or color token. It is accepted by:
160
+
161
+ | Call | Effect |
162
+ | --- | --- |
163
+ | `glaze(hue, saturation, config)` / `glaze(seed, config)` | The theme's override. See [Themes](./theme.md#per-theme-config-override). |
164
+ | `glaze.color(input, config)` | The token's override. See [Color tokens](./color-tokens.md). |
165
+ | `theme.extend({ config })` | Shallow-merged over the parent's override for the child theme. |
166
+ | `theme.export(override)`, `token.export(override)`, `palette.export(override)` | Merged into the frozen snapshot config only; the live instance is unchanged. |
167
+
168
+ ### `GlazeConfigOverride`
169
+
170
+ | Field | Type | Falls back to | Description |
171
+ | --- | --- | --- | --- |
172
+ | `lightTone` | `ToneWindow` | global `lightTone` | Light tone window, or `false` for the full range. Validated and clamped like the global one when the instance resolves. |
173
+ | `darkTone` | `ToneWindow` | global `darkTone` | Dark tone window, or `false` for the full range. Validated the same way. |
174
+ | `darkDesaturation` | `number` | global `darkDesaturation` | Dark saturation reduction (0–1). Clamps to 0–1 when the instance resolves; non-finite numbers throw. |
175
+ | `autoFlip` | `boolean` | global `autoFlip` | Default for each color's `autoFlip`. |
176
+ | `inferRole` | `boolean` | global `inferRole` | Name-based role inference. |
177
+ | `contrastLevel` | `number \| 'auto'` | global `contrastLevel` | Manual contrast level for this instance. `'auto'` opts out of a global level. A per-instance level never changes which modes are emitted. Validated when the instance resolves. |
178
+ | `shadowTuning` | `ShadowTuning` | global `shadowTuning` | Replaces the global shadow tuning as a whole for this instance. Per-color `tuning` still merges over it field by field. Meaningful for themes; a token has no shadows, but a `headroom` here still throws when it resolves. |
179
+ | `glowTuning` | `GlowTuning` | global `glowTuning` | Same for glows, `headroom` included: an override without `headroom` drops the global one. Validated when the instance resolves. |
180
+ | `pastel` | `boolean` | `false` | Instance-wide [`pastel`](./color-definitions.md#per-color-pastel) default for colors that don't set their own. Instance-only: never read from the global config. |
181
+
182
+ `states` and `modes` are not part of the override. They are global, or per call on the [output methods](./output.md).
183
+
184
+ ### Merge rules
185
+
186
+ - **Sparse and live.** An instance stores only the fields you set. At resolve time they are merged over the *current* global config, so fields you didn't set keep following `glaze.configure()`.
187
+ - **Copied on creation.** The theme or token copies the override when it is created (and `extend()` copies the child's), so changing the object or array you passed afterwards has no effect. Fields are read by property access, so a class instance with getters, inherited fields, or a proxy works as an override. Validation still waits for the resolve.
188
+ - **Overridden fields are pinned.** A later `configure()` can't change them for this instance.
189
+ - **`extend`** inherits the parent's override and shallow-merges the child's on top.
190
+ - **Export freezes.** `.export(override?)` writes `getConfig() ∪ instance override ∪ override` into the snapshot, and `themeFrom` / `colorFrom` / `paletteFrom` pin that freeze as the restored instance's override. Nested objects (windows, tunings) are replaced wholesale, not merged. `states` and `modes` are never frozen.
191
+ - **`contrastLevel` is frozen only when authored.** A level set on the instance or passed to `.export()` is written to the snapshot; a level inherited from the global config is a live preference and is left out, so a restored theme keeps following the global slider.
192
+
193
+ ```ts
194
+ import { glaze } from '@tenphi/glaze';
195
+
196
+ const t = glaze(280, 80, { lightTone: [0, 50] });
197
+ t.colors({ text: { tone: 50, saturation: 1 } });
198
+
199
+ t.resolve().get('text')!.light.t;
200
+ // → 0.2534… — inside [0, 50], whatever the global lightTone
201
+ t.resolve().get('text')!.dark.s;
202
+ // → 0.72 (darkDesaturation 0.1, not overridden)
203
+
204
+ glaze.configure({ darkDesaturation: 0.5 });
205
+ t.resolve().get('text')!.dark.s;
206
+ // → 0.4 — follows the global change
207
+
208
+ const child = t.extend({ config: { darkTone: false } });
209
+ child.getConfig().lightTone; // → [0, 50] (inherited)
210
+ child.getConfig().darkTone; // → false (added)
211
+ ```
212
+
213
+ ```ts
214
+ // Standalone token without a dark window: light keeps the input tone,
215
+ // dark is its exact inversion (100 − T)
216
+ glaze.color('#26fcb2', { darkTone: false });
217
+
218
+ // Restore the #000 → white dark flip with a full-height dark window
219
+ glaze.color('#000000', { lightTone: false, darkTone: [15, 100] });
220
+
221
+ // Structured token with an override
222
+ glaze.color(
223
+ { hue: 152, saturation: 95, tone: 74 },
224
+ { darkTone: false, pastel: true },
225
+ );
226
+ ```
227
+
228
+ ## Adaptation modes
229
+
230
+ `mode` on a regular color (`AdaptationMode`) selects which steps of the [scheme mapping](#scheme-mapping) apply to it. What each mode is for, with visuals: [Adaptation modes](../schemes.mdx#adaptation-modes).
231
+
232
+ | Mode | Inverted in dark | Tone windows | `darkDesaturation`, dark seed |
233
+ | --- | --- | --- | --- |
234
+ | `'auto'` (default) | Yes, in `dark` and `darkContrast` | Applied | Applied |
235
+ | `'fixed'` | No | Applied | Applied |
236
+ | `'static'` | No | Skipped | Skipped |
237
+
238
+ Tone deltas (`'+N'` / `'-N'`) are measured from the base. Under `'fixed'` and `'static'` the delta is applied to the base's tone in each scheme, sign kept. Under `'auto'` it is applied as is in `light` and `lightContrast`. In dark, Glaze steps from the base's **anchor**, where the base sits on the authored scale, then inverts and remaps the result like an authored tone:
239
+
240
+ | Base | Anchor |
241
+ | --- | --- |
242
+ | An `'auto'` base placed by a tone (absolute, `from`, `'max'` / `'min'` without a base) | Its authored tone. |
243
+ | An `'auto'` tone delta | Its base's anchor plus the delta. |
244
+ | An `'auto'` base with no `tone` or `contrast` of its own | Its base's anchor. |
245
+ | Any other base: `'fixed'` or `'static'`, or an `'auto'` base placed by a contrast floor, a mix, or a `'max'` / `'min'` on a base | Its own dark tone, read back through the dark window. |
246
+
247
+ At the reference dark `eps`, every dark step is then the dark window's scale times the delta, about 0.87× at the defaults (`'-20'` steps +17.3 on tone 97 and on tone 50 alike), and every `darkContrast` step is exact, under any light `eps`. The dark window sets their size, and a custom dark `eps` reshapes them in `dark` and `darkContrast` alike: a dark delta lands where the same authored tone would. A `'fixed'` or `'static'` base doesn't invert, but the `'auto'` dependent does, around wherever the base sits: `'+10'` on a fixed tone-30 base steps +10 in light and −8.7 in dark. Near the ends of the scale, dark has two clamps of its own (a step off the scale, a base past the dark window), listed under [Tone values](./color-definitions.md#tone-values). Concept: [How relative tone adapts](../schemes.mdx#how-relative-tone-adapts).
248
+
249
+ ## Scheme mapping
250
+
251
+ Every scheme runs the same tone pipeline; there is no fitted dark curve. The transfer functions are specified in [Tone and OKHST](../okhst.mdx) and the [canonical OKHST specification](https://github.com/tenphi/okhst).
252
+
253
+ ### Tone
254
+
255
+ For an authored tone `T` (0–100) and the scheme's active window `{ lo, hi, eps }`:
256
+
257
+ ```text
258
+ T' = 100 − T if mode is 'auto' and the scheme is dark
259
+ = T otherwise
260
+ loT = toTone(lo / 100, eps)
261
+ hiT = toTone(hi / 100, eps)
262
+ l = fromTone(loT + (T' / 100) × (hiT − loT), eps) // OKHSL lightness
263
+ t = toTone(l, REF_EPS) // stored tone
264
+ ```
265
+
266
+ `mode: 'static'` skips all of it: `t = clamp(T, 0, 100)`. The stored `t` is what `ResolvedColorVariant.t` holds (divided by 100), what tone deltas are measured against, and what the contrast solver searches.
267
+
268
+ The active window per variant:
269
+
270
+ | Variant | Window |
271
+ | --- | --- |
272
+ | `light` | `lightTone` (widened by `contrastLevel`) |
273
+ | `dark` | `darkTone` (widened by `contrastLevel`) |
274
+ | `lightContrast`, `darkContrast` | `[0, 100]`, with the configured window's `eps` |
275
+
276
+ With a numeric `contrastLevel` `L` and `f = L / 100`, the normal windows move toward the full range:
277
+
278
+ ```text
279
+ lo(f) = lo × (1 − f)
280
+ hi(f) = hi + (100 − hi) × f
281
+ ```
282
+
283
+ The defaults at level 50 are light `[5, 100]` (stored tone 1.63–100) and dark `[7.5, 97.5]` (stored tone 3.15–97.97). `eps` is not interpolated.
284
+
285
+ Authored tones under the default windows (neutral seed), as stored tone:
286
+
287
+ | Authored | Mode | `light` | `dark` | `lightContrast` | `darkContrast` |
288
+ | ---: | --- | ---: | ---: | ---: | ---: |
289
+ | 0 | `auto` | 4.95 | 95.9 | 0 | 100 |
290
+ | 50 | `auto` | 52.47 | 52.57 | 50 | 50 |
291
+ | 100 | `auto` | 100 | 9.25 | 100 | 0 |
292
+ | 0 | `fixed` | 4.95 | 9.25 | 0 | 0 |
293
+ | 100 | `fixed` | 100 | 95.9 | 100 | 100 |
294
+ | 50 | `static` | 50 | 50 | 50 | 50 |
295
+
296
+ A render `eps` other than the reference bends mid tones: with `lightTone: { lo: 10, hi: 100, eps: 0.2 }`, authored tone 50 stores 63.89 in `light` (instead of 52.47) and 62.95 in `lightContrast`.
297
+
298
+ ### Saturation
299
+
300
+ ```text
301
+ S_dark = S_light × (1 − darkDesaturation) // dark and darkContrast
302
+ ```
303
+
304
+ - Skipped for `mode: 'static'`.
305
+ - Bypassed when the dark saturation is authored: always for a per-color `darkSaturation`, and for a theme `darkSaturation` seed on every color that reads the seed. A [`from`](./color-definitions.md#from) color without its own `saturation` or `darkSaturation` starts from the literal's saturation instead, so a seed `darkSaturation` doesn't bypass its reduction. See [Dark seed](../schemes.mdx#dark-seed).
306
+ - `darkDesaturation` clamps to 0–1 where it is set, so it can remove at most all of a color's saturation and never adds any: `2` acts as `1` (dark variants go gray) and `-0.5` as `0`. A non-finite value throws.
307
+
308
+ With seed saturation 80 and the default `0.1`, an `auto` color's saturation is `0.8` in `light` and `0.72` in `dark` and `darkContrast`; a `static` one stays `0.8`.
309
+
310
+ ### Hue
311
+
312
+ Hue isn't mapped. Dark variants re-anchor relative hues to the theme's `darkHue` seed and use a per-color `darkHue` when one is set. An absolute per-color `hue` (or a `from` color's literal hue, when it has no `hue` or `darkHue`) is kept as is, and `static` colors ignore both. See [Dark seed](../schemes.mdx#dark-seed) and [`darkHue` / `darkSaturation`](./color-definitions.md#dark-seed-darkhue--darksaturation).
313
+
314
+ ## Validation
315
+
316
+ Glaze validates a theme's definitions when it resolves — on the first `resolve()` or output call after the definitions change, not when you call `colors()`. Graph errors throw; recoverable numeric bounds are clamped; a physically unreachable target pins to the limit of its search and warns; risky combinations resolve and warn.
317
+
318
+ ### Errors
319
+
320
+ | Condition | Message |
321
+ | --- | --- |
322
+ | `contrast` without `base` (theme color) | `glaze: color "a" has "contrast" without "base".` |
323
+ | Relative `tone` without `base` (theme color), in either entry of a pair (`[50, '+20']` included) | `glaze: color "a" has relative "tone" without "base".` |
324
+ | No absolute `tone`, no `from`, and no `base` | `glaze: color "a" must have either absolute "tone" (root), "from" (a literal color), or "base" (dependent).` |
325
+ | `from` can't be parsed | `glaze: color "a" has an invalid "from" value. glaze: invalid hex color "#zzz".` |
326
+ | Malformed `contrast`, regular or mix: a misspelled preset (`'aa'`, `{ apca: 'Body' }`), a non-finite number, an object without exactly one of `wcag` / `apca` set (a key holding `undefined` is unset; other keys are ignored), or a pair without two entries | `glaze: color "a" has an invalid "contrast" "aa". Use a WCAG ratio or 'AA' / 'AAA' / 'AA-large' / 'AAA-large', { wcag: … }, { apca: … }, or a [normal, highContrast] pair of these.` |
327
+ | A color field the resolver can't place: a `tone`, `hue`, `darkHue`, `saturation`, `darkSaturation`, mix `value`, or shadow / glow `intensity` that isn't a finite number, a relative string that isn't `'+N'` / `'-N'` (`'+-5'`), or an `opacity` outside 0–1 | `glaze: color "a" has an invalid "tone" NaN.` |
328
+ | A theme seed `hue` / `saturation` (or a given `darkHue` / `darkSaturation`) that isn't a finite number, at `glaze()` | `glaze: theme seed saturation must be a finite number (got undefined).` |
329
+ | `contrast` pair switches metric (`[4.5, { apca: 75 }]`), regular or mix | `glaze: color "a" has a "contrast" pair that switches metric (wcag → apca). A WCAG ratio and an APCA Lc are different scales; use one metric for both entries, e.g. { wcag: [normal, highContrast] }.` |
330
+ | `base` names a missing color | `glaze: color "a" references non-existent base "nope".` |
331
+ | `base` names a shadow or glow | `glaze: color "b" base "a" references a glow color.` |
332
+ | Shadow / glow without a `bg` | `glaze: shadow "a" is missing "bg".` (`glaze.shadow()` / `glaze.glow()`: `glaze.shadow(): "bg" is required.`) |
333
+ | Shadow / glow `bg` or `fg` names a missing color | `glaze: shadow "a" references non-existent bg "nope".` |
334
+ | Shadow / glow `bg` or `fg` names a shadow or glow | `glaze: glow "b" bg "a" references a shadow color.` |
335
+ | Mix `base` names a missing color | `glaze: mix "a" references non-existent base "nope".` |
336
+ | Mix `target` names a missing color | `glaze: mix "a" references non-existent target "nope".` |
337
+ | Mix `base` / `target` names a shadow or glow | `glaze: mix "a" target "sh" references a shadow color.` |
338
+ | Circular reference through `base`, `bg`, `fg`, or a mix's `base` / `target` | `glaze: circular base reference detected involving "a".` |
339
+ | Non-finite or non-number `contrastLevel` other than `'auto'`, `null` included (thrown by `configure()`, or when an instance with such an override resolves) | `glaze: contrastLevel must be a finite number 0–100 or 'auto' (got NaN).` |
340
+ | Non-finite or non-number `darkDesaturation` (same two places) | `glaze: darkDesaturation must be a finite number 0–1 (got NaN).` |
341
+ | Tone window with `lo` above `hi` (same two places, for `lightTone` or `darkTone`) | `glaze: lightTone lo (90) is above hi (20). A window runs from its dark end to its light end: [lo, hi] with lo ≤ hi, e.g. [20, 90].` |
342
+ | Tone window endpoint that isn't a finite number | `glaze: lightTone lo must be a finite number (got NaN).` |
343
+ | Object tone window whose `eps` isn't a finite number above 0 and at most `1e6` (missing included) | `glaze: lightTone eps must be a finite number above 0 and at most 1e6 (got 0).` |
344
+ | Tone window that isn't `[lo, hi]`, `{ lo, hi, eps }`, or `false` (`null` included) | `glaze: lightTone must be [lo, hi], { lo, hi, eps }, or false (got [10]).` |
345
+ | Glow `headroom` that isn't a finite number ≥ 0: in `glowTuning` (same two places), a glow's `tuning` (at resolve), or `glaze.glow()` | `glaze: glowTuning.headroom must be a finite number ≥ 0 — stops above SDR white (got -1).` The prefix names the source (`glow "g" tuning`, `glaze.glow() tuning`), and the value is shown as written: `(got "2")`, `(got [1])`. |
346
+ | `objectInfluence`, `tint`, `reach`, or `hueBleed` set to something other than a finite number, in any tuning | `glaze: shadowTuning.reach must be a finite number (got NaN).` (prefixed like `headroom`) |
347
+ | `headroom` in `shadowTuning`, a shadow's `tuning`, or `glaze.shadow()` | `glaze: shadowTuning.headroom is not supported — a shadow darkens toward black, and nothing goes below black. Headroom is for glows (glowTuning or a glow's tuning).` |
348
+
349
+ ### Color value errors
350
+
351
+ Every place that takes a `GlazeColorValue` — a theme color's `from`, `glaze.color()`, and the `bg` / `fg` of `glaze.shadow()` / `glaze.glow()` — uses one parser. It throws immediately for standalone calls; for `from` the message is wrapped in the color's name (above).
352
+
353
+ | Input | Message |
354
+ | --- | --- |
355
+ | Hex of the wrong length (not 3, 4, 6, or 8 digits) or with non-hex digits | `glaze: invalid hex color "#zzz".` |
356
+ | A string that isn't hex or `rgb()` / `rgba()` / `hsl()` / `hsla()` / `okhsl()` / `okhst()` / `oklch()` (named colors included) | `glaze: unsupported color string "red".` |
357
+ | A color function without exactly 3 components (alpha aside) | `glaze: expected 3 components in "rgb(1 2)".` |
358
+ | A color-function component that isn't a number (or `none`), or has a unit it can't take | `glaze: invalid component "abc" in "hsl(abc 50% 50%)".` |
359
+ | `{ h, s, l }` with a non-finite field | `glaze.color: OkhslColor h/s/l must be finite numbers.` |
360
+ | `{ h, s, l }` with `s` or `l` above 1.5 | `glaze.color: OkhslColor s/l must be in 0–1 range. Did you mean the structured form { hue, saturation, tone } (which uses 0–100)?` |
361
+ | `{ h, s, l }`, `{ h, s, t }`, or `{ l, c, h }` with a 0–1 field below 0 | `glaze.color: OkhslColor s must be 0 or more (got -0.1).` (`OkhstColor` / `OklchColor` likewise) |
362
+ | `{ h, s, t }` with a non-finite field, or `s` / `t` above 1.5 | `glaze.color: OkhstColor h/s/t must be finite numbers.` / `glaze.color: OkhstColor s/t must be in 0–1 range. …` |
363
+ | `{ r, g, b }` with a channel outside 0–255 | `glaze.color: RgbColor r must be a finite number in 0–255 (got 300).` |
364
+ | `{ l, c, h }` with a non-finite field, or `l` / `c` above 1.5 | `glaze.color: OklchColor l/c/h must be finite numbers.` / `glaze.color: OklchColor l/c must be in 0–1 range (matching oklch() strings).` |
365
+ | An `[r, g, b]` array | `glaze.color: RGB tuple [r, g, b] is no longer supported — use { r, g, b } instead.` |
366
+
367
+ Errors specific to the structured `glaze.color()` input, restoring snapshots, and building palettes are listed on [Color tokens](./color-tokens.md), [Themes](./theme.md#glazethemefromdata--glazefromdata), and [Palettes](./palette.md).
368
+
369
+ ### Allowed without `base`
370
+
371
+ | Condition | Behavior |
372
+ | --- | --- |
373
+ | `tone: 'max'` / `'min'` | A root color at the scheme's tone extreme. |
374
+ | `contrast` without `base` in `glaze.color()` | Measured against the token's own input color. |
375
+ | A tone delta (`tone: '+N'`, or in either entry of a pair) without `base` in `glaze.color()` | Measured from the token's own input color. |
376
+
377
+ ### Clamped silently
378
+
379
+ | Value | Range |
380
+ | --- | --- |
381
+ | Resolved `tone` | 0–100 |
382
+ | Per-color `saturation` / `darkSaturation` factor | 0–1 |
383
+ | Shadow / glow `intensity` | 0–100 (an unreachable step then warns) |
384
+ | Lighting `tint`, `reach`, `hueBleed` | 0–1. `objectInfluence` isn't clamped; the object factor `k` floors at 0. |
385
+ | Mix `value` | 0–100 |
386
+ | `contrastLevel` | 0–100 |
387
+ | `darkDesaturation` | 0–1 |
388
+ | Tone window `lo` / `hi` | 0–100 (after the `lo ≤ hi` check) |
389
+ | WCAG numeric target | At least 1 |
390
+ | APCA numeric target | Its magnitude (`-60` → `60`) |
391
+
392
+ A tone delta that overshoots `[0, 100]` mirrors to the other side of its base when `autoFlip` is on; if the mirror also overshoots, it clamps on the authored side. With `autoFlip: false` it clamps on the authored side. For example, a `'static'` color with `tone: '-20'` on a static base at tone 10 resolves to 30 with `autoFlip` and to 0 without, in every scheme; with `'-60'` on a base at 50 it resolves to 0 either way. An `'auto'` child gets the same results in light. In dark it mirrors around wherever the base sits, by the anchor rule in [Adaptation modes](#adaptation-modes): with `autoFlip`, the +20 it took in light turns into a step down from tone 10, runs off the scale, and stops at the dark window's edge, 9.25; without it, the `'-20'` becomes 17.3 tones up, 27.3. The flip is always decided in light, and dark mirrors whichever side light took, with two dark-only clamps (a step off the scale, a base past the dark window) listed under [Tone values](./color-definitions.md#tone-values).
393
+
394
+ ### Resolved with a warning
395
+
396
+ | Condition | Result |
397
+ | --- | --- |
398
+ | A regular color's `contrast` floor is out of reach | Pins to the extreme of the side searched first; [contrast unmet](#contrast-unmet) warning. |
399
+ | A mix's `contrast` floor is out of reach | Pins to the end of the base → target range it searched; [mix contrast unmet](#mix-contrast-unmet) warning. |
400
+ | A shadow / glow step is out of the pigment's reach | Pins to the pigment's strongest composite (or stays transparent if it can't move the surface at all); [lighting](#shadow-or-glow-out-of-reach) warning. A glow whose HDR value covers the step doesn't warn. |
401
+ | A glow step past white that its `headroom` cap leaves short | The HDR value pins at the cap, `2^headroom` on the brightest linear channel; the HDR form of the [lighting](#shadow-or-glow-out-of-reach) warning. |
402
+ | A glow `headroom` above 2 | Clamps to 2; [headroom clamped](#headroom-clamped) warning. |
403
+ | `contrast` and `opacity` on the same color | Resolves the floor for the opaque color; [contrast and opacity](#contrast-and-opacity) warning. |
404
+ | A color string with an alpha component (`#rrggbbaa`, `rgb(… / a)`) where Glaze has no opacity field | Alpha dropped; [alpha dropped](#alpha-dropped) warning. |
405
+
406
+ An unmet mix normally pins to the pure target. A mix whose value is already 100, including the high-contrast entry of `value: [80, 100]`, can only search toward the base, so when its pure target misses the floor it pins to the pure base instead. A pure target that clears the floor, even inside the solver's margin, is kept.
407
+
408
+ ## Warnings
409
+
410
+ Glaze reports problems it can recover from through `console.warn`, always prefixed with `glaze:`. Nothing is thrown.
411
+
412
+ | Warning | Emitted when | Deduplicated |
413
+ | --- | --- | --- |
414
+ | [Contrast unmet](#contrast-unmet) | The solver can't reach a regular color's floor in a scheme. | Yes |
415
+ | [Mix contrast unmet](#mix-contrast-unmet) | The solver can't reach a mix's floor in a scheme. | Yes |
416
+ | [Contrast drift](#contrast-drift) | A chromatic color the solver reported as meeting its floor measures under it after resolution. | Yes |
417
+ | [Shadow or glow out of reach](#shadow-or-glow-out-of-reach) | A `shadow` / `glow` step exceeds what its pigment can do, or a glow's `headroom` cap leaves its step short. | Yes |
418
+ | [Headroom clamped](#headroom-clamped) | A glow `headroom` above 2 is clamped. | Yes |
419
+ | [Contrast and opacity](#contrast-and-opacity) | A regular color has both `contrast` and `opacity`. | No — once per resolve |
420
+ | [Alpha dropped](#alpha-dropped) | A color value carries alpha that Glaze discards. | Once per color string |
421
+ | [Percentage scale](#percentage-scale) | A `format*` writer gets a value above 1. | Once per writer |
422
+ | Palette collision | Two palette themes produce the same token name. See [Collision detection](./palette.md#collision-detection). | No |
423
+
424
+ The first five share one session cache of 256 keys, which starts over when full. A key includes the color name, scheme, target, and measured value (for a clamp, the field and value), so a dev server that re-resolves on every change prints each distinct problem once. Resolved themes are cached until their definitions or the config change, so the contrast-and-opacity warning prints once per actual resolve. The alpha-dropped warning keeps its own cache of 256 color strings, so it prints once per string.
425
+
426
+ ### Contrast unmet
427
+
428
+ ```text
429
+ glaze: color "badge-text" cannot meet WCAG 7.00 in light scheme
430
+ (got 4.94).
431
+ Try widening the tone window, lowering the contrast target, or picking
432
+ a base color further from this color's tone.
433
+ ```
434
+
435
+ Fires when the result is more than 2% under a WCAG target, or more than 1.5 Lc under an APCA target (the solver aims slightly past the target, so smaller shortfalls are rounding). The scheme is `light`, `dark`, `lightContrast`, or `darkContrast`; APCA targets print as `APCA Lc 75.0`. Standalone tokens print their `name` option, or `"value"` when it isn't set. Fixes: [When a floor can't be met](../contrast.mdx#when-a-floor-cant-be-met).
436
+
437
+ ### Mix contrast unmet
438
+
439
+ ```text
440
+ glaze: mix "accent-wash" cannot meet WCAG 4.50 against its base in
441
+ light scheme (got 3.54).
442
+ Even the pure target falls short; try lowering the contrast target or
443
+ mixing toward a color further from the base.
444
+ ```
445
+
446
+ This one is for `accent-wash: { type: 'mix', base: 'surface', target: 'accent', value: 10, contrast: 'AA' }` in `glaze(280, 80)`, with `surface` at tone 97 and `accent` at tone 55. It uses the same margins and scheme labels as [contrast unmet](#contrast-unmet), measured between the mix and its `base`. `got` is the contrast of the value the mix settled at: the pure target, or the pure base when the value is already 100 (`got 1.00`, or `0.00` for an APCA floor). Fixes: [Contrast on mixes](../mixes.mdx#contrast-on-mixes).
447
+
448
+ When the pastel cap is why a mix misses (its target is a standard color outside the pastel gamut that would meet the floor painted in full), the hint names the cap:
449
+
450
+ ```text
451
+ Even the pure target falls short once the mix's pastel gamut caps its
452
+ saturation; try `pastel: false` on the mix, lowering the contrast
453
+ target, or mixing toward a color further from the base.
454
+ ```
455
+
456
+ See [Pastel mixes](../mixes.mdx#pastel-mixes).
457
+
458
+ ### Contrast drift
459
+
460
+ ```text
461
+ glaze: color "link" drifts below WCAG 4.50 in light scheme
462
+ (measured <ratio>).
463
+ Chromatic luminance differs from the gray tone; nudge the tone or
464
+ saturation if the floor matters.
465
+ ```
466
+
467
+ After all schemes resolve, Glaze re-measures every regular color with `contrast` and `base` against its base, in the floor's metric and the color's role polarity, with the same margins. It skips a variant whose floor the solver reported as unmet, which already has its [contrast unmet](#contrast-unmet) warning, and an achromatic variant (saturation 0), whose tone is exact. The solver measures the real chromatic color, so a floor it meets passes this check too: the advisory is a safety net and normally stays silent. Mixes are not re-measured; an unmet mix has [its own warning](#mix-contrast-unmet). At an effective `contrastLevel` of 100, global or per instance, the high-contrast slots are skipped because they mirror the normal ones. Background: [Chromatic drift advisory](../contrast.mdx#chromatic-drift-advisory).
468
+
469
+ ### Shadow or glow out of reach
470
+
471
+ ```text
472
+ glaze: glow "g" cannot reach a 5.0-tone step in light scheme
473
+ (reached 0.0 at full strength).
474
+ Lower the intensity, move the surface away from white, or raise a
475
+ tuned-down `reach`.
476
+ ```
477
+
478
+ Fires when the composite lands more than 0.05 tones short of `intensity × k`. The standalone functions name themselves instead of a color and omit the scheme: `glaze: glaze.glow() cannot reach a 5.0-tone step (reached 0.0 at full strength). …`. A shadow's message suggests moving the surface away from black. See [Unreachable steps](../shadows-and-glows.mdx#unreachable-steps).
479
+
480
+ With a [`headroom`](#glow-headroom), a glow whose HDR value covers the step prints nothing. One the cap leaves short gets this form instead, here for a 30-tone glow on a tone-100 surface at `headroom: 1`:
481
+
482
+ ```text
483
+ glaze: glow "big" cannot reach a 30.0-tone step in light scheme even
484
+ with 1 stop of headroom (reached 22.0).
485
+ Lower the intensity or raise `headroom`.
486
+ ```
487
+
488
+ At `headroom: 2` the advice reads ``Lower the intensity; `headroom` is already at its 2-stop maximum.`` A glow without an HDR value, such as one whose pigment can't lift a non-white surface at all (`reach: 0` on a gray surface), keeps the first form; see [When a glow gets an HDR value](../hdr.mdx#when-a-glow-gets-an-hdr-value).
489
+
490
+ ### Headroom clamped
491
+
492
+ ```text
493
+ glaze: glowTuning.headroom 3 is above the 2-stop maximum; using 2.
494
+ Headroom counts stops (doublings of SDR white's luminance), not a
495
+ ratio: 1 stop is 2× white, 2 stops 4×.
496
+ ```
497
+
498
+ A `headroom` above 2 is likely a ratio typed where stops were meant (`4` for "4× white"). The prefix names where the value came from: `glowTuning`, `glow "g" tuning`, or `glaze.glow() tuning`. Printed once per field and value, and only after the whole `configure()` call, instance override, or color map has validated, so a call that throws on another field leaves the warning for the corrected retry.
499
+
500
+ ### Contrast and opacity
501
+
502
+ ```text
503
+ glaze: color "text" has both "contrast" and "opacity".
504
+ Opacity makes perceived tone unpredictable.
505
+ ```
506
+
507
+ Emitted during validation. For a translucent color with a floor, use a [transparent mix](../mixes.mdx).
508
+
509
+ ### Alpha dropped
510
+
511
+ ```text
512
+ glaze: alpha component dropped from "#ffffff80"; use the color's
513
+ `opacity` (or a transparent mix) for translucency.
514
+ ```
515
+
516
+ Any color string Glaze parses — a `from` value, a `glaze.color()` value, or a `glaze.shadow()` / `glaze.glow()` input — keeps only its color. Use `opacity` on a regular color, or a transparent mix, for translucency.
517
+
518
+ ### Percentage scale
519
+
520
+ ```text
521
+ glaze: formatRgb() got a value above 1 — it takes the 0-1 factors every
522
+ Glaze producer returns (resolve(), variantToOkhsl, srgbToOkhsl), not
523
+ 0-100 percentages.
524
+ Divide by 100, or drop the "* 100" left over from the pre-2.0 signature.
525
+ ```
526
+
527
+ The [format writers](./utilities.md#format-writers) take `s` / `l` / `t` on 0–1. A value above 1 can only be pre-2.0 percentage input, so each writer warns the first time it sees one in the process (and still formats the value). Upgrade notes: [2.0 — `format*` takes 0–1](../migration.md#20--format-takes-01).