@tenphi/glaze 0.10.1 → 0.11.1
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 +19 -1384
- package/dist/index.cjs +714 -575
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -20
- package/dist/index.d.mts +20 -20
- package/dist/index.mjs +714 -575
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +1074 -0
- package/docs/methodology.md +330 -0
- package/docs/migration.md +237 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
Glaze generates robust **light**, **dark**, and **high-contrast** color schemes from a single hue/saturation seed.
|
|
19
|
+
Glaze generates robust **light**, **dark**, and **high-contrast** color schemes from a single hue/saturation seed. WCAG contrast is preserved via explicit dependency declarations — no hidden role math, no magic multipliers.
|
|
20
20
|
|
|
21
21
|
## Features
|
|
22
22
|
|
|
@@ -53,1408 +53,43 @@ yarn add @tenphi/glaze
|
|
|
53
53
|
```ts
|
|
54
54
|
import { glaze } from '@tenphi/glaze';
|
|
55
55
|
|
|
56
|
-
// Create a theme from a hue (0–360) and saturation (0–100)
|
|
57
56
|
const primary = glaze(280, 80);
|
|
58
57
|
|
|
59
|
-
// Define colors with explicit lightness and contrast relationships
|
|
60
58
|
primary.colors({
|
|
61
|
-
surface:
|
|
62
|
-
text:
|
|
63
|
-
border:
|
|
64
|
-
'accent-fill':
|
|
65
|
-
'accent-text':
|
|
59
|
+
surface: { lightness: 97, saturation: 0.75 },
|
|
60
|
+
text: { base: 'surface', lightness: '-52', contrast: 'AAA' },
|
|
61
|
+
border: { base: 'surface', lightness: ['-7', '-20'], contrast: 'AA-large' },
|
|
62
|
+
'accent-fill': { lightness: 52, mode: 'fixed' },
|
|
63
|
+
'accent-text': { base: 'accent-fill', lightness: '+48', contrast: 'AA', mode: 'fixed' },
|
|
64
|
+
'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
|
|
66
65
|
});
|
|
67
66
|
|
|
68
|
-
// Create status themes by rotating the hue
|
|
69
67
|
const danger = primary.extend({ hue: 23 });
|
|
70
68
|
const success = primary.extend({ hue: 157 });
|
|
71
69
|
|
|
72
|
-
// Compose into a palette and export
|
|
73
|
-
const palette = glaze.palette({ primary, danger, success }, { primary: 'primary' });
|
|
74
|
-
const tokens = palette.tokens();
|
|
75
|
-
// → { light: { 'primary-surface': 'okhsl(...)', 'surface': 'okhsl(...)', ... }, dark: { ... } }
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
## Core Concepts
|
|
79
|
-
|
|
80
|
-
### One Theme = One Hue Family
|
|
81
|
-
|
|
82
|
-
A single `glaze` theme is tied to one hue/saturation seed. Status colors (danger, success, warning) are derived via `extend`, which inherits all color definitions and replaces the seed.
|
|
83
|
-
|
|
84
|
-
Individual colors can override the hue via the `hue` prop (see [Per-Color Hue Override](#per-color-hue-override)), but the primary purpose of a theme is to scope colors with the same hue.
|
|
85
|
-
|
|
86
|
-
### Color Definitions
|
|
87
|
-
|
|
88
|
-
Every color is defined explicitly. No implicit roles — every value is stated.
|
|
89
|
-
|
|
90
|
-
#### Root Colors (explicit position)
|
|
91
|
-
|
|
92
|
-
```ts
|
|
93
|
-
primary.colors({
|
|
94
|
-
surface: { lightness: 97, saturation: 0.75 },
|
|
95
|
-
border: { lightness: 90, saturation: 0.20 },
|
|
96
|
-
});
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
- `lightness` — lightness in the light scheme (0–100)
|
|
100
|
-
- `saturation` — saturation factor applied to the seed saturation (0–1, default: `1`)
|
|
101
|
-
|
|
102
|
-
#### Dependent Colors (relative to base)
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
primary.colors({
|
|
106
|
-
surface: { lightness: 97, saturation: 0.75 },
|
|
107
|
-
text: { base: 'surface', lightness: '-52', contrast: 'AAA' },
|
|
108
|
-
});
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
- `base` — name of another color in the same theme
|
|
112
|
-
- `lightness` — position of this color (see [Lightness Values](#lightness-values))
|
|
113
|
-
- `contrast` — ensures the WCAG contrast ratio meets a target floor against the base
|
|
114
|
-
|
|
115
|
-
### Lightness Values
|
|
116
|
-
|
|
117
|
-
The `lightness` prop accepts two forms:
|
|
118
|
-
|
|
119
|
-
| Form | Example | Meaning |
|
|
120
|
-
|---|---|---|
|
|
121
|
-
| Number (absolute) | `lightness: 45` | Absolute lightness 0–100 |
|
|
122
|
-
| String (relative) | `lightness: '-52'` | Relative to base color's lightness |
|
|
123
|
-
|
|
124
|
-
**Absolute lightness** on a dependent color (with `base`) positions the color independently. In dark mode, it is dark-mapped on its own. The `contrast` WCAG solver acts as a safety net.
|
|
125
|
-
|
|
126
|
-
**Relative lightness** applies a signed delta to the base color's resolved lightness. In dark mode with `auto` adaptation, the sign flips automatically.
|
|
127
|
-
|
|
128
|
-
```ts
|
|
129
|
-
// Relative: 97 - 52 = 45 in light mode
|
|
130
|
-
'text': { base: 'surface', lightness: '-52' }
|
|
131
|
-
|
|
132
|
-
// Absolute: lightness 45 in light mode, dark-mapped independently
|
|
133
|
-
'text': { base: 'surface', lightness: 45 }
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
A dependent color with `base` but no `lightness` inherits the base's lightness (equivalent to a delta of 0).
|
|
137
|
-
|
|
138
|
-
### Per-Color Hue Override
|
|
139
|
-
|
|
140
|
-
Individual colors can override the theme's hue. The `hue` prop accepts:
|
|
141
|
-
|
|
142
|
-
| Form | Example | Meaning |
|
|
143
|
-
|---|---|---|
|
|
144
|
-
| Number (absolute) | `hue: 120` | Absolute hue 0–360 |
|
|
145
|
-
| String (relative) | `hue: '+20'` | Relative to the **theme seed** hue |
|
|
146
|
-
|
|
147
|
-
**Important:** Relative hue is always relative to the **theme seed hue**, not to a base color's hue.
|
|
148
|
-
|
|
149
|
-
```ts
|
|
150
|
-
const theme = glaze(280, 80);
|
|
151
|
-
theme.colors({
|
|
152
|
-
surface: { lightness: 97 },
|
|
153
|
-
// Gradient end — slight hue shift from seed (280 + 20 = 300)
|
|
154
|
-
gradientEnd: { lightness: 90, hue: '+20' },
|
|
155
|
-
// Entirely different hue
|
|
156
|
-
warning: { lightness: 60, hue: 40 },
|
|
157
|
-
});
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
### contrast (WCAG Floor)
|
|
161
|
-
|
|
162
|
-
Ensures the WCAG contrast ratio meets a target floor. Accepts a numeric ratio or a preset string:
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
type MinContrast = number | 'AA' | 'AAA' | 'AA-large' | 'AAA-large';
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
| Preset | Ratio |
|
|
169
|
-
|---|---|
|
|
170
|
-
| `'AA'` | 4.5 |
|
|
171
|
-
| `'AAA'` | 7 |
|
|
172
|
-
| `'AA-large'` | 3 |
|
|
173
|
-
| `'AAA-large'` | 4.5 |
|
|
174
|
-
|
|
175
|
-
You can also pass any numeric ratio directly (e.g., `contrast: 4.5`, `contrast: 7`, `contrast: 11`).
|
|
176
|
-
|
|
177
|
-
The constraint is applied independently for each scheme. If the `lightness` already satisfies the floor, it's kept. Otherwise, the solver adjusts lightness until the target is met.
|
|
178
|
-
|
|
179
|
-
### High-Contrast via Array Values
|
|
180
|
-
|
|
181
|
-
`lightness` and `contrast` accept a `[normal, high-contrast]` pair:
|
|
182
|
-
|
|
183
|
-
```ts
|
|
184
|
-
'border': { base: 'surface', lightness: ['-7', '-20'], contrast: 'AA-large' }
|
|
185
|
-
// ↑ ↑
|
|
186
|
-
// normal high-contrast
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
A single value applies to both modes. All control is local and explicit.
|
|
190
|
-
|
|
191
|
-
```ts
|
|
192
|
-
'text': { base: 'surface', lightness: '-52', contrast: 'AAA' }
|
|
193
|
-
'border': { base: 'surface', lightness: ['-7', '-20'], contrast: 'AA-large' }
|
|
194
|
-
'muted': { base: 'surface', lightness: ['-35', '-50'], contrast: ['AA-large', 'AA'] }
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
**Full lightness spectrum in HC mode:** In high-contrast variants, the `lightLightness` and `darkLightness` window constraints are bypassed entirely. Colors can reach the full 0–100 lightness range, maximizing perceivable contrast. Normal (non-HC) variants continue to use the configured windows.
|
|
198
|
-
|
|
199
|
-
## Theme Color Management
|
|
200
|
-
|
|
201
|
-
### Adding Colors
|
|
202
|
-
|
|
203
|
-
`.colors(defs)` performs an **additive merge** — it adds new colors and overwrites existing ones by name, but does not remove other colors:
|
|
204
|
-
|
|
205
|
-
```ts
|
|
206
|
-
const theme = glaze(280, 80);
|
|
207
|
-
theme.colors({ surface: { lightness: 97 } });
|
|
208
|
-
theme.colors({ text: { lightness: 30 } });
|
|
209
|
-
// Both 'surface' and 'text' are now defined
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
### Single Color Getter/Setter
|
|
213
|
-
|
|
214
|
-
`.color(name)` returns the definition, `.color(name, def)` sets it:
|
|
215
|
-
|
|
216
|
-
```ts
|
|
217
|
-
theme.color('surface', { lightness: 97, saturation: 0.75 }); // set
|
|
218
|
-
const def = theme.color('surface'); // get → { lightness: 97, saturation: 0.75 }
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### Removing Colors
|
|
222
|
-
|
|
223
|
-
`.remove(name)` or `.remove([name1, name2])` deletes color definitions:
|
|
224
|
-
|
|
225
|
-
```ts
|
|
226
|
-
theme.remove('surface');
|
|
227
|
-
theme.remove(['text', 'border']);
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### Introspection
|
|
231
|
-
|
|
232
|
-
```ts
|
|
233
|
-
theme.has('surface'); // → true/false
|
|
234
|
-
theme.list(); // → ['surface', 'text', 'border', ...]
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
### Clearing All Colors
|
|
238
|
-
|
|
239
|
-
```ts
|
|
240
|
-
theme.reset(); // removes all color definitions
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
## Import / Export
|
|
244
|
-
|
|
245
|
-
Serialize a theme's configuration (hue, saturation, color definitions) to a plain JSON-safe object, and restore it later:
|
|
246
|
-
|
|
247
|
-
```ts
|
|
248
|
-
// Export
|
|
249
|
-
const snapshot = theme.export();
|
|
250
|
-
// → { hue: 280, saturation: 80, colors: { surface: { lightness: 97, saturation: 0.75 }, ... } }
|
|
251
|
-
|
|
252
|
-
const jsonString = JSON.stringify(snapshot);
|
|
253
|
-
|
|
254
|
-
// Import
|
|
255
|
-
const restored = glaze.from(JSON.parse(jsonString));
|
|
256
|
-
// restored is a fully functional GlazeTheme
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
The export contains only the configuration — not resolved color values. Resolved values are recomputed on demand.
|
|
260
|
-
|
|
261
|
-
## Standalone Color Token
|
|
262
|
-
|
|
263
|
-
Create a single color token without a full theme:
|
|
264
|
-
|
|
265
|
-
```ts
|
|
266
|
-
const accent = glaze.color({ hue: 280, saturation: 80, lightness: 52 });
|
|
267
|
-
|
|
268
|
-
accent.resolve(); // → ResolvedColor with light/dark/lightContrast/darkContrast
|
|
269
|
-
accent.token(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' } (tasty format)
|
|
270
|
-
accent.tasty(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' } (same as token)
|
|
271
|
-
accent.json(); // → { light: 'okhsl(...)', dark: 'okhsl(...)' }
|
|
272
|
-
accent.css({ name: 'accent' });
|
|
273
|
-
// → { light: '--accent-color: rgb(...);', dark: '--accent-color: rgb(...);', ... }
|
|
274
|
-
accent.export(); // → JSON-safe snapshot — pass to `glaze.colorFrom(...)` to rehydrate
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
### Defaults
|
|
278
|
-
|
|
279
|
-
`glaze.color()` is tuned for "render this exact color, but adapt the
|
|
280
|
-
dark variant" — different from theme colors, which are seeds that
|
|
281
|
-
adapt to both lightness windows. The defaults vary by input form,
|
|
282
|
-
because string inputs are typically end-user values (color pickers,
|
|
283
|
-
theme settings) where natural light/dark inversion is the expectation:
|
|
284
|
-
|
|
285
|
-
- **String value-shorthand** (hex, `rgb()`, `hsl()`, `okhsl()`,
|
|
286
|
-
`oklch()`):
|
|
287
|
-
- Light variant preserves the input lightness exactly.
|
|
288
|
-
- Dark variant is **Möbius-inverted** into `[globalConfig.darkLightness[0], 100]`,
|
|
289
|
-
so `glaze.color('#000')` renders as `#fff` in dark mode and
|
|
290
|
-
`glaze.color('#fff')` falls to the dark `lo` floor (default `0.15`).
|
|
291
|
-
- Adaptation mode defaults to `'auto'`.
|
|
292
|
-
- The dark `lo` is snapshotted from `globalConfig` at color-creation
|
|
293
|
-
time, matching how an explicit `scaling.darkLightness: [lo, hi]`
|
|
294
|
-
behaves.
|
|
295
|
-
|
|
296
|
-
- **Object / tuple value-shorthand** (`{ h, s, l }`, `[r, g, b]`) and
|
|
297
|
-
the **structured form** (`{ hue, saturation, lightness, ... }`):
|
|
298
|
-
- Light variant preserves the input lightness exactly.
|
|
299
|
-
- Dark variant is linearly mapped into `globalConfig.darkLightness`
|
|
300
|
-
(default `[15, 95]`), snapshotted at color-creation time so later
|
|
301
|
-
`glaze.configure()` calls don't retroactively change exported tokens.
|
|
302
|
-
- Adaptation mode defaults to `'fixed'` (linear, no Möbius curve).
|
|
303
|
-
|
|
304
|
-
To opt back into the old fixed-linear default for string inputs, pass
|
|
305
|
-
either `{ mode: 'fixed' }` as the second arg, or supply an explicit
|
|
306
|
-
`scaling` as the third arg (see [Lightness scaling](#lightness-scaling)).
|
|
307
|
-
|
|
308
|
-
```ts
|
|
309
|
-
// Default: pure black inverts to pure white in dark mode.
|
|
310
|
-
glaze.color('#000000').tasty();
|
|
311
|
-
// → { '': 'okhsl(0 0% 0%)', '@dark': 'okhsl(... 100%)' }
|
|
312
|
-
|
|
313
|
-
// Opt back into the fixed-linear behavior:
|
|
314
|
-
glaze.color('#000000', { mode: 'fixed' }).tasty();
|
|
315
|
-
// → { '': 'okhsl(0 0% 0%)', '@dark': 'okhsl(... 15%)' }
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
### Value Shorthand
|
|
319
|
-
|
|
320
|
-
The first argument can also be a color value — Glaze extracts the seed
|
|
321
|
-
hue/saturation/lightness for you. All forms support the same exports
|
|
322
|
-
(`resolve / token / tasty / json / css`):
|
|
323
|
-
|
|
324
|
-
```ts
|
|
325
|
-
// Hex (3, 6, or 8 digits — alpha dropped with warning)
|
|
326
|
-
glaze.color('#26fcb2').tasty();
|
|
327
|
-
glaze.color('#26fcb2ff').tasty(); // alpha dropped
|
|
328
|
-
|
|
329
|
-
// CSS color functions Glaze itself emits (`rgb()`, `hsl()`, `okhsl()`, `oklch()`)
|
|
330
|
-
// — anything from theme.tasty()/json()/css() round-trips back in.
|
|
331
|
-
glaze.color('rgb(38 252 178)').tasty();
|
|
332
|
-
glaze.color('hsl(152 97% 57%)').tasty();
|
|
333
|
-
glaze.color('okhsl(152 95% 74%)').tasty();
|
|
334
|
-
glaze.color('oklch(0.85 0.18 152)').tasty();
|
|
335
|
-
|
|
336
|
-
// OKHSL object — Glaze's native shape (h: 0–360, s/l: 0–1).
|
|
337
|
-
// Passing 0–100 values for s/l throws with a hint to use the
|
|
338
|
-
// structured form { hue, saturation, lightness }.
|
|
339
|
-
glaze.color({ h: 152, s: 0.95, l: 0.74 }).tasty();
|
|
340
|
-
|
|
341
|
-
// RGB tuple, 0–255 (same range as glaze.fromRgb).
|
|
342
|
-
glaze.color([38, 252, 178]).tasty();
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
The optional second argument supplies overrides — the WCAG `contrast`
|
|
346
|
-
solver, relative `hue` / `lightness`, plus the usual seed knobs:
|
|
347
|
-
|
|
348
|
-
```ts
|
|
349
|
-
// Brand color seeded from a hex, with saturation/mode overrides
|
|
350
|
-
glaze.color('#26fcb2', { saturation: 80, mode: 'fixed' }).tasty();
|
|
351
|
-
|
|
352
|
-
// Brand text guaranteed AAA against the seed itself.
|
|
353
|
-
// Relative `lightness: '+48'` is anchored to the literal seed value.
|
|
354
|
-
glaze.color('#1a1a2e', {
|
|
355
|
-
lightness: '+48',
|
|
356
|
-
contrast: 'AAA',
|
|
357
|
-
}).tasty();
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
By default, relative `lightness: '+N'` and `contrast: <ratio>` are
|
|
361
|
-
anchored to the literal seed (the value passed to `glaze.color()`).
|
|
362
|
-
Internally Glaze synthesizes a hidden `mode: 'static'` reference of
|
|
363
|
-
the seed so the contrast solver compares against the unmapped color
|
|
364
|
-
across every variant. Pass `base` (another `glaze.color()` token) to
|
|
365
|
-
anchor against another color's resolved variant per scheme instead —
|
|
366
|
-
see [Pairing Colors](#pairing-colors).
|
|
367
|
-
|
|
368
|
-
All overrides:
|
|
369
|
-
|
|
370
|
-
| Option | Notes |
|
|
371
|
-
|---|---|
|
|
372
|
-
| `hue` | Number (absolute 0–360) or `'+N'`/`'-N'` (relative to seed — never to `base`) |
|
|
373
|
-
| `saturation` | Override seed saturation (0–100) |
|
|
374
|
-
| `lightness` | Number (absolute 0–100) or `'+N'`/`'-N'`. Without `base`, relative is anchored to the literal seed; with `base`, anchored to `base`'s lightness per scheme. Supports `[normal, hc]` pairs |
|
|
375
|
-
| `saturationFactor` | Multiplier on seed (0–1, default 1) |
|
|
376
|
-
| `mode` | `'auto'` (default for string inputs) / `'fixed'` (default for object / tuple / structured inputs) / `'static'` — see [Adaptation Modes](#adaptation-modes) |
|
|
377
|
-
| `contrast` | WCAG floor. Without `base`, anchored to the literal seed; with `base`, solved per scheme against `base`'s resolved variant. Same shape as `RegularColorDef.contrast`. When the target can't be physically met, `glaze` emits a `console.warn` and returns the closest passing variant |
|
|
378
|
-
| `base` | Another `glaze.color()` token **or** a raw `GlazeColorValue` (hex / `rgb()` / `OkhslColor` / `[r, g, b]`). Raw values are auto-wrapped via `glaze.color(value)` so they pick up the same auto-invert defaults as an explicit wrap. When set, `contrast` and relative `lightness` anchor to it per scheme; relative `hue` still anchors to the seed |
|
|
379
|
-
| `opacity` | Fixed alpha 0–1 applied to every variant. Surfaces in `rgb(... / A)`, `okhsl(... / A)`, etc. Combining with `contrast` is not recommended (perceived lightness becomes unpredictable) — `glaze` emits a `console.warn` |
|
|
380
|
-
| `name` | **Debug label only** — surfaces in error and `console.warn` messages instead of the internal `"value"` sentinel. Does **not** change `.token()` / `.tasty()` / `.json()` / `.css()` output keys (those still use `''`, `light`, etc.). Reserved names (`"value"`, `"seed"`, `"externalBase"`) are rejected |
|
|
381
|
-
|
|
382
|
-
Alpha components in `rgb(... / A)` / `hsl(... / A)` / `rgba(...)` /
|
|
383
|
-
`hsla(...)` and 8-digit hex (`#rrggbbaa` / `#rgba`) are parsed but the
|
|
384
|
-
alpha channel is dropped with a `console.warn`. To set a fixed alpha
|
|
385
|
-
on a standalone color, use the `opacity` override (or `opacity` on a
|
|
386
|
-
theme color). Named CSS colors (`'red'`, `'blueviolet'`) are not
|
|
387
|
-
supported.
|
|
388
|
-
|
|
389
|
-
### Lightness Scaling
|
|
390
|
-
|
|
391
|
-
The optional third positional argument lets you override the lightness
|
|
392
|
-
windows used by `glaze.color()`. Both keys mirror the field names from
|
|
393
|
-
`GlazeConfig`:
|
|
394
|
-
|
|
395
|
-
```ts
|
|
396
|
-
// Preserve raw lightness in dark mode too:
|
|
397
|
-
glaze.color('#26fcb2', undefined, { darkLightness: false }).tasty();
|
|
398
|
-
|
|
399
|
-
// Or opt back into a theme-style window:
|
|
400
|
-
glaze.color('#26fcb2', undefined, {
|
|
401
|
-
lightLightness: [10, 100],
|
|
402
|
-
darkLightness: [15, 95],
|
|
403
|
-
}).tasty();
|
|
404
|
-
|
|
405
|
-
// Structured form takes scaling as the second positional arg:
|
|
406
|
-
glaze
|
|
407
|
-
.color({ hue: 152, saturation: 95, lightness: 74 }, { darkLightness: false })
|
|
408
|
-
.tasty();
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
| Key | Default for `glaze.color()` (string input) | Default for `glaze.color()` (object / tuple / structured) | Effect |
|
|
412
|
-
|---|---|---|---|
|
|
413
|
-
| `lightLightness` | `false` | `false` | `false` = preserve input. Pass `[lo, hi]` to opt into a remap window. |
|
|
414
|
-
| `darkLightness` | `[globalConfig.darkLightness[0], 100]` (snapshotted; default `[15, 100]`) | `globalConfig.darkLightness` (snapshotted; default `[15, 95]`) | `false` = preserve input in dark too. Pass `[lo, hi]` to override the window. |
|
|
415
|
-
|
|
416
|
-
> Note: `scaling` is all-or-nothing — passing it replaces both fields
|
|
417
|
-
> at once. To keep one field's default, restate it explicitly. The
|
|
418
|
-
> default windows are snapshotted from `globalConfig` at color-creation
|
|
419
|
-
> time, so later `glaze.configure()` calls don't retroactively change
|
|
420
|
-
> already-created tokens (and `token.export()` round-trips
|
|
421
|
-
> byte-for-byte across `configure()` changes).
|
|
422
|
-
|
|
423
|
-
### Pairing Colors
|
|
424
|
-
|
|
425
|
-
`glaze.color()` accepts an optional `base` override that ties one
|
|
426
|
-
standalone color to another. When you set `base`, the WCAG contrast
|
|
427
|
-
solver and relative `lightness` offsets switch their anchor from the
|
|
428
|
-
literal seed to the base's resolved variant per scheme — so the same
|
|
429
|
-
text color automatically lands at AA against its background in light,
|
|
430
|
-
dark, and high-contrast modes.
|
|
431
|
-
|
|
432
|
-
```ts
|
|
433
|
-
const bg = glaze.color('#1a1a2e');
|
|
434
|
-
|
|
435
|
-
// Text guaranteed AA against `bg` in every scheme.
|
|
436
|
-
const text = glaze.color('#ffffff', { base: bg, contrast: 'AA' });
|
|
437
|
-
|
|
438
|
-
// Border 8 lightness units lighter than `bg` in each scheme.
|
|
439
|
-
const border = glaze.color('#000000', {
|
|
440
|
-
base: bg,
|
|
441
|
-
lightness: '+8',
|
|
442
|
-
mode: 'fixed',
|
|
443
|
-
});
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
`base` also accepts a raw `GlazeColorValue` for one-off pairs without
|
|
447
|
-
a separate token binding:
|
|
448
|
-
|
|
449
|
-
```ts
|
|
450
|
-
// Equivalent to `base: glaze.color('#1a1a2e')` — `glaze` auto-wraps it.
|
|
451
|
-
const text = glaze.color('#ffffff', { base: '#1a1a2e', contrast: 'AA' });
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
Behavior with `base`:
|
|
455
|
-
|
|
456
|
-
- `contrast` is solved per scheme against `base`'s resolved variant
|
|
457
|
-
(light / dark / lightContrast / darkContrast).
|
|
458
|
-
- Relative `lightness: '+N'` / `'-N'` is anchored to `base`'s lightness
|
|
459
|
-
per scheme (matches theme behavior).
|
|
460
|
-
- Relative `hue: '+N'` still anchors to the **seed** (the value passed
|
|
461
|
-
to `glaze.color()`), not the base. Absolute hue overrides take
|
|
462
|
-
precedence as usual.
|
|
463
|
-
- `mode` works as a per-pair knob — pass `mode: 'fixed'` to disable
|
|
464
|
-
Möbius inversion for the dependent color, or `mode: 'auto'` to keep
|
|
465
|
-
it (defaults follow the same string-vs-object rules as standalone).
|
|
466
|
-
- The base token's `.resolve()` is called lazily on the first resolve
|
|
467
|
-
of the dependent and the result is captured by reference; later
|
|
468
|
-
mutations to the base don't apply (matches existing snapshot
|
|
469
|
-
semantics for `scaling.darkLightness`).
|
|
470
|
-
- Raw value bases (`base: '#fff'`, `base: { h, s, l }`, `base: [r, g, b]`)
|
|
471
|
-
are auto-wrapped via `glaze.color(value)` and inherit the same
|
|
472
|
-
string-vs-object defaults. To skip auto-invert on the base, wrap it
|
|
473
|
-
yourself: `base: glaze.color(value, undefined, { darkLightness: false })`.
|
|
474
|
-
- When the contrast target is physically unreachable (e.g. AAA against
|
|
475
|
-
a mid-grey base), `glaze` emits a single `console.warn` per
|
|
476
|
-
`(name, scheme, target)` triple and returns the closest passing
|
|
477
|
-
variant. Use the `name` override to make the warning more
|
|
478
|
-
identifiable in your logs.
|
|
479
|
-
|
|
480
|
-
Chains compose:
|
|
481
|
-
|
|
482
|
-
```ts
|
|
483
|
-
const bg = glaze.color('#000000');
|
|
484
|
-
const surface = glaze.color('#222222', { base: bg, contrast: 'AAA' });
|
|
485
|
-
const text = glaze.color('#ffffff', { base: surface, contrast: 'AA' });
|
|
486
|
-
// Each level meets its contrast budget against its base in every scheme.
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
### Naming Standalone Colors
|
|
490
|
-
|
|
491
|
-
The `name` override is a **debug label**, not an output key:
|
|
492
|
-
|
|
493
|
-
```ts
|
|
494
|
-
const cardBg = glaze.color('#1a1a2e', {
|
|
495
|
-
name: 'card-bg', // surfaces in `console.warn` / Error messages
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
cardBg.token(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' }
|
|
499
|
-
cardBg.json(); // → { light: 'okhsl(...)', dark: 'okhsl(...)' }
|
|
500
|
-
cardBg.css({ name: 'card' }); // CSS variable name comes from `css({ name })`,
|
|
501
|
-
// NOT from the override above
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
Use it to make warnings traceable when you have many `glaze.color()`
|
|
505
|
-
calls in a project — without it, `glaze` falls back to the internal
|
|
506
|
-
sentinel `"value"`:
|
|
507
|
-
|
|
508
|
-
```ts
|
|
509
|
-
// With name:
|
|
510
|
-
// > glaze: color "card-bg" cannot meet contrast "AAA" (7.00) in dark scheme...
|
|
511
|
-
|
|
512
|
-
// Without name:
|
|
513
|
-
// > glaze: color "value" cannot meet contrast "AAA" (7.00) in dark scheme...
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
The reserved internal sentinels (`"value"`, `"seed"`, `"externalBase"`)
|
|
517
|
-
are rejected with a clear error pointing at the conflict.
|
|
518
|
-
|
|
519
|
-
### Persisting Standalone Colors
|
|
520
|
-
|
|
521
|
-
`glaze.color()` tokens can be serialized to JSON-safe data and
|
|
522
|
-
rehydrated later — useful for color pickers, theme settings UIs, and
|
|
523
|
-
URL state.
|
|
524
|
-
|
|
525
|
-
```ts
|
|
526
|
-
const text = glaze.color('#1a1a1a', {
|
|
527
|
-
contrast: 'AA',
|
|
528
|
-
opacity: 0.9,
|
|
529
|
-
name: 'profile-text',
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
const data = text.export(); // JSON-safe snapshot
|
|
533
|
-
const json = JSON.stringify(data); // ship to localStorage / API / URL
|
|
534
|
-
const restored = glaze.colorFrom(JSON.parse(json));
|
|
535
|
-
// `restored.resolve()` matches `text.resolve()` byte-for-byte.
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
The export captures the original `value`, all overrides, and the
|
|
539
|
-
effective `scaling` (snapshotted from `globalConfig` at create time so
|
|
540
|
-
later `glaze.configure()` calls don't change exported tokens).
|
|
541
|
-
Token-typed `base` is recursively serialized, value-typed `base` is
|
|
542
|
-
preserved as the raw value.
|
|
543
|
-
|
|
544
|
-
Both forms round-trip:
|
|
545
|
-
|
|
546
|
-
```ts
|
|
547
|
-
// Value form
|
|
548
|
-
const a = glaze.color('#26fcb2', { contrast: 'AA' });
|
|
549
|
-
const aBack = glaze.colorFrom(a.export());
|
|
550
|
-
|
|
551
|
-
// Structured form
|
|
552
|
-
const b = glaze.color({
|
|
553
|
-
hue: 280,
|
|
554
|
-
saturation: 50,
|
|
555
|
-
lightness: 50,
|
|
556
|
-
opacity: 0.5,
|
|
557
|
-
});
|
|
558
|
-
const bBack = glaze.colorFrom(b.export());
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
## From Existing Colors
|
|
562
|
-
|
|
563
|
-
Create a theme from an existing brand color by extracting its OKHSL hue and saturation:
|
|
564
|
-
|
|
565
|
-
```ts
|
|
566
|
-
// From hex
|
|
567
|
-
const brand = glaze.fromHex('#7a4dbf');
|
|
568
|
-
|
|
569
|
-
// From RGB (0–255)
|
|
570
|
-
const brand = glaze.fromRgb(122, 77, 191);
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
The resulting theme has the extracted hue and saturation. Add colors as usual:
|
|
574
|
-
|
|
575
|
-
```ts
|
|
576
|
-
brand.colors({
|
|
577
|
-
surface: { lightness: 97, saturation: 0.75 },
|
|
578
|
-
text: { base: 'surface', lightness: '-52', contrast: 'AAA' },
|
|
579
|
-
});
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
## Shadow Colors
|
|
583
|
-
|
|
584
|
-
Shadow colors are colors with computed alpha. Instead of a parallel shadow system, they extend the existing color pipeline. All math is done natively in OKHSL.
|
|
585
|
-
|
|
586
|
-
### Defining Shadow Colors
|
|
587
|
-
|
|
588
|
-
Shadow colors use `type: 'shadow'` and reference a `bg` (background) color and optionally an `fg` (foreground) color for tinting and intensity modulation:
|
|
589
|
-
|
|
590
|
-
```ts
|
|
591
|
-
theme.colors({
|
|
592
|
-
surface: { lightness: 95 },
|
|
593
|
-
text: { base: 'surface', lightness: '-52', contrast: 'AAA' },
|
|
594
|
-
|
|
595
|
-
'shadow-sm': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 5 },
|
|
596
|
-
'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
|
|
597
|
-
'shadow-lg': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 20 },
|
|
598
|
-
});
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
Shadow colors are included in all output methods (`tokens()`, `tasty()`, `css()`, `json()`) alongside regular colors:
|
|
602
|
-
|
|
603
|
-
```ts
|
|
604
|
-
theme.tokens({ format: 'oklch' });
|
|
605
|
-
// light: { 'shadow-md': 'oklch(0.15 0.009 282 / 0.1)', ... }
|
|
606
|
-
// dark: { 'shadow-md': 'oklch(0.06 0.004 0 / 0.49)', ... }
|
|
607
|
-
```
|
|
608
|
-
|
|
609
|
-
### How Shadows Work
|
|
610
|
-
|
|
611
|
-
The shadow algorithm computes a dark, low-saturation pigment color and an alpha value that produces the desired visual intensity:
|
|
612
|
-
|
|
613
|
-
1. **Contrast weight** — when `fg` is provided, shadow strength scales with `|l_bg - l_fg|`. Dark text on a light background produces a strong shadow; near-background-lightness elements produce barely visible shadows.
|
|
614
|
-
2. **Pigment color** — hue blended between fg and bg, low saturation, dark lightness.
|
|
615
|
-
3. **Alpha** — computed via a `tanh` curve that saturates smoothly toward `alphaMax` (default 0.6), ensuring well-separated shadow levels even on dark backgrounds.
|
|
616
|
-
|
|
617
|
-
### Achromatic Shadows
|
|
618
|
-
|
|
619
|
-
Omit `fg` for a pure achromatic shadow at full user-specified intensity:
|
|
620
|
-
|
|
621
|
-
```ts
|
|
622
|
-
theme.colors({
|
|
623
|
-
'drop-shadow': { type: 'shadow', bg: 'surface', intensity: 12 },
|
|
624
|
-
});
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
### High-Contrast Intensity
|
|
628
|
-
|
|
629
|
-
`intensity` supports `[normal, highContrast]` pairs:
|
|
630
|
-
|
|
631
|
-
```ts
|
|
632
|
-
theme.colors({
|
|
633
|
-
'shadow-card': { type: 'shadow', bg: 'surface', fg: 'text', intensity: [10, 20] },
|
|
634
|
-
});
|
|
635
|
-
```
|
|
636
|
-
|
|
637
|
-
### Fixed Opacity (Regular Colors)
|
|
638
|
-
|
|
639
|
-
For a simple fixed-alpha color (no shadow algorithm), use `opacity` on a regular color:
|
|
640
|
-
|
|
641
|
-
```ts
|
|
642
|
-
theme.colors({
|
|
643
|
-
overlay: { lightness: 0, opacity: 0.5 },
|
|
644
|
-
});
|
|
645
|
-
// → 'oklch(0 0 0 / 0.5)'
|
|
646
|
-
```
|
|
647
|
-
|
|
648
|
-
### Shadow Tuning
|
|
649
|
-
|
|
650
|
-
Fine-tune shadow behavior per-color or globally:
|
|
651
|
-
|
|
652
|
-
```ts
|
|
653
|
-
// Per-color tuning
|
|
654
|
-
theme.colors({
|
|
655
|
-
'shadow-soft': {
|
|
656
|
-
type: 'shadow', bg: 'surface', intensity: 10,
|
|
657
|
-
tuning: { alphaMax: 0.3, saturationFactor: 0.1 },
|
|
658
|
-
},
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
// Global tuning
|
|
662
|
-
glaze.configure({
|
|
663
|
-
shadowTuning: { alphaMax: 0.5, bgHueBlend: 0.3 },
|
|
664
|
-
});
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
Available tuning parameters:
|
|
668
|
-
|
|
669
|
-
| Parameter | Default | Description |
|
|
670
|
-
|---|---|---|
|
|
671
|
-
| `saturationFactor` | 0.18 | Fraction of fg saturation kept in pigment |
|
|
672
|
-
| `maxSaturation` | 0.25 | Upper clamp on pigment saturation |
|
|
673
|
-
| `lightnessFactor` | 0.25 | Multiplier for bg lightness to pigment lightness |
|
|
674
|
-
| `lightnessBounds` | [0.05, 0.20] | Clamp range for pigment lightness |
|
|
675
|
-
| `minGapTarget` | 0.05 | Target minimum gap between pigment and bg lightness |
|
|
676
|
-
| `alphaMax` | 0.6 | Asymptotic maximum alpha |
|
|
677
|
-
| `bgHueBlend` | 0.2 | Blend weight pulling pigment hue toward bg hue |
|
|
678
|
-
|
|
679
|
-
### Standalone Shadow Computation
|
|
680
|
-
|
|
681
|
-
Compute a shadow outside of a theme. `bg` and `fg` accept any
|
|
682
|
-
`GlazeColorValue`: hex (`#rgb` / `#rrggbb` / `#rrggbbaa`), `rgb()` /
|
|
683
|
-
`hsl()` / `okhsl()` / `oklch()` strings, OKHSL objects, or `[r, g, b]`
|
|
684
|
-
(0–255) tuples.
|
|
685
|
-
|
|
686
|
-
```ts
|
|
687
|
-
const v = glaze.shadow({
|
|
688
|
-
bg: '#f0eef5',
|
|
689
|
-
fg: '#1a1a2e',
|
|
690
|
-
intensity: 10,
|
|
691
|
-
});
|
|
692
|
-
// → { h: 280, s: 0.14, l: 0.2, alpha: 0.1 }
|
|
693
|
-
|
|
694
|
-
// Equivalent with non-hex inputs:
|
|
695
|
-
glaze.shadow({
|
|
696
|
-
bg: 'rgb(240 238 245)',
|
|
697
|
-
fg: { h: 280, s: 0.06, l: 0.13 },
|
|
698
|
-
intensity: 10,
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
const css = glaze.format(v, 'oklch');
|
|
702
|
-
// → 'oklch(0.15 0.014 280 / 0.1)'
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
### Consuming in CSS
|
|
706
|
-
|
|
707
|
-
```css
|
|
708
|
-
.card {
|
|
709
|
-
box-shadow: 0 2px 6px var(--shadow-sm-color),
|
|
710
|
-
0 8px 24px var(--shadow-md-color);
|
|
711
|
-
}
|
|
712
|
-
```
|
|
713
|
-
|
|
714
|
-
## Mix Colors
|
|
715
|
-
|
|
716
|
-
Mix colors blend two existing colors together. Use them for hover overlays, tints, shades, and any derived color that sits between two reference colors.
|
|
717
|
-
|
|
718
|
-
### Opaque Mix
|
|
719
|
-
|
|
720
|
-
Produces a solid color by interpolating between `base` and `target`:
|
|
721
|
-
|
|
722
|
-
```ts
|
|
723
|
-
theme.colors({
|
|
724
|
-
surface: { lightness: 95 },
|
|
725
|
-
accent: { lightness: 30 },
|
|
726
|
-
|
|
727
|
-
// 30% of the way from surface toward accent
|
|
728
|
-
tint: { type: 'mix', base: 'surface', target: 'accent', value: 30 },
|
|
729
|
-
});
|
|
730
|
-
```
|
|
731
|
-
|
|
732
|
-
- `value` — mix ratio 0–100 (0 = pure base, 100 = pure target)
|
|
733
|
-
- The result is a fully opaque color (alpha = 1)
|
|
734
|
-
- Adapts to light/dark/HC schemes automatically via the resolved base and target
|
|
735
|
-
|
|
736
|
-
### Transparent Mix
|
|
737
|
-
|
|
738
|
-
Produces the target color with a controlled opacity — useful for hover overlays:
|
|
739
|
-
|
|
740
|
-
```ts
|
|
741
|
-
theme.colors({
|
|
742
|
-
surface: { lightness: 95 },
|
|
743
|
-
black: { lightness: 0, saturation: 0 },
|
|
744
|
-
|
|
745
|
-
hover: {
|
|
746
|
-
type: 'mix',
|
|
747
|
-
base: 'surface',
|
|
748
|
-
target: 'black',
|
|
749
|
-
value: 8,
|
|
750
|
-
blend: 'transparent',
|
|
751
|
-
},
|
|
752
|
-
});
|
|
753
|
-
// hover → target color (black) with alpha = 0.08
|
|
754
|
-
```
|
|
755
|
-
|
|
756
|
-
The output color has `h`, `s`, `l` from the target and `alpha = value / 100`.
|
|
757
|
-
|
|
758
|
-
### Blend Space
|
|
759
|
-
|
|
760
|
-
By default, opaque mixing interpolates in OKHSL (perceptually uniform, consistent with Glaze's model). Use `space: 'srgb'` for linear sRGB interpolation, which matches browser compositing:
|
|
761
|
-
|
|
762
|
-
```ts
|
|
763
|
-
theme.colors({
|
|
764
|
-
surface: { lightness: 95 },
|
|
765
|
-
accent: { lightness: 30 },
|
|
766
|
-
|
|
767
|
-
// sRGB blend — matches what the browser would render
|
|
768
|
-
hover: { type: 'mix', base: 'surface', target: 'accent', value: 20, space: 'srgb' },
|
|
769
|
-
});
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
| Space | Behavior | Best for |
|
|
773
|
-
|---|---|---|
|
|
774
|
-
| `'okhsl'` (default) | Perceptually uniform OKHSL interpolation | Design token derivation |
|
|
775
|
-
| `'srgb'` | Linear sRGB channel interpolation | Matching browser compositing |
|
|
776
|
-
|
|
777
|
-
The `space` option only affects opaque blending. Transparent blending always composites in linear sRGB (matching browser alpha compositing).
|
|
778
|
-
|
|
779
|
-
### Contrast Solving
|
|
780
|
-
|
|
781
|
-
Mix colors support the same `contrast` prop as regular colors. The solver adjusts the mix ratio (opaque) or opacity (transparent) to meet the WCAG target:
|
|
782
|
-
|
|
783
|
-
```ts
|
|
784
|
-
theme.colors({
|
|
785
|
-
surface: { lightness: 95 },
|
|
786
|
-
accent: { lightness: 30 },
|
|
787
|
-
|
|
788
|
-
// Ensure the mixed color has at least AA contrast against surface
|
|
789
|
-
tint: {
|
|
790
|
-
type: 'mix',
|
|
791
|
-
base: 'surface',
|
|
792
|
-
target: 'accent',
|
|
793
|
-
value: 10,
|
|
794
|
-
contrast: 'AA',
|
|
795
|
-
},
|
|
796
|
-
|
|
797
|
-
// Ensure the transparent overlay has at least 3:1 contrast
|
|
798
|
-
overlay: {
|
|
799
|
-
type: 'mix',
|
|
800
|
-
base: 'surface',
|
|
801
|
-
target: 'accent',
|
|
802
|
-
value: 5,
|
|
803
|
-
blend: 'transparent',
|
|
804
|
-
contrast: 3,
|
|
805
|
-
},
|
|
806
|
-
});
|
|
807
|
-
```
|
|
808
|
-
|
|
809
|
-
### High-Contrast Pairs
|
|
810
|
-
|
|
811
|
-
Both `value` and `contrast` support `[normal, highContrast]` pairs:
|
|
812
|
-
|
|
813
|
-
```ts
|
|
814
|
-
theme.colors({
|
|
815
|
-
surface: { lightness: 95 },
|
|
816
|
-
accent: { lightness: 30 },
|
|
817
|
-
|
|
818
|
-
tint: {
|
|
819
|
-
type: 'mix',
|
|
820
|
-
base: 'surface',
|
|
821
|
-
target: 'accent',
|
|
822
|
-
value: [20, 40], // stronger mix in high-contrast mode
|
|
823
|
-
contrast: [3, 'AAA'], // stricter contrast in high-contrast mode
|
|
824
|
-
},
|
|
825
|
-
});
|
|
826
|
-
```
|
|
827
|
-
|
|
828
|
-
### Achromatic Colors
|
|
829
|
-
|
|
830
|
-
When mixing with achromatic colors (saturation near zero, e.g., white or black) in `okhsl` space, the hue comes from whichever color has saturation. This prevents meaningless hue artifacts and matches CSS `color-mix()` "missing component" behavior. For purely achromatic mixes, prefer `space: 'srgb'` where hue is irrelevant.
|
|
831
|
-
|
|
832
|
-
### Mix Chaining
|
|
833
|
-
|
|
834
|
-
Mix colors can reference other mix colors, enabling multi-step derivations:
|
|
835
|
-
|
|
836
|
-
```ts
|
|
837
|
-
theme.colors({
|
|
838
|
-
white: { lightness: 100, saturation: 0 },
|
|
839
|
-
black: { lightness: 0, saturation: 0 },
|
|
840
|
-
gray: { type: 'mix', base: 'white', target: 'black', value: 50, space: 'srgb' },
|
|
841
|
-
lightGray: { type: 'mix', base: 'white', target: 'gray', value: 50, space: 'srgb' },
|
|
842
|
-
});
|
|
843
|
-
```
|
|
844
|
-
|
|
845
|
-
Mix colors cannot reference shadow colors (same restriction as regular dependent colors).
|
|
846
|
-
|
|
847
|
-
## Output Formats
|
|
848
|
-
|
|
849
|
-
Control the color format in exports with the `format` option:
|
|
850
|
-
|
|
851
|
-
```ts
|
|
852
|
-
// Default: OKHSL
|
|
853
|
-
theme.tokens(); // → 'okhsl(280 60% 97%)'
|
|
854
|
-
|
|
855
|
-
// RGB (modern space syntax, rounded integers)
|
|
856
|
-
theme.tokens({ format: 'rgb' }); // → 'rgb(244 240 250)'
|
|
857
|
-
|
|
858
|
-
// HSL (modern space syntax)
|
|
859
|
-
theme.tokens({ format: 'hsl' }); // → 'hsl(270.5 45.2% 95.8%)'
|
|
860
|
-
|
|
861
|
-
// OKLCH
|
|
862
|
-
theme.tokens({ format: 'oklch' }); // → 'oklch(0.965 0.0123 280)'
|
|
863
|
-
```
|
|
864
|
-
|
|
865
|
-
The `format` option works on all export methods: `theme.tokens()`, `theme.tasty()`, `theme.json()`, `theme.css()`, `palette.tokens()`, `palette.tasty()`, `palette.json()`, `palette.css()`, and standalone `glaze.color().token()` / `.tasty()` / `.json()` / `.css()`.
|
|
866
|
-
|
|
867
|
-
Colors with `alpha < 1` (shadow colors, or regular colors with `opacity`) include an alpha component:
|
|
868
|
-
|
|
869
|
-
```ts
|
|
870
|
-
// → 'oklch(0.15 0.009 282 / 0.1)'
|
|
871
|
-
// → 'rgb(34 28 42 / 0.1)'
|
|
872
|
-
```
|
|
873
|
-
|
|
874
|
-
Available formats:
|
|
875
|
-
|
|
876
|
-
| Format | Output (alpha = 1) | Output (alpha < 1) | Notes |
|
|
877
|
-
|---|---|---|---|
|
|
878
|
-
| `'okhsl'` (default) | `okhsl(H S% L%)` | `okhsl(H S% L% / A)` | Native format, not a CSS function |
|
|
879
|
-
| `'rgb'` | `rgb(R G B)` | `rgb(R G B / A)` | Rounded integers, space syntax |
|
|
880
|
-
| `'hsl'` | `hsl(H S% L%)` | `hsl(H S% L% / A)` | Modern space syntax |
|
|
881
|
-
| `'oklch'` | `oklch(L C H)` | `oklch(L C H / A)` | OKLab-based LCH |
|
|
882
|
-
|
|
883
|
-
All numeric output strips trailing zeros for cleaner CSS (e.g., `95` not `95.0`).
|
|
884
|
-
|
|
885
|
-
## Adaptation Modes
|
|
886
|
-
|
|
887
|
-
Modes control how colors adapt across schemes:
|
|
888
|
-
|
|
889
|
-
| Mode | Behavior |
|
|
890
|
-
|---|---|
|
|
891
|
-
| `'auto'` (default) | Full adaptation. Light ↔ dark inversion. High-contrast boost. |
|
|
892
|
-
| `'fixed'` | Color stays recognizable. Only safety corrections. For brand buttons, CTAs. |
|
|
893
|
-
| `'static'` | No adaptation. Same value in every scheme. |
|
|
894
|
-
|
|
895
|
-
### How Relative Lightness Adapts
|
|
896
|
-
|
|
897
|
-
**`auto` mode** — relative lightness sign flips in dark scheme:
|
|
898
|
-
|
|
899
|
-
```ts
|
|
900
|
-
// Light: surface L=97, text lightness='-52' → L=45 (dark text on light bg)
|
|
901
|
-
// Dark: surface inverts to L≈20 (Möbius curve), sign flips → L=20+52=72
|
|
902
|
-
// contrast solver may push further (light text on dark bg)
|
|
903
|
-
```
|
|
904
|
-
|
|
905
|
-
**`fixed` mode** — lightness is mapped (not inverted), relative sign preserved:
|
|
906
|
-
|
|
907
|
-
```ts
|
|
908
|
-
// Light: accent-fill L=52, accent-text lightness='+48' → L=100 (white on brand)
|
|
909
|
-
// Dark: accent-fill maps to L≈51.6, sign preserved → L≈99.6
|
|
910
|
-
```
|
|
911
|
-
|
|
912
|
-
**`static` mode** — no adaptation, same value in every scheme.
|
|
913
|
-
|
|
914
|
-
## Light Scheme Mapping
|
|
915
|
-
|
|
916
|
-
### Lightness
|
|
917
|
-
|
|
918
|
-
Absolute lightness values (both root colors and dependent colors with absolute lightness) are mapped linearly within the configured `lightLightness` window:
|
|
919
|
-
|
|
920
|
-
```ts
|
|
921
|
-
const [lo, hi] = lightLightness; // default: [10, 100]
|
|
922
|
-
const mappedL = (lightness * (hi - lo)) / 100 + lo;
|
|
923
|
-
```
|
|
924
|
-
|
|
925
|
-
Both `auto` and `fixed` modes use the same linear formula. `static` mode and high-contrast variants bypass the mapping entirely (identity: `mappedL = l`).
|
|
926
|
-
|
|
927
|
-
| Color | Raw L | Mapped L (default [10, 100]) |
|
|
928
|
-
|---|---|---|
|
|
929
|
-
| surface (L=97) | 97 | 97.3 |
|
|
930
|
-
| accent-fill (L=52) | 52 | 56.8 |
|
|
931
|
-
| near-black (L=0) | 0 | 10 |
|
|
932
|
-
|
|
933
|
-
## Dark Scheme Mapping
|
|
934
|
-
|
|
935
|
-
### Lightness
|
|
936
|
-
|
|
937
|
-
**`auto`** — inverted with a Möbius transformation within the configured window:
|
|
938
|
-
|
|
939
|
-
```ts
|
|
940
|
-
const [lo, hi] = darkLightness; // default: [15, 95]
|
|
941
|
-
const t = (100 - lightness) / 100;
|
|
942
|
-
const invertedL = lo + (hi - lo) * t / (t + darkCurve * (1 - t)); // darkCurve default: 0.5
|
|
943
|
-
```
|
|
944
|
-
|
|
945
|
-
The `darkCurve` parameter (default `0.5`, range 0–1) controls how much the dark-mode inversion expands lightness deltas. Lower values produce stronger expansion; `1` gives linear (legacy) behavior. Accepts a `[normal, highContrast]` pair for separate HC tuning (e.g. `darkCurve: [0.5, 0.3]`); a single number applies to both. 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.
|
|
946
|
-
|
|
947
|
-
**`fixed`** — mapped without inversion (not affected by `darkCurve`):
|
|
948
|
-
|
|
949
|
-
```ts
|
|
950
|
-
const mappedL = (lightness * (hi - lo)) / 100 + lo;
|
|
951
|
-
```
|
|
952
|
-
|
|
953
|
-
| Color | Light L | Auto (curve=0.5) | Auto (curve=1, linear) | Fixed (mapped) |
|
|
954
|
-
|---|---|---|---|---|
|
|
955
|
-
| surface (L=97) | 97 | 19.7 | 17.4 | 92.6 |
|
|
956
|
-
| accent-fill (L=52) | 52 | 66.9 | 53.4 | 56.6 |
|
|
957
|
-
| accent-text (L=100) | 100 | 15 | 15 | 95 |
|
|
958
|
-
|
|
959
|
-
In high-contrast variants, the `darkLightness` window is bypassed — auto uses the Möbius curve over the full [0, 100] range, and fixed uses identity (`L`). To use a different curve shape for HC, pass a `[normal, hc]` pair to `darkCurve` (e.g. `darkCurve: [0.5, 0.3]`).
|
|
960
|
-
|
|
961
|
-
### Saturation
|
|
962
|
-
|
|
963
|
-
`darkDesaturation` reduces saturation for all colors in dark scheme:
|
|
964
|
-
|
|
965
|
-
```ts
|
|
966
|
-
S_dark = S_light * (1 - darkDesaturation) // default: 0.1
|
|
967
|
-
```
|
|
968
|
-
|
|
969
|
-
## Inherited Themes (`extend`)
|
|
970
|
-
|
|
971
|
-
`extend` creates a new theme inheriting all color definitions, replacing the hue and/or saturation seed:
|
|
972
|
-
|
|
973
|
-
```ts
|
|
974
|
-
const primary = glaze(280, 80);
|
|
975
|
-
primary.colors({ /* ... */ });
|
|
976
|
-
|
|
977
|
-
const danger = primary.extend({ hue: 23 });
|
|
978
|
-
const success = primary.extend({ hue: 157 });
|
|
979
|
-
const warning = primary.extend({ hue: 84 });
|
|
980
|
-
```
|
|
981
|
-
|
|
982
|
-
Override individual colors (additive merge):
|
|
983
|
-
|
|
984
|
-
```ts
|
|
985
|
-
const danger = primary.extend({
|
|
986
|
-
hue: 23,
|
|
987
|
-
colors: { 'accent-fill': { lightness: 48, mode: 'fixed' } },
|
|
988
|
-
});
|
|
989
|
-
```
|
|
990
|
-
|
|
991
|
-
## Palette Composition
|
|
992
|
-
|
|
993
|
-
Combine multiple themes into a single palette:
|
|
994
|
-
|
|
995
|
-
```ts
|
|
996
|
-
const palette = glaze.palette({ primary, danger, success, warning });
|
|
997
|
-
```
|
|
998
|
-
|
|
999
|
-
Optionally designate a primary theme at creation time:
|
|
1000
|
-
|
|
1001
|
-
```ts
|
|
1002
|
-
const palette = glaze.palette(
|
|
1003
|
-
{ primary, danger, success, warning },
|
|
1004
|
-
{ primary: 'primary' },
|
|
1005
|
-
);
|
|
1006
|
-
```
|
|
1007
|
-
|
|
1008
|
-
### Prefix Behavior
|
|
1009
|
-
|
|
1010
|
-
Palette export methods (`tokens()`, `tasty()`, `css()`) default to `prefix: true` — all tokens are automatically prefixed with the theme name to avoid collisions:
|
|
1011
|
-
|
|
1012
|
-
```ts
|
|
1013
|
-
const tokens = palette.tokens();
|
|
1014
|
-
// → {
|
|
1015
|
-
// light: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
|
|
1016
|
-
// dark: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
|
|
1017
|
-
// }
|
|
1018
|
-
```
|
|
1019
|
-
|
|
1020
|
-
Custom prefix mapping:
|
|
1021
|
-
|
|
1022
|
-
```ts
|
|
1023
|
-
palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
|
|
1024
|
-
```
|
|
1025
|
-
|
|
1026
|
-
To disable prefixing entirely, pass `prefix: false` explicitly.
|
|
1027
|
-
|
|
1028
|
-
### Collision Detection
|
|
1029
|
-
|
|
1030
|
-
When two themes produce the same output key (via `prefix: false`, custom prefix maps, or primary unprefixed aliases), the first-written value wins and a `console.warn` is emitted:
|
|
1031
|
-
|
|
1032
|
-
```ts
|
|
1033
|
-
const palette = glaze.palette({ a, b });
|
|
1034
|
-
palette.tokens({ prefix: false });
|
|
1035
|
-
// ⚠ glaze: token "surface" from theme "b" collides with theme "a" — skipping.
|
|
1036
|
-
```
|
|
1037
|
-
|
|
1038
|
-
### Primary Theme
|
|
1039
|
-
|
|
1040
|
-
The primary theme's tokens are duplicated without prefix, providing convenient short aliases alongside the prefixed versions. Set at palette creation to apply to all exports automatically:
|
|
1041
|
-
|
|
1042
|
-
```ts
|
|
1043
|
-
const palette = glaze.palette(
|
|
1044
|
-
{ primary, danger, success },
|
|
1045
|
-
{ primary: 'primary' },
|
|
1046
|
-
);
|
|
1047
|
-
const tokens = palette.tokens();
|
|
1048
|
-
// → {
|
|
1049
|
-
// light: {
|
|
1050
|
-
// 'primary-surface': 'okhsl(...)', // prefixed (all themes)
|
|
1051
|
-
// 'danger-surface': 'okhsl(...)',
|
|
1052
|
-
// 'success-surface': 'okhsl(...)',
|
|
1053
|
-
// 'surface': 'okhsl(...)', // unprefixed alias (primary only)
|
|
1054
|
-
// },
|
|
1055
|
-
// }
|
|
1056
|
-
```
|
|
1057
|
-
|
|
1058
|
-
Override or disable per-export:
|
|
1059
|
-
|
|
1060
|
-
```ts
|
|
1061
|
-
palette.tokens({ primary: 'danger' }); // use danger as primary for this call
|
|
1062
|
-
palette.tokens({ primary: false }); // no primary for this call
|
|
1063
|
-
```
|
|
1064
|
-
|
|
1065
|
-
The `primary` option works on `tokens()`, `tasty()`, and `css()`. It combines with any prefix mode — when using a custom prefix map, primary tokens are still duplicated without prefix:
|
|
1066
|
-
|
|
1067
|
-
```ts
|
|
1068
|
-
palette.tokens({ prefix: { primary: 'p-', danger: 'd-' } });
|
|
1069
|
-
// → 'p-surface' + 'surface' (alias from palette-level primary) + 'd-surface'
|
|
1070
|
-
```
|
|
1071
|
-
|
|
1072
|
-
An error is thrown if the primary name doesn't match any theme in the palette.
|
|
1073
|
-
|
|
1074
|
-
### Tasty Export (for [Tasty](https://tasty.style) style system)
|
|
1075
|
-
|
|
1076
|
-
The `tasty()` method exports tokens in the [Tasty](https://tasty.style/docs) style-to-state binding format — `#name` color token keys with state aliases (`''`, `@dark`, etc.). See the [Playground](https://tasty.style/playground) for live examples of Glaze integration:
|
|
1077
|
-
|
|
1078
|
-
```ts
|
|
1079
|
-
const palette = glaze.palette(
|
|
1080
|
-
{ primary, danger, success },
|
|
1081
|
-
{ primary: 'primary' },
|
|
1082
|
-
);
|
|
1083
|
-
const tastyTokens = palette.tasty();
|
|
1084
|
-
// → {
|
|
1085
|
-
// '#primary-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
|
|
1086
|
-
// '#danger-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
|
|
1087
|
-
// '#surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' }, // alias
|
|
1088
|
-
// }
|
|
1089
|
-
```
|
|
1090
|
-
|
|
1091
|
-
Apply as global styles to make color tokens available app-wide:
|
|
1092
|
-
|
|
1093
|
-
```ts
|
|
1094
|
-
import { useGlobalStyles } from '@cube-dev/ui-kit';
|
|
1095
|
-
|
|
1096
|
-
// In your root component
|
|
1097
|
-
useGlobalStyles('body', tastyTokens);
|
|
1098
|
-
```
|
|
1099
|
-
|
|
1100
|
-
For zero-runtime builds, use `tastyStatic` to generate the CSS at build time:
|
|
1101
|
-
|
|
1102
|
-
```ts
|
|
1103
|
-
import { tastyStatic } from '@cube-dev/ui-kit';
|
|
1104
|
-
|
|
1105
|
-
tastyStatic('body', tastyTokens);
|
|
1106
|
-
```
|
|
1107
|
-
|
|
1108
|
-
Alternatively, register as a recipe via `configure()`:
|
|
1109
|
-
|
|
1110
|
-
```ts
|
|
1111
|
-
import { configure, tasty } from '@cube-dev/ui-kit';
|
|
1112
|
-
|
|
1113
|
-
configure({
|
|
1114
|
-
recipes: {
|
|
1115
|
-
'all-themes': tastyTokens,
|
|
1116
|
-
},
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
|
-
const Page = tasty({
|
|
1120
|
-
styles: {
|
|
1121
|
-
recipe: 'all-themes',
|
|
1122
|
-
fill: '#primary-surface',
|
|
1123
|
-
color: '#primary-text',
|
|
1124
|
-
},
|
|
1125
|
-
});
|
|
1126
|
-
```
|
|
1127
|
-
|
|
1128
|
-
Or spread directly into component styles:
|
|
1129
|
-
|
|
1130
|
-
```ts
|
|
1131
|
-
const Card = tasty({
|
|
1132
|
-
styles: {
|
|
1133
|
-
...tastyTokens,
|
|
1134
|
-
fill: '#primary-surface',
|
|
1135
|
-
color: '#primary-text',
|
|
1136
|
-
},
|
|
1137
|
-
});
|
|
1138
|
-
```
|
|
1139
|
-
|
|
1140
|
-
Custom prefix mapping:
|
|
1141
|
-
|
|
1142
|
-
```ts
|
|
1143
|
-
palette.tasty({ prefix: { primary: 'brand-', danger: 'error-' } });
|
|
1144
|
-
```
|
|
1145
|
-
|
|
1146
|
-
Custom state aliases:
|
|
1147
|
-
|
|
1148
|
-
```ts
|
|
1149
|
-
palette.tasty({ states: { dark: '@dark', highContrast: '@hc' } });
|
|
1150
|
-
```
|
|
1151
|
-
|
|
1152
|
-
### JSON Export (Framework-Agnostic)
|
|
1153
|
-
|
|
1154
|
-
JSON export groups by theme name (no prefix needed):
|
|
1155
|
-
|
|
1156
|
-
```ts
|
|
1157
|
-
const data = palette.json();
|
|
1158
|
-
// → {
|
|
1159
|
-
// primary: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
|
|
1160
|
-
// danger: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
|
|
1161
|
-
// }
|
|
1162
|
-
```
|
|
1163
|
-
|
|
1164
|
-
### CSS Export
|
|
1165
|
-
|
|
1166
|
-
Export as CSS custom property declarations, grouped by scheme variant. Each variant is a string of `--name-color: value;` lines that you can wrap in your own selectors and media queries.
|
|
1167
|
-
|
|
1168
|
-
```ts
|
|
1169
|
-
const css = theme.css();
|
|
1170
|
-
// css.light → "--surface-color: rgb(...);\n--text-color: rgb(...);"
|
|
1171
|
-
// css.dark → "--surface-color: rgb(...);\n--text-color: rgb(...);"
|
|
1172
|
-
// css.lightContrast → "--surface-color: rgb(...);\n--text-color: rgb(...);"
|
|
1173
|
-
// css.darkContrast → "--surface-color: rgb(...);\n--text-color: rgb(...);"
|
|
1174
|
-
```
|
|
1175
|
-
|
|
1176
|
-
Use in a stylesheet:
|
|
1177
|
-
|
|
1178
|
-
```ts
|
|
1179
70
|
const palette = glaze.palette(
|
|
1180
71
|
{ primary, danger, success },
|
|
1181
72
|
{ primary: 'primary' },
|
|
1182
73
|
);
|
|
1183
|
-
const css = palette.css();
|
|
1184
|
-
|
|
1185
|
-
const stylesheet = `
|
|
1186
|
-
:root { ${css.light} }
|
|
1187
|
-
@media (prefers-color-scheme: dark) {
|
|
1188
|
-
:root { ${css.dark} }
|
|
1189
|
-
}
|
|
1190
|
-
`;
|
|
1191
|
-
```
|
|
1192
|
-
|
|
1193
|
-
Options:
|
|
1194
|
-
|
|
1195
|
-
| Option | Default | Description |
|
|
1196
|
-
|---|---|---|
|
|
1197
|
-
| `format` | `'rgb'` | Color format (`'rgb'`, `'hsl'`, `'okhsl'`, `'oklch'`) |
|
|
1198
|
-
| `suffix` | `'-color'` | Suffix appended to each CSS property name |
|
|
1199
|
-
| `prefix` | `true` (palette) | (palette only) `true` uses `"<themeName>-"`, or provide a custom map |
|
|
1200
|
-
| `primary` | inherited | (palette only) Override or disable (`false`) the palette-level primary for this call |
|
|
1201
|
-
|
|
1202
|
-
```ts
|
|
1203
|
-
// Custom suffix
|
|
1204
|
-
theme.css({ suffix: '' });
|
|
1205
|
-
// → "--surface: rgb(...);"
|
|
1206
|
-
|
|
1207
|
-
// Custom format
|
|
1208
|
-
theme.css({ format: 'hsl' });
|
|
1209
|
-
// → "--surface-color: hsl(...);"
|
|
1210
|
-
|
|
1211
|
-
// Palette with primary (inherited from palette creation)
|
|
1212
|
-
palette.css();
|
|
1213
|
-
// → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
|
|
1214
|
-
```
|
|
1215
|
-
|
|
1216
|
-
## Output Modes
|
|
1217
|
-
|
|
1218
|
-
Control which scheme variants appear in exports:
|
|
1219
|
-
|
|
1220
|
-
```ts
|
|
1221
|
-
// Light only
|
|
1222
|
-
palette.tokens({ modes: { dark: false, highContrast: false } });
|
|
1223
|
-
// → { light: { ... } }
|
|
1224
|
-
|
|
1225
|
-
// Light + dark (default)
|
|
1226
|
-
palette.tokens({ modes: { highContrast: false } });
|
|
1227
|
-
// → { light: { ... }, dark: { ... } }
|
|
1228
|
-
|
|
1229
|
-
// All four variants
|
|
1230
|
-
palette.tokens({ modes: { dark: true, highContrast: true } });
|
|
1231
|
-
// → { light: { ... }, dark: { ... }, lightContrast: { ... }, darkContrast: { ... } }
|
|
1232
|
-
```
|
|
1233
|
-
|
|
1234
|
-
The `modes` option works the same way on `tokens()`, `tasty()`, `json()`, and `css()`.
|
|
1235
|
-
|
|
1236
|
-
Resolution priority (highest first):
|
|
1237
|
-
|
|
1238
|
-
1. `tokens({ modes })` / `tasty({ modes })` / `json({ modes })` / `css({ ... })` — per-call override
|
|
1239
|
-
2. `glaze.configure({ modes })` — global config
|
|
1240
|
-
3. Built-in default: `{ dark: true, highContrast: false }`
|
|
1241
|
-
|
|
1242
|
-
## Configuration
|
|
1243
|
-
|
|
1244
|
-
```ts
|
|
1245
|
-
glaze.configure({
|
|
1246
|
-
lightLightness: [10, 100], // Light scheme lightness window [lo, hi] (bypassed in HC)
|
|
1247
|
-
darkLightness: [15, 95], // Dark scheme lightness window [lo, hi] (bypassed in HC)
|
|
1248
|
-
darkDesaturation: 0.1, // Saturation reduction in dark scheme (0–1)
|
|
1249
|
-
darkCurve: 0.5, // Möbius beta for dark auto-inversion (0–1); or [normal, hc] pair
|
|
1250
|
-
states: {
|
|
1251
|
-
dark: '@dark', // State alias for dark mode tokens
|
|
1252
|
-
highContrast: '@high-contrast',
|
|
1253
|
-
},
|
|
1254
|
-
modes: {
|
|
1255
|
-
dark: true, // Include dark variants in exports
|
|
1256
|
-
highContrast: false, // Include high-contrast variants
|
|
1257
|
-
},
|
|
1258
|
-
shadowTuning: { // Default tuning for all shadow colors
|
|
1259
|
-
alphaMax: 0.6,
|
|
1260
|
-
bgHueBlend: 0.2,
|
|
1261
|
-
},
|
|
1262
|
-
});
|
|
1263
|
-
```
|
|
1264
|
-
|
|
1265
|
-
## Color Definition Shape
|
|
1266
|
-
|
|
1267
|
-
`ColorDef` is a discriminated union of regular colors, shadow colors, and mix colors:
|
|
1268
|
-
|
|
1269
|
-
```ts
|
|
1270
|
-
type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
|
|
1271
|
-
|
|
1272
|
-
interface RegularColorDef {
|
|
1273
|
-
lightness?: HCPair<number | RelativeValue>;
|
|
1274
|
-
saturation?: number;
|
|
1275
|
-
hue?: number | RelativeValue;
|
|
1276
|
-
base?: string;
|
|
1277
|
-
contrast?: HCPair<MinContrast>;
|
|
1278
|
-
mode?: 'auto' | 'fixed' | 'static';
|
|
1279
|
-
opacity?: number; // fixed alpha (0–1)
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
interface ShadowColorDef {
|
|
1283
|
-
type: 'shadow';
|
|
1284
|
-
bg: string; // background color name (non-shadow)
|
|
1285
|
-
fg?: string; // foreground color name (non-shadow)
|
|
1286
|
-
intensity: HCPair<number>; // 0–100
|
|
1287
|
-
tuning?: ShadowTuning;
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
interface MixColorDef {
|
|
1291
|
-
type: 'mix';
|
|
1292
|
-
base: string; // "from" color name
|
|
1293
|
-
target: string; // "to" color name
|
|
1294
|
-
value: HCPair<number>; // 0–100 (mix ratio or opacity)
|
|
1295
|
-
blend?: 'opaque' | 'transparent'; // default: 'opaque'
|
|
1296
|
-
space?: 'okhsl' | 'srgb'; // default: 'okhsl'
|
|
1297
|
-
contrast?: HCPair<MinContrast>;
|
|
1298
|
-
}
|
|
1299
|
-
```
|
|
1300
|
-
|
|
1301
|
-
A root color must have absolute `lightness` (a number). A dependent color must have `base`. Relative `lightness` (a string) requires `base`. Shadow colors use `type: 'shadow'` and must reference a non-shadow `bg` color. Mix colors use `type: 'mix'` and must reference two non-shadow colors.
|
|
1302
|
-
|
|
1303
|
-
## Validation
|
|
1304
|
-
|
|
1305
|
-
| Condition | Behavior |
|
|
1306
|
-
|---|---|
|
|
1307
|
-
| `contrast` without `base` | Validation error |
|
|
1308
|
-
| Relative `lightness` without `base` | Validation error |
|
|
1309
|
-
| `lightness` resolves outside 0–100 | Clamp silently |
|
|
1310
|
-
| `saturation` outside 0–1 | Clamp silently |
|
|
1311
|
-
| Circular `base` references | Validation error |
|
|
1312
|
-
| `base` references non-existent name | Validation error |
|
|
1313
|
-
| Shadow `bg` references non-existent color | Validation error |
|
|
1314
|
-
| Shadow `fg` references non-existent color | Validation error |
|
|
1315
|
-
| Shadow `bg` references another shadow color | Validation error |
|
|
1316
|
-
| Shadow `fg` references another shadow color | Validation error |
|
|
1317
|
-
| Regular color `base` references a shadow color | Validation error |
|
|
1318
|
-
| Shadow `intensity` outside 0–100 | Clamp silently |
|
|
1319
|
-
| `contrast` + `opacity` combined | Warning |
|
|
1320
|
-
| Mix `base` references non-existent color | Validation error |
|
|
1321
|
-
| Mix `target` references non-existent color | Validation error |
|
|
1322
|
-
| Mix `base` references a shadow color | Validation error |
|
|
1323
|
-
| Mix `target` references a shadow color | Validation error |
|
|
1324
|
-
| Mix `value` outside 0–100 | Clamp silently |
|
|
1325
|
-
| Circular references involving mix colors | Validation error |
|
|
1326
|
-
|
|
1327
|
-
## Advanced: Color Math Utilities
|
|
1328
|
-
|
|
1329
|
-
Glaze re-exports its internal color math for advanced use:
|
|
1330
|
-
|
|
1331
|
-
```ts
|
|
1332
|
-
import {
|
|
1333
|
-
okhslToLinearSrgb,
|
|
1334
|
-
okhslToSrgb,
|
|
1335
|
-
okhslToOklab,
|
|
1336
|
-
srgbToOkhsl,
|
|
1337
|
-
parseHex,
|
|
1338
|
-
relativeLuminanceFromLinearRgb,
|
|
1339
|
-
contrastRatioFromLuminance,
|
|
1340
|
-
formatOkhsl,
|
|
1341
|
-
formatRgb,
|
|
1342
|
-
formatHsl,
|
|
1343
|
-
formatOklch,
|
|
1344
|
-
findLightnessForContrast,
|
|
1345
|
-
resolveMinContrast,
|
|
1346
|
-
} from '@tenphi/glaze';
|
|
1347
|
-
```
|
|
1348
|
-
|
|
1349
|
-
## Full Example
|
|
1350
|
-
|
|
1351
|
-
```ts
|
|
1352
|
-
import { glaze } from '@tenphi/glaze';
|
|
1353
|
-
|
|
1354
|
-
const primary = glaze(280, 80);
|
|
1355
|
-
|
|
1356
|
-
primary.colors({
|
|
1357
|
-
surface: { lightness: 97, saturation: 0.75 },
|
|
1358
|
-
text: { base: 'surface', lightness: '-52', contrast: 'AAA' },
|
|
1359
|
-
border: { base: 'surface', lightness: ['-7', '-20'], contrast: 'AA-large' },
|
|
1360
|
-
bg: { lightness: 97, saturation: 0.75 },
|
|
1361
|
-
icon: { lightness: 60, saturation: 0.94 },
|
|
1362
|
-
'accent-fill': { lightness: 52, mode: 'fixed' },
|
|
1363
|
-
'accent-text': { base: 'accent-fill', lightness: '+48', contrast: 'AA', mode: 'fixed' },
|
|
1364
|
-
disabled: { lightness: 81, saturation: 0.4 },
|
|
1365
|
-
|
|
1366
|
-
// Shadow colors — computed alpha, automatic dark-mode adaptation
|
|
1367
|
-
'shadow-sm': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 5 },
|
|
1368
|
-
'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
|
|
1369
|
-
'shadow-lg': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 20 },
|
|
1370
|
-
|
|
1371
|
-
// Mix colors — hover overlays and tints
|
|
1372
|
-
'hover': { type: 'mix', base: 'surface', target: 'accent-fill', value: 8, blend: 'transparent' },
|
|
1373
|
-
'tint': { type: 'mix', base: 'surface', target: 'accent-fill', value: 20 },
|
|
1374
|
-
|
|
1375
|
-
// Fixed-alpha overlay
|
|
1376
|
-
overlay: { lightness: 0, opacity: 0.5 },
|
|
1377
|
-
});
|
|
1378
|
-
|
|
1379
|
-
const danger = primary.extend({ hue: 23 });
|
|
1380
|
-
const success = primary.extend({ hue: 157 });
|
|
1381
|
-
const warning = primary.extend({ hue: 84 });
|
|
1382
|
-
const note = primary.extend({ hue: 302 });
|
|
1383
|
-
|
|
1384
|
-
const palette = glaze.palette(
|
|
1385
|
-
{ primary, danger, success, warning, note },
|
|
1386
|
-
{ primary: 'primary' },
|
|
1387
|
-
);
|
|
1388
74
|
|
|
1389
|
-
// Export as flat token map grouped by variant (prefix defaults to true)
|
|
1390
75
|
const tokens = palette.tokens();
|
|
1391
|
-
//
|
|
1392
|
-
|
|
1393
|
-
// Export as tasty style-to-state bindings (for Tasty style system)
|
|
1394
|
-
const tastyTokens = palette.tasty();
|
|
1395
|
-
|
|
1396
|
-
// Export as CSS custom properties (rgb format by default)
|
|
1397
|
-
const css = palette.css();
|
|
1398
|
-
// css.light → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
|
|
1399
|
-
|
|
1400
|
-
// Standalone shadow computation
|
|
1401
|
-
const v = glaze.shadow({ bg: '#f0eef5', fg: '#1a1a2e', intensity: 10 });
|
|
1402
|
-
const shadowCss = glaze.format(v, 'oklch');
|
|
1403
|
-
// → 'oklch(0.15 0.014 280 / 0.1)'
|
|
1404
|
-
|
|
1405
|
-
// Save and restore a theme
|
|
1406
|
-
const snapshot = primary.export();
|
|
1407
|
-
const restored = glaze.from(snapshot);
|
|
1408
|
-
|
|
1409
|
-
// Create from an existing brand color
|
|
1410
|
-
const brand = glaze.fromHex('#7a4dbf');
|
|
1411
|
-
brand.colors({ surface: { lightness: 97 }, text: { base: 'surface', lightness: '-52' } });
|
|
76
|
+
// → { light: { 'primary-surface': 'okhsl(...)', 'surface': 'okhsl(...)', ... },
|
|
77
|
+
// dark: { 'primary-surface': 'okhsl(...)', 'surface': 'okhsl(...)', ... } }
|
|
1412
78
|
```
|
|
1413
79
|
|
|
1414
|
-
##
|
|
1415
|
-
|
|
1416
|
-
### Theme Creation
|
|
1417
|
-
|
|
1418
|
-
| Method | Description |
|
|
1419
|
-
|---|---|
|
|
1420
|
-
| `glaze(hue, saturation?)` | Create a theme from hue (0–360) and saturation (0–100) |
|
|
1421
|
-
| `glaze({ hue, saturation })` | Create a theme from an options object |
|
|
1422
|
-
| `glaze.from(data)` | Create a theme from an exported configuration |
|
|
1423
|
-
| `glaze.fromHex(hex)` | Create a theme from a hex color (`#rgb` or `#rrggbb`) |
|
|
1424
|
-
| `glaze.fromRgb(r, g, b)` | Create a theme from RGB values (0–255) |
|
|
1425
|
-
| `glaze.color(input, scaling?)` | Create a standalone color token from `{ hue, saturation, lightness, opacity?, contrast?, base?, name?, ... }`. Optional `scaling` overrides the lightness windows |
|
|
1426
|
-
| `glaze.color(value, overrides?, scaling?)` | Create a standalone color token from a hex string (3/6/8 digits), an `rgb()` / `hsl()` / `okhsl()` / `oklch()` string, an `{ h, s, l }` OKHSL object, or an `[r, g, b]` (0–255) tuple. Overrides accept absolute or relative `hue` / `lightness`, `saturation`, `mode`, `contrast`, `opacity`, `name`, and `base` (a `GlazeColorToken` or any `GlazeColorValue`; raw values are auto-wrapped). When `base` is set, `contrast` and relative `lightness` are anchored to the base per scheme — see [Pairing Colors](#pairing-colors). String inputs default to `mode: 'auto'` with the dark window extended to upper `100`; object / tuple inputs default to `mode: 'fixed'`. |
|
|
1427
|
-
| `glaze.colorFrom(data)` | Rehydrate a `glaze.color()` token from a `.export()` snapshot. Inverse of `token.export()` — see [Persisting Standalone Colors](#persisting-standalone-colors) |
|
|
1428
|
-
| `glaze.shadow(input)` | Compute a standalone shadow color (returns `ResolvedColorVariant`). `bg` / `fg` accept any `GlazeColorValue` form |
|
|
1429
|
-
| `glaze.format(variant, format?)` | Format any `ResolvedColorVariant` as a CSS string |
|
|
1430
|
-
|
|
1431
|
-
### Theme Methods
|
|
80
|
+
## Concepts at a glance
|
|
1432
81
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
| `theme.color(name, def)` | Set a single color definition |
|
|
1438
|
-
| `theme.remove(names)` | Remove one or more colors |
|
|
1439
|
-
| `theme.has(name)` | Check if a color is defined |
|
|
1440
|
-
| `theme.list()` | List all defined color names |
|
|
1441
|
-
| `theme.reset()` | Clear all color definitions |
|
|
1442
|
-
| `theme.export()` | Export configuration as JSON-safe object |
|
|
1443
|
-
| `theme.extend(options)` | Create a child theme |
|
|
1444
|
-
| `theme.resolve()` | Resolve all colors |
|
|
1445
|
-
| `theme.tokens(options?)` | Export as flat token map grouped by variant |
|
|
1446
|
-
| `theme.tasty(options?)` | Export as [Tasty](https://tasty.style/docs) style-to-state bindings |
|
|
1447
|
-
| `theme.json(options?)` | Export as plain JSON |
|
|
1448
|
-
| `theme.css(options?)` | Export as CSS custom property declarations |
|
|
82
|
+
1. **Theme = one hue/saturation seed.** Status themes are siblings created via `extend()` — they inherit every color definition and only swap the seed.
|
|
83
|
+
2. **Every color is explicit.** A color is either a *root* (absolute `lightness`) or *dependent* (`base` + relative offset and/or `contrast`). No implicit roles.
|
|
84
|
+
3. **WCAG `contrast` is a floor, not a target.** The solver only widens a color's lightness when the requested position fails the requested ratio.
|
|
85
|
+
4. **Light, dark, and high-contrast come from one definition.** `mode` (`auto` / `fixed` / `static`) picks how each color adapts; `lightness` / `contrast` / `intensity` / `value` accept an optional `[normal, hc]` pair for explicit high-contrast tuning.
|
|
1449
86
|
|
|
1450
|
-
|
|
87
|
+
## Documentation
|
|
1451
88
|
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
| `glaze.getConfig()` | Get current global config |
|
|
1457
|
-
| `glaze.resetConfig()` | Reset to defaults |
|
|
89
|
+
- [`docs/api.md`](docs/api.md) — full API reference (every method, every option).
|
|
90
|
+
- [`docs/methodology.md`](docs/methodology.md) — palette design methodology for building from scratch.
|
|
91
|
+
- [`docs/migration.md`](docs/migration.md) — wiring tokens into your app and migrating off a legacy color system.
|
|
92
|
+
- [`AGENTS.md`](AGENTS.md) — source-tree orientation for contributors.
|
|
1458
93
|
|
|
1459
94
|
## License
|
|
1460
95
|
|