@tenphi/glaze 0.0.0-snapshot.0fcd6cf → 0.0.0-snapshot.12fd135
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/AGENTS.md +29 -28
- package/README.md +2 -2
- package/dist/index.cjs +604 -180
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +98 -34
- package/dist/index.d.mts +98 -34
- package/dist/index.mjs +602 -181
- package/dist/index.mjs.map +1 -1
- package/docs/exporting.mdx +2 -218
- package/docs/hdr.mdx +523 -0
- package/docs/methodology.mdx +2 -3
- package/docs/migration.md +53 -16
- package/docs/mixes.mdx +48 -5
- package/docs/okhst.mdx +2 -5
- package/docs/reference/color-definitions.md +13 -15
- package/docs/reference/color-tokens.md +11 -7
- package/docs/reference/configuration.md +28 -11
- package/docs/reference/contrast.md +4 -4
- package/docs/reference/index.md +7 -5
- package/docs/reference/output.md +31 -151
- package/docs/reference/palette.md +4 -4
- package/docs/reference/theme.md +10 -10
- package/docs/reference/utilities.md +40 -23
- package/docs/schemes.mdx +4 -5
- package/docs/shadows-and-glows.mdx +7 -225
- package/package.json +2 -2
package/docs/hdr.mdx
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: HDR glows
|
|
3
|
+
description: Glows past SDR white for HDR displays — when Glaze adds an HDR value, how to opt in, and where it can be shown today.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
import Example from '../docs-site/components/Example.astro';
|
|
7
|
+
import GlowsPastWhite from '../docs-site/components/shadows-and-glows/GlowsPastWhite.tsx';
|
|
8
|
+
import { Preview } from '@tenphi/cookbook/components';
|
|
9
|
+
import { hdrHaloPreview } from '../docs-site/components/shadows-and-glows/hdr-preview.ts';
|
|
10
|
+
|
|
11
|
+
export const hdrTheme = {
|
|
12
|
+
hue: 280,
|
|
13
|
+
saturation: 80,
|
|
14
|
+
config: { glowTuning: { headroom: 1 } },
|
|
15
|
+
colors: {
|
|
16
|
+
surface: { tone: 100 },
|
|
17
|
+
'surface-glow': { type: 'glow', bg: 'surface', intensity: 12 },
|
|
18
|
+
near: { tone: 95, saturation: 0, mode: 'static' },
|
|
19
|
+
'near-glow': { type: 'glow', bg: 'near', intensity: 12 },
|
|
20
|
+
inverse: { tone: 12, saturation: 0, mode: 'static' },
|
|
21
|
+
'inverse-glow': { type: 'glow', bg: 'inverse', intensity: 12 },
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const haloTheme = {
|
|
26
|
+
hue: 280,
|
|
27
|
+
saturation: 80,
|
|
28
|
+
config: { glowTuning: { headroom: 1 } },
|
|
29
|
+
colors: {
|
|
30
|
+
page: { tone: 92, saturation: 0.1 },
|
|
31
|
+
card: { base: 'page', tone: '+6', saturation: 0.1 },
|
|
32
|
+
'card-glow': { type: 'glow', bg: 'page', fg: 'card', intensity: 20 },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const haloPreview = hdrHaloPreview(haloTheme, {
|
|
37
|
+
page: 'page',
|
|
38
|
+
card: 'card',
|
|
39
|
+
glow: 'card-glow',
|
|
40
|
+
blur: 40,
|
|
41
|
+
spread: 4,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
A [glow](./shadows-and-glows.mdx) can't go past white in SDR, but an HDR
|
|
45
|
+
display can show brighter than white. Set `glowTuning.headroom`, and a glow
|
|
46
|
+
whose step would pass white also resolves an HDR value: an opaque exposure of
|
|
47
|
+
what SDR already paints there. The SDR value doesn't change, and exporters add
|
|
48
|
+
the HDR one only when you [opt in](#output-is-opt-in). Everything outside this
|
|
49
|
+
page is SDR; skip it unless you target HDR displays.
|
|
50
|
+
|
|
51
|
+
## When a glow gets an HDR value
|
|
52
|
+
|
|
53
|
+
[Tone runs on past 100](./okhst.mdx#tone-above-100) on the same curve: one
|
|
54
|
+
stop above SDR white (`Y` 2) is tone 121.98, two stops (`Y` 4) tone 144.34,
|
|
55
|
+
and 12 tones past white is the same `Y + 0.05` ratio as a 12-tone step
|
|
56
|
+
anywhere. WCAG and APCA end at white, so Glaze makes no contrast claim above
|
|
57
|
+
100. A glow can't be a `bg`, `fg`, `base`, or mix input, so an HDR value never
|
|
58
|
+
reaches the contrast solver.
|
|
59
|
+
|
|
60
|
+
A glow gets an HDR value when three things hold: its `headroom` is above 0,
|
|
61
|
+
its target tone (the surface's luminance tone plus `intensity × k`) passes
|
|
62
|
+
100, and SDR is pinned at its brightest. That means the pigment at full
|
|
63
|
+
opacity, or a white surface (tone 100) at opacity 0, since no pigment can lift
|
|
64
|
+
it. Below white nothing changes, dark-scheme glows included. A pigment that
|
|
65
|
+
can't lift a non-white surface at all (`reach: 0` on a gray surface, say)
|
|
66
|
+
paints nothing in SDR, gets no HDR value, and keeps the SDR warning. On a
|
|
67
|
+
chromatic surface a `reach: 0` pigment, a gray at the surface's tone, can be
|
|
68
|
+
slightly brighter than the surface: it then pins at full opacity and, past
|
|
69
|
+
white, gets an HDR value like any pinned pigment.
|
|
70
|
+
|
|
71
|
+
The resolved variant gains `hdr: { h, s, t, exposure }`. The base (`h`, `s`,
|
|
72
|
+
`t`) is the opaque color SDR paints, with a standard (never pastel) OKHSL
|
|
73
|
+
saturation. The HDR value is that base's linear light times `2^exposure`, the
|
|
74
|
+
gain that lands the target tone exactly; `variant.t` itself stays at most 1. The value is
|
|
75
|
+
opaque on purpose: it stays exact whichever way it is composited, and it maps
|
|
76
|
+
one to one onto native exposure APIs.
|
|
77
|
+
|
|
78
|
+
## Headroom
|
|
79
|
+
|
|
80
|
+
`headroom` counts stops above SDR white, the doublings of its luminance. It
|
|
81
|
+
defaults to 0 (no HDR anywhere) and must be a finite number ≥ 0, or Glaze
|
|
82
|
+
throws. Set it globally (`glaze.configure({ glowTuning: { headroom: 1 } })`),
|
|
83
|
+
in a theme's config override, on one glow's `tuning`, or on
|
|
84
|
+
`glaze.glow()`. A glow's `tuning` merges over `glowTuning` field by field. A
|
|
85
|
+
theme's own `glowTuning` replaces the global one whole, and so does a later
|
|
86
|
+
`glaze.configure({ glowTuning })`, so pass every field you want kept:
|
|
87
|
+
`glaze.configure({ glowTuning: { objectInfluence: 0.6, headroom: 1 } })`. A
|
|
88
|
+
`headroom` on a shadow throws: nothing goes below black.
|
|
89
|
+
|
|
90
|
+
Values above 2 clamp to 2. The warning fires once per field and value, and
|
|
91
|
+
only after the whole input has validated:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
glaze: glowTuning.headroom 4 is above the 2-stop maximum; using 2.
|
|
95
|
+
Headroom counts stops (doublings of SDR white's luminance), not a
|
|
96
|
+
ratio: 1 stop is 2× white, 2 stops 4×.
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The gain is capped per channel, so no linear channel of the HDR value passes
|
|
100
|
+
`2^headroom`. On a neutral glow that caps luminance: a 30-tone glow on white
|
|
101
|
+
with 1 stop of headroom ends at `color(srgb 1.3533 1.3533 1.3533)` (linear 2
|
|
102
|
+
per channel), tone 121.98, and warns (below). A saturated base hits the cap on
|
|
103
|
+
its brightest channel first, so its HDR tone can stay below 100.
|
|
104
|
+
|
|
105
|
+
How much of that a display shows depends on its headroom at the moment, which
|
|
106
|
+
falls as the user turns brightness up. Past it, most platforms clip the
|
|
107
|
+
color, which can move a tinted glow toward white, while Apple's platforms
|
|
108
|
+
tone-map it. A web page can't read the number. Android suggests about 1.5–2×
|
|
109
|
+
white for interfaces that are mostly SDR, so 1 stop is a sensible ceiling for
|
|
110
|
+
UI.
|
|
111
|
+
|
|
112
|
+
With headroom, a glow whose target passes white no longer logs the
|
|
113
|
+
[unreachable-step warning](./shadows-and-glows.mdx#unreachable-steps): its HDR
|
|
114
|
+
value lands the step. Only the headroom cap can leave such a step short, and
|
|
115
|
+
that logs its own warning. A 30-tone glow on white with 1 stop of headroom:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
glaze: glow "surface-flare" cannot reach a 30.0-tone step in light
|
|
119
|
+
scheme even with 1 stop of headroom (reached 22.0).
|
|
120
|
+
Lower the intensity or raise `headroom`.
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
At the 2-stop maximum the second sentence reads "Lower the intensity;
|
|
124
|
+
`headroom` is already at its 2-stop maximum." A glow without an HDR value
|
|
125
|
+
keeps the SDR warning.
|
|
126
|
+
|
|
127
|
+
## Worked example
|
|
128
|
+
|
|
129
|
+
Three 12-tone glows: one on a white `auto` surface, one on a light gray, and
|
|
130
|
+
one on a dark gray.
|
|
131
|
+
|
|
132
|
+
<Example
|
|
133
|
+
{...hdrTheme}
|
|
134
|
+
visual="none"
|
|
135
|
+
after={
|
|
136
|
+
"const glow = theme.resolve().get('surface-glow')!.light;\n" +
|
|
137
|
+
'glaze.formatHdr(glow);\n' +
|
|
138
|
+
"// → 'color(srgb 1.1813 1.1813 1.1813)'"
|
|
139
|
+
}
|
|
140
|
+
/>
|
|
141
|
+
|
|
142
|
+
<GlowsPastWhite
|
|
143
|
+
{...hdrTheme}
|
|
144
|
+
glows={['surface-glow', 'near-glow', 'inverse-glow']}
|
|
145
|
+
caption="Each glow over its surface, in your current scheme, as CSS paints it: the step it reached, the step asked, and its opacity. The HDR values are in the table below."
|
|
146
|
+
/>
|
|
147
|
+
|
|
148
|
+
The same numbers in every scheme:
|
|
149
|
+
|
|
150
|
+
| Glow | SDR | HDR |
|
|
151
|
+
| --- | --- | --- |
|
|
152
|
+
| `surface-glow`, light and light HC | White at α 0 | 1.1813 per channel (linear 1.4631), 0.549 stops, tone 112 |
|
|
153
|
+
| `surface-glow`, dark and dark HC | White at α 0.1237 (α 0.1603 in dark HC) | None: the dark surface has room |
|
|
154
|
+
| `near-glow`, all four | Opaque white, 5 of 12 tones | 1.1026 per channel (linear 1.2494), 0.321 stops, tone 107 |
|
|
155
|
+
| `inverse-glow`, all four | White at α 0.1155 | None: the target is tone 24 |
|
|
156
|
+
|
|
157
|
+
`near-glow` shows the split: SDR pins it to opaque white 7 tones short, and
|
|
158
|
+
its HDR value lands those 7. In dark, `surface` sits at tone 9.25 (luminance
|
|
159
|
+
tone 8.5, so the glow's target is tone 20.5), and `surface-glow` is an
|
|
160
|
+
ordinary translucent glow there.
|
|
161
|
+
|
|
162
|
+
## Crossing white
|
|
163
|
+
|
|
164
|
+
A full-reach glow meets SDR at the boundary. On a gray tone-95 surface (like
|
|
165
|
+
`near`) a 4.9-tone glow is white at α 0.9794 and a 5-tone glow is opaque
|
|
166
|
+
white, both without an HDR value. At 5.1 tones it adds
|
|
167
|
+
`color(srgb 1.0014 1.0014 1.0014)`, 0.0046 stops.
|
|
168
|
+
|
|
169
|
+
A glow with `reach` below 1 doesn't meet it. Its pigment sits below white, so
|
|
170
|
+
it stays short and warns until its target passes 100, then its HDR value lands
|
|
171
|
+
the whole step at once. A `reach: 0.5` glow on a gray tone-90 surface
|
|
172
|
+
paints tone 95 for any step from 5 to 10. Past 10, its HDR value jumps straight to
|
|
173
|
+
tone 100 (0.2315 stops over the pigment). Keep `reach` at 1 on a glow that
|
|
174
|
+
may cross white.
|
|
175
|
+
|
|
176
|
+
## Extended sRGB
|
|
177
|
+
|
|
178
|
+
Glaze writes an HDR value as `color(srgb R G B)` with channels past 1: the
|
|
179
|
+
sRGB curve continued past SDR white, so `1` is white and one stop above it is
|
|
180
|
+
`1.3533`. `glaze.formatHdr(variant, 'srgb-linear')` writes the same color in
|
|
181
|
+
linear light (`color(srgb-linear 1.4631 1.4631 1.4631)` for `surface-glow`),
|
|
182
|
+
and a browser converts either one into the other exactly. The format doesn't
|
|
183
|
+
follow `format`: CSS clamps `oklch()` lightness at 1, and `rgb()` / `hsl()`
|
|
184
|
+
can't go past white.
|
|
185
|
+
|
|
186
|
+
That is all an HDR glow needs. Its value is its SDR base made brighter, so it
|
|
187
|
+
stays inside the sRGB gamut and is measured against SDR white, the way
|
|
188
|
+
displays and native APIs measure headroom. The `rec2100-pq` and
|
|
189
|
+
`rec2100-hlg` spaces from the CSS Color HDR draft would add a conversion, to
|
|
190
|
+
wider primaries and to an HDR transfer curve pinned to an assumed SDR white,
|
|
191
|
+
with nothing to gain for a color like this, and browsers don't support them
|
|
192
|
+
yet.
|
|
193
|
+
|
|
194
|
+
The same numbers are what most HDR APIs take as they are: a WebGPU `'srgb'`
|
|
195
|
+
canvas, Apple's extended sRGB color space, and Android's `EXTENDED_SRGB`. For
|
|
196
|
+
numbers instead of a string, `hdrToSrgb(variant.hdr)` returns these channels,
|
|
197
|
+
and `hdrToLinearSrgb(variant.hdr)` the linear ones, for APIs that take linear
|
|
198
|
+
light.
|
|
199
|
+
|
|
200
|
+
## Output is opt-in
|
|
201
|
+
|
|
202
|
+
Resolving fills in `variant.hdr` whenever headroom allows. Exporters leave it
|
|
203
|
+
out until you turn on `modes.hdr`, so every export is byte-identical until
|
|
204
|
+
then, headroom or not. Turn it on for the export that needs it:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
const theme = glaze(280, 80, { glowTuning: { headroom: 1 } });
|
|
208
|
+
|
|
209
|
+
theme.colors({
|
|
210
|
+
surface: { tone: 100 },
|
|
211
|
+
'surface-glow': { type: 'glow', bg: 'surface', intensity: 12 },
|
|
212
|
+
near: { tone: 95, saturation: 0, mode: 'static' },
|
|
213
|
+
'near-glow': { type: 'glow', bg: 'near', intensity: 12 },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
theme.dtcg({ modes: { hdr: true } });
|
|
217
|
+
theme.css({ hdr: true }); // css() has no `modes`
|
|
218
|
+
// Or for every export:
|
|
219
|
+
glaze.configure({ modes: { hdr: true } });
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Each export then adds the HDR value next to the SDR value it already emits,
|
|
223
|
+
for the schemes the other `modes` include. `surface-glow` has one in the light
|
|
224
|
+
schemes only; on the dark surface its step stays below white. `near-glow` has
|
|
225
|
+
one in all four. HDR values stay literal under `splitHue`, palettes treat them
|
|
226
|
+
like any other entry (prefixes, the `primary` alias, collisions), and
|
|
227
|
+
`glaze.color()` tokens are never glows, so the HDR options don't change their
|
|
228
|
+
output.
|
|
229
|
+
|
|
230
|
+
The data exports (DTCG, `json()`, `tokens()`) are what native apps and
|
|
231
|
+
WebGPU code read. The CSS exports (`css()`, `tasty()`, Tailwind) are
|
|
232
|
+
experimental: no browser paints them past white yet
|
|
233
|
+
([Browsers](#browsers)), and their shape may change once one does. Prefer
|
|
234
|
+
turning HDR on per call, so a setup that only needs DTCG doesn't add the CSS
|
|
235
|
+
tier to every stylesheet.
|
|
236
|
+
|
|
237
|
+
### CSS blocks
|
|
238
|
+
|
|
239
|
+
`css()` takes `hdr` (default: the global `modes.hdr`) and returns four more
|
|
240
|
+
blocks in `hdr`. Wire each one after the SDR blocks, under the same selector
|
|
241
|
+
plus `(dynamic-range: high)`:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const css = theme.css({ hdr: true });
|
|
245
|
+
const hdr = css.hdr!;
|
|
246
|
+
const dark = '(prefers-color-scheme: dark)';
|
|
247
|
+
const more = '(prefers-contrast: more)';
|
|
248
|
+
const high = '(dynamic-range: high)';
|
|
249
|
+
|
|
250
|
+
const stylesheet = `
|
|
251
|
+
:root { ${css.light} }
|
|
252
|
+
@media ${dark} { :root { ${css.dark} } }
|
|
253
|
+
@media ${more} { :root { ${css.lightContrast} } }
|
|
254
|
+
@media ${dark} and ${more} { :root { ${css.darkContrast} } }
|
|
255
|
+
|
|
256
|
+
@media ${high} { :root { ${hdr.light} } }
|
|
257
|
+
@media ${dark} and ${high} { :root { ${hdr.dark} } }
|
|
258
|
+
@media ${more} and ${high} { :root { ${hdr.lightContrast} } }
|
|
259
|
+
@media ${dark} and ${more} and ${high} {
|
|
260
|
+
:root { ${hdr.darkContrast} }
|
|
261
|
+
}
|
|
262
|
+
`;
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The blocks are dense. Each lists every color that has an HDR value in any
|
|
266
|
+
scheme, and repeats a color's SDR declaration where its own scheme has none:
|
|
267
|
+
|
|
268
|
+
```css
|
|
269
|
+
/* hdr.light */
|
|
270
|
+
--surface-glow-color: color(srgb 1.1813 1.1813 1.1813);
|
|
271
|
+
--near-glow-color: color(srgb 1.1026 1.1026 1.1026);
|
|
272
|
+
|
|
273
|
+
/* hdr.dark */
|
|
274
|
+
--surface-glow-color: oklch(1 0 0 / 0.1237);
|
|
275
|
+
--near-glow-color: color(srgb 1.1026 1.1026 1.1026);
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
On a dark HDR display, both the light and the dark HDR blocks match. The dark
|
|
279
|
+
one comes later, so its `surface-glow` line puts the dark SDR value back. A
|
|
280
|
+
block that listed only HDR values would let the light one leak into dark.
|
|
281
|
+
When no glow passes white, all four blocks are empty strings.
|
|
282
|
+
|
|
283
|
+
### Tasty keys
|
|
284
|
+
|
|
285
|
+
`tasty()` adds a key under the `states.hdr` alias (default
|
|
286
|
+
`'@media(dynamic-range: high)'`) right after each SDR key that has an HDR
|
|
287
|
+
value:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
theme.tasty({ modes: { hdr: true } })['#surface-glow'];
|
|
291
|
+
// → {
|
|
292
|
+
// '': 'oklch(1 0 0 / 0)',
|
|
293
|
+
// '@media(dynamic-range: high)': 'color(srgb 1.1813 1.1813 1.1813)',
|
|
294
|
+
// '@media(prefers-color-scheme: dark)': 'oklch(1 0 0 / 0.1237)',
|
|
295
|
+
// }
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Tasty gives later keys priority, so the order is what keeps the light HDR
|
|
299
|
+
value out of dark: `''`, hdr, dark, dark & hdr, hc, hc & hdr, dark & hc,
|
|
300
|
+
dark & hc & hdr, with only the keys that exist. Don't sort the keys if you
|
|
301
|
+
post-process the map. With your own aliases, set `states.hdr` next to `dark`
|
|
302
|
+
and `highContrast` (for example `'@hdr'`, which emits keys such as
|
|
303
|
+
`'@dark & @hdr'`) and register it in Tasty like the others.
|
|
304
|
+
|
|
305
|
+
### Tailwind blocks
|
|
306
|
+
|
|
307
|
+
`tailwind()` appends the same dense blocks after all its SDR blocks, under
|
|
308
|
+
`hdrSelector` (default `'@media (dynamic-range: high)'`), around
|
|
309
|
+
`darkSelector` and `highContrastSelector`:
|
|
310
|
+
|
|
311
|
+
```css
|
|
312
|
+
/* ...the @theme and .dark blocks, unchanged... */
|
|
313
|
+
|
|
314
|
+
@media (dynamic-range: high) {
|
|
315
|
+
:root {
|
|
316
|
+
--color-surface-glow: color(srgb 1.1813 1.1813 1.1813);
|
|
317
|
+
--color-near-glow: color(srgb 1.1026 1.1026 1.1026);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@media (dynamic-range: high) {
|
|
322
|
+
.dark {
|
|
323
|
+
--color-surface-glow: oklch(1 0 0 / 0.1237);
|
|
324
|
+
--color-near-glow: color(srgb 1.1026 1.1026 1.1026);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### JSON and tokens
|
|
330
|
+
|
|
331
|
+
`json()` adds a sparse `<scheme>Hdr` key after each scheme that has an HDR
|
|
332
|
+
value. `tokens()` adds a dense `<scheme>Hdr` map after each scheme it emits,
|
|
333
|
+
always present and possibly empty:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
theme.json({ modes: { hdr: true } })['surface-glow'];
|
|
337
|
+
// → {
|
|
338
|
+
// light: 'oklch(1 0 0 / 0)',
|
|
339
|
+
// lightHdr: 'color(srgb 1.1813 1.1813 1.1813)',
|
|
340
|
+
// dark: 'oklch(1 0 0 / 0.1237)',
|
|
341
|
+
// }
|
|
342
|
+
|
|
343
|
+
theme.tokens({ modes: { hdr: true } }).darkHdr;
|
|
344
|
+
// → {
|
|
345
|
+
// 'surface-glow': 'oklch(1 0 0 / 0.1237)',
|
|
346
|
+
// 'near-glow': 'color(srgb 1.1026 1.1026 1.1026)',
|
|
347
|
+
// }
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
A dense map drops into a cascade the way the CSS blocks do; the sparse JSON
|
|
351
|
+
keys suit code that reads one color at a time. Both carry only the string,
|
|
352
|
+
whose numbers are the [extended sRGB](#extended-srgb) channels. Apple's
|
|
353
|
+
exposure initializers take a base and an exposure, which only DTCG carries.
|
|
354
|
+
|
|
355
|
+
### DTCG
|
|
356
|
+
|
|
357
|
+
`dtcg()` and `dtcgResolver()` keep `$value` SDR: DTCG color components run
|
|
358
|
+
from 0 to 1. The HDR value goes in `$extensions`, only in the scheme
|
|
359
|
+
documents that have one. This is `light['surface-glow']`;
|
|
360
|
+
`dark['surface-glow']` has no `$extensions`:
|
|
361
|
+
|
|
362
|
+
```json
|
|
363
|
+
{
|
|
364
|
+
"$type": "color",
|
|
365
|
+
"$value": {
|
|
366
|
+
"colorSpace": "srgb",
|
|
367
|
+
"components": [1, 1, 1],
|
|
368
|
+
"hex": "#ffffff",
|
|
369
|
+
"alpha": 0
|
|
370
|
+
},
|
|
371
|
+
"$extensions": {
|
|
372
|
+
"me.tenphi.glaze": {
|
|
373
|
+
"hdr": {
|
|
374
|
+
"exposure": 0.548987,
|
|
375
|
+
"tone": 112,
|
|
376
|
+
"base": {
|
|
377
|
+
"colorSpace": "srgb",
|
|
378
|
+
"components": [1, 1, 1],
|
|
379
|
+
"hex": "#ffffff"
|
|
380
|
+
},
|
|
381
|
+
"value": {
|
|
382
|
+
"colorSpace": "srgb",
|
|
383
|
+
"components": [1.181265, 1.181265, 1.181265]
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
- `base` is the opaque SDR color the exposure applies to: always a DTCG sRGB
|
|
392
|
+
color with `hex`, whatever `colorSpace` you export, and never pastel.
|
|
393
|
+
- `exposure` is the gain over `base` in stops.
|
|
394
|
+
- `value` is `base × 2^exposure` in linear light, written as extended sRGB:
|
|
395
|
+
the numbers of the CSS string, to 6 decimals. Only its brightest component is guaranteed above 1.
|
|
396
|
+
- `tone` is the HDR value's tone: past 100 when the headroom covers the step,
|
|
397
|
+
though a saturated base that the cap stops early can land below.
|
|
398
|
+
|
|
399
|
+
Style Dictionary and Terrazzo keep `$extensions`, but their built-in color
|
|
400
|
+
transforms expect components from 0 to 1, so read the extension with a
|
|
401
|
+
transform of your own. [Native apps](#native-apps) maps each field.
|
|
402
|
+
|
|
403
|
+
## Outside a theme
|
|
404
|
+
|
|
405
|
+
[`glaze.glow()`](./shadows-and-glows.mdx#outside-a-theme) takes `headroom` in
|
|
406
|
+
its `tuning` and returns `hdr` when its step passes white:
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
const flare = glaze.glow({
|
|
410
|
+
bg: '#ffffff',
|
|
411
|
+
intensity: 12,
|
|
412
|
+
tuning: { headroom: 1 },
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
glaze.formatHdr(flare);
|
|
416
|
+
// → 'color(srgb 1.1813 1.1813 1.1813)'
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Where it shows
|
|
420
|
+
|
|
421
|
+
### Browsers
|
|
422
|
+
|
|
423
|
+
No browser paints a CSS color brighter than white yet. The CSS Color 4
|
|
424
|
+
spaces, `srgb` included, are SDR by design: values past 1 are valid, but they
|
|
425
|
+
are fit into SDR when painted. A neutral HDR value paints white, the same as
|
|
426
|
+
the SDR value of a glow pinned at opaque white (`near-glow`). A tinted one
|
|
427
|
+
paints a lighter, less saturated tint, or white. And on a white surface,
|
|
428
|
+
where SDR is transparent, the HDR value covers what is drawn under it
|
|
429
|
+
([Fills, not overlays](#fills-not-overlays)).
|
|
430
|
+
|
|
431
|
+
The planned way past white is the CSS Color HDR draft's `hdr-color()` (also
|
|
432
|
+
written `color-hdr()` there), which blends an SDR and an HDR color by the
|
|
433
|
+
display's headroom. No browser implements it and its name isn't settled.
|
|
434
|
+
`@media (dynamic-range: high)` reports what the device can do, not that HDR
|
|
435
|
+
is on: Low Power Mode and the brightness setting change that.
|
|
436
|
+
|
|
437
|
+
### WebGPU
|
|
438
|
+
|
|
439
|
+
A WebGPU canvas can paint past white. Chrome and Edge 129+ show an
|
|
440
|
+
`rgba16float` canvas configured with `toneMapping: { mode: 'extended' }`
|
|
441
|
+
brighter than white on an HDR display: on Windows, macOS, ChromeOS, and
|
|
442
|
+
Android, and on Linux since 144 with Intel Gen12+ GPUs only (not in Android
|
|
443
|
+
WebView). Safari 26 accepts the option, but whether it paints past white is
|
|
444
|
+
unconfirmed, so check on the device. Firefox has WebGPU without extended tone
|
|
445
|
+
mapping, so its canvases stay SDR.
|
|
446
|
+
|
|
447
|
+
The preview below is a card on a tone-92 page, its edges glowing with
|
|
448
|
+
`card-glow`, an `intensity: 20` glow from the card. In the light scheme the
|
|
449
|
+
glow's target is tone 113, past white, so its SDR value is opaque white and
|
|
450
|
+
its HDR value is `color(srgb 1.1972 1.1972 1.1972)` (0.593 stops). CSS draws
|
|
451
|
+
the halo as a `box-shadow` in the SDR value; its 40px blur fades it from about
|
|
452
|
+
58% at the card's edge. On an HDR display with HDR canvases, a WebGPU canvas
|
|
453
|
+
behind the card draws a matching halo in the HDR value instead, so the edge
|
|
454
|
+
glows brighter than the white card.
|
|
455
|
+
|
|
456
|
+
<Example {...haloTheme} visual="none" />
|
|
457
|
+
|
|
458
|
+
<Preview title="A card with an HDR glow (light scheme)" {...haloPreview} />
|
|
459
|
+
|
|
460
|
+
The canvas's default `colorSpace`, `'srgb'`, takes extended sRGB, so the
|
|
461
|
+
channels go in as Glaze writes them:
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
import { hdrToSrgb } from '@tenphi/glaze';
|
|
465
|
+
|
|
466
|
+
const { hdr } = theme.resolve().get('card-glow')!.light;
|
|
467
|
+
const [r, g, b] = hdrToSrgb(hdr!); // 1.1972 per channel
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Check the mode after `configure()`: where a browser doesn't support HDR
|
|
471
|
+
canvases, `context.getConfiguration().toneMapping?.mode` isn't `'extended'`,
|
|
472
|
+
and Chrome 129 and 130 have no `getConfiguration()`. `'extended'` means only
|
|
473
|
+
that the browser supports HDR canvases, not that the screen shows them, so
|
|
474
|
+
the preview also checks `(dynamic-range: high)` before it replaces the CSS
|
|
475
|
+
halo. A page can't read the display's headroom, so keep `headroom` modest.
|
|
476
|
+
|
|
477
|
+
### Native apps
|
|
478
|
+
|
|
479
|
+
Read the [DTCG export](#dtcg), which carries every field these take.
|
|
480
|
+
|
|
481
|
+
- **Apple** (iOS, iPadOS, macOS, tvOS, visionOS 26):
|
|
482
|
+
`UIColor(red:green:blue:alpha:exposure:)`, or its `NSColor` twin, with
|
|
483
|
+
`base.components` and `exposure`, reproduces Glaze's value. In SwiftUI,
|
|
484
|
+
apply `.exposureAdjust(exposure)` to a `Color(.sRGB, …)` made from `base`.
|
|
485
|
+
A view shows it brighter than white only once its layer opts in: set
|
|
486
|
+
`layer.preferredDynamicRange` to `.automatic`, `.constrainedHigh`, or
|
|
487
|
+
`.high`, since the default `.standard` tone-maps it into SDR. Where the
|
|
488
|
+
system limits headroom it tone-maps the color, which isn't Glaze's SDR
|
|
489
|
+
value (often translucent), so keep both tokens and switch yourself.
|
|
490
|
+
- **Android** (14 and later): set the window's color mode to
|
|
491
|
+
`COLOR_MODE_HDR`, and on Android 15 its `desiredHdrHeadroom` to the
|
|
492
|
+
brightest linear channel you draw. `Color.pack(r, g, b, 1f, space)` takes
|
|
493
|
+
`value.components` as they are, with
|
|
494
|
+
`ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)` as the space; in Compose,
|
|
495
|
+
`Color(r, g, b, 1f, ColorSpaces.ExtendedSrgb)`. Int colors, `<color>`
|
|
496
|
+
resources, and Compose text colors can't carry HDR.
|
|
497
|
+
- **Windows:** Direct2D, Win2D, and other FP16 scRGB surfaces take linear
|
|
498
|
+
light where 1 is 80 nits, not SDR white. Multiply the linear channels
|
|
499
|
+
(`hdrToLinearSrgb()`, or `value.components` decoded) by the display's SDR
|
|
500
|
+
white level in nits divided by 80, at run time.
|
|
501
|
+
- Flutter and React Native: no HDR color path in their own drawing has been
|
|
502
|
+
verified, so use the SDR value there, or draw the glow in a native view
|
|
503
|
+
with the Apple or Android APIs above.
|
|
504
|
+
|
|
505
|
+
## Fills, not overlays
|
|
506
|
+
|
|
507
|
+
Where the HDR value clamps, as it does in CSS today, it paints an opaque
|
|
508
|
+
color: white, for a neutral glow. That matches SDR wherever SDR is already
|
|
509
|
+
opaque (`near-glow`). On a tone-100 surface it doesn't: SDR is transparent
|
|
510
|
+
there, and the clamped value covers anything drawn between the surface and the
|
|
511
|
+
glow. Use HDR glows for fills (a card's or button's own background), edge
|
|
512
|
+
highlights, and `box-shadow` colors, never for a state layer drawn over a
|
|
513
|
+
label.
|
|
514
|
+
|
|
515
|
+
## Power and accessibility
|
|
516
|
+
|
|
517
|
+
- HDR is an enhancement. The design must work in SDR, and it does: the SDR
|
|
518
|
+
value is what every other display already shows.
|
|
519
|
+
- Don't animate or flash HDR values. A pulsing glow past white is a large
|
|
520
|
+
luminance swing, and HDR flash thresholds now appear in the WCAG 2.3.1
|
|
521
|
+
Understanding draft and ITU-R BT.1702-3.
|
|
522
|
+
- Brighter-than-white pixels cost power, and systems fall back to SDR to save
|
|
523
|
+
it (iOS Low Power Mode does). Keep headroom low and HDR areas small.
|
package/docs/methodology.mdx
CHANGED
|
@@ -720,9 +720,8 @@ room above its surface, so a glow on a white light-mode surface has nothing to
|
|
|
720
720
|
lighten — Glaze leaves it transparent and warns, even if you only use the token
|
|
721
721
|
in dark. Give glows a surface with room in every scheme, as
|
|
722
722
|
`accent-surface-highlight` has on the mid-tone accent fill. With `headroom`
|
|
723
|
-
above 0, an [HDR glow](./
|
|
724
|
-
|
|
725
|
-
stays transparent.
|
|
723
|
+
above 0, an [HDR glow](./hdr.mdx) can carry a value past white for platforms
|
|
724
|
+
that can show it, but its standard value on white stays transparent.
|
|
726
725
|
|
|
727
726
|
### Fixed opacity and mixes
|
|
728
727
|
|
package/docs/migration.md
CHANGED
|
@@ -209,9 +209,9 @@ Then:
|
|
|
209
209
|
`config.shadowTuning`, and set `version` to `2` (a palette snapshot has one at
|
|
210
210
|
the top and one per theme), then load it and export again. A snapshot
|
|
211
211
|
without shadows passes the version check and restores, but it resolves with
|
|
212
|
-
the other 3.0 changes on this page, and one
|
|
213
|
-
|
|
214
|
-
[stricter input validation](#30--stricter-input-validation)).
|
|
212
|
+
the other 3.0 changes on this page, and one that holds a value 3.0 rejects
|
|
213
|
+
throws, on its first output or (for a token's color value) when restored
|
|
214
|
+
(see [stricter input validation](#30--stricter-input-validation)).
|
|
215
215
|
|
|
216
216
|
`glaze.shadow()` keeps its signature with the new semantics, and `glaze.glow()`
|
|
217
217
|
takes the same input. See
|
|
@@ -221,9 +221,8 @@ HDR glows are new in 3.0 and opt-in, so they need no migration. A glow
|
|
|
221
221
|
variant gets an `hdr` value only with a `headroom` above 0
|
|
222
222
|
(`glowTuning.headroom`, or a glow's own `tuning.headroom`; the default is 0),
|
|
223
223
|
and exports carry it only with `modes.hdr` (default `false`), or for `css()`
|
|
224
|
-
its `hdr` option. At the defaults nothing changes. See
|
|
225
|
-
[
|
|
226
|
-
[HDR output](./exporting.mdx#hdr-output).
|
|
224
|
+
its `hdr` option. At the defaults nothing changes. See [HDR glows](./hdr.mdx)
|
|
225
|
+
and [Output is opt-in](./hdr.mdx#output-is-opt-in).
|
|
227
226
|
|
|
228
227
|
### 3.0 — even dark steps for tone deltas
|
|
229
228
|
|
|
@@ -248,11 +247,15 @@ on its own, mirrored around wherever the base sits. See
|
|
|
248
247
|
What moves:
|
|
249
248
|
|
|
250
249
|
- **Dark variants** of `auto` tone-delta colors, and anything solved, mixed,
|
|
251
|
-
or lit from them in dark:
|
|
252
|
-
2 on a tone-50 base, and a fraction of a
|
|
253
|
-
a contrast-solved, mixed, or extreme base
|
|
254
|
-
above moves 9 tones), and so do deltas on a
|
|
255
|
-
under a custom light
|
|
250
|
+
or lit from them in dark: at the default light window, by up to about
|
|
251
|
+
4 tones on the lowest bases, about 2 on a tone-50 base, and a fraction of a
|
|
252
|
+
tone on a light surface. Deltas on a contrast-solved, mixed, or extreme base
|
|
253
|
+
move more (the `'AAA'` hover above moves 9 tones), and so do deltas on a
|
|
254
|
+
base past the dark window or under a custom light window: the move grows
|
|
255
|
+
with how far the window lifts the base, so `lightTone: [15, 100]` moves a
|
|
256
|
+
tone-50 base's deltas about 4 tones and up to 8 at the bottom, and a `hi`
|
|
257
|
+
below 100 moves a white surface's about 0.7 tones per tone it drops (about
|
|
258
|
+
3.5 at `hi` 95). A custom light `eps` moves them too. A contrast-solved delta whose 2.x dark step
|
|
256
259
|
crossed its base now lands on the side that mirrors light, which can move
|
|
257
260
|
it most of the scale: in `glaze(0, 0)`,
|
|
258
261
|
`{ base: 'fill', tone: '-1', contrast: 3 }` on a tone-50 fill goes from
|
|
@@ -359,6 +362,12 @@ you have pinned:
|
|
|
359
362
|
| A `darkDesaturation` that isn't a finite number (`NaN`, `Infinity`, `'0.5'`, `true`) | `NaN` dark colors (`NaN`), fully gray ones (`Infinity`, `true`), or a string read as a number |
|
|
360
363
|
| A relative tone in the high-contrast entry of a root color (`tone: [50, '+20']`, no `base`) | Read `'+20'` as tone 20. Now throws: add a `base`, or write the tone you got (`[50, 20]`) |
|
|
361
364
|
| A hex color with a non-hex digit (`'#1z2z3z'`) in `glaze.color()`, `glaze.fromHex()`, a color's `from`, or the `bg` / `fg` of `glaze.shadow()` | Read only the first digit of each pair (`#010203`) |
|
|
365
|
+
| A color-function component that isn't a number (`'hsl(abc 50% 50%)'`, `'rgb(Infinity 0 0)'`) | `NaN` colors, or a silent gray |
|
|
366
|
+
| A value object with a negative `s`, `l`, `t`, or `c` (`{ l: 0.5, c: -0.05, h: 30 }`) | A plausible color: a negative chroma turned the hue around |
|
|
367
|
+
| A malformed `contrast`: a misspelled preset (`'aa'`, `{ apca: 'Body' }`), `NaN`, `{}`, both `wcag` and `apca`, or a pair without two entries | A `TypeError` from the warning code, or a color pinned to black or white; both keys used APCA, and a longer pair its first two entries |
|
|
368
|
+
| A color field that isn't a finite number (`tone: NaN`, `saturation: NaN`), a relative string that isn't `'+N'` / `'-N'` (`hue: 'abc'`, `tone: '+-5'`, `tone: '+5px'`), an `opacity` outside 0–1, or a non-finite mix `value` or shadow `intensity` | `NaN` colors, or an alpha outside 0–1; a trailing unit was read past (`'+5px'` as `'+5'`) |
|
|
369
|
+
| A theme seed whose hue or saturation isn't a finite number (`glaze({ hue: 280 })` without `saturation`, `glaze.fromRgb(NaN, 0, 0)`) | `NaN` colors |
|
|
370
|
+
| `null` for `contrastLevel` or `darkDesaturation` | Ignored: the current value stayed |
|
|
362
371
|
|
|
363
372
|
```text
|
|
364
373
|
glaze: lightTone lo (90) is above hi (20).
|
|
@@ -368,14 +377,42 @@ A window runs from its dark end to its light end: [lo, hi] with lo ≤ hi,
|
|
|
368
377
|
glaze: color "a" has relative "tone" without "base".
|
|
369
378
|
|
|
370
379
|
glaze: invalid hex color "#1z2z3z".
|
|
380
|
+
|
|
381
|
+
glaze: color "text" has an invalid "contrast" "aa". Use a WCAG ratio or
|
|
382
|
+
'AA' / 'AAA' / 'AA-large' / 'AAA-large', { wcag: … }, { apca: … }, or a
|
|
383
|
+
[normal, highContrast] pair of these.
|
|
371
384
|
```
|
|
372
385
|
|
|
373
386
|
`configure()` throws at the call and leaves the config as it was. A theme or
|
|
374
387
|
token override (`glaze(hue, saturation, config)`, `glaze.color(value, config)`,
|
|
375
388
|
`extend({ config })`) is checked when it resolves, so it throws on the first
|
|
376
|
-
output or `resolve()
|
|
377
|
-
`glaze.themeFrom()` /
|
|
378
|
-
|
|
389
|
+
output or `resolve()`, and so are a theme's colors. A theme or palette
|
|
390
|
+
snapshot that holds such a value still restores with `glaze.themeFrom()` /
|
|
391
|
+
`paletteFrom()` and throws on its first output, and so does a token snapshot
|
|
392
|
+
whose frozen config holds one. A token snapshot whose color value (its input,
|
|
393
|
+
or a raw `base`) 3.0 rejects throws in `glaze.colorFrom()` itself, as
|
|
394
|
+
`glaze.color()` does, and so does a theme snapshot whose seed does. Fix the
|
|
395
|
+
value in the JSON and export again.
|
|
396
|
+
|
|
397
|
+
The exported `parseHex()` / `parseHexAlpha()` return `null` for such hex, as
|
|
398
|
+
their type always allowed; 2.x read the first digit of each pair, and
|
|
399
|
+
`'#-1-2-3'` gave negative channels. Check for `null` before using the result.
|
|
400
|
+
|
|
401
|
+
Color strings also change in two ways that move colors instead of throwing. A
|
|
402
|
+
hue in `turn`, `rad`, or `grad` is converted (2.x read `hsl(0.5turn 50% 50%)`
|
|
403
|
+
as hue 0.5°, not 180°), and the CSS keyword `none` reads as 0. Out-of-range
|
|
404
|
+
color-function values clamp as CSS clamps them, which moves an `rgb()` channel
|
|
405
|
+
outside 0–255, an `okhsl()` / `okhst()` saturation outside 0–100%, a negative
|
|
406
|
+
`okhsl()` lightness (2.x gave `NaN` or a wrong tone), a negative `oklch()`
|
|
407
|
+
chroma (2.x turned its hue around), and an `oklch()` lightness below about
|
|
408
|
+
−0.37 (2.x emitted `NaN`). A saturation past 100% moves a token in dark, and
|
|
409
|
+
one outside 0–100% moves a theme color's `from` in every scheme: 2.x painted
|
|
410
|
+
its light variant out of gamut, which could turn the hue
|
|
411
|
+
(`'okhsl(200 -20% 50%)'` came out reddish), and colors solved, mixed, or lit
|
|
412
|
+
from it move with it. An `hsl()` or
|
|
413
|
+
`oklch()` lightness past its range already rendered clamped. A value object's
|
|
414
|
+
0–1 field between 1 and 1.5 clamps to 1 too, which moves a saturation there
|
|
415
|
+
the same way.
|
|
379
416
|
|
|
380
417
|
Two out-of-range values clamp instead, which can move output: window endpoints
|
|
381
418
|
clamp to 0–100 (2.x extended a `[-10, 120]` window past the range), and
|
|
@@ -501,5 +538,5 @@ Colors used to be authored on OKHSL lightness. They are now authored on
|
|
|
501
538
|
| `glaze: formatOkhsl() got a value above 1 …` | Pre-2.0 percentage-scale input to a `format*` writer (a leftover `* 100`). | Pass the 0–1 factors the converters return; see [2.0 — `format*` takes 0–1](#20--format-takes-01). |
|
|
502
539
|
| `glaze: color "X" cannot meet WCAG … in light scheme (got …)` | The contrast target is physically unreachable for the color's hue and saturation against its base. The color pins to the extreme of the side it searched first, which may not be the closer one; see [When a floor can't be met](./contrast.mdx#when-a-floor-cant-be-met). | Lower the floor or change the base. If only the high-contrast schemes fail, give `contrast` an explicit, reachable high-contrast entry (`['AA', 'AA']`). On standalone colors, set `name` so the warning is identifiable; see [`name`](./reference/color-tokens.md#name). |
|
|
503
540
|
| `glaze: mix "X" cannot meet WCAG … against its base in light scheme (got …)` | Even the pure target doesn't reach the floor against the mix's `base`. The mix pins to the pure target, or to the pure base when its `value` is already 100; see [Contrast on mixes](./mixes.mdx#contrast-on-mixes). | Mix toward a target further from the base, or lower the floor. If only the high-contrast schemes fail, give `contrast` an explicit, reachable high-contrast entry. Keep `value` below 100 when you set a floor. |
|
|
504
|
-
| `glaze: shadow "X" cannot reach a 10.0-tone step …` (or `glow`) | The surface has fewer tones of room than `intensity` asks for: a glow on white, a deep shadow on near-black, or any shadow on a light `auto` page in dark high contrast (at or near tone 0 there). The overlay is pinned to its strongest composite (transparent when the surface is already at the pigment's extreme). | Lower `intensity` or give the overlay a surface with room in every scheme, and use glows for dark elevation. Otherwise expect the warning: every color resolves in all four schemes, so not using the token in a scheme doesn't silence it. A glow with `headroom` gets an HDR value past white instead and warns only when the cap leaves the step short (`… even with 1 stop of headroom (reached …)`): lower `intensity` or raise `headroom`. See [Unreachable steps](./shadows-and-glows.mdx#unreachable-steps). |
|
|
505
|
-
| `glaze.themeFrom: this snapshot is export version 1 (Glaze 2) …` | A 2.x snapshot with shadow colors or `shadowTuning`; shadows changed meaning in 3.0. | Re-author the shadows and export again; see [3.0 — shadows and glows](#30--shadows-and-glows). |
|
|
541
|
+
| `glaze: shadow "X" cannot reach a 10.0-tone step …` (or `glow`) | The surface has fewer tones of room than `intensity` asks for: a glow on white, a deep shadow on near-black, or any shadow on a light `auto` page in dark high contrast (at or near tone 0 there). The overlay is pinned to its strongest composite (transparent when the surface is already at the pigment's extreme). | Lower `intensity` or give the overlay a surface with room in every scheme, and use glows for dark elevation. Otherwise expect the warning: every color resolves in all four schemes, so not using the token in a scheme doesn't silence it. A glow with `headroom` gets an HDR value past white instead and warns only when the cap leaves the step short (`… even with 1 stop of headroom (reached …)`): lower `intensity` or raise `headroom`. See [Unreachable steps](./shadows-and-glows.mdx#unreachable-steps) and [Headroom](./hdr.mdx#headroom). |
|
|
542
|
+
| `glaze.themeFrom: this snapshot is export version 1 (Glaze 1.1–2) …` | A 2.x snapshot with shadow colors or `shadowTuning`; shadows changed meaning in 3.0. | Re-author the shadows and export again; see [3.0 — shadows and glows](#30--shadows-and-glows). |
|