@tenphi/glaze 0.12.0 → 0.14.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.
- package/README.md +12 -10
- package/dist/index.cjs +980 -574
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +323 -193
- package/dist/index.d.mts +323 -193
- package/dist/index.mjs +970 -574
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +300 -165
- package/docs/methodology.md +64 -54
- package/docs/migration.md +81 -10
- package/docs/okhst.md +224 -0
- package/package.json +1 -1
package/docs/api.md
CHANGED
|
@@ -25,8 +25,8 @@ Full reference for every public method, option, and type exported by `@tenphi/gl
|
|
|
25
25
|
|
|
26
26
|
| Method | Description |
|
|
27
27
|
|---|---|
|
|
28
|
-
| `glaze(hue, saturation?)` | Create a theme from hue (0–360) and saturation (0–100).
|
|
29
|
-
| `glaze({ hue, saturation })` | Create a theme from an options object. |
|
|
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
30
|
| `glaze.from(data)` | Create a theme from an exported configuration (`theme.export()` snapshot). |
|
|
31
31
|
| `glaze.fromHex(hex)` | Create a theme from a hex color (`#rgb` or `#rrggbb`). Extracts hue and saturation. |
|
|
32
32
|
| `glaze.fromRgb(r, g, b)` | Create a theme from RGB values (0–255). Extracts hue and saturation. |
|
|
@@ -37,8 +37,13 @@ const b = glaze({ hue: 280, saturation: 80 });
|
|
|
37
37
|
const c = glaze.fromHex('#7a4dbf');
|
|
38
38
|
const d = glaze.fromRgb(122, 77, 191);
|
|
39
39
|
const e = glaze.from(a.export());
|
|
40
|
+
|
|
41
|
+
// Per-theme config override:
|
|
42
|
+
const rawTheme = glaze(280, 80, { lightTone: false, darkTone: false });
|
|
40
43
|
```
|
|
41
44
|
|
|
45
|
+
The optional `config` parameter is a `GlazeConfigOverride` — see [Per-instance config override](#per-instance-config-override).
|
|
46
|
+
|
|
42
47
|
---
|
|
43
48
|
|
|
44
49
|
## Theme methods
|
|
@@ -67,27 +72,30 @@ A `GlazeTheme` exposes:
|
|
|
67
72
|
### `theme.colors(defs)`
|
|
68
73
|
|
|
69
74
|
```ts
|
|
70
|
-
theme.colors({ surface: {
|
|
71
|
-
theme.colors({ text: {
|
|
75
|
+
theme.colors({ surface: { tone: 97 } });
|
|
76
|
+
theme.colors({ text: { tone: 30 } });
|
|
72
77
|
// Both 'surface' and 'text' are now defined.
|
|
73
78
|
```
|
|
74
79
|
|
|
75
80
|
### `theme.color(name) / theme.color(name, def)`
|
|
76
81
|
|
|
77
82
|
```ts
|
|
78
|
-
theme.color('surface', {
|
|
83
|
+
theme.color('surface', { tone: 97, saturation: 0.75 }); // set
|
|
79
84
|
const def = theme.color('surface'); // get
|
|
80
85
|
```
|
|
81
86
|
|
|
82
87
|
### `theme.extend(options)`
|
|
83
88
|
|
|
84
|
-
Creates a new theme inheriting all color definitions, optionally replacing the hue / saturation seed
|
|
89
|
+
Creates a new theme inheriting all color definitions, optionally replacing the hue / saturation seed, color overrides, and config:
|
|
85
90
|
|
|
86
91
|
```ts
|
|
87
92
|
const danger = primary.extend({
|
|
88
93
|
hue: 23,
|
|
89
|
-
colors: { 'accent-fill': {
|
|
94
|
+
colors: { 'accent-fill': { tone: 48, mode: 'fixed' } },
|
|
90
95
|
});
|
|
96
|
+
|
|
97
|
+
// Inherit parent's config override and widen the dark window further:
|
|
98
|
+
const highSat = base.extend({ config: { darkTone: [10, 100] } });
|
|
91
99
|
```
|
|
92
100
|
|
|
93
101
|
`GlazeExtendOptions`:
|
|
@@ -97,6 +105,7 @@ const danger = primary.extend({
|
|
|
97
105
|
| `hue` | `number` | Replace the hue seed. Defaults to the parent's hue. |
|
|
98
106
|
| `saturation` | `number` | Replace the saturation seed. Defaults to the parent's saturation. |
|
|
99
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. |
|
|
100
109
|
|
|
101
110
|
Colors marked with `inherit: false` on the parent are **not** copied into the child.
|
|
102
111
|
|
|
@@ -202,56 +211,89 @@ type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
|
|
|
202
211
|
|
|
203
212
|
| Field | Type | Description |
|
|
204
213
|
|---|---|---|
|
|
205
|
-
| `
|
|
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]`. |
|
|
206
215
|
| `saturation` | `number` | Saturation factor applied to the seed saturation (0–1). Default: `1`. |
|
|
207
216
|
| `hue` | `number \| RelativeValue` | Number = absolute (0–360). String (`'+N'`/`'-N'`) = relative to the **theme seed hue** (never to a base color). |
|
|
208
217
|
| `base` | `string` | Name of another color in the same theme — makes this a *dependent* color. |
|
|
209
|
-
| `contrast` | `HCPair<
|
|
218
|
+
| `contrast` | `HCPair<ContrastSpec>` | Contrast floor against `base`. Requires `base`. See [`contrast`](#contrast-floor). |
|
|
210
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). |
|
|
211
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). |
|
|
212
222
|
| `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. |
|
|
213
223
|
|
|
214
|
-
####
|
|
224
|
+
#### Tone values
|
|
225
|
+
|
|
226
|
+
`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).)
|
|
215
227
|
|
|
216
228
|
| Form | Example | Meaning |
|
|
217
229
|
|---|---|---|
|
|
218
|
-
| Number (absolute) | `
|
|
219
|
-
| String (relative) | `
|
|
220
|
-
|
|
|
230
|
+
| Number (absolute) | `tone: 45` | Absolute tone 0–100. |
|
|
231
|
+
| String (relative) | `tone: '-52'` | Relative to base color's tone (requires `base`). |
|
|
232
|
+
| Extreme | `tone: 'max'` / `'min'` | Force to the scheme's highest (`'max'` = 100) or lowest (`'min'` = 0) tone. No `base` needed. |
|
|
233
|
+
| HC pair | `tone: ['-7', '-20']` | `[normal, high-contrast]`. A single value applies to both. |
|
|
234
|
+
|
|
235
|
+
**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.
|
|
236
|
+
|
|
237
|
+
**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.
|
|
221
238
|
|
|
222
|
-
**
|
|
239
|
+
**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.
|
|
223
240
|
|
|
224
|
-
|
|
241
|
+
A dependent color with `base` but no `tone` inherits the base's tone (equivalent to a delta of 0).
|
|
225
242
|
|
|
226
|
-
|
|
243
|
+
#### `flip`
|
|
227
244
|
|
|
228
|
-
|
|
245
|
+
`flip` governs what happens when a result would fall outside its valid range:
|
|
246
|
+
|
|
247
|
+
- **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.
|
|
248
|
+
- **`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`).
|
|
249
|
+
|
|
250
|
+
`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.
|
|
251
|
+
|
|
252
|
+
#### `contrast` (floor)
|
|
229
253
|
|
|
230
254
|
```ts
|
|
231
|
-
type
|
|
255
|
+
type ContrastPreset = 'AA' | 'AAA' | 'AA-large' | 'AAA-large';
|
|
256
|
+
type ContrastSpec =
|
|
257
|
+
| number // bare WCAG ratio
|
|
258
|
+
| ContrastPreset // named WCAG preset
|
|
259
|
+
| { wcag: HCPair<number | ContrastPreset> }
|
|
260
|
+
| { apca: HCPair<number> }; // APCA Lc target
|
|
232
261
|
```
|
|
233
262
|
|
|
234
|
-
| Preset |
|
|
263
|
+
| Preset | WCAG ratio |
|
|
235
264
|
|---|---|
|
|
236
265
|
| `'AA-large'` | 3 |
|
|
237
266
|
| `'AA'` | 4.5 |
|
|
238
267
|
| `'AAA-large'` | 4.5 |
|
|
239
268
|
| `'AAA'` | 7 |
|
|
240
269
|
|
|
241
|
-
|
|
270
|
+
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] }`).
|
|
242
271
|
|
|
243
|
-
|
|
272
|
+
```ts
|
|
273
|
+
contrast: 4.5 // WCAG 4.5
|
|
274
|
+
contrast: 'AAA' // WCAG 7
|
|
275
|
+
contrast: { wcag: 6 } // WCAG 6
|
|
276
|
+
contrast: { wcag: [4.5, 7] } // WCAG 4.5 normal / 7 high-contrast
|
|
277
|
+
contrast: { apca: 60 } // APCA Lc 60
|
|
278
|
+
contrast: { apca: [45, 60] } // APCA Lc 45 normal / 60 high-contrast
|
|
279
|
+
```
|
|
244
280
|
|
|
245
|
-
|
|
281
|
+
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.
|
|
282
|
+
|
|
283
|
+
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.
|
|
284
|
+
|
|
285
|
+
**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.
|
|
286
|
+
|
|
287
|
+
**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.
|
|
246
288
|
|
|
247
289
|
#### Per-color hue override
|
|
248
290
|
|
|
249
291
|
```ts
|
|
250
292
|
const theme = glaze(280, 80);
|
|
251
293
|
theme.colors({
|
|
252
|
-
surface: {
|
|
253
|
-
gradientEnd: {
|
|
254
|
-
warning: {
|
|
294
|
+
surface: { tone: 97 },
|
|
295
|
+
gradientEnd: { tone: 90, hue: '+20' }, // 280 + 20 = 300
|
|
296
|
+
warning: { tone: 60, hue: 40 }, // absolute
|
|
255
297
|
});
|
|
256
298
|
```
|
|
257
299
|
|
|
@@ -280,7 +322,7 @@ See [Shadows](#shadows) below for the algorithm and tuning details.
|
|
|
280
322
|
| `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. |
|
|
281
323
|
| `blend` | `'opaque' \| 'transparent'` | Default `'opaque'`. |
|
|
282
324
|
| `space` | `'okhsl' \| 'srgb'` | Interpolation space for opaque blending. Default `'okhsl'`. Ignored for `'transparent'` (always composites in linear sRGB). |
|
|
283
|
-
| `contrast` | `HCPair<
|
|
325
|
+
| `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). |
|
|
284
326
|
| `inherit` | `boolean` | Inheritance flag, default `true`. |
|
|
285
327
|
|
|
286
328
|
See [Mix colors](#mix-colors) below.
|
|
@@ -289,96 +331,99 @@ See [Mix colors](#mix-colors) below.
|
|
|
289
331
|
|
|
290
332
|
## Standalone color tokens
|
|
291
333
|
|
|
292
|
-
`glaze.color()` creates a single color token without a full theme.
|
|
334
|
+
`glaze.color()` creates a single color token without a full theme.
|
|
293
335
|
|
|
294
336
|
```ts
|
|
295
|
-
|
|
296
|
-
|
|
337
|
+
// arg1: the color (four shapes — see below)
|
|
338
|
+
// arg2: optional config override (GlazeConfigOverride — see below)
|
|
339
|
+
glaze.color(color: GlazeFromInput | GlazeColorInput | GlazeColorValue, config?: GlazeConfigOverride): GlazeColorToken;
|
|
297
340
|
```
|
|
298
341
|
|
|
299
342
|
### Input forms
|
|
300
343
|
|
|
301
|
-
`
|
|
344
|
+
`glaze.color()` accepts **four input shapes**, discriminated by structure:
|
|
345
|
+
|
|
346
|
+
| Shape | Example | Notes |
|
|
347
|
+
|---|---|---|
|
|
348
|
+
| **Bare string** | `'#26fcb2'` | Hex or CSS color function (`rgb()`, `hsl()`, `okhsl()`, `okhst()`, `oklch()`). |
|
|
349
|
+
| **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). |
|
|
350
|
+
| **`{ from, ...overrides }`** | `{ from: '#1a1a2e', base: bg, contrast: 'AA' }` | Value + color overrides in one object. |
|
|
351
|
+
| **Structured** | `{ hue: 152, saturation: 95, tone: 74 }` | Full theme-style token (hue/saturation in 0–100, tone in 0–100). |
|
|
352
|
+
|
|
353
|
+
`GlazeColorValue` (bare string or value-object forms) accepts:
|
|
302
354
|
|
|
303
355
|
| Form | Example | Notes |
|
|
304
356
|
|---|---|---|
|
|
305
|
-
| Hex | `'#26fcb2'`, `'#26fcb2ff'`, `'#abc'` | 3, 6, or 8 digits. Alpha
|
|
357
|
+
| Hex | `'#26fcb2'`, `'#26fcb2ff'`, `'#abc'` | 3, 6, or 8 digits. Alpha is dropped with a `console.warn` — use `opacity` instead. |
|
|
306
358
|
| `rgb()` | `'rgb(38 252 178)'`, `'rgb(38 252 178 / 0.8)'` | Modern space syntax. Alpha dropped with warning. |
|
|
307
359
|
| `hsl()` | `'hsl(152 97% 57%)'` | Modern space syntax. Alpha dropped with warning. |
|
|
308
360
|
| `okhsl()` | `'okhsl(152 95% 74%)'` | Glaze's own emit format. Alpha dropped with warning. |
|
|
361
|
+
| `okhst()` | `'okhst(152 95% 70%)'` | OKHST tone input (third value is tone 0–100). **Input only** — never emitted. Alpha dropped with warning. |
|
|
309
362
|
| `oklch()` | `'oklch(0.85 0.18 152)'` | Glaze's own emit format. Alpha dropped with warning. |
|
|
310
|
-
| `OkhslColor` object | `{ h: 152, s: 0.95, l: 0.74 }` |
|
|
363
|
+
| `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. |
|
|
364
|
+
| `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.** |
|
|
311
365
|
| `RgbColor` object | `{ r: 38, g: 252, b: 178 }` | sRGB 0–255. RGB tuple `[r, g, b]` is not supported — use this object form. |
|
|
312
366
|
| `OklchColor` object | `{ l: 0.85, c: 0.18, h: 152 }` | OKLCh (L/C: 0–1, H: degrees), same semantics as `oklch()` strings. |
|
|
313
367
|
|
|
314
|
-
`GlazeColorInput` (
|
|
368
|
+
`GlazeColorInput` (structured form) is `{ hue, saturation, tone, ... }`:
|
|
315
369
|
|
|
316
370
|
| Field | Type | Description |
|
|
317
371
|
|---|---|---|
|
|
318
372
|
| `hue` | `number` | 0–360. |
|
|
319
373
|
| `saturation` | `number` | 0–100. |
|
|
320
|
-
| `
|
|
374
|
+
| `tone` | `HCPair<number \| ExtremeValue>` | 0–100 (contrast-uniform) or `'max'`/`'min'`, optional HC pair. |
|
|
321
375
|
| `saturationFactor` | `number` | Multiplier on the seed (0–1). Default: `1`. |
|
|
322
376
|
| `mode` | `AdaptationMode` | Default: `'auto'`. |
|
|
377
|
+
| `flip` | `boolean` | Flip out-of-bounds results instead of clamping. Default: global `autoFlip`. |
|
|
323
378
|
| `opacity` | `number` | Fixed alpha 0–1. |
|
|
324
379
|
| `base` | `GlazeColorToken \| GlazeColorValue` | Optional dependency. See [Pairing colors](#pairing-colors). |
|
|
325
|
-
| `contrast` | `HCPair<
|
|
380
|
+
| `contrast` | `HCPair<ContrastSpec>` | Contrast floor against `base` (WCAG or APCA). Without `base`, anchored to the literal seed. |
|
|
326
381
|
| `name` | `string` | Debug label for warnings; doesn't change output keys. Reserved names (`'value'`, `'seed'`, `'externalBase'`) are rejected. |
|
|
327
382
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
### Defaults
|
|
331
|
-
|
|
332
|
-
Every input form defaults to `mode: 'auto'` so the resolved token adapts between light and dark like an ordinary theme color. The *scaling* snapshot taken at create time differs by input form:
|
|
383
|
+
`GlazeFromInput` (from form) is `{ from: GlazeColorValue, ...colorOverrides }`:
|
|
333
384
|
|
|
334
|
-
|
|
335
|
-
- Light variant preserves the input lightness exactly (`lightLightness: false`).
|
|
336
|
-
- Dark variant uses `globalConfig.darkLightness` (default `[15, 95]`), snapshotted at create time.
|
|
337
|
-
- **Structured input** (`{ hue, saturation, lightness, ... }`):
|
|
338
|
-
- Both variants use `globalConfig.lightLightness` / `globalConfig.darkLightness` (defaults `[10, 100]` / `[15, 95]`) — same as a theme color.
|
|
339
|
-
- All windows are **snapshotted at color-creation time** so later `glaze.configure()` calls don't retroactively change exported tokens. `token.export()` round-trips byte-for-byte.
|
|
340
|
-
|
|
341
|
-
To opt back into the legacy fixed-linear default (no Möbius inversion), pass `{ mode: 'fixed' }` as the second arg, or supply an explicit `scaling` (see [`GlazeColorScaling`](#glazecolorscaling)).
|
|
342
|
-
|
|
343
|
-
### `GlazeColorOverrides`
|
|
344
|
-
|
|
345
|
-
Overrides for the value-shorthand overload's second argument:
|
|
346
|
-
|
|
347
|
-
| Option | Notes |
|
|
385
|
+
| Field | Notes |
|
|
348
386
|
|---|---|
|
|
349
|
-
| `
|
|
387
|
+
| `from` | **Required.** The source color value — same forms as `GlazeColorValue`. |
|
|
388
|
+
| `hue` | Number (absolute 0–360) or `'+N'`/`'-N'` (relative to seed, never to `base`). |
|
|
350
389
|
| `saturation` | Override seed saturation (0–100). |
|
|
351
|
-
| `
|
|
352
|
-
| `saturationFactor` | Multiplier on the seed (0–1).
|
|
353
|
-
| `mode` | `'auto'` (default
|
|
354
|
-
| `
|
|
355
|
-
| `
|
|
356
|
-
| `
|
|
357
|
-
| `
|
|
390
|
+
| `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. |
|
|
391
|
+
| `saturationFactor` | Multiplier on the seed (0–1). |
|
|
392
|
+
| `mode` | `'auto'` (default) / `'fixed'` / `'static'`. |
|
|
393
|
+
| `flip` | Flip out-of-bounds results instead of clamping. Default: global `autoFlip`. |
|
|
394
|
+
| `contrast` | Contrast floor (WCAG or APCA). Without `base`, anchored to the literal seed; with `base`, solved per scheme. |
|
|
395
|
+
| `base` | `GlazeColorToken` or raw `GlazeColorValue`. See [Pairing colors](#pairing-colors). |
|
|
396
|
+
| `opacity` | Fixed alpha 0–1. Combining with `contrast` is not recommended — `console.warn` is emitted. |
|
|
397
|
+
| `name` | Debug label only — surfaces in warnings/errors. Does not change output keys. |
|
|
358
398
|
|
|
359
|
-
|
|
399
|
+
Named CSS colors (`'red'`, `'blueviolet'`) are not supported.
|
|
360
400
|
|
|
361
|
-
|
|
401
|
+
### Defaults
|
|
362
402
|
|
|
363
|
-
|
|
364
|
-
|---|---|---|---|
|
|
365
|
-
| `lightLightness` | `false` | `globalConfig.lightLightness` (snapshotted) | `false` = preserve input. Pass `[lo, hi]` to opt into a remap window. |
|
|
366
|
-
| `darkLightness` | `globalConfig.darkLightness` (snapshotted) | `globalConfig.darkLightness` (snapshotted) | `false` = preserve input in dark too. Pass `[lo, hi]` to override the window (e.g. `[15, 100]` for a `#000` → white dark flip). |
|
|
403
|
+
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:
|
|
367
404
|
|
|
368
|
-
|
|
405
|
+
- **Value-shorthand** (bare strings, value objects, and `{ from, ...overrides }`):
|
|
406
|
+
- Light variant preserves the input tone exactly (`lightTone: false`).
|
|
407
|
+
- All other config fields (`darkTone`, `darkDesaturation`, `saturationTaper`, `autoFlip`) snapshot from `globalConfig` at create time.
|
|
408
|
+
- **Structured input** (`{ hue, saturation, tone, ... }`):
|
|
409
|
+
- Both tone windows snapshot from `globalConfig` at create time (same as a theme color).
|
|
410
|
+
- All fields are **snapshotted at color-creation time** — later `glaze.configure()` calls don't retroactively change existing tokens.
|
|
369
411
|
|
|
370
412
|
```ts
|
|
371
|
-
//
|
|
372
|
-
glaze.color('#26fcb2'
|
|
413
|
+
// Bare string — adapts automatically
|
|
414
|
+
glaze.color('#26fcb2')
|
|
373
415
|
|
|
374
|
-
//
|
|
375
|
-
glaze.color(
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
})
|
|
416
|
+
// Value-object — same behavior
|
|
417
|
+
glaze.color({ h: 152, s: 0.95, l: 0.74 })
|
|
418
|
+
|
|
419
|
+
// OKHST value-object — tone axis
|
|
420
|
+
glaze.color({ h: 152, s: 0.95, t: 0.70 })
|
|
421
|
+
|
|
422
|
+
// From form — value + color overrides
|
|
423
|
+
glaze.color({ from: '#1a1a2e', hue: '+20', contrast: 'AA' })
|
|
379
424
|
|
|
380
|
-
// Structured form
|
|
381
|
-
glaze.color({ hue: 152, saturation: 95,
|
|
425
|
+
// Structured form — explicit hue/saturation/tone (0–100)
|
|
426
|
+
glaze.color({ hue: 152, saturation: 95, tone: 74 })
|
|
382
427
|
```
|
|
383
428
|
|
|
384
429
|
### Token methods
|
|
@@ -394,12 +439,72 @@ A `GlazeColorToken` exposes:
|
|
|
394
439
|
| `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`). |
|
|
395
440
|
| `token.export()` | JSON-safe snapshot — pass to `glaze.colorFrom(...)` to rehydrate. |
|
|
396
441
|
|
|
442
|
+
### Per-instance config override
|
|
443
|
+
|
|
444
|
+
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).
|
|
445
|
+
|
|
446
|
+
`GlazeConfigOverride`:
|
|
447
|
+
|
|
448
|
+
| Field | Default (from global) | Description |
|
|
449
|
+
|---|---|---|
|
|
450
|
+
| `lightTone` | `[10, 100]` | Light tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` (disable clamping). |
|
|
451
|
+
| `darkTone` | `[15, 95]` | Dark tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` (disable clamping). |
|
|
452
|
+
| `darkDesaturation` | `0.1` | Saturation reduction in dark scheme (0–1). |
|
|
453
|
+
| `saturationTaper` | `0.15` | Saturation taper strength toward the tone extremes (0–1). |
|
|
454
|
+
| `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. |
|
|
455
|
+
| `shadowTuning` | `undefined` | Default shadow tuning (meaningful for themes; harmless on color tokens). |
|
|
456
|
+
|
|
457
|
+
Config overrides apply to both `glaze.color()` tokens and `glaze()` themes:
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
// Standalone color — preserve raw tone in both schemes
|
|
461
|
+
glaze.color('#26fcb2', { darkTone: false })
|
|
462
|
+
|
|
463
|
+
// Restore the #000 → white dark flip (full dark range)
|
|
464
|
+
glaze.color('#000000', {
|
|
465
|
+
lightTone: false,
|
|
466
|
+
darkTone: [15, 100],
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
// Structured form with config override
|
|
470
|
+
glaze.color({ hue: 152, saturation: 95, tone: 74 }, { darkTone: false })
|
|
471
|
+
|
|
472
|
+
// Theme with config override
|
|
473
|
+
const rawTheme = glaze(280, 80, { lightTone: false })
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
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)).
|
|
477
|
+
|
|
478
|
+
### Theme config override
|
|
479
|
+
|
|
480
|
+
When a theme is created with a `GlazeConfigOverride`, the override is **merged over the live global config at resolve time**. This means:
|
|
481
|
+
|
|
482
|
+
- Fields you overrode are fixed — `glaze.configure()` can't change them for this theme.
|
|
483
|
+
- Fields you didn't override still react to later `glaze.configure()` calls.
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
const t = glaze(280, 80, { lightTone: [0, 50] });
|
|
487
|
+
t.colors({ text: { tone: 50, saturation: 1 } });
|
|
488
|
+
// text.light lands inside the [0, 50] window — always, regardless of
|
|
489
|
+
// global lightTone changes.
|
|
490
|
+
// text.dark.s reacts to glaze.configure({ darkDesaturation }) since it's not overridden.
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
`extend` inherits the parent's override and shallow-merges the child's:
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
const child = t.extend({ config: { darkTone: false } });
|
|
497
|
+
// child: lightTone { lo: 0, hi: 50 } (inherited) + darkTone: false (added)
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
`theme.export()` includes `config`; `glaze.from(data)` restores it.
|
|
501
|
+
|
|
397
502
|
### `glaze.colorFrom(data)`
|
|
398
503
|
|
|
399
|
-
Inverse of `token.export()`.
|
|
504
|
+
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.
|
|
400
505
|
|
|
401
506
|
```ts
|
|
402
|
-
const text = glaze.color('#1a1a1a',
|
|
507
|
+
const text = glaze.color({ from: '#1a1a1a', contrast: 'AA' });
|
|
403
508
|
const data = text.export();
|
|
404
509
|
const restored = glaze.colorFrom(data);
|
|
405
510
|
// restored.resolve() === text.resolve() byte-for-byte
|
|
@@ -409,41 +514,41 @@ Both value-form and structured-form tokens round-trip.
|
|
|
409
514
|
|
|
410
515
|
### Pairing colors
|
|
411
516
|
|
|
412
|
-
Set `base` to anchor a standalone color to another standalone color or raw value. The
|
|
517
|
+
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.
|
|
413
518
|
|
|
414
519
|
```ts
|
|
415
520
|
const bg = glaze.color('#1a1a2e');
|
|
416
521
|
|
|
417
522
|
// Text guaranteed AA against `bg` in every scheme.
|
|
418
|
-
const text = glaze.color('#ffffff',
|
|
523
|
+
const text = glaze.color({ from: '#ffffff', base: bg, contrast: 'AA' });
|
|
419
524
|
|
|
420
|
-
// Border 8
|
|
421
|
-
const border = glaze.color('#000000',
|
|
525
|
+
// Border 8 tone units lighter than `bg` in each scheme.
|
|
526
|
+
const border = glaze.color({ from: '#000000',
|
|
422
527
|
base: bg,
|
|
423
|
-
|
|
528
|
+
tone: '+8',
|
|
424
529
|
mode: 'fixed',
|
|
425
530
|
});
|
|
426
531
|
|
|
427
532
|
// Raw-value base — Glaze auto-wraps it via `glaze.color(value)`.
|
|
428
|
-
const text2 = glaze.color('#ffffff',
|
|
533
|
+
const text2 = glaze.color({ from: '#ffffff', base: '#1a1a2e', contrast: 'AA' });
|
|
429
534
|
```
|
|
430
535
|
|
|
431
536
|
Behavior with `base`:
|
|
432
537
|
|
|
433
538
|
- `contrast` is solved per scheme against `base`'s resolved variant (light / dark / lightContrast / darkContrast).
|
|
434
|
-
- Relative `
|
|
539
|
+
- Relative `tone: '+N'` / `'-N'` is anchored to `base`'s tone per scheme (matches theme behavior).
|
|
435
540
|
- Relative `hue: '+N'` / `'-N'` still anchors to the **seed** (the value passed to `glaze.color()`), not the base.
|
|
436
541
|
- `mode` works as a per-pair knob.
|
|
437
542
|
- 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.
|
|
438
|
-
-
|
|
543
|
+
- **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.
|
|
439
544
|
- 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.
|
|
440
545
|
|
|
441
546
|
Chains compose:
|
|
442
547
|
|
|
443
548
|
```ts
|
|
444
549
|
const bg = glaze.color('#000000');
|
|
445
|
-
const surface = glaze.color('#222222',
|
|
446
|
-
const text = glaze.color('#ffffff',
|
|
550
|
+
const surface = glaze.color({ from: '#222222', base: bg, contrast: 'AAA' });
|
|
551
|
+
const text = glaze.color({ from: '#ffffff', base: surface, contrast: 'AA' });
|
|
447
552
|
```
|
|
448
553
|
|
|
449
554
|
### `name` is a debug label
|
|
@@ -458,8 +563,8 @@ The `name` override appears in `console.warn` / Error messages but **does not**
|
|
|
458
563
|
|
|
459
564
|
```ts
|
|
460
565
|
theme.colors({
|
|
461
|
-
surface: {
|
|
462
|
-
text: { base: 'surface',
|
|
566
|
+
surface: { tone: 95 },
|
|
567
|
+
text: { base: 'surface', tone: '-52', contrast: 'AAA' },
|
|
463
568
|
|
|
464
569
|
'shadow-sm': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 5 },
|
|
465
570
|
'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
|
|
@@ -552,7 +657,7 @@ For a simple fixed-alpha color (no shadow algorithm), use `opacity` on a regular
|
|
|
552
657
|
|
|
553
658
|
```ts
|
|
554
659
|
theme.colors({
|
|
555
|
-
overlay: {
|
|
660
|
+
overlay: { tone: 0, opacity: 0.5 },
|
|
556
661
|
});
|
|
557
662
|
// → 'oklch(0 0 0 / 0.5)'
|
|
558
663
|
```
|
|
@@ -567,8 +672,8 @@ Produces a solid color by interpolating between `base` and `target`:
|
|
|
567
672
|
|
|
568
673
|
```ts
|
|
569
674
|
theme.colors({
|
|
570
|
-
surface: {
|
|
571
|
-
accent: {
|
|
675
|
+
surface: { tone: 95 },
|
|
676
|
+
accent: { tone: 30 },
|
|
572
677
|
tint: { type: 'mix', base: 'surface', target: 'accent', value: 30 },
|
|
573
678
|
});
|
|
574
679
|
```
|
|
@@ -583,8 +688,8 @@ Produces the target color with controlled opacity — useful for hover overlays:
|
|
|
583
688
|
|
|
584
689
|
```ts
|
|
585
690
|
theme.colors({
|
|
586
|
-
surface: {
|
|
587
|
-
black: {
|
|
691
|
+
surface: { tone: 95 },
|
|
692
|
+
black: { tone: 0, saturation: 0 },
|
|
588
693
|
hover: {
|
|
589
694
|
type: 'mix', base: 'surface', target: 'black',
|
|
590
695
|
value: 8, blend: 'transparent',
|
|
@@ -631,8 +736,8 @@ Mix colors can reference other mix colors:
|
|
|
631
736
|
|
|
632
737
|
```ts
|
|
633
738
|
theme.colors({
|
|
634
|
-
white: {
|
|
635
|
-
black: {
|
|
739
|
+
white: { tone: 100, saturation: 0 },
|
|
740
|
+
black: { tone: 0, saturation: 0 },
|
|
636
741
|
gray: { type: 'mix', base: 'white', target: 'black', value: 50, space: 'srgb' },
|
|
637
742
|
lightGray: { type: 'mix', base: 'white', target: 'gray', value: 50, space: 'srgb' },
|
|
638
743
|
});
|
|
@@ -813,78 +918,66 @@ The `format` option works on every export: `theme.tokens()`, `theme.tasty()`, `t
|
|
|
813
918
|
|
|
814
919
|
| Mode | Behavior |
|
|
815
920
|
|---|---|
|
|
816
|
-
| `'auto'` (default) | Full adaptation.
|
|
817
|
-
| `'fixed'` | Color stays recognizable.
|
|
818
|
-
| `'static'` | No adaptation. Same
|
|
921
|
+
| `'auto'` (default) | Full adaptation. Dark is a single tone inversion (`100 − t`) remapped into the dark window. High-contrast uses the full range. |
|
|
922
|
+
| `'fixed'` | Color stays recognizable. Tone is *mapped* (not inverted) into the dark window. Use for brand buttons, CTAs, status banners. |
|
|
923
|
+
| `'static'` | No adaptation. Same tone in every scheme. |
|
|
819
924
|
|
|
820
|
-
### How relative
|
|
925
|
+
### How relative tone adapts
|
|
821
926
|
|
|
822
|
-
**`auto`** —
|
|
927
|
+
**`auto`** — the offset is anchored to the base's per-scheme tone:
|
|
823
928
|
|
|
824
929
|
```
|
|
825
|
-
Light: surface
|
|
826
|
-
Dark: surface inverts to
|
|
827
|
-
|
|
930
|
+
Light: surface tone=97, text tone='-52' → tone 45 (dark text on light bg)
|
|
931
|
+
Dark: surface inverts to a low tone; the '-52' offset re-anchors to the
|
|
932
|
+
base's light tone and maps into the dark window (light text on dark bg)
|
|
828
933
|
```
|
|
829
934
|
|
|
830
|
-
**`fixed`** —
|
|
935
|
+
**`fixed`** — tone is mapped (not inverted), relative sign preserved:
|
|
831
936
|
|
|
832
937
|
```
|
|
833
|
-
Light: accent-fill
|
|
834
|
-
Dark: accent-fill maps
|
|
938
|
+
Light: accent-fill tone=52, accent-text tone='+20' → lighter than the fill
|
|
939
|
+
Dark: accent-fill maps into the dark window, sign preserved
|
|
835
940
|
```
|
|
836
941
|
|
|
837
|
-
|
|
942
|
+
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.
|
|
943
|
+
|
|
944
|
+
**`static`** — no adaptation, same tone in every scheme.
|
|
838
945
|
|
|
839
946
|
---
|
|
840
947
|
|
|
841
948
|
## Light / dark scheme mapping
|
|
842
949
|
|
|
843
|
-
|
|
950
|
+
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.
|
|
844
951
|
|
|
845
|
-
|
|
952
|
+
### Light scheme
|
|
846
953
|
|
|
847
|
-
|
|
848
|
-
const [lo, hi] = lightLightness; // default: [10, 100]
|
|
849
|
-
const mappedL = (lightness * (hi - lo)) / 100 + lo;
|
|
850
|
-
```
|
|
851
|
-
|
|
852
|
-
Both `auto` and `fixed` modes use the same linear formula. `static` mode and HC variants bypass the mapping (identity: `mappedL = l`).
|
|
954
|
+
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.
|
|
853
955
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
956
|
+
```
|
|
957
|
+
window = lightTone // default [10, 100]
|
|
958
|
+
finalTone = remap(authorTone, window)
|
|
959
|
+
finalL = fromTone(finalTone) // OKHSL lightness
|
|
960
|
+
```
|
|
859
961
|
|
|
860
|
-
### Dark scheme
|
|
962
|
+
### Dark scheme
|
|
861
963
|
|
|
862
|
-
**`auto`** —
|
|
964
|
+
**`auto`** — invert the tone, then remap into the dark window:
|
|
863
965
|
|
|
864
|
-
```
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
// darkCurve default: 0.5
|
|
966
|
+
```
|
|
967
|
+
window = darkTone // default [15, 95]
|
|
968
|
+
inverted = 100 - authorTone
|
|
969
|
+
finalTone = remap(inverted, window)
|
|
869
970
|
```
|
|
870
971
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
Unlike a power curve, the Möbius transformation provides **proportional expansion** — small and large deltas are scaled by similar ratios, preserving the visual hierarchy of the light theme.
|
|
972
|
+
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`).
|
|
874
973
|
|
|
875
|
-
**`fixed`** —
|
|
974
|
+
**`fixed`** — remap into the dark window without inversion:
|
|
876
975
|
|
|
877
|
-
```
|
|
878
|
-
|
|
976
|
+
```
|
|
977
|
+
finalTone = remap(authorTone, darkTone)
|
|
879
978
|
```
|
|
880
979
|
|
|
881
|
-
|
|
882
|
-
|---|---|---|---|---|
|
|
883
|
-
| surface (L=97) | 97 | 19.7 | 17.4 | 92.6 |
|
|
884
|
-
| accent-fill (L=52) | 52 | 66.9 | 53.4 | 56.6 |
|
|
885
|
-
| accent-text (L=100) | 100 | 15 | 15 | 95 |
|
|
886
|
-
|
|
887
|
-
In high-contrast variants the `darkLightness` window is bypassed — `auto` uses the Möbius curve over the full `[0, 100]` range, `fixed` uses identity.
|
|
980
|
+
In high-contrast variants both windows are bypassed (forced to the full `[0, 100]` range): `auto` still inverts, `fixed`/`static` do not.
|
|
888
981
|
|
|
889
982
|
### Dark scheme — saturation
|
|
890
983
|
|
|
@@ -896,16 +989,20 @@ S_dark = S_light * (1 - darkDesaturation) // default: 0.1
|
|
|
896
989
|
|
|
897
990
|
`static` mode skips desaturation.
|
|
898
991
|
|
|
992
|
+
### Saturation taper
|
|
993
|
+
|
|
994
|
+
`saturationTaper` (default `0.15`) gently reduces saturation toward the tone extremes, where in-gamut chroma collapses. It is the *strength* (0–1) — the maximum fraction of saturation removed at the very edges — ramped in smoothly over the outer ~15% of tone on each end. Mid-tones are untouched; `0` disables it.
|
|
995
|
+
|
|
899
996
|
---
|
|
900
997
|
|
|
901
998
|
## Configuration
|
|
902
999
|
|
|
903
1000
|
```ts
|
|
904
1001
|
glaze.configure({
|
|
905
|
-
|
|
906
|
-
|
|
1002
|
+
lightTone: [10, 100], // [lo, hi]; or { lo, hi, eps } / false to disable clamping
|
|
1003
|
+
darkTone: [15, 95], // [lo, hi]; or { lo, hi, eps } / false to disable clamping
|
|
907
1004
|
darkDesaturation: 0.1,
|
|
908
|
-
|
|
1005
|
+
saturationTaper: 0.15,
|
|
909
1006
|
states: {
|
|
910
1007
|
dark: '@dark',
|
|
911
1008
|
highContrast: '@high-contrast',
|
|
@@ -921,20 +1018,22 @@ glaze.configure({
|
|
|
921
1018
|
});
|
|
922
1019
|
```
|
|
923
1020
|
|
|
1021
|
+
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.
|
|
1022
|
+
|
|
924
1023
|
`GlazeConfig`:
|
|
925
1024
|
|
|
926
1025
|
| Field | Default | Description |
|
|
927
1026
|
|---|---|---|
|
|
928
|
-
| `
|
|
929
|
-
| `
|
|
1027
|
+
| `lightTone` | `[10, 100]` | Light scheme tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` to disable clamping. Bypassed in HC. |
|
|
1028
|
+
| `darkTone` | `[15, 95]` | Dark scheme tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` to disable clamping. Bypassed in HC. |
|
|
930
1029
|
| `darkDesaturation` | `0.1` | Saturation reduction in dark scheme (0–1). |
|
|
931
|
-
| `
|
|
1030
|
+
| `saturationTaper` | `0.15` | Saturation taper strength toward the tone extremes (0–1). `0` disables. |
|
|
932
1031
|
| `states.dark` | `'@dark'` | State alias for dark mode tokens (Tasty export). |
|
|
933
1032
|
| `states.highContrast` | `'@high-contrast'` | State alias for HC tokens. |
|
|
934
1033
|
| `modes.dark` | `true` | Include dark variants in exports. |
|
|
935
1034
|
| `modes.highContrast` | `false` | Include HC variants. |
|
|
936
1035
|
| `shadowTuning` | `undefined` | Default tuning for all shadow colors. Per-color tuning merges field-by-field. |
|
|
937
|
-
| `autoFlip` | `true` | When solving `contrast
|
|
1036
|
+
| `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). |
|
|
938
1037
|
|
|
939
1038
|
| Method | Description |
|
|
940
1039
|
|---|---|
|
|
@@ -942,7 +1041,7 @@ glaze.configure({
|
|
|
942
1041
|
| `glaze.getConfig()` | Snapshot the current resolved config (shallow copy). |
|
|
943
1042
|
| `glaze.resetConfig()` | Reset to defaults (also bumps the version counter). |
|
|
944
1043
|
|
|
945
|
-
Standalone `glaze.color()` tokens snapshot the relevant fields at create time, so later `configure()` calls don't change already-created tokens.
|
|
1044
|
+
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`.
|
|
946
1045
|
|
|
947
1046
|
---
|
|
948
1047
|
|
|
@@ -974,9 +1073,13 @@ Resolution priority (highest first):
|
|
|
974
1073
|
|
|
975
1074
|
| Condition | Behavior |
|
|
976
1075
|
|---|---|
|
|
977
|
-
| `contrast` without `base` | Validation error |
|
|
978
|
-
| Relative `
|
|
979
|
-
| `
|
|
1076
|
+
| `contrast` without `base` in a **theme** color | Validation error |
|
|
1077
|
+
| Relative `tone` without `base` in a **theme** color | Validation error |
|
|
1078
|
+
| `contrast` without `base` in `glaze.color()` | Anchors against the literal seed (no error) |
|
|
1079
|
+
| Relative `tone` without `base` in `glaze.color()` | Anchors against the literal seed (no error) |
|
|
1080
|
+
| Relative `tone` overshoots `[0, 100]` | Mirror to the other side of the base (`flip` on, default), or clamp to the boundary (`flip` off) |
|
|
1081
|
+
| `tone` resolves outside 0–100 | Clamp silently |
|
|
1082
|
+
| `'max'` / `'min'` without `base` | Allowed — resolves to the scheme's tone extreme (root color) |
|
|
980
1083
|
| `saturation` outside 0–1 | Clamp silently |
|
|
981
1084
|
| Circular `base` references | Validation error |
|
|
982
1085
|
| `base` references non-existent name | Validation error |
|
|
@@ -1046,35 +1149,67 @@ formatOklch(280, 60, 95); // 'oklch(0.95 ... 280)'
|
|
|
1046
1149
|
|
|
1047
1150
|
To attach an alpha component, use `glaze.format(variant, format)` on a `ResolvedColorVariant` (which carries the `alpha` channel) instead of these raw writers.
|
|
1048
1151
|
|
|
1152
|
+
### OKHST tone utilities
|
|
1153
|
+
|
|
1154
|
+
```ts
|
|
1155
|
+
import {
|
|
1156
|
+
toTone,
|
|
1157
|
+
fromTone,
|
|
1158
|
+
toneFromY,
|
|
1159
|
+
yFromTone,
|
|
1160
|
+
okhstToOkhsl,
|
|
1161
|
+
okhslToOkhst,
|
|
1162
|
+
variantToOkhsl,
|
|
1163
|
+
REF_EPS,
|
|
1164
|
+
} from '@tenphi/glaze';
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
| Function | Description |
|
|
1168
|
+
|---|---|
|
|
1169
|
+
| `toTone(l, eps?)` | OKHSL lightness (0–1) → tone (0–100). Defaults to `REF_EPS`. |
|
|
1170
|
+
| `fromTone(t, eps?)` | Tone (0–100) → OKHSL lightness (0–1). Inverse of `toTone`. |
|
|
1171
|
+
| `toneFromY(y, eps?)` / `yFromTone(t, eps?)` | Same transfer in luminance space (0–1). |
|
|
1172
|
+
| `okhstToOkhsl({ h, s, t })` | OKHST → OKHSL (`{ h, s, l }`). |
|
|
1173
|
+
| `okhslToOkhst({ h, s, l })` | OKHSL → OKHST (`{ h, s, t }`). |
|
|
1174
|
+
| `variantToOkhsl(variant)` | `ResolvedColorVariant` (stores `t`) → `{ h, s, l, alpha }` for rendering. |
|
|
1175
|
+
| `REF_EPS` | Reference epsilon (`0.05`) for the canonical tone axis. |
|
|
1176
|
+
|
|
1177
|
+
`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.
|
|
1178
|
+
|
|
1049
1179
|
### Contrast solver
|
|
1050
1180
|
|
|
1051
1181
|
```ts
|
|
1052
1182
|
import {
|
|
1053
|
-
|
|
1183
|
+
findToneForContrast,
|
|
1054
1184
|
findValueForMixContrast,
|
|
1185
|
+
resolveContrastForMode,
|
|
1055
1186
|
resolveMinContrast,
|
|
1187
|
+
apcaContrast,
|
|
1056
1188
|
} from '@tenphi/glaze';
|
|
1057
1189
|
```
|
|
1058
1190
|
|
|
1059
1191
|
| Function | Description |
|
|
1060
1192
|
|---|---|
|
|
1061
|
-
| `
|
|
1193
|
+
| `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? }`. |
|
|
1062
1194
|
| `findValueForMixContrast(opts)` | Same, but searches for a mix `value` (0–1) that meets a contrast floor between a base and a target. |
|
|
1063
|
-
| `
|
|
1195
|
+
| `resolveContrastForMode(spec, isHC)` | Resolves a `ContrastSpec` to `{ metric: 'wcag' \| 'apca', target }` for the requested mode (picks the normal or HC entry of any pair). |
|
|
1196
|
+
| `resolveMinContrast(value)` | Resolves a `MinContrast` (WCAG preset or number) to a numeric ratio. |
|
|
1197
|
+
| `apcaContrast(yText, yBg)` | APCA Lc magnitude (0–106) for two relative luminances. |
|
|
1064
1198
|
|
|
1065
|
-
`
|
|
1199
|
+
`findToneForContrast` options:
|
|
1066
1200
|
|
|
1067
1201
|
| Option | Default | Description |
|
|
1068
1202
|
|---|---|---|
|
|
1069
1203
|
| `hue` | — | Candidate hue (0–360). |
|
|
1070
1204
|
| `saturation` | — | Candidate saturation (0–1). |
|
|
1071
|
-
| `
|
|
1205
|
+
| `preferredTone` | — | Preferred candidate tone (0–1). Kept if it already meets the target. |
|
|
1072
1206
|
| `baseLinearRgb` | — | Base color as linear sRGB tuple. |
|
|
1073
|
-
| `contrast` | — |
|
|
1074
|
-
| `
|
|
1207
|
+
| `contrast` | — | `ResolvedContrast` (`{ metric, target }`). |
|
|
1208
|
+
| `toneRange` | `[0, 1]` | Search bounds in tone. |
|
|
1075
1209
|
| `epsilon` | `1e-4` | Convergence threshold. |
|
|
1076
|
-
| `maxIterations` | `
|
|
1077
|
-
| `initialDirection` | higher-contrast side | Direction to search first (`'lighter'` or `'darker'`).
|
|
1210
|
+
| `maxIterations` | `18` | Max binary-search iterations per branch. |
|
|
1211
|
+
| `initialDirection` | higher-contrast side | Direction to search first (`'lighter'` or `'darker'`). |
|
|
1078
1212
|
| `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. |
|
|
1213
|
+
| `saturationTaper` | `0` | When `> 0`, candidate saturation rolls off toward the tone extremes (same envelope the renderer applies), so the solved tone meets the floor at its rendered saturation. |
|
|
1079
1214
|
|
|
1080
|
-
Result: `{
|
|
1215
|
+
Result: `{ tone, contrast, met, branch: 'lighter' | 'darker' | 'preferred', flipped? }`. `flipped: true` indicates the initial direction failed and the opposite direction satisfied the target.
|