@tenphi/cookbook 0.32.0 → 0.33.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.
Files changed (28) hide show
  1. package/docs/customization-rules.md +7 -5
  2. package/docs/migration.md +52 -26
  3. package/docs/theme-and-components.md +32 -15
  4. package/docs/upstream/glaze/README.md +46 -26
  5. package/docs/upstream/glaze/docs/contrast.mdx +751 -0
  6. package/docs/upstream/glaze/docs/exporting.mdx +634 -0
  7. package/docs/upstream/glaze/docs/getting-started.mdx +489 -0
  8. package/docs/upstream/glaze/docs/hdr.mdx +523 -0
  9. package/docs/upstream/glaze/docs/methodology.mdx +922 -0
  10. package/docs/upstream/glaze/docs/migration.md +477 -286
  11. package/docs/upstream/glaze/docs/mixes.mdx +348 -0
  12. package/docs/upstream/glaze/docs/okhst.mdx +679 -0
  13. package/docs/upstream/glaze/docs/reference/color-definitions.md +552 -0
  14. package/docs/upstream/glaze/docs/reference/color-tokens.md +420 -0
  15. package/docs/upstream/glaze/docs/reference/configuration.md +527 -0
  16. package/docs/upstream/glaze/docs/reference/contrast.md +277 -0
  17. package/docs/upstream/glaze/docs/reference/index.md +225 -0
  18. package/docs/upstream/glaze/docs/reference/output.md +713 -0
  19. package/docs/upstream/glaze/docs/reference/palette.md +434 -0
  20. package/docs/upstream/glaze/docs/reference/theme.md +331 -0
  21. package/docs/upstream/glaze/docs/reference/utilities.md +646 -0
  22. package/docs/upstream/glaze/docs/schemes.mdx +635 -0
  23. package/docs/upstream/glaze/docs/shadows-and-glows.mdx +519 -0
  24. package/docs/upstream/manifest.json +2 -2
  25. package/package.json +3 -3
  26. package/docs/upstream/glaze/docs/api.md +0 -2021
  27. package/docs/upstream/glaze/docs/methodology.md +0 -686
  28. package/docs/upstream/glaze/docs/okhst.md +0 -207
@@ -0,0 +1,634 @@
1
+ ---
2
+ title: Exporting and integration
3
+ description: Choose an export shape for your renderer or design tool, then wire Glaze tokens into Tasty, CSS, JSON, DTCG, or Tailwind.
4
+ ---
5
+ import CssExample from '../docs-site/components/exporting/CssExample.astro';
6
+ import PrefixDemo from '../docs-site/components/exporting/PrefixDemo.astro';
7
+
8
+ Glaze resolves every color once and emits the result in whatever shape your
9
+ app or design tool reads. This page helps you pick a shape, wire it in, and
10
+ name tokens across a multi-theme palette. If you are still designing the
11
+ palette itself, start with [Designing a palette](./methodology.mdx); every
12
+ option mentioned here is listed in full in the
13
+ [Output formats](./reference/output.md) and
14
+ [Palettes](./reference/palette.md) references.
15
+
16
+ ## Snapshots and resolved output
17
+
18
+ Glaze has two kinds of "export", and they are not interchangeable:
19
+
20
+ | Kind | Methods | What it carries | Restore with |
21
+ | ------------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
22
+ | Authoring snapshot | `theme.export()`, `token.export()`, `palette.export()` | Seeds, color definitions and their relations, the effective config frozen at export time, plus `kind` and `version` | `glaze.themeFrom`, `glaze.colorFrom`, `glaze.paletteFrom` |
23
+ | Resolved output | `tasty`, `tokens`, `css`, `json`, `dtcg`, `dtcgResolver`, `tailwind` | Color strings or design-token documents for apps and tools | Nothing: it is not authoring config |
24
+
25
+ ```ts
26
+ import { glaze } from '@tenphi/glaze';
27
+
28
+ // Persist the authoring graph and rebuild it later.
29
+ const snapshot = theme.export();
30
+ // → { kind: 'theme', version: 2, hue, saturation, colors, config }
31
+ const restored = glaze.themeFrom(JSON.parse(JSON.stringify(snapshot)));
32
+
33
+ // Emit colors for the app.
34
+ const css = theme.css();
35
+ ```
36
+
37
+ A live theme, token, or palette keeps only the config fields you set on it;
38
+ the rest track `glaze.configure()` until you export, when the effective resolve
39
+ settings are frozen into the snapshot. A global `contrastLevel` is not frozen
40
+ (it is a live preference, like `modes` and `states`); see the
41
+ [merge rules](./reference/configuration.md#merge-rules). Use snapshots to store
42
+ or ship the palette definition (a theme editor, a config file, a server), and
43
+ resolved output for everything that paints pixels. Snapshot versioning and the
44
+ `kind` / `version` checks are covered in the
45
+ [reference overview](./reference/index.md#authoring-snapshots-and-versioning).
46
+
47
+ ## Choosing an export
48
+
49
+ Every method below exists on a theme and on a palette, and emits the same
50
+ resolved colors. Pick the one your renderer or tooling reads.
51
+
52
+ | Method | Output shape | Use it for |
53
+ | ------------------------ | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
54
+ | `palette.tasty()` | `{ '#name': { '': value, [state]: value } }` | The [Tasty](../../tasty/README.md) style system. One object; the scheme states are keyed inside each token. |
55
+ | `palette.tokens()` | `{ light: { name: value }, dark: { name: value }, ... }` | Most CSS-in-JS systems. Flat maps per scheme, easy to feed into a `:root` rule through your framework's globals. |
56
+ | `palette.css()` | `{ light: '--name-color: ...;', dark: '...', lightContrast: '...', darkContrast: '...' }` | Framework-free CSS and static stylesheets. CSS custom-property declarations per scheme, ready to wrap in `:root` and media queries. |
57
+ | `palette.json()` | `{ themeName: { name: { light, dark, ... } } }` | Tooling and JSON pipelines. |
58
+ | `palette.dtcg()` | `{ light: { name: { $type, $value } }, dark: { ... }, ... }` | W3C [DTCG 2025.10](https://www.designtokens.org/) `.tokens.json` files for Figma, Tokens Studio, Style Dictionary, Terrazzo, and Penpot. One document per scheme. |
59
+ | `palette.dtcgResolver()` | `{ version, sets, modifiers, resolutionOrder }` | The W3C DTCG **Resolver Module**: one document that describes every scheme as `sets` plus a `scheme` modifier with a context per scheme. For resolver tools such as Dispersa. |
60
+ | `palette.tailwind()` | `'@theme { --color-*: ... } .dark { ... } ...'` | Tailwind CSS v4. One ready-to-paste `@theme` block plus dark and high-contrast overrides. |
61
+
62
+ A few options cut across the shapes:
63
+
64
+ * **Schemes.** `tasty()`, `tokens()`, `json()`, `dtcg()`, `dtcgResolver()`, and
65
+ `tailwind()` take `modes: { dark, highContrast }` to choose which schemes
66
+ they emit; by default you get light and dark. `css()` takes no `modes` and
67
+ always returns all four strings. See [Light, dark, and high contrast](./schemes.mdx) for what each
68
+ scheme is.
69
+ * **Color format.** The CSS-string exports take `format`: `'oklch'` (the
70
+ default), `'rgb'`, or `'hsl'`. `'okhsl'` and `'okhst'` are not CSS color
71
+ spaces, so only the Tasty-shaped exports accept them (`tasty()`, and a
72
+ standalone color's `token()` / `tasty()`); `tokens()`, `json()`, `css()`, and
73
+ `tailwind()` throw.
74
+ * **DTCG color space.** `dtcg()` and `dtcgResolver()` emit structured
75
+ `$value` objects, not CSS strings, so they ignore `format`. Use `colorSpace`
76
+ instead: `'srgb'` (the default) or `'oklch'`.
77
+ * **HDR.** `modes: { hdr: true }` (`css({ hdr: true })` for `css()`) adds the
78
+ values of [HDR glows](./hdr.mdx) next to the SDR ones. It is off by
79
+ default; see [Output is opt-in](./hdr.mdx#output-is-opt-in).
80
+
81
+ Here is one theme and the CSS custom properties `theme.css()` emits for it.
82
+ Switch to the Output tab to see the declarations, wrapped the way the
83
+ [CSS custom properties](#css-custom-properties) section below wires them:
84
+
85
+ <CssExample
86
+ hue={280}
87
+ saturation={80}
88
+ colors={{
89
+ surface: { tone: 100, saturation: 0.11 },
90
+ 'surface-text': { base: 'surface', tone: 2, saturation: 0.475 },
91
+ border: { base: 'surface', tone: ['-10', '-20'], saturation: 0.175 },
92
+ 'accent-surface-text': { tone: 100, mode: 'fixed' },
93
+ 'accent-surface': {
94
+ base: 'accent-surface-text',
95
+ tone: '-1',
96
+ contrast: { apca: ['content', 'body'] },
97
+ mode: 'fixed',
98
+ },
99
+ 'accent-text': {
100
+ base: 'surface',
101
+ tone: '-1',
102
+ saturation: 1,
103
+ contrast: { apca: ['content', 'body'] },
104
+ },
105
+ }}
106
+ after={"const css = theme.css({ format: 'rgb' });\n// css.light → :root, css.dark → (prefers-color-scheme: dark)"}
107
+ caption="Each color and the rgb() value css() emits for it, in the scheme you are viewing. The Output tab lists the light and dark declarations."
108
+ />
109
+
110
+ ## Wiring exports into the app
111
+
112
+ The examples below export a palette; a single theme has the same methods, and
113
+ `prefix` / `primary` only matter for palettes. The values shown are real output
114
+ for the `default` + `danger` palette from
115
+ [Prefix map strategies](#prefix-map-strategies).
116
+
117
+ ### Tasty
118
+
119
+ `tasty()` returns [Tasty](../../tasty/README.md) color tokens: `#name` keys whose
120
+ values are state maps. Register them as global tokens:
121
+
122
+ ```ts
123
+ import { configure } from '@tenphi/tasty';
124
+
125
+ configure({ tokens: palette.tasty({ prefix: { default: '' } }) });
126
+ ```
127
+
128
+ Or spread them into a global style call:
129
+
130
+ ```ts
131
+ import type { Styles } from '@tenphi/tasty';
132
+ import { useGlobalStyles } from '@tenphi/tasty';
133
+ import { tastyStatic } from '@tenphi/tasty/static';
134
+
135
+ export const PALETTE_TOKENS = palette.tasty({
136
+ prefix: { default: '' },
137
+ }) as Styles;
138
+
139
+ // In your root component:
140
+ useGlobalStyles('body', PALETTE_TOKENS);
141
+
142
+ // Or, for zero-runtime builds:
143
+ tastyStatic('body', PALETTE_TOKENS);
144
+ ```
145
+
146
+ By default the dark and high-contrast variants are keyed by media-query states,
147
+ so the tokens follow the OS preference with no extra Tasty setup:
148
+
149
+ ```ts
150
+ palette.tasty({ prefix: { default: '' } })['#surface'];
151
+ // → {
152
+ // '': 'oklch(1 0 0)',
153
+ // '@media(prefers-color-scheme: dark)': 'oklch(0.2534 0.0081 280)',
154
+ // }
155
+ ```
156
+
157
+ To drive the schemes from your own state aliases instead (for example a manual
158
+ toggle that falls back to the OS preference), set
159
+ [`glaze.configure({ states })`](./reference/output.md#state-aliases) and register
160
+ what those states mean in Tasty:
161
+
162
+ ```ts
163
+ import { setGlobalPredefinedStates } from '@tenphi/tasty';
164
+
165
+ glaze.configure({ states: { dark: '@dark', highContrast: '@hc' } });
166
+
167
+ setGlobalPredefinedStates({
168
+ '@dark':
169
+ '@root(schema=dark) | ' +
170
+ '(!@root(schema) & @media(prefers-color-scheme: dark))',
171
+ '@hc':
172
+ '@root(contrast=high) | ' +
173
+ '(!@root(contrast) & @media(prefers-contrast: more))',
174
+ });
175
+ ```
176
+
177
+ The state names must match the `states` you set in `glaze.configure()`. To
178
+ override them for one export only, pass `states` to the call:
179
+ `palette.tasty({ states: { dark: '@dark', highContrast: '@hc' } })`. With
180
+ `modes: { highContrast: true }` each token then carries four keys: `''`,
181
+ `'@dark'`, `'@hc'`, and `'@dark & @hc'`.
182
+
183
+ You can also register the tokens as a Tasty recipe instead of spreading them
184
+ globally:
185
+
186
+ ```ts
187
+ import { configure, tasty } from '@tenphi/tasty';
188
+
189
+ configure({
190
+ recipes: { 'theme-tokens': palette.tasty({ prefix: { default: '' } }) },
191
+ });
192
+
193
+ const Page = tasty({
194
+ styles: {
195
+ recipe: 'theme-tokens',
196
+ fill: '#surface',
197
+ color: '#surface-text',
198
+ },
199
+ });
200
+ ```
201
+
202
+ ### CSS custom properties
203
+
204
+ `css()` returns one string of declarations per scheme. Wrap them in the rules
205
+ that select each scheme:
206
+
207
+ ```ts
208
+ const css = palette.css({ prefix: { default: '' } });
209
+ const stylesheet = `
210
+ :root { ${css.light} }
211
+ @media (prefers-color-scheme: dark) { :root { ${css.dark} } }
212
+ @media (prefers-contrast: more) { :root { ${css.lightContrast} } }
213
+ @media (prefers-color-scheme: dark) and (prefers-contrast: more) {
214
+ :root { ${css.darkContrast} }
215
+ }
216
+ `;
217
+ ```
218
+
219
+ Keep that order: the dark high-contrast block comes last so it wins over both
220
+ the dark and the light high-contrast blocks.
221
+
222
+ Each property is named `--<prefix><name>-color` by default. Change the suffix
223
+ or the color format per call:
224
+
225
+ ```ts
226
+ palette.css({ prefix: { default: '' }, suffix: '' });
227
+ // → '--surface: oklch(1 0 0);'
228
+ palette.css({ prefix: { default: '' }, format: 'rgb' });
229
+ // → '--surface-color: rgb(255 255 255);'
230
+ ```
231
+
232
+ ### JSON
233
+
234
+ ```ts
235
+ const data = palette.json();
236
+ // → {
237
+ // default: {
238
+ // surface: {
239
+ // light: 'oklch(1 0 0)',
240
+ // dark: 'oklch(0.2534 0.0081 280)',
241
+ // },
242
+ // ...
243
+ // },
244
+ // danger: {
245
+ // surface: {
246
+ // light: 'oklch(0.9602 0.0148 23)',
247
+ // dark: 'oklch(0.287 0.065 23)',
248
+ // },
249
+ // ...
250
+ // },
251
+ // }
252
+ ```
253
+
254
+ Each color is grouped by theme name, then token name, then scheme, so there is
255
+ no prefix logic to undo: `json()` takes only `modes` and `format`. It has no
256
+ `prefix` option and does not emit the `primary` alias.
257
+
258
+ ### W3C DTCG
259
+
260
+ `dtcg()` returns one [Design Tokens](https://www.designtokens.org/) document
261
+ per scheme. Write each to its own file:
262
+
263
+ ```ts
264
+ import { writeFileSync } from 'node:fs';
265
+
266
+ const dtcg = palette.dtcg({ prefix: { default: '' } });
267
+
268
+ writeFileSync('light.tokens.json', JSON.stringify(dtcg.light, null, 2));
269
+ if (dtcg.dark) {
270
+ writeFileSync('dark.tokens.json', JSON.stringify(dtcg.dark, null, 2));
271
+ }
272
+ ```
273
+
274
+ Each document is a spec-conformant token tree:
275
+
276
+ ```json
277
+ {
278
+ "surface-text": {
279
+ "$type": "color",
280
+ "$value": {
281
+ "colorSpace": "srgb",
282
+ "components": [0.096174, 0.101248, 0.179183],
283
+ "hex": "#191a2e"
284
+ }
285
+ }
286
+ }
287
+ ```
288
+
289
+ One file per scheme is the most tool-compatible convention: Style Dictionary
290
+ treats the files as themes, Tokens Studio as sets, and Figma as variable modes.
291
+ Feed them straight into Style Dictionary v4+, Tokens Studio, or any
292
+ DTCG-compatible tool; no Glaze-specific transform is needed.
293
+
294
+ * The default `colorSpace: 'srgb'` emits gamma sRGB components in 0–1 plus a
295
+ `hex` hint that every reader understands.
296
+ * `colorSpace: 'oklch'` emits `[L, C, H]` components and no `hex`: wide-gamut,
297
+ Glaze-native values.
298
+ * Translucent colors (shadows, glows, transparent mixes, `opacity`) carry an
299
+ `alpha` field; opaque ones omit it.
300
+ * A scheme you didn't ask for is `undefined`, hence the `if (dtcg.dark)` check.
301
+
302
+ ### W3C DTCG Resolver Module
303
+
304
+ When you want **one file** that describes every scheme, for a resolver tool
305
+ such as [Dispersa](https://github.com/dispersa-core/dispersa), use
306
+ `dtcgResolver()` instead of `dtcg()`:
307
+
308
+ ```ts
309
+ import { writeFileSync } from 'node:fs';
310
+
311
+ const resolver = palette.dtcgResolver({ modes: { highContrast: true } });
312
+
313
+ writeFileSync('resolver.json', JSON.stringify(resolver, null, 2));
314
+ ```
315
+
316
+ ```ts
317
+ // → {
318
+ // version: '2025.10',
319
+ // sets: { base: { sources: [ /* light tokens */ ] } },
320
+ // modifiers: {
321
+ // scheme: {
322
+ // default: 'light',
323
+ // contexts: {
324
+ // light: [],
325
+ // dark: [ /* dark tokens */ ],
326
+ // lightContrast: [ /* light high-contrast tokens */ ],
327
+ // darkContrast: [ /* dark high-contrast tokens */ ],
328
+ // },
329
+ // },
330
+ // },
331
+ // resolutionOrder: [
332
+ // { $ref: '#/sets/base' },
333
+ // { $ref: '#/modifiers/scheme' },
334
+ // ],
335
+ // }
336
+ ```
337
+
338
+ The light tokens are the base set (`sets.base.sources[0]`) and the modifier's
339
+ default context, so the `light` context is empty. Every other context holds
340
+ that scheme's exact resolved tokens. Glaze resolves `darkContrast`
341
+ independently, which is why it gets its own context: two independent modifiers
342
+ (dark, high contrast) would compose additively and produce wrong dark
343
+ high-contrast values.
344
+
345
+ Rename the set, the modifier, or the contexts with `setName`, `modifierName`,
346
+ and `contextNames` if your resolver expects other labels. Prefer `dtcg()` for
347
+ the widest per-file tool support, and `dtcgResolver()` when a resolver tool
348
+ consumes the document for you.
349
+
350
+ ### Tailwind CSS v4
351
+
352
+ ```ts
353
+ const css = palette.tailwind({ prefix: { default: '' } });
354
+ // Write it to your CSS entry, next to `@import "tailwindcss";`
355
+ ```
356
+
357
+ ```css
358
+ @theme {
359
+ --color-surface: oklch(1 0 0);
360
+ --color-surface-text: oklch(0.2263 0.0385 280);
361
+ /* ... */
362
+ }
363
+
364
+ .dark {
365
+ --color-surface: oklch(0.2534 0.0081 280);
366
+ --color-surface-text: oklch(0.9394 0.0137 280);
367
+ /* ... */
368
+ }
369
+ ```
370
+
371
+ The `--color-*` namespace makes every color available as `bg-*`, `text-*`,
372
+ `border-*`, and the other color utilities. Change it with `namespace` (for
373
+ example `namespace: ''` emits `--surface`); it is separate from the palette's
374
+ theme `prefix`.
375
+
376
+ To follow the OS preference instead of a class, pass a media query as
377
+ `darkSelector`; Glaze nests `:root` inside it:
378
+
379
+ ```ts
380
+ palette.tailwind({
381
+ prefix: { default: '' },
382
+ darkSelector: '@media (prefers-color-scheme: dark)',
383
+ });
384
+ // @media (prefers-color-scheme: dark) {
385
+ // :root {
386
+ // --color-surface: oklch(0.2534 0.0081 280);
387
+ // ...
388
+ // }
389
+ // }
390
+ ```
391
+
392
+ High-contrast overrides are omitted unless `modes.highContrast` is on. They
393
+ land under `.high-contrast` and `.dark.high-contrast` (both classes on the same
394
+ element) by default; change the class with `highContrastSelector`, which also
395
+ accepts a media query such as `'@media (prefers-contrast: more)'`.
396
+
397
+ ## Prefix map strategies
398
+
399
+ When a palette merges several themes into one namespace, the `prefix` option
400
+ decides how each theme's tokens are named. It applies to `tokens()`, `tasty()`,
401
+ `css()`, `dtcg()`, `dtcgResolver()`, and `tailwind()` (`json()` groups by theme
402
+ instead):
403
+
404
+ | Value | Result |
405
+ | ------------------------ | ------------------------------------------------------------------------------------------ |
406
+ | `true` (default) | Every theme prefixes its tokens with `<themeName>-`. |
407
+ | `false` | No prefixes. Colliding names warn and the first theme wins; see [Collisions](#collisions). |
408
+ | `Record<string, string>` | Per-theme prefixes. Themes not listed fall back to `<themeName>-`. |
409
+
410
+ Below, one palette exported three ways. Each column lists the names
411
+ `palette.tokens()` actually returns for that call:
412
+
413
+ <PrefixDemo
414
+ themes={{
415
+ default: {
416
+ hue: 280,
417
+ saturation: 80,
418
+ colors: {
419
+ surface: { tone: 100, saturation: 0.11 },
420
+ 'surface-text': {
421
+ base: 'surface',
422
+ tone: 2,
423
+ saturation: 0.475,
424
+ inherit: false,
425
+ },
426
+ 'accent-text': {
427
+ base: 'surface',
428
+ tone: '-1',
429
+ saturation: 1,
430
+ contrast: { apca: ['content', 'body'] },
431
+ },
432
+ },
433
+ },
434
+ danger: {
435
+ extends: 'default',
436
+ hue: 23,
437
+ colors: { surface: { tone: 96, saturation: 0.8 } },
438
+ },
439
+ }}
440
+ calls={[{}, { prefix: { default: '' } }, { primary: 'default' }]}
441
+ caption="Same palette, three prefix settings. surface-text is inherit: false, so the danger theme doesn't emit it."
442
+ />
443
+
444
+ The most common production pattern is a **default theme without a prefix and
445
+ every other theme prefixed with its name**:
446
+
447
+ ```ts
448
+ palette.tasty({
449
+ prefix: {
450
+ default: '',
451
+ primary: 'primary-',
452
+ success: 'success-',
453
+ danger: 'danger-',
454
+ warning: 'warning-',
455
+ note: 'note-',
456
+ },
457
+ });
458
+ ```
459
+
460
+ Neutral tokens are then consumed as `#surface`, `#border`, and
461
+ `#disabled-surface` (no theme namespace), while status colors live under
462
+ `#danger-surface`, `#success-accent-text`, and so on. Listing every theme
463
+ spells the namespace out; since unlisted themes fall back to `<themeName>-`,
464
+ `{ default: '' }` alone emits the same names.
465
+
466
+ An explicit map like this is usually the clearest choice for a palette with a
467
+ neutral `default` theme. The separate [`primary`](#the-primary-alias) option
468
+ solves a different problem: it duplicates one named theme without a prefix
469
+ while keeping that theme's prefixed tokens too.
470
+
471
+ ### Alias themes for legacy names
472
+
473
+ Register the same theme instance under two palette names to get identical
474
+ token values under two prefixes. This supports a legacy token name without
475
+ duplicating any definitions:
476
+
477
+ ```ts
478
+ const palette = glaze.palette({
479
+ default: defaultTheme,
480
+ primary: primaryTheme,
481
+ purple: primaryTheme, // legacy alias: same theme, different prefix
482
+ });
483
+
484
+ palette.tasty({
485
+ prefix: {
486
+ default: '',
487
+ primary: 'primary-',
488
+ purple: 'purple-', // emits #purple-surface alongside #primary-surface
489
+ },
490
+ });
491
+ ```
492
+
493
+ Both `#primary-surface` and `#purple-surface` resolve to exactly the same
494
+ color. Drop the alias once nothing references the legacy name.
495
+
496
+ <PrefixDemo
497
+ themes={{
498
+ default: {
499
+ hue: 280,
500
+ saturation: 80,
501
+ colors: {
502
+ surface: { tone: 100, saturation: 0.11 },
503
+ 'accent-text': {
504
+ base: 'surface',
505
+ tone: '-1',
506
+ saturation: 1,
507
+ contrast: { apca: ['content', 'body'] },
508
+ },
509
+ },
510
+ },
511
+ primary: {
512
+ extends: 'default',
513
+ colors: { surface: { tone: 96, saturation: 0.8 } },
514
+ },
515
+ purple: { alias: 'primary' },
516
+ }}
517
+ calls={[
518
+ { prefix: { default: '', primary: 'primary-', purple: 'purple-' } },
519
+ ]}
520
+ caption="The purple- names repeat the primary- colors exactly: both come from one theme instance."
521
+ />
522
+
523
+ ### The `primary` alias
524
+
525
+ `glaze.palette(themes, { primary })`, and the per-export `primary` option,
526
+ duplicate one theme's tokens **without** a prefix on top of whatever the prefix
527
+ setting produces. It is equivalent to listing that theme twice with different
528
+ prefixes, and it is useful when the primary theme is conceptually distinct
529
+ from `default` and you want its tokens both namespaced and short.
530
+
531
+ ```ts
532
+ const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
533
+
534
+ palette.tokens();
535
+ // → {
536
+ // light: {
537
+ // 'brand-surface': '...',
538
+ // surface: '...',
539
+ // 'accent-surface': '...',
540
+ // ...
541
+ // },
542
+ // ...
543
+ // }
544
+ ```
545
+
546
+ Override it per call with `palette.tokens({ primary: 'accent' })`, or turn it
547
+ off with `palette.tokens({ primary: false })`. A `primary` that names no theme
548
+ in the palette throws.
549
+
550
+ ### Collisions
551
+
552
+ When two themes produce the same output name, whether through `prefix: false`,
553
+ a prefix map that gives two themes the same prefix, or a `primary` alias that
554
+ lands on names another theme already emits unprefixed, the **first-written
555
+ value wins** and Glaze warns about the rest:
556
+
557
+ ```text
558
+ glaze: token "surface" from theme "danger" collides with theme
559
+ "default" — skipping.
560
+ ```
561
+
562
+ Themes are written in palette order. A frequent cause is combining
563
+ `prefix: { default: '' }` with `primary`: both emit unprefixed names, so the
564
+ primary's alias collides with the default theme
565
+ (`token "surface" from theme "danger (primary)" collides with theme "default"`).
566
+ Choose one of the two patterns for the namespace your components expect.
567
+
568
+ ## Hue channels with `splitHue`
569
+
570
+ `splitHue: true` emits each hue as its own custom property and references it
571
+ with `var()` inside the colors, so you can re-skin the palette at runtime by
572
+ changing a single number:
573
+
574
+ ```ts
575
+ const brand = glaze(240, 18, { pastel: true });
576
+
577
+ brand.colors({
578
+ surface: { tone: 97 },
579
+ accent: { tone: 55, hue: '+20', saturation: 1 },
580
+ });
581
+
582
+ brand.css({ splitHue: true, name: 'brand' }).light;
583
+ ```
584
+
585
+ ```css
586
+ --brand-hue: 240;
587
+ --accent-hue: calc(var(--brand-hue) + 20);
588
+ --surface-color: oklch(0.97 0.0025 var(--brand-hue));
589
+ --accent-color: oklch(0.6175 0.0189 var(--accent-hue));
590
+ ```
591
+
592
+ The colors are declared on `:root`, and a custom property resolves `var()` on
593
+ the element that declares it, so set the hue there:
594
+
595
+ ```ts
596
+ document.documentElement.style.setProperty('--brand-hue', '150');
597
+ ```
598
+
599
+ In practice:
600
+
601
+ * **Pastel only.** Every exported color must be pastel (`pastel: true` on the
602
+ theme, the token, or the color), so its chroma stays inside sRGB at any hue.
603
+ Otherwise the export throws and names the non-pastel colors.
604
+ * **`oklch` only.** The hue slot is native CSS `var()` only in `oklch()`; with
605
+ another `format` the export throws.
606
+ * **One name per hue variable.** A color that sets its own hue gets
607
+ `--<color>-hue`, so it can't share its name with the theme hue (`name`), or,
608
+ in an unprefixed palette, with another theme. `css()` value properties share
609
+ the namespace, so `suffix: '-hue'` (or `suffix: ''` with a color named
610
+ `*-hue`) collides too. The export throws and names both sides rather than
611
+ let one silently override the other. A color that is gray in light declares
612
+ no hue variable, so it is exempt.
613
+ * **Palettes name the hue after the theme.** `palette.css({ splitHue: true })`
614
+ emits `--brand-hue`, `--brand-accent-hue`, and so on for a theme named
615
+ `brand`; a `primary` alias references the same hue variables.
616
+ * **Dark hues re-declare the set.** When the theme or a color has a
617
+ [dark hue](./schemes.mdx#dark-seed), the dark block repeats every hue
618
+ variable the light block declares, with its dark value. A color that is
619
+ gray in light stays a literal `oklch()` in every scheme.
620
+ * **Tasty** gets the hue variables as `$brand-hue`-style keys from
621
+ `tasty({ splitHue: true })`.
622
+
623
+ `splitHue` is available on `css()` and `tasty()` for themes and palettes, and on
624
+ a standalone color's `css()`. The full rules, including which colors stay
625
+ inline, are in the
626
+ [Output formats](./reference/output.md#hue-channel-splitting-splithue) reference.
627
+
628
+ ## Next steps
629
+
630
+ * [Migration and upgrading](./migration.md): move an existing color system to
631
+ Glaze, keep its token names, and upgrade between major versions.
632
+ * [Output formats](./reference/output.md): every output method and option.
633
+ * [Palettes](./reference/palette.md): palette options, prefix
634
+ behavior, and primary aliases.