@tenphi/glaze 2.0.2 → 3.0.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.
@@ -0,0 +1,421 @@
1
+ ---
2
+ title: Color tokens
3
+ description: glaze.color() standalone tokens — input forms, color values, defaults, methods, config overrides, pairing, and snapshots.
4
+ ---
5
+
6
+ `glaze.color()` creates a single adaptive color without a theme. A token resolves the same four variants as a theme color (`light`, `dark`, `lightContrast`, `darkContrast`) and has the same output methods. For a guided introduction, see [Getting started](../getting-started.mdx#one-color-without-a-theme); for contrast between tokens, see [Contrast solving](../contrast.mdx#pairing-standalone-colors).
7
+
8
+ ```ts
9
+ import { glaze } from '@tenphi/glaze';
10
+ import type {
11
+ GlazeColorToken,
12
+ GlazeColorValue,
13
+ GlazeColorInput,
14
+ GlazeFromInput,
15
+ } from '@tenphi/glaze';
16
+ ```
17
+
18
+ ## `glaze.color(input, config?)`
19
+
20
+ ```ts
21
+ glaze.color(
22
+ input: GlazeColorValue | GlazeFromInput | GlazeColorInput,
23
+ config?: GlazeConfigOverride,
24
+ ): GlazeColorToken;
25
+ ```
26
+
27
+ The first argument is the color, in one of four shapes told apart by structure. The optional second argument is a [config override](#config-overrides) and works with every shape.
28
+
29
+ | Shape | Recognized by | Example | Form |
30
+ | --- | --- | --- | --- |
31
+ | Bare string | A string | `'#26fcb2'`, `'oklch(0.85 0.18 152)'` | Value |
32
+ | Value object | Any other object | `{ h: 152, s: 0.95, l: 0.74 }`, `{ r: 38, g: 252, b: 178 }` | Value |
33
+ | `{ from, ...overrides }` | Has a `from` key | `{ from: '#1a1a2e', base: bg, contrast: 'AA' }` | Value |
34
+ | Structured | Has a `hue` key | `{ hue: 152, saturation: 95, tone: 74 }` | Structured |
35
+
36
+ ```ts
37
+ // bare string
38
+ glaze.color('#26fcb2');
39
+ // OKHSL value object
40
+ glaze.color({ h: 152, s: 0.95, l: 0.74 });
41
+ // OKHST value object (tone axis)
42
+ glaze.color({ h: 152, s: 0.95, t: 0.7 });
43
+ // value + overrides
44
+ glaze.color({ from: '#1a1a2e', hue: '+20', contrast: 'AA' });
45
+ // structured, 0–100 scales
46
+ glaze.color({ hue: 152, saturation: 95, tone: 74 });
47
+ ```
48
+
49
+ The three **value** forms start from a literal color and take [`GlazeColorOverrides`](#glazecoloroverrides). The **structured** form is a theme-style definition on 0–100 scales; see [`GlazeColorInput`](#glazecolorinput).
50
+
51
+ ## `GlazeColorValue`
52
+
53
+ ```ts
54
+ type GlazeColorValue =
55
+ | string
56
+ | OkhslColor
57
+ | OkhstColor
58
+ | RgbColor
59
+ | OklchColor;
60
+ // An editor hint; strings are validated at runtime.
61
+ type HexColor = `#${string}`;
62
+
63
+ interface OkhslColor { h: number; s: number; l: number }
64
+ interface OkhstColor { h: number; s: number; t: number }
65
+ interface RgbColor { r: number; g: number; b: number }
66
+ interface OklchColor { l: number; c: number; h: number }
67
+ ```
68
+
69
+ The literal color forms. They are accepted by `glaze.color()`, by [`from`](./color-definitions.md#from) on a theme color, by a token's [`base`](#pairing-colors), and by `glaze.shadow()` / `glaze.glow()`.
70
+
71
+ | Form | Example | Notes |
72
+ | --- | --- | --- |
73
+ | Hex | `'#26fcb2'`, `'#abc'`, `'#26fcb2ff'`, `'#abcd'` | 3, 4, 6, or 8 digits after `#`. |
74
+ | `rgb()` / `rgba()` | `'rgb(38 252 178)'`, `'rgba(38, 252, 178, 0.8)'` | Channels 0–255 or percentages. |
75
+ | `hsl()` / `hsla()` | `'hsl(152 97% 57%)'` | Saturation and lightness as percentages, or bare 0–1 fractions. Unlike CSS, a bare `hsl(120 50 50)` is not 50%: it is out of range and resolves to white, without a warning. |
76
+ | `okhsl()` | `'okhsl(152 95% 74%)'` | OKHSL, Glaze's own output format. Saturation and lightness as percentages or bare 0–1 fractions, as for `hsl()`. |
77
+ | `okhst()` | `'okhst(152 95% 70%)'` | OKHST: the third value is tone (`70%` = tone 70). Percentages or bare 0–1 fractions, as for `hsl()`. Not a native CSS color; [Tasty](https://tasty.style) output can carry it. |
78
+ | `oklch()` | `'oklch(0.85 0.18 152)'` | L 0–1 (or a percentage), C (a percentage maps `100%` to 0.4), H in degrees. |
79
+ | `OkhslColor` | `{ h: 152, s: 0.95, l: 0.74 }` | `h` 0–360; `s`, `l` 0–1. |
80
+ | `OkhstColor` | `{ h: 152, s: 0.95, t: 0.7 }` | `h` 0–360; `s`, `t` 0–1. The `t` key tells it apart from OKHSL. |
81
+ | `RgbColor` | `{ r: 38, g: 252, b: 178 }` | sRGB, 0–255. |
82
+ | `OklchColor` | `{ l: 0.85, c: 0.18, h: 152 }` | Same meaning as `oklch()`: L and C 0–1, H in degrees. |
83
+
84
+ - Function names are case-insensitive. Components can be separated by spaces or commas.
85
+ - An alpha component (`#rrggbbaa`, `#rgba`, `/ 0.8`, or any fourth component, comma- or space-separated) is dropped with a `console.warn`. Use [`opacity`](#glazecoloroverrides) instead:
86
+
87
+ ```text
88
+ glaze: alpha component dropped from "rgb(1 2 3 4)"; use the color's
89
+ `opacity` (or a transparent mix) for translucency.
90
+ ```
91
+ - Value objects are recognized by their keys: `r`, `g`, `b` → RGB; `l`, `c`, `h` → OKLCh; `h`, `s`, `t` → OKHST; otherwise OKHSL.
92
+ - A hue is a bare number (degrees) or takes `deg`, `turn`, `rad`, or `grad`: `hsl(0.5turn 50% 50%)` has hue 180°.
93
+ - A component may be the CSS keyword `none`, read as 0 as CSS renders a missing component: `oklch(0.97 0 none)` is a gray. Any other component that isn't a number throws (`glaze: invalid component "abc" in "hsl(abc 50% 50%)".`).
94
+ - Out-of-range values clamp, as CSS clamps them: an `rgb()` channel to 0–255, `hsl()` / `okhsl()` / `okhst()` percentages to 0–100%, `oklch()` L to 0–1 and C to at least 0. A value object clamps a 0–1 field between 1 and 1.5 the same way, but throws for one below 0 or above 1.5, and for an RGB channel outside 0–255 (below).
95
+ - Not supported: named colors (`'red'`), relative color syntax, and `[r, g, b]` tuples.
96
+
97
+ | Input | Error |
98
+ | --- | --- |
99
+ | `'red'` | `glaze: unsupported color string "red".` |
100
+ | `'#12'` | `glaze: invalid hex color "#12".` |
101
+ | `'rgb(38 252)'` | `glaze: expected 3 components in "rgb(38 252)".` |
102
+ | `'rgb(38 252 x)'` | `glaze: invalid component "x" in "rgb(38 252 x)".` |
103
+ | `{ h: 152, s: 95, l: 74 }` | `glaze.color: OkhslColor s/l must be in 0–1 range. Did you mean the structured form { hue, saturation, tone } (which uses 0–100)?` |
104
+ | `{ h: 152, s: -0.1, l: 0.74 }` | `glaze.color: OkhslColor s must be 0 or more (got -0.1).` |
105
+ | `[38, 252, 178]` | `glaze.color: RGB tuple [r, g, b] is no longer supported — use { r, g, b } instead.` |
106
+ | `{ r: 300, g: 0, b: 0 }` | `glaze.color: RgbColor r must be a finite number in 0–255 (got 300).` |
107
+
108
+ The OKHSL, OKHST, and OKLCh object checks reject `s`, `l`, `t`, or `c` below 0, and above 1.5, which catches 0–100 input meant for the structured form. String components have no such guard (see `hsl()` above).
109
+
110
+ ## `GlazeColorOverrides`
111
+
112
+ The fields a value form can carry next to its color. `GlazeFromInput` is `GlazeColorOverrides` plus a required `from: GlazeColorValue`; a bare string or value object is the same with no overrides.
113
+
114
+ | Field | Type | Default | Description |
115
+ | --- | --- | --- | --- |
116
+ | `hue` | `number \| RelativeValue` | The value's hue | Absolute (0–360) replaces the hue. `'+N'` / `'-N'` is relative to the value's own hue, never to `base`. |
117
+ | `saturation` | `number` | The value's saturation | Saturation seed, 0–100. Replaces the value's saturation. |
118
+ | `saturationFactor` | `number` | `1` | Multiplier (0–1) on the saturation seed. |
119
+ | `tone` | `HCPair<ToneValue>` | The value's tone | Absolute (0–100), a tone delta (`'+N'` / `'-N'`), or `'max'` / `'min'`. A delta is measured from the value's own tone, or, with `base`, from the base. Under `mode: 'auto'` the dark variants mirror it; see [Pairing colors](#pairing-colors). |
120
+ | `darkHue` | `number \| RelativeValue` | `hue` | Hue for the dark variants. Absolute, or relative to the seed hue (the value's hue, or an absolute `hue` override). |
121
+ | `darkSaturation` | `number` | `saturation` | Dark saturation seed, 0–100. When set, `darkDesaturation` is not applied on top. |
122
+ | `darkSaturationFactor` | `number` | The light factor | Dark multiplier (0–1) on the dark seed. When set, `darkDesaturation` is not applied on top. |
123
+ | `mode` | `AdaptationMode` | `'auto'` | `'fixed'` maps without inverting; `'static'` keeps the same tone in every variant. See [`mode`](./color-definitions.md#mode). |
124
+ | `autoFlip` | `boolean` | The config's `autoFlip` | Mirror out-of-range results instead of clamping. See [`autoFlip`](./color-definitions.md#autoflip). |
125
+ | `contrast` | `HCPair<ContrastSpec>` | — | Contrast floor. Without `base`, measured against the input value itself; with `base`, against the base in each scheme. See [Contrast and high contrast](./contrast.md#contrastspec). |
126
+ | `base` | `GlazeColorToken \| GlazeColorValue` | — | The color this token's contrast and tone delta are measured against. See [Pairing colors](#pairing-colors). |
127
+ | `opacity` | `number` | `1` | Fixed alpha, 0–1. Out of range throws. With `contrast`, emits a `console.warn`. |
128
+ | `pastel` | `boolean` | The config override's `pastel` (`false`) | See [Per-color `pastel`](./color-definitions.md#per-color-pastel). |
129
+ | `role` | `RoleInput` | `'text'` | Fixes APCA polarity. See [Roles on tokens](#roles-on-tokens). |
130
+ | `name` | `string` | — | Debug label. See [`name`](#name). |
131
+
132
+ ```ts
133
+ // hue 282.9 → 302.9
134
+ glaze.color({ from: '#1a1a2e', hue: '+20' });
135
+ // saturation 0.375 → 0.5
136
+ glaze.color({ from: '#1a1a2e', saturation: 50 });
137
+ // saturation 0.375 → 0.187
138
+ glaze.color({ from: '#1a1a2e', saturationFactor: 0.5 });
139
+ ```
140
+
141
+ ## `GlazeColorInput`
142
+
143
+ The structured form: the same definition a theme color would have, with its own seed. Hue is in degrees; saturation and tone are on 0–100.
144
+
145
+ | Field | Type | Default | Description |
146
+ | --- | --- | --- | --- |
147
+ | `hue` | `number` | — | Hue seed, 0–360. Required. |
148
+ | `saturation` | `number` | — | Saturation seed, 0–100. Required. |
149
+ | `tone` | `HCPair<number \| ExtremeValue>` | — | Tone 0–100, or `'max'` / `'min'`. Required. Tone deltas aren't accepted. |
150
+ | `saturationFactor` | `number` | `1` | Multiplier (0–1) on the seed. |
151
+ | `darkHue` | `number \| RelativeValue` | `hue` | Dark hue: absolute, or relative to `hue`. |
152
+ | `darkSaturation` | `number` | `saturation` | Dark saturation seed, 0–100. |
153
+ | `darkSaturationFactor` | `number` | The light factor | Dark multiplier (0–1) on the dark seed. |
154
+ | `mode` | `AdaptationMode` | `'auto'` | Adaptation mode. |
155
+ | `autoFlip` | `boolean` | The config's `autoFlip` | Mirror out-of-range results instead of clamping. |
156
+ | `opacity` | `number` | `1` | Fixed alpha, 0–1. |
157
+ | `base` | `GlazeColorToken \| GlazeColorValue` | — | See [Pairing colors](#pairing-colors). |
158
+ | `contrast` | `HCPair<ContrastSpec>` | — | Without `base`, measured against the token's own input color (`hue`, `saturation`, normal `tone`). |
159
+ | `pastel` | `boolean` | The config override's `pastel` (`false`) | Per-color `pastel`. |
160
+ | `role` | `RoleInput` | `'text'` | Fixes APCA polarity. See [Roles on tokens](#roles-on-tokens). |
161
+ | `name` | `string` | — | Debug label. |
162
+
163
+ Out-of-range numbers throw before anything resolves, for example `glaze.color: structured saturation must be a finite number in 0–100 (got 120).` or `glaze.color: structured tone must be a finite number in 0–100 or 'max'/'min' (got +5).` `hue` and `darkHue` must be finite; the saturation seeds 0–100; the factors and `opacity` 0–1.
164
+
165
+ ## Defaults
166
+
167
+ - Every form defaults to `mode: 'auto'`, so a token adapts between light and dark like a theme color.
168
+ - **Value forms** (bare strings, value objects, and `{ from, ... }`) keep the input tone exactly in the light variant: they resolve with a local `lightTone: false`. Pass `lightTone` in the config override to change that.
169
+ - **The structured form** uses the live global tone windows, like a theme color.
170
+ - Every other config field follows the live global config at resolve time, unless the [config override](#config-overrides) sets it. `pastel` is instance-only: it comes from the override or the color's `pastel`, never from `glaze.configure()`.
171
+
172
+ ```ts
173
+ const brand = glaze.color('#26fcb2');
174
+ brand.resolve().light; // renders exactly #26fcb2
175
+ brand.json();
176
+ // → {
177
+ // light: 'oklch(0.8789 0.1857 162.47)',
178
+ // dark: 'oklch(0.3446 0.066 162.47)',
179
+ // }
180
+ ```
181
+
182
+ ## Token methods
183
+
184
+ | Method | Returns | Description |
185
+ | --- | --- | --- |
186
+ | `token.resolve()` | `ResolvedColor` | The four variants. `name` is the [`name`](#name), or `'value'` without one. |
187
+ | `token.token(options?)` | `Record<string, string>` | One color's state map, keyed by state alias: `''`, the dark state, the high-contrast state, and the two combined. Options: `format`, `modes`, `states`. |
188
+ | `token.tasty(options?)` | `Record<string, string>` | The same map as `token.token()`, for [Tasty](https://tasty.style). |
189
+ | `token.json(options?)` | `Record<string, string>` | `{ light, dark, lightContrast?, darkContrast? }`. Options: `format`, `modes`. |
190
+ | `token.css(options)` | `GlazeCssResult` | CSS custom property declarations, one string per variant. Always all four. Options: [`GlazeColorCssOptions`](#glazecolorcssoptions). |
191
+ | `token.dtcg(options?)` | `GlazeColorDtcgResult` | One W3C DTCG token (`{ $type: 'color', $value }`) per variant, under `light`, `dark?`, `lightContrast?`, `darkContrast?`. Options: `colorSpace` (`'srgb'` default, or `'oklch'`), `modes`. |
192
+ | `token.dtcgResolver(options)` | `GlazeDtcgResolverDocument` | One DTCG Resolver-Module document with the color under `name`. Options: `name` (required) plus the [`theme.dtcgResolver()` options](./output.md#dtcgresolveroptions). |
193
+ | `token.tailwind(options)` | `string` | A Tailwind v4 `@theme` block for `--color-<name>` plus dark and high-contrast overrides. Options: `name` (required) plus the [`theme.tailwind()` options](./output.md#tailwindoptions). |
194
+ | `token.export(override?)` | `GlazeColorTokenExport` | JSON-safe authoring snapshot. See [Snapshots](#tokenexportoverride--glazecolorfromdata). |
195
+
196
+ ```ts
197
+ const brand = glaze.color('#26fcb2');
198
+
199
+ brand.token();
200
+ // → {
201
+ // '': 'oklch(0.8789 0.1857 162.47)',
202
+ // '@media(prefers-color-scheme: dark)': 'oklch(0.3446 0.066 162.47)',
203
+ // }
204
+
205
+ brand.css({ name: 'brand' }).light;
206
+ // → '--brand-color: oklch(0.8789 0.1857 162.47);'
207
+
208
+ brand.tailwind({ name: 'brand' });
209
+ // → '@theme {\n' +
210
+ // ' --color-brand: oklch(0.8789 0.1857 162.47);\n' +
211
+ // '}\n\n.dark {\n' +
212
+ // ' --color-brand: oklch(0.3446 0.066 162.47);\n' +
213
+ // '}'
214
+ ```
215
+
216
+ - Every output that takes `format` defaults to `'oklch'`; `dtcg()` and `dtcgResolver()` take a `colorSpace` instead (`'srgb'` by default). `json()`, `css()`, and `tailwind()` accept only the native CSS formats (`'oklch'`, `'rgb'`, `'hsl'`) and throw on `'okhsl'` / `'okhst'`; `token()` and `tasty()` accept all five. See [Color formats](./output.md#color-formats).
217
+ - `modes` and `states` default to the global config, as on themes. `css()` ignores `modes`. See [Output modes](./output.md#output-modes).
218
+ - A token is never a glow, so it never carries an [HDR value](../hdr.mdx): `modes.hdr`, `states.hdr`, and `hdrSelector` change nothing, and `css()` has no `hdr` option.
219
+ - The output methods share their option types with themes, except four. `token()` and `tasty()` take `GlazeTokenOptions` — `theme.tasty()`'s options without `splitHue` and `name`. `css()` takes the `GlazeColorCssOptions` below. `tailwind()` and `dtcgResolver()` take `GlazeColorTailwindOptions` and `GlazeColorDtcgResolverOptions`: the theme types plus a required `name`. Full shapes are in [Output formats](./output.md).
220
+
221
+ ### `GlazeColorCssOptions`
222
+
223
+ | Field | Type | Default | Description |
224
+ | --- | --- | --- | --- |
225
+ | `name` | `string` | — | Required. The custom property name without `--`: `'brand'` gives `--brand-color`. |
226
+ | `format` | `GlazeColorFormat` | `'oklch'` | Native formats only. |
227
+ | `suffix` | `string` | `'-color'` | Appended to `name`, matching `theme.css()`. With `splitHue`, `'-hue'` throws (below). |
228
+ | `splitHue` | `boolean` | `false` | Emit the hue as a separate `--<name>-hue` property referenced with `var()`, rounded to 2 decimals like the hue in `oklch()` output. Requires `format: 'oklch'`: any other format throws. Throws when the token isn't pastel. See [`splitHue`](./output.md#hue-channel-splitting-splithue). |
229
+
230
+ ```ts
231
+ const brand = glaze.color('#26fcb2', { pastel: true });
232
+ brand.css({ name: 'brand', splitHue: true }).light;
233
+ // → '--brand-hue: 162.47;\n' +
234
+ // '--brand-color: oklch(0.8789 0.0572 var(--brand-hue));'
235
+ ```
236
+
237
+ The hue var and the value property share one namespace, so `suffix: '-hue'` would put both on `--brand-hue`. That throws:
238
+
239
+ ```text
240
+ glaze: splitHue would declare "--brand-hue" twice: for the hue of token
241
+ "brand" and for the value of token "brand".
242
+ One would silently override the other.
243
+ Pass a `suffix` other than '-hue'.
244
+ ```
245
+
246
+ A token achromatic in light declares no hue var, so `'-hue'` is accepted there: `glaze.color('#808080', { pastel: true })` with `name: 'gray'` gives `--gray-hue: oklch(0.5999 0 0);`.
247
+
248
+ ## Config overrides
249
+
250
+ The second argument of `glaze.color()` is a `GlazeConfigOverride`, the same type a theme takes: the resolve-relevant subset of the global config plus the instance-only `pastel`. The fields are listed in [`GlazeConfigOverride`](./configuration.md#glazeconfigoverride). It is sparse: fields it sets are fixed for the token, and fields it omits follow the live global config at resolve time; the details are in [Merge rules](./configuration.md#merge-rules).
251
+
252
+ On a token specifically:
253
+
254
+ - The override is copied when the token is created, so changing the object you passed afterwards has no effect. Its fields are read by property access, so a class instance with getters or a proxy works.
255
+ - Value forms resolve with a local `lightTone: false`; a `lightTone` in the override replaces it (see [Defaults](#defaults)).
256
+ - `shadowTuning` and `glowTuning` have no effect: a token is never a shadow or a glow. They are still validated at resolve, so a `headroom` that isn't a finite number ≥ 0, or any `shadowTuning.headroom`, throws.
257
+ - `inferRole` is accepted but has no effect: a token never infers its role from `name` (see [Roles on tokens](#roles-on-tokens)).
258
+ - A `contrastLevel` moves only this token. On a token with a `base`, set it on both; see [Clearing it, exporting it, base links](./contrast.md#clearing-it-exporting-it-base-links).
259
+ - `modes` and `states` aren't part of the override. Pass them to the output methods or set them globally.
260
+
261
+ ```ts
262
+ // Let black flip all the way to white in dark.
263
+ glaze.color('#000000', { lightTone: false, darkTone: [15, 100] });
264
+ // dark: #ffffff (with the default darkTone, #f1f1f1)
265
+
266
+ // The structured form takes an override too.
267
+ glaze.color(
268
+ { hue: 152, saturation: 95, tone: 74 },
269
+ { darkTone: false, pastel: true },
270
+ );
271
+ ```
272
+
273
+ ## Pairing colors
274
+
275
+ Set `base` to anchor a token to another token or a raw color value. `contrast` and a tone delta then measure against the base's resolved variant in each scheme instead of against the token's own input, so text keeps its floor against its background in light, dark, and high contrast.
276
+
277
+ ```ts
278
+ const bg = glaze.color('#1a1a2e');
279
+
280
+ // AA against bg in every scheme.
281
+ const text = glaze.color({ from: '#ffffff', base: bg, contrast: 'AA' });
282
+ // light: #ffffff on #1a1a2e (17.06:1); dark: #222222 on #dadbec (11.58:1)
283
+
284
+ // 8 tones lighter than bg, except in darkContrast: bg sits at tone 93
285
+ // there, so autoFlip mirrors the step to 8 tones darker.
286
+ const border = glaze.color({
287
+ from: '#000000',
288
+ base: bg,
289
+ tone: '+8',
290
+ mode: 'fixed',
291
+ });
292
+
293
+ // A raw value is wrapped with glaze.color(value), so this equals `text`.
294
+ const text2 = glaze.color({
295
+ from: '#ffffff',
296
+ base: '#1a1a2e',
297
+ contrast: 'AA',
298
+ });
299
+ ```
300
+
301
+ With `base`:
302
+
303
+ - `contrast` is solved against the base's `light`, `dark`, `lightContrast`, and `darkContrast` variant in turn: what the base token paints, under its own config, whatever form it was made in.
304
+ - A tone delta (`'+N'` / `'-N'`) is measured from the base, as for a theme's [dependent colors](./color-definitions.md#tone-values). In light and high contrast the step is exact: a `'+0'` lands on the base. Under `mode: 'auto'` the dark variant steps from where the base sits on its own authored scale — its authored tone, read under the base's own `lightTone`, or, for a `'fixed'` or `'static'` base or an `'auto'` base placed by a contrast floor or an extreme, its own dark tone read back through the dark window, as in a theme — then inverts and maps the result through the dark window, so the step flips direction and shrinks to the dark window's scale (about 0.87×), as in a theme: an `'auto'` version of `border` above is 6.93 tones darker than `bg` in dark. `'fixed'` and `'static'` apply it to the base's tone in each scheme, sign kept.
305
+ - A base whose active dark window differs from the dependent's — its own `darkTone`, or another `contrastLevel` — is read back through the dependent's dark window instead, so a `'+0'` dependent lands on it and steps mirror at the dependent's scale. On `glaze.color('#808080', { darkTone: false })`, `'+6'` steps 5.2 tones darker in dark and `'-6'` 5.2 lighter.
306
+ - A relative `hue` still anchors to the token's own seed (the value passed to `glaze.color()`), not to the base.
307
+ - `mode` applies to the pair: `'auto'` mirrors the relationship in dark, `'fixed'` keeps its sign.
308
+ - The base is resolved lazily, when the dependent first resolves.
309
+ - An unreachable floor pins the token to the extreme of the side the solver searched first and emits a `console.warn`, such as `glaze: color "value" cannot meet WCAG 4.50 in dark scheme (got <ratio>). …`, deduplicated by color, scheme, target, and measured value. Set [`name`](#name) to make the warning identifiable.
310
+
311
+ Chains compose:
312
+
313
+ ```ts
314
+ const page = glaze.color('#000000');
315
+ const surface = glaze.color({
316
+ from: '#222222',
317
+ base: page,
318
+ contrast: 'AAA',
319
+ });
320
+ const label = glaze.color({
321
+ from: '#ffffff',
322
+ base: surface,
323
+ contrast: 'AA',
324
+ });
325
+ ```
326
+
327
+ ### Roles on tokens
328
+
329
+ A token's [role](./color-definitions.md#roles) is its explicit `role`, or `'text'`. The token is the foreground and its anchor is the background, whichever anchor it has:
330
+
331
+ - With a `base`, the base is the background.
332
+ - Without a `base`, a `contrast` floor or tone delta is measured against a hidden copy of the token's own input color, and that copy is the background.
333
+
334
+ ```ts
335
+ // Text on white: light is rgb(141 141 141), Lc 60.5. Identical to the
336
+ // same token with base: glaze.color({ from: '#ffffff', mode: 'static' }).
337
+ glaze.color({ from: '#ffffff', contrast: { apca: 60 } });
338
+
339
+ // A fill with white on top: light is rgb(152 152 152), Lc 60.5 the
340
+ // other way.
341
+ glaze.color({ from: '#ffffff', contrast: { apca: 60 }, role: 'surface' });
342
+ ```
343
+
344
+ Set `role: 'surface'` (or an alias such as `'bg'`) for a token that is a fill with its anchor drawn on top. Unlike a theme color, a token never infers its role from [`name`](#name), so `inferRole` has no effect on it. WCAG ignores the role.
345
+
346
+ ## `name`
347
+
348
+ `name` is a debug label. It appears in warnings and errors instead of `"value"`, and as `resolve().name`. It doesn't change output keys: `token()`, `tasty()`, and `json()` stay keyed by scheme, and the custom property name comes from `css({ name })`. It doesn't change the [role](#roles-on-tokens) either: `name: 'card-bg'` is still measured as text unless you set `role`.
349
+
350
+ | Invalid `name` | Error |
351
+ | --- | --- |
352
+ | `''` or whitespace | ``glaze.color: name must be a non-empty string. Omit `name` if you do not want to set a debug label.`` |
353
+ | `'value'`, `'seed'`, `'externalBase'` | `glaze.color: name "value" is reserved (used internally). Reserved names are: "value", "seed", "externalBase". Pick a different name.` |
354
+
355
+ ## `token.export(override?)` / `glaze.colorFrom(data)`
356
+
357
+ `token.export()` returns a detached, JSON-safe authoring snapshot: the original input, the overrides (with the `base` exported recursively), and the effective config frozen at call time. `glaze.colorFrom()` rebuilds the token from it and pins the frozen config, so later `glaze.configure()` calls don't change the restored token.
358
+
359
+ ```ts
360
+ const bg = glaze.color('#1a1a2e');
361
+ const text = glaze.color({ from: '#ffffff', base: bg, contrast: 'AA' });
362
+
363
+ const data = text.export();
364
+ // → {
365
+ // kind: 'color',
366
+ // version: 2,
367
+ // form: 'value',
368
+ // input: '#ffffff',
369
+ // overrides: {
370
+ // contrast: 'AA',
371
+ // base: {
372
+ // kind: 'color',
373
+ // version: 2,
374
+ // form: 'value',
375
+ // input: '#1a1a2e',
376
+ // config: { … },
377
+ // },
378
+ // },
379
+ // config: {
380
+ // lightTone: false,
381
+ // darkTone: { lo: 15, hi: 95, eps: 0.05 },
382
+ // darkDesaturation: 0.1,
383
+ // autoFlip: true,
384
+ // pastel: false,
385
+ // inferRole: true,
386
+ // },
387
+ // }
388
+
389
+ const restored = glaze.colorFrom(JSON.parse(JSON.stringify(data)));
390
+ // restored.resolve() is identical to text.resolve()
391
+ ```
392
+
393
+ - Both the value and the structured forms round-trip.
394
+ - An explicit `role`, an alias such as `'bg'` included, is recorded as written — in `overrides.role` (value form) or `input.role` (structured form) — and restored by `glaze.colorFrom()`. A token without `role` isn't frozen to one; it restores with the `'text'` default.
395
+ - `override` merges over the token's own override for the snapshot only, and is passed to a nested `base` export too.
396
+ - The config freeze follows the same rules as [`theme.export()`](./theme.md#themeexportoverride): `contrastLevel` is frozen only when set on the token or passed as `override`; `modes` and `states` never are.
397
+
398
+ `GlazeColorTokenExport`:
399
+
400
+ | Field | Type | Description |
401
+ | --- | --- | --- |
402
+ | `kind` | `'color'` | Always written; optional on legacy snapshots. |
403
+ | `version` | `number` | `GLAZE_EXPORT_VERSION` (`2`). Always written; optional on legacy snapshots. |
404
+ | `form` | `'value' \| 'structured'` | Which input shape created the token. |
405
+ | `input` | `GlazeColorValue \| GlazeColorInputExport` | The raw value (`form: 'value'`) or the structured input. |
406
+ | `overrides` | `GlazeColorOverridesExport` | The value form's overrides, when any were given. |
407
+ | `config` | `GlazeConfigOverride` | The frozen effective config. |
408
+
409
+ `GlazeColorInputExport` and `GlazeColorOverridesExport` have the same fields as `GlazeColorInput` and `GlazeColorOverrides`, except that a `base` is stored as its `GlazeColorTokenExport`: a raw-value base as the snapshot of the token it is wrapped in, so its config is frozen too. A snapshot written by an earlier release with a raw-value base still restores, with that base following the live config.
410
+
411
+ `glaze.colorFrom()` throws when:
412
+
413
+ | Condition | Error |
414
+ | --- | --- |
415
+ | `data` isn't an object | `glaze.colorFrom: expected an object from token.export(), got <type>.` |
416
+ | `kind` is present and not `'color'` | `glaze.colorFrom: expected kind "color", got "<kind>".` |
417
+ | `version` is invalid or newer than the library | See [Restore checks](./index.md#restore-checks). |
418
+ | `form` isn't `'value'` or `'structured'` | `glaze.colorFrom: invalid "form" field — expected "value" or "structured" (got <form>).` |
419
+ | `input` is missing | `glaze.colorFrom: missing "input" field — expected the original GlazeColorValue.` (or `GlazeColorInput`) |
420
+
421
+ `glaze.isColorTokenExport(data)` tells a token snapshot apart from theme and palette snapshots; see [Type guards](./index.md#type-guards).