@tenphi/glaze 0.1.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/LICENSE +21 -0
- package/README.md +582 -0
- package/dist/index.cjs +1196 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +298 -0
- package/dist/index.d.mts +298 -0
- package/dist/index.mjs +1181 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +82 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrey Yamanov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/glaze.svg" width="128" height="128" alt="Glaze logo">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Glaze</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
OKHSL-based color theme generator with WCAG contrast solving
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@tenphi/glaze"><img src="https://img.shields.io/npm/v/@tenphi/glaze.svg" alt="npm version"></a>
|
|
13
|
+
<a href="https://github.com/tenphi/glaze/actions/workflows/ci.yml"><img src="https://github.com/tenphi/glaze/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
14
|
+
<a href="https://github.com/tenphi/glaze/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@tenphi/glaze.svg" alt="license"></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
Glaze generates robust **light**, **dark**, and **high-contrast** color schemes from a single hue/saturation seed. It preserves WCAG contrast ratios for UI color pairs via explicit dependency declarations — no hidden role math, no magic multipliers.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- **OKHSL color space** — perceptually uniform hue and saturation
|
|
24
|
+
- **WCAG 2 contrast solving** — automatic lightness adjustment to meet AA/AAA targets
|
|
25
|
+
- **Light + Dark + High-Contrast** — all schemes from one definition
|
|
26
|
+
- **Multi-format output** — `okhsl`, `rgb`, `hsl`, `oklch`
|
|
27
|
+
- **Import/Export** — serialize and restore theme configurations
|
|
28
|
+
- **Create from hex/RGB** — start from an existing brand color
|
|
29
|
+
- **Zero dependencies** — pure math, runs anywhere (Node.js, browser, edge)
|
|
30
|
+
- **Tree-shakeable ESM + CJS** — dual-format package
|
|
31
|
+
- **TypeScript-first** — full type definitions included
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pnpm add @tenphi/glaze
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install @tenphi/glaze
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
yarn add @tenphi/glaze
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { glaze } from '@tenphi/glaze';
|
|
51
|
+
|
|
52
|
+
// Create a theme from a hue (0–360) and saturation (0–100)
|
|
53
|
+
const primary = glaze(280, 80);
|
|
54
|
+
|
|
55
|
+
// Define colors with explicit lightness and contrast relationships
|
|
56
|
+
primary.colors({
|
|
57
|
+
surface: { l: 97, sat: 0.75 },
|
|
58
|
+
text: { base: 'surface', contrast: 52, ensureContrast: 'AAA' },
|
|
59
|
+
border: { base: 'surface', contrast: [7, 20], ensureContrast: 'AA-large' },
|
|
60
|
+
'accent-fill': { l: 52, mode: 'fixed' },
|
|
61
|
+
'accent-text': { base: 'accent-fill', contrast: 48, ensureContrast: 'AA', mode: 'fixed' },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Create status themes by rotating the hue
|
|
65
|
+
const danger = primary.extend({ hue: 23 });
|
|
66
|
+
const success = primary.extend({ hue: 157 });
|
|
67
|
+
|
|
68
|
+
// Compose into a palette and export
|
|
69
|
+
const palette = glaze.palette({ primary, danger, success });
|
|
70
|
+
const tokens = palette.tokens({ prefix: true });
|
|
71
|
+
// → { '#primary-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' }, ... }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Core Concepts
|
|
75
|
+
|
|
76
|
+
### One Theme = One Hue Family
|
|
77
|
+
|
|
78
|
+
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.
|
|
79
|
+
|
|
80
|
+
### Color Definitions
|
|
81
|
+
|
|
82
|
+
Every color is defined explicitly. No implicit roles — every value is stated.
|
|
83
|
+
|
|
84
|
+
#### Root Colors (explicit position)
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
primary.colors({
|
|
88
|
+
surface: { l: 97, sat: 0.75 },
|
|
89
|
+
border: { l: 90, sat: 0.20 },
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- `l` — lightness in the light scheme (0–100)
|
|
94
|
+
- `sat` — saturation factor applied to the seed saturation (0–1, default: `1`)
|
|
95
|
+
|
|
96
|
+
#### Dependent Colors (relative to base)
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
primary.colors({
|
|
100
|
+
surface: { l: 97, sat: 0.75 },
|
|
101
|
+
text: { base: 'surface', contrast: 52, ensureContrast: 'AAA' },
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- `base` — name of another color in the same theme
|
|
106
|
+
- `contrast` — lightness delta from the base color
|
|
107
|
+
- `ensureContrast` — ensures the WCAG contrast ratio meets a target floor against the base
|
|
108
|
+
|
|
109
|
+
Both `contrast` and `ensureContrast` are considered. The effective lightness satisfies both constraints — the more demanding one wins.
|
|
110
|
+
|
|
111
|
+
### Contrast Sign Convention
|
|
112
|
+
|
|
113
|
+
| Sign | Behavior |
|
|
114
|
+
|---|---|
|
|
115
|
+
| Negative (`-52`) | Always darker than base |
|
|
116
|
+
| Positive (`+48`) | Always lighter than base |
|
|
117
|
+
| Unsigned (`52`) | Auto-resolved: if `base_L + contrast > 100`, flips to negative |
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
// Surface L=97
|
|
121
|
+
'text': { base: 'surface', contrast: 52 }
|
|
122
|
+
// → 97 + 52 = 149 > 100 → flips to 97 - 52 = 45 ✓
|
|
123
|
+
|
|
124
|
+
// Button fill L=52
|
|
125
|
+
'accent-text': { base: 'accent-fill', contrast: 48 }
|
|
126
|
+
// → 52 + 48 = 100 → keeps as 100 ✓
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### ensureContrast (WCAG Floor)
|
|
130
|
+
|
|
131
|
+
Ensures the WCAG contrast ratio meets a target floor. Accepts a numeric ratio or a preset string:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
type MinContrast = number | 'AA' | 'AAA' | 'AA-large' | 'AAA-large';
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
| Preset | Ratio |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `'AA'` | 4.5 |
|
|
140
|
+
| `'AAA'` | 7 |
|
|
141
|
+
| `'AA-large'` | 3 |
|
|
142
|
+
| `'AAA-large'` | 4.5 |
|
|
143
|
+
|
|
144
|
+
You can also pass any numeric ratio directly (e.g., `ensureContrast: 4.5`, `ensureContrast: 7`, `ensureContrast: 11`).
|
|
145
|
+
|
|
146
|
+
The constraint is applied independently for each scheme. If the `contrast` delta already satisfies the floor, it's kept. Otherwise, the solver adjusts lightness until the target is met.
|
|
147
|
+
|
|
148
|
+
### High-Contrast via Array Values
|
|
149
|
+
|
|
150
|
+
`contrast`, `ensureContrast`, and `l` accept a `[normal, high-contrast]` pair:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
'border': { base: 'surface', contrast: [7, 20], ensureContrast: 'AA-large' }
|
|
154
|
+
// ↑ ↑
|
|
155
|
+
// normal high-contrast
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
A single value applies to both modes. All control is local and explicit.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
'text': { base: 'surface', contrast: 52, ensureContrast: 'AAA' }
|
|
162
|
+
'border': { base: 'surface', contrast: [7, 20], ensureContrast: 'AA-large' }
|
|
163
|
+
'muted': { base: 'surface', contrast: [35, 50], ensureContrast: ['AA-large', 'AA'] }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Theme Color Management
|
|
167
|
+
|
|
168
|
+
### Adding Colors
|
|
169
|
+
|
|
170
|
+
`.colors(defs)` performs an **additive merge** — it adds new colors and overwrites existing ones by name, but does not remove other colors:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const theme = glaze(280, 80);
|
|
174
|
+
theme.colors({ surface: { l: 97 } });
|
|
175
|
+
theme.colors({ text: { l: 30 } });
|
|
176
|
+
// Both 'surface' and 'text' are now defined
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Single Color Getter/Setter
|
|
180
|
+
|
|
181
|
+
`.color(name)` returns the definition, `.color(name, def)` sets it:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
theme.color('surface', { l: 97, sat: 0.75 }); // set
|
|
185
|
+
const def = theme.color('surface'); // get → { l: 97, sat: 0.75 }
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Removing Colors
|
|
189
|
+
|
|
190
|
+
`.remove(name)` or `.remove([name1, name2])` deletes color definitions:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
theme.remove('surface');
|
|
194
|
+
theme.remove(['text', 'border']);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Introspection
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
theme.has('surface'); // → true/false
|
|
201
|
+
theme.list(); // → ['surface', 'text', 'border', ...]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Clearing All Colors
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
theme.reset(); // removes all color definitions
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Import / Export
|
|
211
|
+
|
|
212
|
+
Serialize a theme's configuration (hue, saturation, color definitions) to a plain JSON-safe object, and restore it later:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// Export
|
|
216
|
+
const snapshot = theme.export();
|
|
217
|
+
// → { hue: 280, saturation: 80, colors: { surface: { l: 97, sat: 0.75 }, ... } }
|
|
218
|
+
|
|
219
|
+
const jsonString = JSON.stringify(snapshot);
|
|
220
|
+
|
|
221
|
+
// Import
|
|
222
|
+
const restored = glaze.from(JSON.parse(jsonString));
|
|
223
|
+
// restored is a fully functional GlazeTheme
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The export contains only the configuration — not resolved color values. Resolved values are recomputed on demand.
|
|
227
|
+
|
|
228
|
+
## Standalone Color Token
|
|
229
|
+
|
|
230
|
+
Create a single color token without a full theme:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
const accent = glaze.color({ hue: 280, saturation: 80, l: 52, mode: 'fixed' });
|
|
234
|
+
|
|
235
|
+
accent.resolve(); // → ResolvedColor with light/dark/lightContrast/darkContrast
|
|
236
|
+
accent.token(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' }
|
|
237
|
+
accent.json(); // → { light: 'okhsl(...)', dark: 'okhsl(...)' }
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Standalone colors are always root colors (no `base`/`contrast`/`ensureContrast`).
|
|
241
|
+
|
|
242
|
+
## From Existing Colors
|
|
243
|
+
|
|
244
|
+
Create a theme from an existing brand color by extracting its OKHSL hue and saturation:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
// From hex
|
|
248
|
+
const brand = glaze.fromHex('#7a4dbf');
|
|
249
|
+
|
|
250
|
+
// From RGB (0–255)
|
|
251
|
+
const brand = glaze.fromRgb(122, 77, 191);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The resulting theme has the extracted hue and saturation. Add colors as usual:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
brand.colors({
|
|
258
|
+
surface: { l: 97, sat: 0.75 },
|
|
259
|
+
text: { base: 'surface', contrast: 52, ensureContrast: 'AAA' },
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Output Formats
|
|
264
|
+
|
|
265
|
+
Control the color format in exports with the `format` option:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
// Default: OKHSL
|
|
269
|
+
theme.tokens(); // → 'okhsl(280.0 60.0% 97.0%)'
|
|
270
|
+
|
|
271
|
+
// RGB with fractional precision
|
|
272
|
+
theme.tokens({ format: 'rgb' }); // → 'rgb(244.123, 240.456, 249.789)'
|
|
273
|
+
|
|
274
|
+
// HSL
|
|
275
|
+
theme.tokens({ format: 'hsl' }); // → 'hsl(270.5, 45.2%, 95.8%)'
|
|
276
|
+
|
|
277
|
+
// OKLCH
|
|
278
|
+
theme.tokens({ format: 'oklch' }); // → 'oklch(96.5% 0.0123 280.0)'
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The `format` option works on all export methods: `theme.tokens()`, `theme.json()`, `palette.tokens()`, `palette.json()`, and standalone `glaze.color().token()` / `.json()`.
|
|
282
|
+
|
|
283
|
+
Available formats:
|
|
284
|
+
|
|
285
|
+
| Format | Output | Notes |
|
|
286
|
+
|---|---|---|
|
|
287
|
+
| `'okhsl'` (default) | `okhsl(H S% L%)` | Native format, perceptually uniform |
|
|
288
|
+
| `'rgb'` | `rgb(R, G, B)` | Fractional 0–255 values (3 decimals) |
|
|
289
|
+
| `'hsl'` | `hsl(H, S%, L%)` | Standard CSS HSL |
|
|
290
|
+
| `'oklch'` | `oklch(L% C H)` | OKLab-based LCH |
|
|
291
|
+
|
|
292
|
+
## Adaptation Modes
|
|
293
|
+
|
|
294
|
+
Modes control how colors adapt across schemes:
|
|
295
|
+
|
|
296
|
+
| Mode | Behavior |
|
|
297
|
+
|---|---|
|
|
298
|
+
| `'auto'` (default) | Full adaptation. Light ↔ dark inversion. High-contrast boost. |
|
|
299
|
+
| `'fixed'` | Color stays recognizable. Only safety corrections. For brand buttons, CTAs. |
|
|
300
|
+
| `'static'` | No adaptation. Same value in every scheme. |
|
|
301
|
+
|
|
302
|
+
### How `contrast` Adapts
|
|
303
|
+
|
|
304
|
+
**`auto` mode** — contrast sign flips in dark scheme:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
// Light: surface L=97, text contrast=52 → L=45 (dark text on light bg)
|
|
308
|
+
// Dark: surface inverts to L≈14, sign flips → L=14+52=66
|
|
309
|
+
// ensureContrast solver may push further (light text on dark bg)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**`fixed` mode** — lightness is mapped (not inverted), contrast sign preserved:
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
// Light: accent-fill L=52, accent-text contrast=+48 → L=100 (white on brand)
|
|
316
|
+
// Dark: accent-fill maps to L≈51.6, sign preserved → L≈99.6
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**`static` mode** — no adaptation, same value in every scheme.
|
|
320
|
+
|
|
321
|
+
## Dark Scheme Mapping
|
|
322
|
+
|
|
323
|
+
### Lightness
|
|
324
|
+
|
|
325
|
+
**`auto`** — inverted within the configured window:
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
const [lo, hi] = darkLightness; // default: [10, 90]
|
|
329
|
+
const invertedL = ((100 - lightness) * (hi - lo)) / 100 + lo;
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**`fixed`** — mapped without inversion:
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
const mappedL = (lightness * (hi - lo)) / 100 + lo;
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
| Color | Light L | Auto (inverted) | Fixed (mapped) |
|
|
339
|
+
|---|---|---|---|
|
|
340
|
+
| surface (L=97) | 97 | 12.4 | 87.6 |
|
|
341
|
+
| accent-fill (L=52) | 52 | 48.4 | 51.6 |
|
|
342
|
+
| accent-text (L=100) | 100 | 10 | 90 |
|
|
343
|
+
|
|
344
|
+
### Saturation
|
|
345
|
+
|
|
346
|
+
`darkDesaturation` reduces saturation for all colors in dark scheme:
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
S_dark = S_light * (1 - darkDesaturation) // default: 0.1
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Inherited Themes (`extend`)
|
|
353
|
+
|
|
354
|
+
`extend` creates a new theme inheriting all color definitions, replacing the hue and/or saturation seed:
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
const primary = glaze(280, 80);
|
|
358
|
+
primary.colors({ /* ... */ });
|
|
359
|
+
|
|
360
|
+
const danger = primary.extend({ hue: 23 });
|
|
361
|
+
const success = primary.extend({ hue: 157 });
|
|
362
|
+
const warning = primary.extend({ hue: 84 });
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Override individual colors (additive merge):
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
const danger = primary.extend({
|
|
369
|
+
hue: 23,
|
|
370
|
+
colors: { 'accent-fill': { l: 48, mode: 'fixed' } },
|
|
371
|
+
});
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## Palette Composition
|
|
375
|
+
|
|
376
|
+
Combine multiple themes into a single palette:
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
const palette = glaze.palette({ primary, danger, success, warning });
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Token Export
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
const tokens = palette.tokens({ prefix: true });
|
|
386
|
+
// → {
|
|
387
|
+
// '#primary-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
|
|
388
|
+
// '#danger-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
|
|
389
|
+
// }
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Custom prefix mapping:
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### JSON Export (Framework-Agnostic)
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
const data = palette.json({ prefix: true });
|
|
402
|
+
// → {
|
|
403
|
+
// primary: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
|
|
404
|
+
// danger: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
|
|
405
|
+
// }
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Output Modes
|
|
409
|
+
|
|
410
|
+
Control which scheme variants appear in exports:
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
// Light only
|
|
414
|
+
palette.tokens({ modes: { dark: false, highContrast: false } });
|
|
415
|
+
|
|
416
|
+
// Light + dark (default)
|
|
417
|
+
palette.tokens({ modes: { highContrast: false } });
|
|
418
|
+
|
|
419
|
+
// All four variants
|
|
420
|
+
palette.tokens({ modes: { dark: true, highContrast: true } });
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Resolution priority (highest first):
|
|
424
|
+
|
|
425
|
+
1. `tokens({ modes })` / `json({ modes })` — per-call override
|
|
426
|
+
2. `glaze.configure({ modes })` — global config
|
|
427
|
+
3. Built-in default: `{ dark: true, highContrast: false }`
|
|
428
|
+
|
|
429
|
+
## Configuration
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
glaze.configure({
|
|
433
|
+
darkLightness: [10, 90], // Dark scheme lightness window [lo, hi]
|
|
434
|
+
darkDesaturation: 0.1, // Saturation reduction in dark scheme (0–1)
|
|
435
|
+
states: {
|
|
436
|
+
dark: '@dark', // State alias for dark mode tokens
|
|
437
|
+
highContrast: '@high-contrast',
|
|
438
|
+
},
|
|
439
|
+
modes: {
|
|
440
|
+
dark: true, // Include dark variants in exports
|
|
441
|
+
highContrast: false, // Include high-contrast variants
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
## Color Definition Shape
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
type HCPair<T> = T | [T, T]; // [normal, high-contrast]
|
|
450
|
+
|
|
451
|
+
interface ColorDef {
|
|
452
|
+
// Root color (explicit position)
|
|
453
|
+
l?: HCPair<number>; // 0–100, light scheme lightness
|
|
454
|
+
sat?: number; // 0–1, saturation factor (default: 1)
|
|
455
|
+
|
|
456
|
+
// Dependent color (relative to base)
|
|
457
|
+
base?: string; // name of another color
|
|
458
|
+
contrast?: HCPair<number>; // lightness delta from base
|
|
459
|
+
ensureContrast?: HCPair<MinContrast>; // ensures WCAG contrast ratio meets target floor
|
|
460
|
+
|
|
461
|
+
// Adaptation mode
|
|
462
|
+
mode?: 'auto' | 'fixed' | 'static'; // default: 'auto'
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Every color must have either `l` (root) or `base` + `contrast` (dependent).
|
|
467
|
+
|
|
468
|
+
## Validation
|
|
469
|
+
|
|
470
|
+
| Condition | Behavior |
|
|
471
|
+
|---|---|
|
|
472
|
+
| Both `l` and `base` on same color | Warning, `l` takes precedence |
|
|
473
|
+
| `contrast` without `base` | Validation error |
|
|
474
|
+
| `l` resolves outside 0–100 | Clamp silently |
|
|
475
|
+
| `sat` outside 0–1 | Clamp silently |
|
|
476
|
+
| Circular `base` references | Validation error |
|
|
477
|
+
| `base` references non-existent name | Validation error |
|
|
478
|
+
|
|
479
|
+
## Advanced: Color Math Utilities
|
|
480
|
+
|
|
481
|
+
Glaze re-exports its internal color math for advanced use:
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
import {
|
|
485
|
+
okhslToLinearSrgb,
|
|
486
|
+
okhslToSrgb,
|
|
487
|
+
okhslToOklab,
|
|
488
|
+
srgbToOkhsl,
|
|
489
|
+
parseHex,
|
|
490
|
+
relativeLuminanceFromLinearRgb,
|
|
491
|
+
contrastRatioFromLuminance,
|
|
492
|
+
formatOkhsl,
|
|
493
|
+
formatRgb,
|
|
494
|
+
formatHsl,
|
|
495
|
+
formatOklch,
|
|
496
|
+
findLightnessForContrast,
|
|
497
|
+
resolveMinContrast,
|
|
498
|
+
} from '@tenphi/glaze';
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
## Full Example
|
|
502
|
+
|
|
503
|
+
```ts
|
|
504
|
+
import { glaze } from '@tenphi/glaze';
|
|
505
|
+
|
|
506
|
+
const primary = glaze(280, 80);
|
|
507
|
+
|
|
508
|
+
primary.colors({
|
|
509
|
+
surface: { l: 97, sat: 0.75 },
|
|
510
|
+
text: { base: 'surface', contrast: 52, ensureContrast: 'AAA' },
|
|
511
|
+
border: { base: 'surface', contrast: [7, 20], ensureContrast: 'AA-large' },
|
|
512
|
+
bg: { l: 97, sat: 0.75 },
|
|
513
|
+
icon: { l: 60, sat: 0.94 },
|
|
514
|
+
'accent-fill': { l: 52, mode: 'fixed' },
|
|
515
|
+
'accent-text': { base: 'accent-fill', contrast: 48, ensureContrast: 'AA', mode: 'fixed' },
|
|
516
|
+
disabled: { l: 81, sat: 0.40 },
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const danger = primary.extend({ hue: 23 });
|
|
520
|
+
const success = primary.extend({ hue: 157 });
|
|
521
|
+
const warning = primary.extend({ hue: 84 });
|
|
522
|
+
const note = primary.extend({ hue: 302 });
|
|
523
|
+
|
|
524
|
+
const palette = glaze.palette({ primary, danger, success, warning, note });
|
|
525
|
+
|
|
526
|
+
// Export as OKHSL tokens (default)
|
|
527
|
+
const tokens = palette.tokens({ prefix: true });
|
|
528
|
+
|
|
529
|
+
// Export as RGB for broader CSS compatibility
|
|
530
|
+
const rgbTokens = palette.tokens({ prefix: true, format: 'rgb' });
|
|
531
|
+
|
|
532
|
+
// Save and restore a theme
|
|
533
|
+
const snapshot = primary.export();
|
|
534
|
+
const restored = glaze.from(snapshot);
|
|
535
|
+
|
|
536
|
+
// Create from an existing brand color
|
|
537
|
+
const brand = glaze.fromHex('#7a4dbf');
|
|
538
|
+
brand.colors({ surface: { l: 97 }, text: { base: 'surface', contrast: 52 } });
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
## API Reference
|
|
542
|
+
|
|
543
|
+
### Theme Creation
|
|
544
|
+
|
|
545
|
+
| Method | Description |
|
|
546
|
+
|---|---|
|
|
547
|
+
| `glaze(hue, saturation?)` | Create a theme from hue (0–360) and saturation (0–100) |
|
|
548
|
+
| `glaze({ hue, saturation })` | Create a theme from an options object |
|
|
549
|
+
| `glaze.from(data)` | Create a theme from an exported configuration |
|
|
550
|
+
| `glaze.fromHex(hex)` | Create a theme from a hex color (`#rgb` or `#rrggbb`) |
|
|
551
|
+
| `glaze.fromRgb(r, g, b)` | Create a theme from RGB values (0–255) |
|
|
552
|
+
| `glaze.color(input)` | Create a standalone color token |
|
|
553
|
+
|
|
554
|
+
### Theme Methods
|
|
555
|
+
|
|
556
|
+
| Method | Description |
|
|
557
|
+
|---|---|
|
|
558
|
+
| `theme.colors(defs)` | Add/replace colors (additive merge) |
|
|
559
|
+
| `theme.color(name)` | Get a color definition |
|
|
560
|
+
| `theme.color(name, def)` | Set a single color definition |
|
|
561
|
+
| `theme.remove(names)` | Remove one or more colors |
|
|
562
|
+
| `theme.has(name)` | Check if a color is defined |
|
|
563
|
+
| `theme.list()` | List all defined color names |
|
|
564
|
+
| `theme.reset()` | Clear all color definitions |
|
|
565
|
+
| `theme.export()` | Export configuration as JSON-safe object |
|
|
566
|
+
| `theme.extend(options)` | Create a child theme |
|
|
567
|
+
| `theme.resolve()` | Resolve all colors |
|
|
568
|
+
| `theme.tokens(options?)` | Export as token map |
|
|
569
|
+
| `theme.json(options?)` | Export as plain JSON |
|
|
570
|
+
|
|
571
|
+
### Global Configuration
|
|
572
|
+
|
|
573
|
+
| Method | Description |
|
|
574
|
+
|---|---|
|
|
575
|
+
| `glaze.configure(config)` | Set global configuration |
|
|
576
|
+
| `glaze.palette(themes)` | Compose themes into a palette |
|
|
577
|
+
| `glaze.getConfig()` | Get current global config |
|
|
578
|
+
| `glaze.resetConfig()` | Reset to defaults |
|
|
579
|
+
|
|
580
|
+
## License
|
|
581
|
+
|
|
582
|
+
[MIT](LICENSE)
|