@tenphi/glaze 0.0.0-snapshot.4c063ef → 0.0.0-snapshot.4e8eab7
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 +587 -51
- package/dist/index.cjs +1064 -137
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +424 -33
- package/dist/index.d.mts +424 -33
- package/dist/index.mjs +1060 -138
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -7
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ Glaze generates robust **light**, **dark**, and **high-contrast** color schemes
|
|
|
22
22
|
|
|
23
23
|
- **OKHSL color space** — perceptually uniform hue and saturation
|
|
24
24
|
- **WCAG 2 contrast solving** — automatic lightness adjustment to meet AA/AAA targets
|
|
25
|
+
- **Mix colors** — blend two colors with OKHSL or sRGB interpolation, opaque or transparent, with optional contrast solving
|
|
25
26
|
- **Shadow colors** — OKHSL-native shadow computation with automatic alpha, fg/bg tinting, and per-scheme adaptation
|
|
26
27
|
- **Light + Dark + High-Contrast** — all schemes from one definition
|
|
27
28
|
- **Per-color hue override** — absolute or relative hue shifts within a theme
|
|
@@ -69,9 +70,9 @@ const danger = primary.extend({ hue: 23 });
|
|
|
69
70
|
const success = primary.extend({ hue: 157 });
|
|
70
71
|
|
|
71
72
|
// Compose into a palette and export
|
|
72
|
-
const palette = glaze.palette({ primary, danger, success });
|
|
73
|
-
const tokens = palette.tokens(
|
|
74
|
-
// → { light: { 'primary-surface': 'okhsl(...)',
|
|
73
|
+
const palette = glaze.palette({ primary, danger, success }, { primary: 'primary' });
|
|
74
|
+
const tokens = palette.tokens();
|
|
75
|
+
// → { light: { 'primary-surface': 'okhsl(...)', 'surface': 'okhsl(...)', ... }, dark: { ... } }
|
|
75
76
|
```
|
|
76
77
|
|
|
77
78
|
## Core Concepts
|
|
@@ -193,6 +194,8 @@ A single value applies to both modes. All control is local and explicit.
|
|
|
193
194
|
'muted': { base: 'surface', lightness: ['-35', '-50'], contrast: ['AA-large', 'AA'] }
|
|
194
195
|
```
|
|
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
|
+
|
|
196
199
|
## Theme Color Management
|
|
197
200
|
|
|
198
201
|
### Adding Colors
|
|
@@ -260,15 +263,306 @@ The export contains only the configuration — not resolved color values. Resolv
|
|
|
260
263
|
Create a single color token without a full theme:
|
|
261
264
|
|
|
262
265
|
```ts
|
|
263
|
-
const accent = glaze.color({ hue: 280, saturation: 80, lightness: 52
|
|
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
|
+
Every input form defaults to **`mode: 'auto'`** so the resolved token
|
|
286
|
+
adapts between light and dark like an ordinary theme color. The
|
|
287
|
+
*scaling* snapshot taken at create time differs by input form:
|
|
288
|
+
|
|
289
|
+
- **String value-shorthand** (hex, `rgb()`, `hsl()`, `okhsl()`,
|
|
290
|
+
`oklch()`):
|
|
291
|
+
- Light variant preserves the input lightness exactly.
|
|
292
|
+
- Dark variant is **Möbius-inverted** into `[globalConfig.darkLightness[0], 100]`,
|
|
293
|
+
so `glaze.color('#000')` renders as `#fff` in dark mode and
|
|
294
|
+
`glaze.color('#fff')` falls to the dark `lo` floor (default `0.15`).
|
|
295
|
+
- The dark `lo` is snapshotted from `globalConfig` at color-creation
|
|
296
|
+
time, matching how an explicit `scaling.darkLightness: [lo, hi]`
|
|
297
|
+
behaves.
|
|
298
|
+
|
|
299
|
+
- **Object / tuple value-shorthand** (`{ h, s, l }`, `[r, g, b]`) and
|
|
300
|
+
the **structured form** (`{ hue, saturation, lightness, ... }`):
|
|
301
|
+
- Both light and dark variants are mapped through
|
|
302
|
+
`globalConfig.lightLightness` / `globalConfig.darkLightness`
|
|
303
|
+
(defaults `[10, 100]` / `[15, 95]`) — the same windows a theme color
|
|
304
|
+
uses. With the `'auto'` default the dark variant is Möbius-inverted
|
|
305
|
+
into that dark window, so a near-white seed lands at a near-dark
|
|
306
|
+
dark variant.
|
|
307
|
+
- Both windows are snapshotted at color-creation time so later
|
|
308
|
+
`glaze.configure()` calls don't retroactively change exported tokens.
|
|
309
|
+
|
|
310
|
+
To opt back into the legacy fixed-linear default (no Möbius inversion),
|
|
311
|
+
pass `{ mode: 'fixed' }` as the second arg, or supply an explicit
|
|
312
|
+
`scaling` as the third arg (see [Lightness scaling](#lightness-scaling)).
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
// Default: pure black inverts to pure white in dark mode.
|
|
316
|
+
glaze.color('#000000').tasty();
|
|
317
|
+
// → { '': 'okhsl(0 0% 0%)', '@dark': 'okhsl(... 100%)' }
|
|
318
|
+
|
|
319
|
+
// Opt back into the fixed-linear behavior:
|
|
320
|
+
glaze.color('#000000', { mode: 'fixed' }).tasty();
|
|
321
|
+
// → { '': 'okhsl(0 0% 0%)', '@dark': 'okhsl(... 15%)' }
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Value Shorthand
|
|
325
|
+
|
|
326
|
+
The first argument can also be a color value — Glaze extracts the seed
|
|
327
|
+
hue/saturation/lightness for you. All forms support the same exports
|
|
328
|
+
(`resolve / token / tasty / json / css`):
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
// Hex (3, 6, or 8 digits — alpha dropped with warning)
|
|
332
|
+
glaze.color('#26fcb2').tasty();
|
|
333
|
+
glaze.color('#26fcb2ff').tasty(); // alpha dropped
|
|
334
|
+
|
|
335
|
+
// CSS color functions Glaze itself emits (`rgb()`, `hsl()`, `okhsl()`, `oklch()`)
|
|
336
|
+
// — anything from theme.tasty()/json()/css() round-trips back in.
|
|
337
|
+
glaze.color('rgb(38 252 178)').tasty();
|
|
338
|
+
glaze.color('hsl(152 97% 57%)').tasty();
|
|
339
|
+
glaze.color('okhsl(152 95% 74%)').tasty();
|
|
340
|
+
glaze.color('oklch(0.85 0.18 152)').tasty();
|
|
341
|
+
|
|
342
|
+
// OKHSL object — Glaze's native shape (h: 0–360, s/l: 0–1).
|
|
343
|
+
// Passing 0–100 values for s/l throws with a hint to use the
|
|
344
|
+
// structured form { hue, saturation, lightness }.
|
|
345
|
+
glaze.color({ h: 152, s: 0.95, l: 0.74 }).tasty();
|
|
346
|
+
|
|
347
|
+
// RGB tuple, 0–255 (same range as glaze.fromRgb).
|
|
348
|
+
glaze.color([38, 252, 178]).tasty();
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The optional second argument supplies overrides — the WCAG `contrast`
|
|
352
|
+
solver, relative `hue` / `lightness`, plus the usual seed knobs:
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
// Brand color seeded from a hex, with saturation/mode overrides
|
|
356
|
+
glaze.color('#26fcb2', { saturation: 80, mode: 'fixed' }).tasty();
|
|
357
|
+
|
|
358
|
+
// Brand text guaranteed AAA against the seed itself.
|
|
359
|
+
// Relative `lightness: '+48'` is anchored to the literal seed value.
|
|
360
|
+
glaze.color('#1a1a2e', {
|
|
361
|
+
lightness: '+48',
|
|
362
|
+
contrast: 'AAA',
|
|
363
|
+
}).tasty();
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
By default, relative `lightness: '+N'` and `contrast: <ratio>` are
|
|
367
|
+
anchored to the literal seed (the value passed to `glaze.color()`).
|
|
368
|
+
Internally Glaze synthesizes a hidden `mode: 'static'` reference of
|
|
369
|
+
the seed so the contrast solver compares against the unmapped color
|
|
370
|
+
across every variant. Pass `base` (another `glaze.color()` token) to
|
|
371
|
+
anchor against another color's resolved variant per scheme instead —
|
|
372
|
+
see [Pairing Colors](#pairing-colors).
|
|
373
|
+
|
|
374
|
+
All overrides:
|
|
375
|
+
|
|
376
|
+
| Option | Notes |
|
|
377
|
+
|---|---|
|
|
378
|
+
| `hue` | Number (absolute 0–360) or `'+N'`/`'-N'` (relative to seed — never to `base`) |
|
|
379
|
+
| `saturation` | Override seed saturation (0–100) |
|
|
380
|
+
| `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 |
|
|
381
|
+
| `saturationFactor` | Multiplier on seed (0–1, default 1) |
|
|
382
|
+
| `mode` | `'auto'` (default for every input form) / `'fixed'` / `'static'` — see [Adaptation Modes](#adaptation-modes) |
|
|
383
|
+
| `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 |
|
|
384
|
+
| `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 |
|
|
385
|
+
| `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` |
|
|
386
|
+
| `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 |
|
|
387
|
+
|
|
388
|
+
Alpha components in `rgb(... / A)` / `hsl(... / A)` / `rgba(...)` /
|
|
389
|
+
`hsla(...)` and 8-digit hex (`#rrggbbaa` / `#rgba`) are parsed but the
|
|
390
|
+
alpha channel is dropped with a `console.warn`. To set a fixed alpha
|
|
391
|
+
on a standalone color, use the `opacity` override (or `opacity` on a
|
|
392
|
+
theme color). Named CSS colors (`'red'`, `'blueviolet'`) are not
|
|
393
|
+
supported.
|
|
394
|
+
|
|
395
|
+
### Lightness Scaling
|
|
396
|
+
|
|
397
|
+
The optional third positional argument lets you override the lightness
|
|
398
|
+
windows used by `glaze.color()`. Both keys mirror the field names from
|
|
399
|
+
`GlazeConfig`:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
// Preserve raw lightness in dark mode too:
|
|
403
|
+
glaze.color('#26fcb2', undefined, { darkLightness: false }).tasty();
|
|
404
|
+
|
|
405
|
+
// Or opt back into a theme-style window:
|
|
406
|
+
glaze.color('#26fcb2', undefined, {
|
|
407
|
+
lightLightness: [10, 100],
|
|
408
|
+
darkLightness: [15, 95],
|
|
409
|
+
}).tasty();
|
|
410
|
+
|
|
411
|
+
// Structured form takes scaling as the second positional arg:
|
|
412
|
+
glaze
|
|
413
|
+
.color({ hue: 152, saturation: 95, lightness: 74 }, { darkLightness: false })
|
|
414
|
+
.tasty();
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
| Key | Default for `glaze.color()` (string input) | Default for `glaze.color()` (object / tuple / structured) | Effect |
|
|
418
|
+
|---|---|---|---|
|
|
419
|
+
| `lightLightness` | `false` | `false` | `false` = preserve input. Pass `[lo, hi]` to opt into a remap window. |
|
|
420
|
+
| `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. |
|
|
421
|
+
|
|
422
|
+
> Note: `scaling` is all-or-nothing — passing it replaces both fields
|
|
423
|
+
> at once. To keep one field's default, restate it explicitly. The
|
|
424
|
+
> default windows are snapshotted from `globalConfig` at color-creation
|
|
425
|
+
> time, so later `glaze.configure()` calls don't retroactively change
|
|
426
|
+
> already-created tokens (and `token.export()` round-trips
|
|
427
|
+
> byte-for-byte across `configure()` changes).
|
|
428
|
+
|
|
429
|
+
### Pairing Colors
|
|
264
430
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
431
|
+
`glaze.color()` accepts an optional `base` override that ties one
|
|
432
|
+
standalone color to another. When you set `base`, the WCAG contrast
|
|
433
|
+
solver and relative `lightness` offsets switch their anchor from the
|
|
434
|
+
literal seed to the base's resolved variant per scheme — so the same
|
|
435
|
+
text color automatically lands at AA against its background in light,
|
|
436
|
+
dark, and high-contrast modes.
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
const bg = glaze.color('#1a1a2e');
|
|
440
|
+
|
|
441
|
+
// Text guaranteed AA against `bg` in every scheme.
|
|
442
|
+
const text = glaze.color('#ffffff', { base: bg, contrast: 'AA' });
|
|
443
|
+
|
|
444
|
+
// Border 8 lightness units lighter than `bg` in each scheme.
|
|
445
|
+
const border = glaze.color('#000000', {
|
|
446
|
+
base: bg,
|
|
447
|
+
lightness: '+8',
|
|
448
|
+
mode: 'fixed',
|
|
449
|
+
});
|
|
269
450
|
```
|
|
270
451
|
|
|
271
|
-
|
|
452
|
+
`base` also accepts a raw `GlazeColorValue` for one-off pairs without
|
|
453
|
+
a separate token binding:
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
// Equivalent to `base: glaze.color('#1a1a2e')` — `glaze` auto-wraps it.
|
|
457
|
+
const text = glaze.color('#ffffff', { base: '#1a1a2e', contrast: 'AA' });
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Behavior with `base`:
|
|
461
|
+
|
|
462
|
+
- `contrast` is solved per scheme against `base`'s resolved variant
|
|
463
|
+
(light / dark / lightContrast / darkContrast).
|
|
464
|
+
- Relative `lightness: '+N'` / `'-N'` is anchored to `base`'s lightness
|
|
465
|
+
per scheme (matches theme behavior).
|
|
466
|
+
- Relative `hue: '+N'` still anchors to the **seed** (the value passed
|
|
467
|
+
to `glaze.color()`), not the base. Absolute hue overrides take
|
|
468
|
+
precedence as usual.
|
|
469
|
+
- `mode` works as a per-pair knob — pass `mode: 'fixed'` to disable
|
|
470
|
+
Möbius inversion for the dependent color, or `mode: 'auto'` to keep
|
|
471
|
+
it (defaults follow the same string-vs-object rules as standalone).
|
|
472
|
+
- The base token's `.resolve()` is called lazily on the first resolve
|
|
473
|
+
of the dependent and the result is captured by reference; later
|
|
474
|
+
mutations to the base don't apply (matches existing snapshot
|
|
475
|
+
semantics for `scaling.darkLightness`).
|
|
476
|
+
- Raw value bases (`base: '#fff'`, `base: { h, s, l }`, `base: [r, g, b]`)
|
|
477
|
+
are auto-wrapped via `glaze.color(value)` and inherit the same
|
|
478
|
+
string-vs-object defaults. To skip auto-invert on the base, wrap it
|
|
479
|
+
yourself: `base: glaze.color(value, undefined, { darkLightness: false })`.
|
|
480
|
+
- When the contrast target is physically unreachable (e.g. AAA against
|
|
481
|
+
a mid-grey base), `glaze` emits a single `console.warn` per
|
|
482
|
+
`(name, scheme, target)` triple and returns the closest passing
|
|
483
|
+
variant. Use the `name` override to make the warning more
|
|
484
|
+
identifiable in your logs.
|
|
485
|
+
|
|
486
|
+
Chains compose:
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
const bg = glaze.color('#000000');
|
|
490
|
+
const surface = glaze.color('#222222', { base: bg, contrast: 'AAA' });
|
|
491
|
+
const text = glaze.color('#ffffff', { base: surface, contrast: 'AA' });
|
|
492
|
+
// Each level meets its contrast budget against its base in every scheme.
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Naming Standalone Colors
|
|
496
|
+
|
|
497
|
+
The `name` override is a **debug label**, not an output key:
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
const cardBg = glaze.color('#1a1a2e', {
|
|
501
|
+
name: 'card-bg', // surfaces in `console.warn` / Error messages
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
cardBg.token(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' }
|
|
505
|
+
cardBg.json(); // → { light: 'okhsl(...)', dark: 'okhsl(...)' }
|
|
506
|
+
cardBg.css({ name: 'card' }); // CSS variable name comes from `css({ name })`,
|
|
507
|
+
// NOT from the override above
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Use it to make warnings traceable when you have many `glaze.color()`
|
|
511
|
+
calls in a project — without it, `glaze` falls back to the internal
|
|
512
|
+
sentinel `"value"`:
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
// With name:
|
|
516
|
+
// > glaze: color "card-bg" cannot meet contrast "AAA" (7.00) in dark scheme...
|
|
517
|
+
|
|
518
|
+
// Without name:
|
|
519
|
+
// > glaze: color "value" cannot meet contrast "AAA" (7.00) in dark scheme...
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
The reserved internal sentinels (`"value"`, `"seed"`, `"externalBase"`)
|
|
523
|
+
are rejected with a clear error pointing at the conflict.
|
|
524
|
+
|
|
525
|
+
### Persisting Standalone Colors
|
|
526
|
+
|
|
527
|
+
`glaze.color()` tokens can be serialized to JSON-safe data and
|
|
528
|
+
rehydrated later — useful for color pickers, theme settings UIs, and
|
|
529
|
+
URL state.
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
const text = glaze.color('#1a1a1a', {
|
|
533
|
+
contrast: 'AA',
|
|
534
|
+
opacity: 0.9,
|
|
535
|
+
name: 'profile-text',
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const data = text.export(); // JSON-safe snapshot
|
|
539
|
+
const json = JSON.stringify(data); // ship to localStorage / API / URL
|
|
540
|
+
const restored = glaze.colorFrom(JSON.parse(json));
|
|
541
|
+
// `restored.resolve()` matches `text.resolve()` byte-for-byte.
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
The export captures the original `value`, all overrides, and the
|
|
545
|
+
effective `scaling` (snapshotted from `globalConfig` at create time so
|
|
546
|
+
later `glaze.configure()` calls don't change exported tokens).
|
|
547
|
+
Token-typed `base` is recursively serialized, value-typed `base` is
|
|
548
|
+
preserved as the raw value.
|
|
549
|
+
|
|
550
|
+
Both forms round-trip:
|
|
551
|
+
|
|
552
|
+
```ts
|
|
553
|
+
// Value form
|
|
554
|
+
const a = glaze.color('#26fcb2', { contrast: 'AA' });
|
|
555
|
+
const aBack = glaze.colorFrom(a.export());
|
|
556
|
+
|
|
557
|
+
// Structured form
|
|
558
|
+
const b = glaze.color({
|
|
559
|
+
hue: 280,
|
|
560
|
+
saturation: 50,
|
|
561
|
+
lightness: 50,
|
|
562
|
+
opacity: 0.5,
|
|
563
|
+
});
|
|
564
|
+
const bBack = glaze.colorFrom(b.export());
|
|
565
|
+
```
|
|
272
566
|
|
|
273
567
|
## From Existing Colors
|
|
274
568
|
|
|
@@ -390,7 +684,10 @@ Available tuning parameters:
|
|
|
390
684
|
|
|
391
685
|
### Standalone Shadow Computation
|
|
392
686
|
|
|
393
|
-
Compute a shadow outside of a theme
|
|
687
|
+
Compute a shadow outside of a theme. `bg` and `fg` accept any
|
|
688
|
+
`GlazeColorValue`: hex (`#rgb` / `#rrggbb` / `#rrggbbaa`), `rgb()` /
|
|
689
|
+
`hsl()` / `okhsl()` / `oklch()` strings, OKHSL objects, or `[r, g, b]`
|
|
690
|
+
(0–255) tuples.
|
|
394
691
|
|
|
395
692
|
```ts
|
|
396
693
|
const v = glaze.shadow({
|
|
@@ -400,6 +697,13 @@ const v = glaze.shadow({
|
|
|
400
697
|
});
|
|
401
698
|
// → { h: 280, s: 0.14, l: 0.2, alpha: 0.1 }
|
|
402
699
|
|
|
700
|
+
// Equivalent with non-hex inputs:
|
|
701
|
+
glaze.shadow({
|
|
702
|
+
bg: 'rgb(240 238 245)',
|
|
703
|
+
fg: { h: 280, s: 0.06, l: 0.13 },
|
|
704
|
+
intensity: 10,
|
|
705
|
+
});
|
|
706
|
+
|
|
403
707
|
const css = glaze.format(v, 'oklch');
|
|
404
708
|
// → 'oklch(0.15 0.014 280 / 0.1)'
|
|
405
709
|
```
|
|
@@ -413,6 +717,139 @@ const css = glaze.format(v, 'oklch');
|
|
|
413
717
|
}
|
|
414
718
|
```
|
|
415
719
|
|
|
720
|
+
## Mix Colors
|
|
721
|
+
|
|
722
|
+
Mix colors blend two existing colors together. Use them for hover overlays, tints, shades, and any derived color that sits between two reference colors.
|
|
723
|
+
|
|
724
|
+
### Opaque Mix
|
|
725
|
+
|
|
726
|
+
Produces a solid color by interpolating between `base` and `target`:
|
|
727
|
+
|
|
728
|
+
```ts
|
|
729
|
+
theme.colors({
|
|
730
|
+
surface: { lightness: 95 },
|
|
731
|
+
accent: { lightness: 30 },
|
|
732
|
+
|
|
733
|
+
// 30% of the way from surface toward accent
|
|
734
|
+
tint: { type: 'mix', base: 'surface', target: 'accent', value: 30 },
|
|
735
|
+
});
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
- `value` — mix ratio 0–100 (0 = pure base, 100 = pure target)
|
|
739
|
+
- The result is a fully opaque color (alpha = 1)
|
|
740
|
+
- Adapts to light/dark/HC schemes automatically via the resolved base and target
|
|
741
|
+
|
|
742
|
+
### Transparent Mix
|
|
743
|
+
|
|
744
|
+
Produces the target color with a controlled opacity — useful for hover overlays:
|
|
745
|
+
|
|
746
|
+
```ts
|
|
747
|
+
theme.colors({
|
|
748
|
+
surface: { lightness: 95 },
|
|
749
|
+
black: { lightness: 0, saturation: 0 },
|
|
750
|
+
|
|
751
|
+
hover: {
|
|
752
|
+
type: 'mix',
|
|
753
|
+
base: 'surface',
|
|
754
|
+
target: 'black',
|
|
755
|
+
value: 8,
|
|
756
|
+
blend: 'transparent',
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
// hover → target color (black) with alpha = 0.08
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
The output color has `h`, `s`, `l` from the target and `alpha = value / 100`.
|
|
763
|
+
|
|
764
|
+
### Blend Space
|
|
765
|
+
|
|
766
|
+
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:
|
|
767
|
+
|
|
768
|
+
```ts
|
|
769
|
+
theme.colors({
|
|
770
|
+
surface: { lightness: 95 },
|
|
771
|
+
accent: { lightness: 30 },
|
|
772
|
+
|
|
773
|
+
// sRGB blend — matches what the browser would render
|
|
774
|
+
hover: { type: 'mix', base: 'surface', target: 'accent', value: 20, space: 'srgb' },
|
|
775
|
+
});
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
| Space | Behavior | Best for |
|
|
779
|
+
|---|---|---|
|
|
780
|
+
| `'okhsl'` (default) | Perceptually uniform OKHSL interpolation | Design token derivation |
|
|
781
|
+
| `'srgb'` | Linear sRGB channel interpolation | Matching browser compositing |
|
|
782
|
+
|
|
783
|
+
The `space` option only affects opaque blending. Transparent blending always composites in linear sRGB (matching browser alpha compositing).
|
|
784
|
+
|
|
785
|
+
### Contrast Solving
|
|
786
|
+
|
|
787
|
+
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:
|
|
788
|
+
|
|
789
|
+
```ts
|
|
790
|
+
theme.colors({
|
|
791
|
+
surface: { lightness: 95 },
|
|
792
|
+
accent: { lightness: 30 },
|
|
793
|
+
|
|
794
|
+
// Ensure the mixed color has at least AA contrast against surface
|
|
795
|
+
tint: {
|
|
796
|
+
type: 'mix',
|
|
797
|
+
base: 'surface',
|
|
798
|
+
target: 'accent',
|
|
799
|
+
value: 10,
|
|
800
|
+
contrast: 'AA',
|
|
801
|
+
},
|
|
802
|
+
|
|
803
|
+
// Ensure the transparent overlay has at least 3:1 contrast
|
|
804
|
+
overlay: {
|
|
805
|
+
type: 'mix',
|
|
806
|
+
base: 'surface',
|
|
807
|
+
target: 'accent',
|
|
808
|
+
value: 5,
|
|
809
|
+
blend: 'transparent',
|
|
810
|
+
contrast: 3,
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
### High-Contrast Pairs
|
|
816
|
+
|
|
817
|
+
Both `value` and `contrast` support `[normal, highContrast]` pairs:
|
|
818
|
+
|
|
819
|
+
```ts
|
|
820
|
+
theme.colors({
|
|
821
|
+
surface: { lightness: 95 },
|
|
822
|
+
accent: { lightness: 30 },
|
|
823
|
+
|
|
824
|
+
tint: {
|
|
825
|
+
type: 'mix',
|
|
826
|
+
base: 'surface',
|
|
827
|
+
target: 'accent',
|
|
828
|
+
value: [20, 40], // stronger mix in high-contrast mode
|
|
829
|
+
contrast: [3, 'AAA'], // stricter contrast in high-contrast mode
|
|
830
|
+
},
|
|
831
|
+
});
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
### Achromatic Colors
|
|
835
|
+
|
|
836
|
+
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.
|
|
837
|
+
|
|
838
|
+
### Mix Chaining
|
|
839
|
+
|
|
840
|
+
Mix colors can reference other mix colors, enabling multi-step derivations:
|
|
841
|
+
|
|
842
|
+
```ts
|
|
843
|
+
theme.colors({
|
|
844
|
+
white: { lightness: 100, saturation: 0 },
|
|
845
|
+
black: { lightness: 0, saturation: 0 },
|
|
846
|
+
gray: { type: 'mix', base: 'white', target: 'black', value: 50, space: 'srgb' },
|
|
847
|
+
lightGray: { type: 'mix', base: 'white', target: 'gray', value: 50, space: 'srgb' },
|
|
848
|
+
});
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
Mix colors cannot reference shadow colors (same restriction as regular dependent colors).
|
|
852
|
+
|
|
416
853
|
## Output Formats
|
|
417
854
|
|
|
418
855
|
Control the color format in exports with the `format` option:
|
|
@@ -431,7 +868,7 @@ theme.tokens({ format: 'hsl' }); // → 'hsl(270.5 45.2% 95.8%)'
|
|
|
431
868
|
theme.tokens({ format: 'oklch' }); // → 'oklch(0.965 0.0123 280)'
|
|
432
869
|
```
|
|
433
870
|
|
|
434
|
-
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()`.
|
|
871
|
+
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()`.
|
|
435
872
|
|
|
436
873
|
Colors with `alpha < 1` (shadow colors, or regular colors with `opacity`) include an alpha component:
|
|
437
874
|
|
|
@@ -467,7 +904,7 @@ Modes control how colors adapt across schemes:
|
|
|
467
904
|
|
|
468
905
|
```ts
|
|
469
906
|
// Light: surface L=97, text lightness='-52' → L=45 (dark text on light bg)
|
|
470
|
-
// Dark: surface inverts to L≈
|
|
907
|
+
// Dark: surface inverts to L≈20 (Möbius curve), sign flips → L=20+52=72
|
|
471
908
|
// contrast solver may push further (light text on dark bg)
|
|
472
909
|
```
|
|
473
910
|
|
|
@@ -484,14 +921,14 @@ Modes control how colors adapt across schemes:
|
|
|
484
921
|
|
|
485
922
|
### Lightness
|
|
486
923
|
|
|
487
|
-
|
|
924
|
+
Absolute lightness values (both root colors and dependent colors with absolute lightness) are mapped linearly within the configured `lightLightness` window:
|
|
488
925
|
|
|
489
926
|
```ts
|
|
490
927
|
const [lo, hi] = lightLightness; // default: [10, 100]
|
|
491
928
|
const mappedL = (lightness * (hi - lo)) / 100 + lo;
|
|
492
929
|
```
|
|
493
930
|
|
|
494
|
-
Both `auto` and `fixed` modes use the same linear formula. `static` mode
|
|
931
|
+
Both `auto` and `fixed` modes use the same linear formula. `static` mode and high-contrast variants bypass the mapping entirely (identity: `mappedL = l`).
|
|
495
932
|
|
|
496
933
|
| Color | Raw L | Mapped L (default [10, 100]) |
|
|
497
934
|
|---|---|---|
|
|
@@ -503,24 +940,29 @@ Both `auto` and `fixed` modes use the same linear formula. `static` mode bypasse
|
|
|
503
940
|
|
|
504
941
|
### Lightness
|
|
505
942
|
|
|
506
|
-
**`auto`** — inverted within the configured window:
|
|
943
|
+
**`auto`** — inverted with a Möbius transformation within the configured window:
|
|
507
944
|
|
|
508
945
|
```ts
|
|
509
946
|
const [lo, hi] = darkLightness; // default: [15, 95]
|
|
510
|
-
const
|
|
947
|
+
const t = (100 - lightness) / 100;
|
|
948
|
+
const invertedL = lo + (hi - lo) * t / (t + darkCurve * (1 - t)); // darkCurve default: 0.5
|
|
511
949
|
```
|
|
512
950
|
|
|
513
|
-
|
|
951
|
+
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.
|
|
952
|
+
|
|
953
|
+
**`fixed`** — mapped without inversion (not affected by `darkCurve`):
|
|
514
954
|
|
|
515
955
|
```ts
|
|
516
956
|
const mappedL = (lightness * (hi - lo)) / 100 + lo;
|
|
517
957
|
```
|
|
518
958
|
|
|
519
|
-
| Color | Light L | Auto (
|
|
520
|
-
|
|
521
|
-
| surface (L=97) | 97 | 17.4 | 92.6 |
|
|
522
|
-
| accent-fill (L=52) | 52 | 53.4 | 56.6 |
|
|
523
|
-
| accent-text (L=100) | 100 | 15 | 95 |
|
|
959
|
+
| Color | Light L | Auto (curve=0.5) | Auto (curve=1, linear) | Fixed (mapped) |
|
|
960
|
+
|---|---|---|---|---|
|
|
961
|
+
| surface (L=97) | 97 | 19.7 | 17.4 | 92.6 |
|
|
962
|
+
| accent-fill (L=52) | 52 | 66.9 | 53.4 | 56.6 |
|
|
963
|
+
| accent-text (L=100) | 100 | 15 | 15 | 95 |
|
|
964
|
+
|
|
965
|
+
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]`).
|
|
524
966
|
|
|
525
967
|
### Saturation
|
|
526
968
|
|
|
@@ -560,12 +1002,21 @@ Combine multiple themes into a single palette:
|
|
|
560
1002
|
const palette = glaze.palette({ primary, danger, success, warning });
|
|
561
1003
|
```
|
|
562
1004
|
|
|
563
|
-
|
|
1005
|
+
Optionally designate a primary theme at creation time:
|
|
564
1006
|
|
|
565
|
-
|
|
1007
|
+
```ts
|
|
1008
|
+
const palette = glaze.palette(
|
|
1009
|
+
{ primary, danger, success, warning },
|
|
1010
|
+
{ primary: 'primary' },
|
|
1011
|
+
);
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
### Prefix Behavior
|
|
1015
|
+
|
|
1016
|
+
Palette export methods (`tokens()`, `tasty()`, `css()`) default to `prefix: true` — all tokens are automatically prefixed with the theme name to avoid collisions:
|
|
566
1017
|
|
|
567
1018
|
```ts
|
|
568
|
-
const tokens = palette.tokens(
|
|
1019
|
+
const tokens = palette.tokens();
|
|
569
1020
|
// → {
|
|
570
1021
|
// light: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
|
|
571
1022
|
// dark: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
|
|
@@ -578,15 +1029,68 @@ Custom prefix mapping:
|
|
|
578
1029
|
palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
|
|
579
1030
|
```
|
|
580
1031
|
|
|
581
|
-
|
|
1032
|
+
To disable prefixing entirely, pass `prefix: false` explicitly.
|
|
1033
|
+
|
|
1034
|
+
### Collision Detection
|
|
1035
|
+
|
|
1036
|
+
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:
|
|
1037
|
+
|
|
1038
|
+
```ts
|
|
1039
|
+
const palette = glaze.palette({ a, b });
|
|
1040
|
+
palette.tokens({ prefix: false });
|
|
1041
|
+
// ⚠ glaze: token "surface" from theme "b" collides with theme "a" — skipping.
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
### Primary Theme
|
|
582
1045
|
|
|
583
|
-
The
|
|
1046
|
+
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:
|
|
584
1047
|
|
|
585
1048
|
```ts
|
|
586
|
-
const
|
|
1049
|
+
const palette = glaze.palette(
|
|
1050
|
+
{ primary, danger, success },
|
|
1051
|
+
{ primary: 'primary' },
|
|
1052
|
+
);
|
|
1053
|
+
const tokens = palette.tokens();
|
|
1054
|
+
// → {
|
|
1055
|
+
// light: {
|
|
1056
|
+
// 'primary-surface': 'okhsl(...)', // prefixed (all themes)
|
|
1057
|
+
// 'danger-surface': 'okhsl(...)',
|
|
1058
|
+
// 'success-surface': 'okhsl(...)',
|
|
1059
|
+
// 'surface': 'okhsl(...)', // unprefixed alias (primary only)
|
|
1060
|
+
// },
|
|
1061
|
+
// }
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
Override or disable per-export:
|
|
1065
|
+
|
|
1066
|
+
```ts
|
|
1067
|
+
palette.tokens({ primary: 'danger' }); // use danger as primary for this call
|
|
1068
|
+
palette.tokens({ primary: false }); // no primary for this call
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
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:
|
|
1072
|
+
|
|
1073
|
+
```ts
|
|
1074
|
+
palette.tokens({ prefix: { primary: 'p-', danger: 'd-' } });
|
|
1075
|
+
// → 'p-surface' + 'surface' (alias from palette-level primary) + 'd-surface'
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
An error is thrown if the primary name doesn't match any theme in the palette.
|
|
1079
|
+
|
|
1080
|
+
### Tasty Export (for [Tasty](https://tasty.style) style system)
|
|
1081
|
+
|
|
1082
|
+
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:
|
|
1083
|
+
|
|
1084
|
+
```ts
|
|
1085
|
+
const palette = glaze.palette(
|
|
1086
|
+
{ primary, danger, success },
|
|
1087
|
+
{ primary: 'primary' },
|
|
1088
|
+
);
|
|
1089
|
+
const tastyTokens = palette.tasty();
|
|
587
1090
|
// → {
|
|
588
1091
|
// '#primary-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
|
|
589
1092
|
// '#danger-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
|
|
1093
|
+
// '#surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' }, // alias
|
|
590
1094
|
// }
|
|
591
1095
|
```
|
|
592
1096
|
|
|
@@ -653,8 +1157,10 @@ palette.tasty({ states: { dark: '@dark', highContrast: '@hc' } });
|
|
|
653
1157
|
|
|
654
1158
|
### JSON Export (Framework-Agnostic)
|
|
655
1159
|
|
|
1160
|
+
JSON export groups by theme name (no prefix needed):
|
|
1161
|
+
|
|
656
1162
|
```ts
|
|
657
|
-
const data = palette.json(
|
|
1163
|
+
const data = palette.json();
|
|
658
1164
|
// → {
|
|
659
1165
|
// primary: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
|
|
660
1166
|
// danger: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
|
|
@@ -676,7 +1182,11 @@ const css = theme.css();
|
|
|
676
1182
|
Use in a stylesheet:
|
|
677
1183
|
|
|
678
1184
|
```ts
|
|
679
|
-
const
|
|
1185
|
+
const palette = glaze.palette(
|
|
1186
|
+
{ primary, danger, success },
|
|
1187
|
+
{ primary: 'primary' },
|
|
1188
|
+
);
|
|
1189
|
+
const css = palette.css();
|
|
680
1190
|
|
|
681
1191
|
const stylesheet = `
|
|
682
1192
|
:root { ${css.light} }
|
|
@@ -692,7 +1202,8 @@ Options:
|
|
|
692
1202
|
|---|---|---|
|
|
693
1203
|
| `format` | `'rgb'` | Color format (`'rgb'`, `'hsl'`, `'okhsl'`, `'oklch'`) |
|
|
694
1204
|
| `suffix` | `'-color'` | Suffix appended to each CSS property name |
|
|
695
|
-
| `prefix` |
|
|
1205
|
+
| `prefix` | `true` (palette) | (palette only) `true` uses `"<themeName>-"`, or provide a custom map |
|
|
1206
|
+
| `primary` | inherited | (palette only) Override or disable (`false`) the palette-level primary for this call |
|
|
696
1207
|
|
|
697
1208
|
```ts
|
|
698
1209
|
// Custom suffix
|
|
@@ -703,9 +1214,9 @@ theme.css({ suffix: '' });
|
|
|
703
1214
|
theme.css({ format: 'hsl' });
|
|
704
1215
|
// → "--surface-color: hsl(...);"
|
|
705
1216
|
|
|
706
|
-
// Palette with
|
|
707
|
-
palette.css(
|
|
708
|
-
// → "--primary-surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
|
|
1217
|
+
// Palette with primary (inherited from palette creation)
|
|
1218
|
+
palette.css();
|
|
1219
|
+
// → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
|
|
709
1220
|
```
|
|
710
1221
|
|
|
711
1222
|
## Output Modes
|
|
@@ -738,9 +1249,10 @@ Resolution priority (highest first):
|
|
|
738
1249
|
|
|
739
1250
|
```ts
|
|
740
1251
|
glaze.configure({
|
|
741
|
-
lightLightness: [10, 100], // Light scheme lightness window [lo, hi]
|
|
742
|
-
darkLightness: [15, 95], // Dark scheme lightness window [lo, hi]
|
|
1252
|
+
lightLightness: [10, 100], // Light scheme lightness window [lo, hi] (bypassed in HC)
|
|
1253
|
+
darkLightness: [15, 95], // Dark scheme lightness window [lo, hi] (bypassed in HC)
|
|
743
1254
|
darkDesaturation: 0.1, // Saturation reduction in dark scheme (0–1)
|
|
1255
|
+
darkCurve: 0.5, // Möbius beta for dark auto-inversion (0–1); or [normal, hc] pair
|
|
744
1256
|
states: {
|
|
745
1257
|
dark: '@dark', // State alias for dark mode tokens
|
|
746
1258
|
highContrast: '@high-contrast',
|
|
@@ -758,10 +1270,10 @@ glaze.configure({
|
|
|
758
1270
|
|
|
759
1271
|
## Color Definition Shape
|
|
760
1272
|
|
|
761
|
-
`ColorDef` is a discriminated union of regular colors and
|
|
1273
|
+
`ColorDef` is a discriminated union of regular colors, shadow colors, and mix colors:
|
|
762
1274
|
|
|
763
1275
|
```ts
|
|
764
|
-
type ColorDef = RegularColorDef | ShadowColorDef;
|
|
1276
|
+
type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
|
|
765
1277
|
|
|
766
1278
|
interface RegularColorDef {
|
|
767
1279
|
lightness?: HCPair<number | RelativeValue>;
|
|
@@ -780,15 +1292,24 @@ interface ShadowColorDef {
|
|
|
780
1292
|
intensity: HCPair<number>; // 0–100
|
|
781
1293
|
tuning?: ShadowTuning;
|
|
782
1294
|
}
|
|
1295
|
+
|
|
1296
|
+
interface MixColorDef {
|
|
1297
|
+
type: 'mix';
|
|
1298
|
+
base: string; // "from" color name
|
|
1299
|
+
target: string; // "to" color name
|
|
1300
|
+
value: HCPair<number>; // 0–100 (mix ratio or opacity)
|
|
1301
|
+
blend?: 'opaque' | 'transparent'; // default: 'opaque'
|
|
1302
|
+
space?: 'okhsl' | 'srgb'; // default: 'okhsl'
|
|
1303
|
+
contrast?: HCPair<MinContrast>;
|
|
1304
|
+
}
|
|
783
1305
|
```
|
|
784
1306
|
|
|
785
|
-
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.
|
|
1307
|
+
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.
|
|
786
1308
|
|
|
787
1309
|
## Validation
|
|
788
1310
|
|
|
789
1311
|
| Condition | Behavior |
|
|
790
1312
|
|---|---|
|
|
791
|
-
| Both absolute `lightness` and `base` on same color | Warning, `lightness` takes precedence |
|
|
792
1313
|
| `contrast` without `base` | Validation error |
|
|
793
1314
|
| Relative `lightness` without `base` | Validation error |
|
|
794
1315
|
| `lightness` resolves outside 0–100 | Clamp silently |
|
|
@@ -802,6 +1323,12 @@ A root color must have absolute `lightness` (a number). A dependent color must h
|
|
|
802
1323
|
| Regular color `base` references a shadow color | Validation error |
|
|
803
1324
|
| Shadow `intensity` outside 0–100 | Clamp silently |
|
|
804
1325
|
| `contrast` + `opacity` combined | Warning |
|
|
1326
|
+
| Mix `base` references non-existent color | Validation error |
|
|
1327
|
+
| Mix `target` references non-existent color | Validation error |
|
|
1328
|
+
| Mix `base` references a shadow color | Validation error |
|
|
1329
|
+
| Mix `target` references a shadow color | Validation error |
|
|
1330
|
+
| Mix `value` outside 0–100 | Clamp silently |
|
|
1331
|
+
| Circular references involving mix colors | Validation error |
|
|
805
1332
|
|
|
806
1333
|
## Advanced: Color Math Utilities
|
|
807
1334
|
|
|
@@ -847,6 +1374,10 @@ primary.colors({
|
|
|
847
1374
|
'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
|
|
848
1375
|
'shadow-lg': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 20 },
|
|
849
1376
|
|
|
1377
|
+
// Mix colors — hover overlays and tints
|
|
1378
|
+
'hover': { type: 'mix', base: 'surface', target: 'accent-fill', value: 8, blend: 'transparent' },
|
|
1379
|
+
'tint': { type: 'mix', base: 'surface', target: 'accent-fill', value: 20 },
|
|
1380
|
+
|
|
850
1381
|
// Fixed-alpha overlay
|
|
851
1382
|
overlay: { lightness: 0, opacity: 0.5 },
|
|
852
1383
|
});
|
|
@@ -856,18 +1387,21 @@ const success = primary.extend({ hue: 157 });
|
|
|
856
1387
|
const warning = primary.extend({ hue: 84 });
|
|
857
1388
|
const note = primary.extend({ hue: 302 });
|
|
858
1389
|
|
|
859
|
-
const palette = glaze.palette(
|
|
1390
|
+
const palette = glaze.palette(
|
|
1391
|
+
{ primary, danger, success, warning, note },
|
|
1392
|
+
{ primary: 'primary' },
|
|
1393
|
+
);
|
|
860
1394
|
|
|
861
|
-
// Export as flat token map grouped by variant
|
|
862
|
-
const tokens = palette.tokens(
|
|
863
|
-
// tokens.light → { 'primary-surface': '
|
|
1395
|
+
// Export as flat token map grouped by variant (prefix defaults to true)
|
|
1396
|
+
const tokens = palette.tokens();
|
|
1397
|
+
// tokens.light → { 'primary-surface': '...', 'surface': '...', 'danger-surface': '...' }
|
|
864
1398
|
|
|
865
1399
|
// Export as tasty style-to-state bindings (for Tasty style system)
|
|
866
|
-
const tastyTokens = palette.tasty(
|
|
1400
|
+
const tastyTokens = palette.tasty();
|
|
867
1401
|
|
|
868
1402
|
// Export as CSS custom properties (rgb format by default)
|
|
869
|
-
const css = palette.css(
|
|
870
|
-
// css.light → "--primary-surface-color: rgb(...);\n--
|
|
1403
|
+
const css = palette.css();
|
|
1404
|
+
// css.light → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
|
|
871
1405
|
|
|
872
1406
|
// Standalone shadow computation
|
|
873
1407
|
const v = glaze.shadow({ bg: '#f0eef5', fg: '#1a1a2e', intensity: 10 });
|
|
@@ -894,8 +1428,10 @@ brand.colors({ surface: { lightness: 97 }, text: { base: 'surface', lightness: '
|
|
|
894
1428
|
| `glaze.from(data)` | Create a theme from an exported configuration |
|
|
895
1429
|
| `glaze.fromHex(hex)` | Create a theme from a hex color (`#rgb` or `#rrggbb`) |
|
|
896
1430
|
| `glaze.fromRgb(r, g, b)` | Create a theme from RGB values (0–255) |
|
|
897
|
-
| `glaze.color(input)` | Create a standalone color token |
|
|
898
|
-
| `glaze.
|
|
1431
|
+
| `glaze.color(input, scaling?)` | Create a standalone color token from `{ hue, saturation, lightness, opacity?, contrast?, base?, name?, ... }`. Optional `scaling` overrides the lightness windows |
|
|
1432
|
+
| `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). Every input form defaults to `mode: 'auto'`; string inputs additionally preserve light lightness exactly and extend the dark window to `[lo, 100]`, while object / tuple / structured inputs snapshot both windows from `globalConfig.lightLightness` / `globalConfig.darkLightness`. |
|
|
1433
|
+
| `glaze.colorFrom(data)` | Rehydrate a `glaze.color()` token from a `.export()` snapshot. Inverse of `token.export()` — see [Persisting Standalone Colors](#persisting-standalone-colors) |
|
|
1434
|
+
| `glaze.shadow(input)` | Compute a standalone shadow color (returns `ResolvedColorVariant`). `bg` / `fg` accept any `GlazeColorValue` form |
|
|
899
1435
|
| `glaze.format(variant, format?)` | Format any `ResolvedColorVariant` as a CSS string |
|
|
900
1436
|
|
|
901
1437
|
### Theme Methods
|
|
@@ -913,7 +1449,7 @@ brand.colors({ surface: { lightness: 97 }, text: { base: 'surface', lightness: '
|
|
|
913
1449
|
| `theme.extend(options)` | Create a child theme |
|
|
914
1450
|
| `theme.resolve()` | Resolve all colors |
|
|
915
1451
|
| `theme.tokens(options?)` | Export as flat token map grouped by variant |
|
|
916
|
-
| `theme.tasty(options?)` | Export as [Tasty](https://
|
|
1452
|
+
| `theme.tasty(options?)` | Export as [Tasty](https://tasty.style/docs) style-to-state bindings |
|
|
917
1453
|
| `theme.json(options?)` | Export as plain JSON |
|
|
918
1454
|
| `theme.css(options?)` | Export as CSS custom property declarations |
|
|
919
1455
|
|
|
@@ -922,7 +1458,7 @@ brand.colors({ surface: { lightness: 97 }, text: { base: 'surface', lightness: '
|
|
|
922
1458
|
| Method | Description |
|
|
923
1459
|
|---|---|
|
|
924
1460
|
| `glaze.configure(config)` | Set global configuration |
|
|
925
|
-
| `glaze.palette(themes)` | Compose themes into a palette |
|
|
1461
|
+
| `glaze.palette(themes, options?)` | Compose themes into a palette (options: `{ primary? }`) |
|
|
926
1462
|
| `glaze.getConfig()` | Get current global config |
|
|
927
1463
|
| `glaze.resetConfig()` | Reset to defaults |
|
|
928
1464
|
|