@tenphi/glaze 2.0.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,435 @@
1
+ ---
2
+ title: Palettes
3
+ description: glaze.palette() — composing themes into one token namespace, prefixes, collisions, the primary alias, and palette snapshots.
4
+ ---
5
+
6
+ A palette composes several [themes](./theme.md) into a single token namespace: one `tokens()` / `css()` / `tailwind()` call emits every theme, each under its own prefix. How to choose a prefix strategy for an app is covered in [Exporting and integration](../exporting.mdx).
7
+
8
+ ## `glaze.palette(themes, options?)`
9
+
10
+ ```ts
11
+ import { glaze } from '@tenphi/glaze';
12
+
13
+ const primary = glaze(280, 80);
14
+ primary.colors({
15
+ surface: { tone: 97 },
16
+ 'surface-text': { base: 'surface', tone: '-52', contrast: 'AA' },
17
+ accent: { tone: 52, mode: 'fixed' },
18
+ });
19
+ const danger = primary.extend({ hue: 23 });
20
+ const success = primary.extend({ hue: 145 });
21
+
22
+ const palette = glaze.palette({ primary, danger, success });
23
+ const withAlias = glaze.palette(
24
+ { primary, danger, success },
25
+ { primary: 'primary' },
26
+ );
27
+ ```
28
+
29
+ | Parameter | Type | Description |
30
+ | --- | --- | --- |
31
+ | `themes` | `Record<string, GlazeTheme>` | Themes keyed by name. The name becomes the default token prefix (`<name>-`). Insertion order is the output order. |
32
+ | `options` | `GlazePaletteOptions` | Optional. |
33
+
34
+ `GlazePaletteOptions`:
35
+
36
+ | Option | Type | Description |
37
+ | --- | --- | --- |
38
+ | `primary` | `string` | Name of the primary theme. Its tokens are also emitted **without** a prefix in every output method except `json()`, as short aliases next to the prefixed ones. See [Primary theme aliases](#primary-theme-aliases). Throws `glaze: primary theme "<name>" not found in palette. Available: <names>.` when no theme has that name. |
39
+
40
+ The palette holds the theme instances you pass, not copies: changing a theme later changes the palette's next export.
41
+
42
+ ## Palette members
43
+
44
+ | Member | Description |
45
+ | --- | --- |
46
+ | `palette.list()` | Theme names in insertion order. |
47
+ | `palette.primary` | The primary theme name set at creation, or `undefined`. Readonly. |
48
+ | `palette.theme(name)` | The live theme instance with that name, or `undefined`. |
49
+ | `palette.themes()` | Shallow copy of the theme map. The values are the same instances the palette holds. |
50
+ | `palette.export(override?)` | Authoring snapshot, restorable with `glaze.paletteFrom()`. See [below](#paletteexportoverride--glazepalettefromdata). |
51
+ | `palette.tokens(options?)` | Flat token map grouped by scheme variant. |
52
+ | `palette.tasty(options?)` | [Tasty](https://tasty.style) style-to-state bindings. |
53
+ | `palette.json(options?)` | Per-theme resolved color JSON. Not restorable as authoring config. |
54
+ | `palette.css(options?)` | CSS custom property declaration strings. |
55
+ | `palette.dtcg(options?)` | Per-scheme W3C DTCG token documents. |
56
+ | `palette.dtcgResolver(options?)` | One DTCG Resolver-Module document for every scheme. |
57
+ | `palette.tailwind(options?)` | One Tailwind CSS v4 theme with scheme overrides. |
58
+
59
+ ## `palette.export(override?)` / `glaze.paletteFrom(data)`
60
+
61
+ `palette.export()` returns a JSON-safe authoring snapshot: each theme's [`theme.export()`](./theme.md#themeexportoverride) plus the primary name. The optional `override` (`GlazeConfigOverride`) is forwarded to every nested `theme.export(override)`.
62
+
63
+ ```ts
64
+ const snapshot = withAlias.export();
65
+ // → {
66
+ // kind: 'palette',
67
+ // version: 2,
68
+ // themes: {
69
+ // primary: {
70
+ // kind: 'theme',
71
+ // version: 2,
72
+ // hue: 280,
73
+ // saturation: 80,
74
+ // colors: {…},
75
+ // config: {…},
76
+ // },
77
+ // danger: { kind: 'theme', version: 2, hue: 23, … },
78
+ // success: { kind: 'theme', version: 2, hue: 145, … },
79
+ // },
80
+ // primary: 'primary', // omitted when the palette has none
81
+ // }
82
+
83
+ const restored = glaze.paletteFrom(JSON.parse(JSON.stringify(snapshot)));
84
+ const brand = restored.theme('primary')!;
85
+ ```
86
+
87
+ `GlazePaletteExport`:
88
+
89
+ | Field | Type | Description |
90
+ | --- | --- | --- |
91
+ | `kind` | `'palette'` | Snapshot kind. Always written. |
92
+ | `version` | `number` | Schema version, `GLAZE_EXPORT_VERSION` (`2`). Always written. |
93
+ | `themes` | `Record<string, GlazeThemeExport>` | One theme snapshot per theme, keyed by name. |
94
+ | `primary` | `string` | The primary theme name. Omitted when the palette has none. |
95
+
96
+ Use `export()` / `paletteFrom()` to persist and restore the authoring graph (themes, color definitions, relations). Use `json()` / `tokens()` / `css()` / … to emit resolved color strings for apps and design tools.
97
+
98
+ `glaze.paletteFrom(data)` rebuilds every theme with the [`themeFrom`](./theme.md#glazethemefromdata--glazefromdata) rules and recreates the palette with the snapshot's `primary`. It throws when:
99
+
100
+ - `data` is not an object, or `kind` is present and not `'palette'`, or `version` is invalid or newer than `GLAZE_EXPORT_VERSION` (messages prefixed `glaze.paletteFrom:`).
101
+ - `themes` is missing or not an object: `glaze.paletteFrom: expected a "themes" object map of theme exports.`
102
+ - A theme entry fails a `themeFrom` check. The message is prefixed with the theme, for example `glaze.paletteFrom (theme "brand"): …`.
103
+ - A theme entry has no `version` of its own, the palette is version `1`, and the entry has shadow colors or a `shadowTuning` — the entry inherits the palette's version for this check. See [Authoring snapshots](./index.md#authoring-snapshots-and-versioning).
104
+ - `primary` names a theme that isn't in `themes`.
105
+
106
+ ## `GlazePaletteExportOptions`
107
+
108
+ Every palette output method except `json()` accepts these on top of its own options:
109
+
110
+ | Option | Type | Default | Description |
111
+ | --- | --- | --- | --- |
112
+ | `prefix` | `boolean \| Record<string, string>` | `true` | `true` prefixes each theme's tokens with `"<themeName>-"`. A map sets a custom prefix per theme; themes not in the map fall back to `"<themeName>-"`. `false` disables prefixing. |
113
+ | `primary` | `string \| false` | The palette's `primary` | A theme name sets or changes the primary for this call (throws if no theme has that name). `false` disables the primary alias for this call. |
114
+
115
+ Each method's own options:
116
+
117
+ | Method | Own options (type) | Notes |
118
+ | --- | --- | --- |
119
+ | `palette.tokens(options?)` | `format`, `modes` (`GlazeJsonOptions`) | |
120
+ | `palette.tasty(options?)` | `format`, `modes`, `states`, `splitHue` (`GlazePaletteTastyOptions`) | No `name`: the hue var base name is each theme's name. `states` defaults to the global config. |
121
+ | `palette.json(options?)` | `format`, `modes` (`GlazeJsonOptions`) | Only these two: no prefix, primary, or collision handling — see [`palette.json()`](#palettejson). |
122
+ | `palette.css(options?)` | `format`, `suffix`, `splitHue`, `hdr` (`GlazePaletteCssOptions`) | No `modes`: always returns all four strings, plus `hdr` when HDR output is on. No `name`, as for `tasty()`. |
123
+ | `palette.dtcg(options?)` | `colorSpace`, `modes` (`GlazeDtcgOptions`) | |
124
+ | `palette.dtcgResolver(options?)` | `colorSpace`, `modes`, `setName`, `modifierName`, `contextNames`, `version` (`GlazeDtcgResolverOptions`) | |
125
+ | `palette.tailwind(options?)` | `format`, `modes`, `namespace`, `darkSelector`, `highContrastSelector`, `hdrSelector` (`GlazeTailwindOptions`) | `prefix` (theme prefix) and `namespace` (the `--color-` CSS namespace) are separate options. |
126
+
127
+ The option semantics and defaults are documented once, in [Output formats](./output.md).
128
+
129
+ ## Prefix behavior
130
+
131
+ By default every token is prefixed with its theme name:
132
+
133
+ ```ts
134
+ palette.tokens();
135
+ // → {
136
+ // light: {
137
+ // 'primary-surface': 'oklch(0.97 0.0128 280)',
138
+ // 'primary-surface-text': 'oklch(0.5286 0.1912 280)',
139
+ // 'primary-accent': 'oklch(0.5961 0.181 280)',
140
+ // 'danger-surface': 'oklch(0.97 0.0129 23)',
141
+ // …
142
+ // },
143
+ // dark: { … },
144
+ // }
145
+ ```
146
+
147
+ A custom map renames some prefixes; any theme not listed falls back to `"<themeName>-"`:
148
+
149
+ ```ts
150
+ palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
151
+ // → 'brand-surface', 'error-surface', 'success-surface', …
152
+ ```
153
+
154
+ Disable prefixing:
155
+
156
+ ```ts
157
+ palette.tokens({ prefix: false });
158
+ ```
159
+
160
+ With `prefix: false`, themes that share color names collide; see the next section.
161
+
162
+ ## Collision detection
163
+
164
+ When two themes produce the same output key — through `prefix: false`, a custom prefix map, or the primary's unprefixed aliases — the **first-written value wins** and Glaze logs a `console.warn` for every skipped token:
165
+
166
+ ```text
167
+ glaze: token "surface" from theme "danger" collides with theme
168
+ "primary" — skipping.
169
+ ```
170
+
171
+ Write order is theme insertion order. For each theme, its prefixed tokens are written first and, if it is the primary, its unprefixed aliases immediately after (labeled `"<name> (primary)"` in the warning). So a primary alias wins over a later theme that emits the same unprefixed name.
172
+
173
+ > [!TIP]
174
+ > With `prefix: false`, also pass `primary: false` (or create the palette without a primary). Otherwise the primary's alias pass collides with the primary's own unprefixed tokens and warns once per token.
175
+
176
+ ## Primary theme aliases
177
+
178
+ The primary theme's tokens are duplicated without a prefix:
179
+
180
+ ```ts
181
+ const palette = glaze.palette(
182
+ { primary, danger, success },
183
+ { primary: 'primary' },
184
+ );
185
+
186
+ palette.tokens();
187
+ // → {
188
+ // light: {
189
+ // 'primary-surface': 'oklch(0.97 0.0128 280)',
190
+ // …
191
+ // surface: 'oklch(0.97 0.0128 280)', // unprefixed alias
192
+ // 'surface-text': 'oklch(0.5286 0.1912 280)',
193
+ // accent: 'oklch(0.5961 0.181 280)',
194
+ // 'danger-surface': 'oklch(0.97 0.0129 23)',
195
+ // …
196
+ // },
197
+ // …
198
+ // }
199
+ ```
200
+
201
+ Override the primary per export:
202
+
203
+ ```ts
204
+ // danger's tokens get the unprefixed aliases
205
+ palette.tokens({ primary: 'danger' });
206
+ // no aliases
207
+ palette.tokens({ primary: false });
208
+ ```
209
+
210
+ The alias works with any prefix mode. With a custom map, the primary's tokens are still duplicated without a prefix:
211
+
212
+ ```ts
213
+ palette.tokens({ prefix: { primary: 'p-', danger: 'd-' } });
214
+ // → 'p-surface' + 'surface' (alias) + 'd-surface' + 'success-surface'
215
+ ```
216
+
217
+ The alias applies to `tokens`, `tasty`, `css`, `dtcg`, `dtcgResolver`, and `tailwind` — not `json`.
218
+
219
+ ## Palette output methods
220
+
221
+ ### `palette.tokens()`
222
+
223
+ Same shape as [`theme.tokens()`](./output.md#tokensoptions), with every theme merged into each variant map.
224
+
225
+ ### `palette.tasty()`
226
+
227
+ Same shape as [`theme.tasty()`](./output.md#tastyoptions), with every theme's `#<prefix><name>` keys in one object. State aliases come from the per-call `states` option, then the global config.
228
+
229
+ ### `palette.json()`
230
+
231
+ Groups by theme name instead of prefixing, so there is nothing to collide:
232
+
233
+ ```ts
234
+ palette.json();
235
+ // → {
236
+ // primary: {
237
+ // surface: {
238
+ // light: 'oklch(0.97 0.0128 280)',
239
+ // dark: 'oklch(0.279 0.1015 280)',
240
+ // },
241
+ // …
242
+ // },
243
+ // danger: {
244
+ // surface: {
245
+ // light: 'oklch(0.97 0.0129 23)',
246
+ // dark: 'oklch(0.279 0.0791 23)',
247
+ // },
248
+ // …
249
+ // },
250
+ // success: { … },
251
+ // }
252
+ ```
253
+
254
+ Accepts only `format` and `modes` (`GlazeJsonOptions`): no `prefix`, and no primary alias. The per-theme entries have the [`theme.json()`](./output.md#jsonoptions) shape, `lightHdr` and the other HDR keys included.
255
+
256
+ ### `palette.css()`
257
+
258
+ Accepts `GlazePaletteCssOptions`: `format`, `suffix`, `splitHue`, and `hdr` from [`theme.css()`](./output.md#cssoptions) (without its `name`) plus `GlazePaletteExportOptions`. It has no `modes` option: all four strings (`light`, `dark`, `lightContrast`, `darkContrast`) are always returned, each holding every theme's declarations joined by newlines. When HDR output is on, `hdr` holds the four HDR blocks, merged the same way.
259
+
260
+ ```ts
261
+ const css = palette.css();
262
+ const stylesheet = `
263
+ :root { ${css.light} }
264
+ @media (prefers-color-scheme: dark) {
265
+ :root { ${css.dark} }
266
+ }
267
+ `;
268
+ ```
269
+
270
+ ### `palette.dtcg()`
271
+
272
+ Accepts `GlazeDtcgOptions` plus `GlazePaletteExportOptions`. Prefix defaults to `true` and the palette `primary` is honored:
273
+
274
+ ```ts
275
+ palette.dtcg();
276
+ // → {
277
+ // light: {
278
+ // 'primary-surface': {
279
+ // $type: 'color',
280
+ // $value: {
281
+ // colorSpace: 'srgb',
282
+ // components: […],
283
+ // hex: '#f3f4fe',
284
+ // },
285
+ // },
286
+ // …
287
+ // // unprefixed alias
288
+ // surface: { …, $value: { …, hex: '#f3f4fe' } },
289
+ // …
290
+ // 'danger-surface': { …, $value: { …, hex: '#fef2f1' } },
291
+ // …
292
+ // },
293
+ // dark: { … },
294
+ // }
295
+ ```
296
+
297
+ ### `palette.dtcgResolver()`
298
+
299
+ Same document as [`theme.dtcgResolver()`](./output.md#dtcgresolveroptions), with every theme (prefixed, plus primary aliases) merged into the single `sets.base` source and into each `scheme` context. Accepts `GlazeDtcgResolverOptions` plus `GlazePaletteExportOptions`.
300
+
301
+ ```ts
302
+ palette.dtcgResolver();
303
+ // → {
304
+ // version: '2025.10',
305
+ // sets: {
306
+ // base: {
307
+ // sources: [
308
+ // {
309
+ // 'primary-surface': {…},
310
+ // …
311
+ // surface: {…},
312
+ // …
313
+ // 'danger-surface': {…},
314
+ // …
315
+ // },
316
+ // ],
317
+ // },
318
+ // },
319
+ // modifiers: {
320
+ // scheme: {
321
+ // default: 'light',
322
+ // contexts: { light: [], dark: [{ … }] },
323
+ // },
324
+ // },
325
+ // resolutionOrder: [
326
+ // { $ref: '#/sets/base' },
327
+ // { $ref: '#/modifiers/scheme' },
328
+ // ],
329
+ // }
330
+ ```
331
+
332
+ ### `palette.tailwind()`
333
+
334
+ All themes merge into one `@theme` block plus one block per override, so every color is reachable as a Tailwind utility. Accepts `GlazeTailwindOptions` plus `GlazePaletteExportOptions`.
335
+
336
+ ```css
337
+ /* palette.tailwind() */
338
+ @theme {
339
+ --color-primary-surface: oklch(0.97 0.0128 280);
340
+ /* … */
341
+ --color-surface: oklch(0.97 0.0128 280); /* unprefixed alias */
342
+ /* … */
343
+ --color-danger-surface: oklch(0.97 0.0129 23);
344
+ /* … */
345
+ }
346
+ .dark {
347
+ --color-primary-surface: oklch(0.279 0.1015 280);
348
+ /* … */
349
+ }
350
+ ```
351
+
352
+ ## `splitHue` in palettes
353
+
354
+ `palette.css({ splitHue: true })` and `palette.tasty({ splitHue: true })` follow the [`splitHue` rules](./output.md#hue-channel-splitting-splithue) per theme, with two differences:
355
+
356
+ - The theme-level hue var is named after the theme (`--primary-hue`, `--danger-hue`); there is no `name` option.
357
+ - Per-color hue vars use the theme's prefix (`--primary-accent-hue`). The primary's unprefixed aliases reference those same vars instead of declaring their own, so they never collide with another theme's hue vars.
358
+
359
+ The [name-collision check](./output.md#variable-names) spans the whole palette. Without a prefix, a color that sets its own hue declares `--<color>-hue`, which is another theme's hue var when the color is named after that theme. The export throws rather than let one override the other:
360
+
361
+ ```text
362
+ glaze: splitHue would declare "--accent-hue" twice: for color "accent"'s
363
+ own hue (theme "brand") and for the hue of theme "accent".
364
+ One would silently override the other.
365
+ Rename the color or the theme, or give the theme a prefix that keeps its
366
+ hue vars apart.
367
+ ```
368
+
369
+ In `css()` the check also covers each color's value property, the primary's unprefixed aliases included. With `suffix: '-hue'`, the primary alias of a color named `ui` lands on the hue var of a theme named `ui`:
370
+
371
+ ```text
372
+ glaze: splitHue would declare "--ui-hue" twice: for the primary alias of
373
+ color "ui" (theme "brand") and for the hue of theme "ui".
374
+ One would silently override the other.
375
+ Rename the color or the theme, or pass a different `suffix` or theme
376
+ prefix.
377
+ ```
378
+
379
+ ```css
380
+ /* palette.css({ splitHue: true }).light
381
+ primary: 'primary', every theme pastel */
382
+ --primary-hue: 280;
383
+ --primary-accent-hue: calc(var(--primary-hue) + 20);
384
+ --primary-surface-color: oklch(0.97 0.0084 var(--primary-hue));
385
+ --primary-accent-color: oklch(0.5961 0.0608 var(--primary-accent-hue));
386
+ --surface-color: oklch(0.97 0.0084 var(--primary-hue));
387
+ --accent-color: oklch(0.5961 0.0608 var(--primary-accent-hue));
388
+ --danger-hue: 23;
389
+ --danger-accent-hue: calc(var(--danger-hue) + 20);
390
+ --danger-surface-color: oklch(0.97 0.0084 var(--danger-hue));
391
+ --danger-accent-color: oklch(0.5961 0.0608 var(--danger-accent-hue));
392
+ ```
393
+
394
+ That output comes from two pastel themes:
395
+
396
+ ```ts
397
+ const primary = glaze(280, 60, { pastel: true });
398
+ primary.colors({
399
+ surface: { tone: 97 },
400
+ accent: { tone: 52, hue: '+20' },
401
+ });
402
+ const danger = primary.extend({ hue: 23 });
403
+
404
+ glaze
405
+ .palette({ primary, danger }, { primary: 'primary' })
406
+ .css({ splitHue: true });
407
+ ```
408
+
409
+ ## HDR in palettes
410
+
411
+ With `modes.hdr` (for `css()`, its `hdr` option), palette output carries the [HDR values](../hdr.mdx) of glows past SDR white, in the [shapes `theme` output uses](./output.md#hdr-in-each-exporter):
412
+
413
+ - HDR entries are built from the same prefixed, collision-filtered maps as the SDR ones, plus the primary alias pass. Prefix maps, `primary: false`, and first-write-wins collisions apply unchanged: a token skipped as a collision loses its HDR entry too.
414
+ - The dense blocks of `tokens()`, `css()`, and `tailwind()` span the whole palette: every token with an HDR value in any emitted scheme, from any theme.
415
+ - `palette.css()` merges each theme's HDR blocks into `result.hdr`, which is present only when HDR output is on.
416
+ - `palette.json()` still groups by theme name, with each color's sparse `lightHdr` / … keys.
417
+
418
+ ```ts
419
+ const brand = glaze(280, 80, { glowTuning: { headroom: 1 } });
420
+ brand.colors({
421
+ surface: { tone: 100 },
422
+ 'surface-glow': { type: 'glow', bg: 'surface', intensity: 12 },
423
+ });
424
+ const lit = glaze.palette(
425
+ { brand, danger: brand.extend({ hue: 23 }) },
426
+ { primary: 'brand' },
427
+ );
428
+
429
+ lit.tokens({ modes: { hdr: true } }).lightHdr;
430
+ // → {
431
+ // 'brand-surface-glow': 'color(srgb 1.1813 1.1813 1.1813)',
432
+ // 'surface-glow': 'color(srgb 1.1813 1.1813 1.1813)',
433
+ // 'danger-surface-glow': 'color(srgb 1.1813 1.1813 1.1813)',
434
+ // }
435
+ ```