@tenphi/glaze 0.15.1 → 0.16.1
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 +3 -0
- package/dist/index.cjs +834 -89
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +453 -7
- package/dist/index.d.mts +453 -7
- package/dist/index.mjs +825 -90
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +257 -14
- package/docs/methodology.md +437 -222
- package/docs/migration.md +70 -5
- package/docs/okhst.md +45 -2
- package/package.json +1 -1
package/docs/methodology.md
CHANGED
|
@@ -1,346 +1,561 @@
|
|
|
1
1
|
# Methodology
|
|
2
2
|
|
|
3
|
-
A practical
|
|
3
|
+
A practical way to design a Glaze palette without fighting dark mode. Start from
|
|
4
|
+
tone relationships, add contrast only where a role needs a readable floor, and
|
|
5
|
+
let `extend()` carry the same decisions across status hues.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
A Glaze palette is a **default neutral theme** plus a small fan of **colored sibling themes** (`success`, `danger`, `warning`, `note`, …) created via `extend()`. Most colors live in the default theme as neutrals; brand-tinted colors come from `extend()` swapping the hue.
|
|
8
|
-
|
|
9
|
-
The default theme is what most components consume — its tokens are emitted unprefixed (`#surface`, `#border`). Colored themes are scoped to status surfaces and accent variants, emitted with a theme-name prefix (`#success-surface`, `#danger-accent-surface`).
|
|
10
|
-
|
|
11
|
-
You design the default theme once, and `extend()` propagates that design across every status hue.
|
|
12
|
-
|
|
13
|
-
Every color definition has an **`inherit`** flag (default: `true`) controlling whether it flows into child themes via `extend()`. Set `inherit: false` to scope a color to its parent theme only — this is how sibling themes stay lean, carrying only the tokens they actually need.
|
|
7
|
+
For the full API surface, see [api.md](api.md). For the color model details, see
|
|
8
|
+
[okhst.md](okhst.md).
|
|
14
9
|
|
|
15
|
-
##
|
|
10
|
+
## The mental model
|
|
16
11
|
|
|
17
|
-
|
|
12
|
+
Glaze palettes work best as one default neutral theme plus a few colored sibling
|
|
13
|
+
themes:
|
|
14
|
+
|
|
15
|
+
- `default` owns the neutral system most components consume: `#surface`,
|
|
16
|
+
`#surface-text`, `#border`, `#disabled-surface`, and so on.
|
|
17
|
+
- Status themes (`success`, `danger`, `warning`, `note`, ...) are created with
|
|
18
|
+
`extend()`. They swap the hue and keep only the inherited tokens that should
|
|
19
|
+
become status-aware.
|
|
20
|
+
- `inherit: false` keeps a token local to the parent theme. Use it for neutral
|
|
21
|
+
ladders, shadows, code colors, overlays, and anything that should not be
|
|
22
|
+
repeated for every status hue.
|
|
23
|
+
|
|
24
|
+
The main simplification is OKHST tone. Dark mode is now a single inversion
|
|
25
|
+
(`100 - t`) plus a scheme window, so relative tone shifts stay consistent between
|
|
26
|
+
light and dark. A token that is `-4` tone from its base is the same kind of step
|
|
27
|
+
in both schemes.
|
|
28
|
+
|
|
29
|
+
## Authoring Rules
|
|
30
|
+
|
|
31
|
+
Use this order when defining a token:
|
|
32
|
+
|
|
33
|
+
1. Pick the base it visually belongs to.
|
|
34
|
+
2. Use `tone` for visual distance: surface ladders, soft chips, disabled states,
|
|
35
|
+
hover ramps, and other low-stakes relationships.
|
|
36
|
+
3. Add `contrast` only when readability or recognizability needs a measured
|
|
37
|
+
floor.
|
|
38
|
+
4. Use APCA presets for content-like colors:
|
|
39
|
+
`contrast: { apca: 'content' }` or
|
|
40
|
+
`contrast: { apca: ['content', 'body'] }`.
|
|
41
|
+
5. Use WCAG numbers or presets when you specifically need a ratio target:
|
|
42
|
+
`contrast: 4.5`, `contrast: 'AAA'`, or `contrast: { wcag: [4.5, 7] }`.
|
|
43
|
+
6. Let token names infer APCA roles. Names ending in `text`, `label`, `border`,
|
|
44
|
+
`surface`, `fill`, `bg`, and similar aliases already tell Glaze which side is
|
|
45
|
+
foreground or background. Set `role` only when a name is ambiguous.
|
|
46
|
+
7. Add high-contrast pairs only where HC should intentionally tighten:
|
|
47
|
+
text/content contrast, border tone, shadow intensity, mix value, or similar.
|
|
48
|
+
|
|
49
|
+
`mode: 'auto'` is the default and should be the default choice. Use
|
|
50
|
+
`mode: 'fixed'` for brand fills or inverse surfaces that must stay recognizable
|
|
51
|
+
instead of tone-inverting. Use `mode: 'static'` only when the exact authored tone
|
|
52
|
+
must render in every scheme.
|
|
53
|
+
|
|
54
|
+
## Seed and Configure
|
|
55
|
+
|
|
56
|
+
Keep hue decisions named and configure output modes once:
|
|
18
57
|
|
|
19
58
|
```ts
|
|
20
|
-
const PURPLE_HUE
|
|
59
|
+
const PURPLE_HUE = 280.3;
|
|
21
60
|
const SUCCESS_HUE = 156.9;
|
|
22
|
-
const DANGER_HUE
|
|
61
|
+
const DANGER_HUE = 23.1;
|
|
23
62
|
const WARNING_HUE = 84.3;
|
|
24
|
-
const NOTE_HUE
|
|
63
|
+
const NOTE_HUE = 302.3;
|
|
25
64
|
|
|
26
65
|
const SEED_SATURATION = 80;
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Hues are design tokens too — keeping them named in one place beats burying numbers in `extend()` calls. The shared `SEED_SATURATION` keeps every status theme on the same saturation budget; per-color `saturation` factors below are 0–1 *of this seed*, not absolute.
|
|
30
|
-
|
|
31
|
-
## Global `glaze.configure()`
|
|
32
66
|
|
|
33
|
-
Configure state aliases and output modes once at module load:
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
67
|
glaze.configure({
|
|
37
68
|
states: { dark: '@dark', highContrast: '@hc' },
|
|
38
|
-
modes:
|
|
69
|
+
modes: { dark: true, highContrast: true },
|
|
39
70
|
});
|
|
40
71
|
```
|
|
41
72
|
|
|
42
|
-
|
|
73
|
+
Per-color `saturation` is a factor of the theme seed, not an absolute
|
|
74
|
+
saturation. With `SEED_SATURATION = 80`, `saturation: 0.25` means one quarter of
|
|
75
|
+
that seed.
|
|
43
76
|
|
|
44
|
-
## Naming
|
|
77
|
+
## Naming
|
|
45
78
|
|
|
46
|
-
|
|
79
|
+
Prefer purpose first and variant last:
|
|
47
80
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
| Brand-tinted disabled | `accent-disabled-surface`, `accent-disabled-surface-text` |
|
|
59
|
-
| Code syntax highlighting | `code-comment`, `code-keyword`, `code-string`, `code-number`, … |
|
|
60
|
-
| Loading animation | `loading-face-1`, `loading-face-2`, `loading-face-3` |
|
|
61
|
-
| Shadows | `shadow-sm`, `shadow-md`, `shadow-lg` |
|
|
62
|
-
| Backdrop | `overlay` |
|
|
81
|
+
- Surfaces: `surface`, `surface-2`, `surface-3`.
|
|
82
|
+
- Foregrounds: `surface-text`, `surface-text-soft`, `surface-text-soft-2`.
|
|
83
|
+
- Structure: `border`, `divider`, `outline`, `placeholder`, `focus`.
|
|
84
|
+
- Fills: `accent-surface`, `accent-surface-2`,
|
|
85
|
+
`accent-surface-hover`.
|
|
86
|
+
- Foregrounds on neutral surfaces: `accent-text`, `accent-text-soft`,
|
|
87
|
+
`accent-icon`.
|
|
88
|
+
- Disabled states: `disabled-surface`, `disabled-surface-text`,
|
|
89
|
+
`accent-disabled-surface`, `accent-disabled-surface-text`.
|
|
90
|
+
- Effects: `shadow-sm`, `shadow-md`, `shadow-lg`, `overlay`, `hover`, `tint`.
|
|
63
91
|
|
|
64
|
-
|
|
92
|
+
These names are not only readable. They also help APCA role inference pick the
|
|
93
|
+
right polarity. For example, `button-text` is foreground, `input-bg` is a
|
|
94
|
+
surface, and `card-outline` is a border.
|
|
65
95
|
|
|
66
|
-
##
|
|
96
|
+
## Build the Default Theme
|
|
67
97
|
|
|
68
|
-
|
|
98
|
+
Start with the surface family. It is mostly tone, with small saturation changes
|
|
99
|
+
to keep the ladder visually coherent:
|
|
69
100
|
|
|
70
101
|
```ts
|
|
102
|
+
const defaultTheme = glaze(PURPLE_HUE, SEED_SATURATION);
|
|
103
|
+
|
|
71
104
|
defaultTheme.colors({
|
|
72
|
-
surface:
|
|
73
|
-
'surface-2': {
|
|
74
|
-
|
|
105
|
+
surface: { tone: 100, saturation: 0.11 },
|
|
106
|
+
'surface-2': {
|
|
107
|
+
base: 'surface',
|
|
108
|
+
tone: '-2',
|
|
109
|
+
saturation: 0.15,
|
|
110
|
+
inherit: false,
|
|
111
|
+
},
|
|
112
|
+
'surface-3': {
|
|
113
|
+
base: 'surface',
|
|
114
|
+
tone: '-4',
|
|
115
|
+
saturation: 0.19,
|
|
116
|
+
inherit: false,
|
|
117
|
+
},
|
|
75
118
|
});
|
|
76
119
|
```
|
|
77
120
|
|
|
78
|
-
|
|
121
|
+
Because tone shifts are consistent across schemes, these small relative offsets
|
|
122
|
+
are enough. There is no separate dark-mode curve to tune.
|
|
79
123
|
|
|
80
|
-
|
|
124
|
+
### Text and Borders
|
|
81
125
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
The headline trick of the whole methodology. Strong text uses an **absolute `tone` near the edge of the window**; soft variants use a **directional relative hint plus a numeric `contrast`**.
|
|
126
|
+
Use a hard edge tone for maximum-prominence text, and APCA floors for softer
|
|
127
|
+
content:
|
|
85
128
|
|
|
86
129
|
```ts
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
'surface
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
},
|
|
130
|
+
defaultTheme.colors({
|
|
131
|
+
'surface-text': {
|
|
132
|
+
base: 'surface',
|
|
133
|
+
tone: 2,
|
|
134
|
+
saturation: 0.475,
|
|
135
|
+
},
|
|
136
|
+
'surface-text-soft': {
|
|
137
|
+
base: 'surface',
|
|
138
|
+
tone: '-1',
|
|
139
|
+
saturation: 0.375,
|
|
140
|
+
contrast: { apca: ['content', 'body'] },
|
|
141
|
+
inherit: false,
|
|
142
|
+
},
|
|
143
|
+
'surface-text-soft-2': {
|
|
144
|
+
base: 'surface',
|
|
145
|
+
tone: '-1',
|
|
146
|
+
saturation: 0.24,
|
|
147
|
+
contrast: { apca: ['large', 'content'] },
|
|
148
|
+
inherit: false,
|
|
149
|
+
},
|
|
150
|
+
border: {
|
|
151
|
+
base: 'surface',
|
|
152
|
+
tone: ['-10', '-20'],
|
|
153
|
+
saturation: 0.175,
|
|
154
|
+
inherit: false,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
98
157
|
```
|
|
99
158
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
The soft variants use `tone: '-1'` only as a *directional hint* — the real positioning comes from the numeric `contrast`. Numeric ratios give designers precise perceived weight where presets would only guarantee the AA/AAA floor.
|
|
105
|
-
|
|
106
|
-
In high-contrast mode the tone window is bypassed entirely (full `[0, 100]` range), so `tone: 2` reaches close to black in light HC and close to white in dark HC — maximal contrast against the surface in both.
|
|
159
|
+
`surface-text` does not need a contrast floor because it is intentionally pinned
|
|
160
|
+
near the edge. The soft variants use `tone: '-1'` as the direction and APCA as
|
|
161
|
+
the readable floor. `border` uses an HC tone pair because borders usually need a
|
|
162
|
+
larger visible step in high contrast.
|
|
107
163
|
|
|
108
|
-
|
|
164
|
+
Repeat the same pattern for `surface-2` and `surface-3` only if components need
|
|
165
|
+
text directly on those surfaces.
|
|
109
166
|
|
|
110
|
-
|
|
167
|
+
### Neutral Utility Tokens
|
|
111
168
|
|
|
112
|
-
|
|
113
|
-
border: { base: 'surface', tone: ['-10', '-20'], saturation: 0.175, inherit: false },
|
|
114
|
-
placeholder: { base: 'surface', tone: 67, saturation: 0.175, inherit: false },
|
|
115
|
-
focus: { base: 'surface', tone: 71, saturation: 0.8625, inherit: false },
|
|
116
|
-
disabled: { tone: 80.8, saturation: 0.4, inherit: false },
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
`border` uses an HC pair — the border darkens twice as much in high-contrast mode for visibility. `placeholder` and `focus` give a `base` for namespacing but use absolute tone independently. `disabled` is a root color (no `base`) — it's used as a plain "muted text" token in some places, free of the surface chain.
|
|
120
|
-
|
|
121
|
-
## Disabled chip (contrast-driven for scheme symmetry)
|
|
122
|
-
|
|
123
|
-
The disabled chip + label pair uses `mode: 'auto'` and **explicit numeric contrast** against `surface`, not preset `'AA'` / `'AAA'`:
|
|
169
|
+
Keep neutral-only primitives local to the default theme:
|
|
124
170
|
|
|
125
171
|
```ts
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
172
|
+
defaultTheme.colors({
|
|
173
|
+
placeholder: {
|
|
174
|
+
base: 'surface',
|
|
175
|
+
tone: 67,
|
|
176
|
+
saturation: 0.175,
|
|
177
|
+
inherit: false,
|
|
178
|
+
},
|
|
179
|
+
focus: {
|
|
180
|
+
base: 'surface',
|
|
181
|
+
tone: 71,
|
|
182
|
+
saturation: 0.8625,
|
|
183
|
+
inherit: false,
|
|
184
|
+
},
|
|
185
|
+
disabled: {
|
|
186
|
+
tone: 80.8,
|
|
187
|
+
saturation: 0.4,
|
|
188
|
+
inherit: false,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
134
191
|
```
|
|
135
192
|
|
|
136
|
-
|
|
193
|
+
Absolute tones are fine for primitives whose job is visual placement rather than
|
|
194
|
+
a strict relationship to a specific surface.
|
|
137
195
|
|
|
138
|
-
|
|
196
|
+
## Chips and Disabled States
|
|
139
197
|
|
|
140
|
-
|
|
198
|
+
For subtle fills, tone is usually clearer than contrast:
|
|
141
199
|
|
|
142
200
|
```ts
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
201
|
+
defaultTheme.colors({
|
|
202
|
+
'disabled-surface': {
|
|
203
|
+
base: 'surface',
|
|
204
|
+
tone: '-3',
|
|
205
|
+
saturation: 0.2,
|
|
206
|
+
inherit: false,
|
|
207
|
+
},
|
|
208
|
+
'disabled-surface-text': {
|
|
209
|
+
base: 'disabled-surface',
|
|
210
|
+
tone: '+18',
|
|
211
|
+
saturation: 0.3,
|
|
212
|
+
flip: false,
|
|
213
|
+
inherit: false,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
146
216
|
```
|
|
147
217
|
|
|
148
|
-
|
|
218
|
+
This says exactly what the pair should do: the chip sits a few tone steps off
|
|
219
|
+
the page, and the label sits a muted distance from the chip. `flip: false` keeps
|
|
220
|
+
the relative label offset on the authored side when it reaches the edge.
|
|
149
221
|
|
|
150
|
-
Use
|
|
151
|
-
|
|
152
|
-
This is the canonical "I want this color to stay recognizable" pattern. The other `mode: 'fixed'` use is the entire accent system below.
|
|
153
|
-
|
|
154
|
-
## Accent system (anchor pattern)
|
|
155
|
-
|
|
156
|
-
The load-bearing trick. Define a single fixed white anchor `accent-surface-text`, then derive every accent surface from it with a small relative tone offset and a numeric contrast under `mode: 'fixed'`:
|
|
222
|
+
Use contrast instead when the chip must hit an explicit accessibility floor:
|
|
157
223
|
|
|
158
224
|
```ts
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
225
|
+
defaultTheme.colors({
|
|
226
|
+
'disabled-surface-text': {
|
|
227
|
+
base: 'disabled-surface',
|
|
228
|
+
tone: '+1',
|
|
229
|
+
saturation: 0.3,
|
|
230
|
+
contrast: { apca: 'non-text' },
|
|
231
|
+
inherit: false,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
165
234
|
```
|
|
166
235
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
- **`mode: 'fixed'` keeps the brand recognizable.** Without it, the dark-scheme tone inversion would turn the brand fill into a tone-inverted counterpart that may no longer read as the intended brand surface. Fixed remaps the tone into the dark window without inverting, so a `tone: 52` brand color stays mid-toned in dark mode — still recognizably the same color.
|
|
171
|
-
- **Numeric contrasts, not presets.** `'AA'` / `'AAA'` would let the solver push the color far away from its anchor in dark schemes, breaking the relationship between `accent-surface` and its neighbors. Numeric ratios make the darkening between `accent-surface` (4.5/7), `-2` (4.8/7.5), `-3` (5.2/8), and `-hover` (6/8.5) a tight, designed sequence — a stepped gradient rather than four solver-generated outliers.
|
|
236
|
+
When a token needs the scheme extreme, use `tone: 'min'` or `tone: 'max'`
|
|
237
|
+
directly. Avoid large magic numbers or fake contrast floors just to push a color
|
|
238
|
+
to the edge.
|
|
172
239
|
|
|
173
|
-
|
|
240
|
+
## Fixed Surfaces and Accent Fills
|
|
174
241
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
The opposite of the fills. Brand-colored *foregrounds* are anchored to **`surface`, not `accent-surface`**, with `mode: 'auto'` (default) and full saturation:
|
|
242
|
+
Use `mode: 'fixed'` when the authored color should stay recognizable across
|
|
243
|
+
schemes.
|
|
178
244
|
|
|
179
245
|
```ts
|
|
180
|
-
|
|
181
|
-
'
|
|
182
|
-
|
|
246
|
+
defaultTheme.colors({
|
|
247
|
+
'surface-inverse': {
|
|
248
|
+
tone: 12,
|
|
249
|
+
saturation: 0.475,
|
|
250
|
+
mode: 'fixed',
|
|
251
|
+
inherit: false,
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
'accent-surface-text': {
|
|
255
|
+
tone: 100,
|
|
256
|
+
mode: 'fixed',
|
|
257
|
+
},
|
|
258
|
+
'accent-surface': {
|
|
259
|
+
base: 'accent-surface-text',
|
|
260
|
+
tone: '-1',
|
|
261
|
+
contrast: { apca: ['content', 'body'] },
|
|
262
|
+
mode: 'fixed',
|
|
263
|
+
},
|
|
264
|
+
'accent-surface-2': {
|
|
265
|
+
base: 'accent-surface-text',
|
|
266
|
+
tone: '-1',
|
|
267
|
+
contrast: { apca: [65, 80] },
|
|
268
|
+
mode: 'fixed',
|
|
269
|
+
},
|
|
270
|
+
'accent-surface-hover': {
|
|
271
|
+
base: 'accent-surface-text',
|
|
272
|
+
tone: '-1',
|
|
273
|
+
contrast: { apca: ['body', 'preferred'] },
|
|
274
|
+
mode: 'fixed',
|
|
275
|
+
},
|
|
276
|
+
});
|
|
183
277
|
```
|
|
184
278
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
279
|
+
The accent fill family is a fixed chain against a fixed text anchor. The names
|
|
280
|
+
infer `surface` and `text` roles, so APCA gets the right polarity without extra
|
|
281
|
+
fields.
|
|
188
282
|
|
|
189
|
-
##
|
|
283
|
+
## Adaptive Accent Foregrounds
|
|
190
284
|
|
|
191
|
-
|
|
285
|
+
Brand foregrounds that sit on neutral surfaces should stay adaptive:
|
|
192
286
|
|
|
193
287
|
```ts
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
'
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
},
|
|
288
|
+
defaultTheme.colors({
|
|
289
|
+
'accent-text': {
|
|
290
|
+
base: 'surface',
|
|
291
|
+
tone: '-1',
|
|
292
|
+
saturation: 1,
|
|
293
|
+
contrast: { apca: ['content', 'body'] },
|
|
294
|
+
},
|
|
295
|
+
'accent-text-soft': {
|
|
296
|
+
base: 'surface',
|
|
297
|
+
tone: '-1',
|
|
298
|
+
saturation: 1,
|
|
299
|
+
contrast: { apca: ['large', 'content'] },
|
|
300
|
+
},
|
|
301
|
+
'accent-icon': {
|
|
302
|
+
base: 'surface',
|
|
303
|
+
tone: '-1',
|
|
304
|
+
saturation: 0.9375,
|
|
305
|
+
contrast: { apca: ['non-text', 'large'] },
|
|
306
|
+
},
|
|
307
|
+
});
|
|
201
308
|
```
|
|
202
309
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
310
|
+
Anchor these to `surface`, not to `accent-surface`. Their real job is to remain
|
|
311
|
+
readable on neutral UI, so `mode: 'auto'` and a surface base are the right
|
|
312
|
+
defaults.
|
|
206
313
|
|
|
207
|
-
|
|
314
|
+
Brand-tinted disabled states can usually be pure tone:
|
|
208
315
|
|
|
209
316
|
```ts
|
|
210
|
-
|
|
211
|
-
'
|
|
317
|
+
defaultTheme.colors({
|
|
318
|
+
'accent-disabled-surface': {
|
|
319
|
+
base: 'surface',
|
|
320
|
+
tone: '+3',
|
|
321
|
+
saturation: 0.5,
|
|
322
|
+
},
|
|
323
|
+
'accent-disabled-surface-text': {
|
|
324
|
+
base: 'accent-disabled-surface',
|
|
325
|
+
tone: '+18',
|
|
326
|
+
saturation: 0.4,
|
|
327
|
+
flip: false,
|
|
328
|
+
},
|
|
329
|
+
});
|
|
212
330
|
```
|
|
213
331
|
|
|
214
|
-
|
|
332
|
+
These are inherited, so status themes automatically get
|
|
333
|
+
`success-accent-disabled-surface`, `danger-accent-disabled-surface`, and the
|
|
334
|
+
matching text tokens.
|
|
215
335
|
|
|
216
|
-
|
|
336
|
+
## Special Purpose Colors
|
|
217
337
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
The `code-*` tokens use **absolute `hue` numbers** regardless of the seed. Each is `base: 'surface'` with `mode: 'auto'`, a per-token saturation, and a numeric contrast floor:
|
|
338
|
+
Use absolute `hue` overrides for tokens that should come from another hue family
|
|
339
|
+
but keep the same adaptation behavior:
|
|
221
340
|
|
|
222
341
|
```ts
|
|
223
|
-
|
|
224
|
-
'code-
|
|
225
|
-
|
|
226
|
-
|
|
342
|
+
defaultTheme.colors({
|
|
343
|
+
'code-comment': {
|
|
344
|
+
base: 'surface',
|
|
345
|
+
hue: 280,
|
|
346
|
+
saturation: 0.1,
|
|
347
|
+
tone: '-1',
|
|
348
|
+
contrast: { apca: ['large', 'content'] },
|
|
349
|
+
inherit: false,
|
|
350
|
+
},
|
|
351
|
+
'code-keyword': {
|
|
352
|
+
base: 'surface',
|
|
353
|
+
hue: 348,
|
|
354
|
+
saturation: 1,
|
|
355
|
+
tone: '-1',
|
|
356
|
+
contrast: { apca: ['content', 'body'] },
|
|
357
|
+
inherit: false,
|
|
358
|
+
},
|
|
359
|
+
'code-string': {
|
|
360
|
+
base: 'surface',
|
|
361
|
+
hue: SUCCESS_HUE,
|
|
362
|
+
saturation: 1,
|
|
363
|
+
tone: '-1',
|
|
364
|
+
contrast: { apca: ['large', 'content'] },
|
|
365
|
+
inherit: false,
|
|
366
|
+
},
|
|
367
|
+
});
|
|
227
368
|
```
|
|
228
369
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
## Loading-animation faces
|
|
232
|
-
|
|
233
|
-
A 3-step ramp using *absolute* tones with high saturation factors and tight numeric contrasts:
|
|
370
|
+
Use small tone ramps for decorative motion:
|
|
234
371
|
|
|
235
372
|
```ts
|
|
236
|
-
|
|
237
|
-
'loading-face-
|
|
238
|
-
|
|
373
|
+
defaultTheme.colors({
|
|
374
|
+
'loading-face-1': {
|
|
375
|
+
base: 'surface',
|
|
376
|
+
tone: 98,
|
|
377
|
+
saturation: 0.3,
|
|
378
|
+
inherit: false,
|
|
379
|
+
},
|
|
380
|
+
'loading-face-2': {
|
|
381
|
+
base: 'surface',
|
|
382
|
+
tone: 91,
|
|
383
|
+
saturation: 0.62,
|
|
384
|
+
inherit: false,
|
|
385
|
+
},
|
|
386
|
+
'loading-face-3': {
|
|
387
|
+
base: 'surface',
|
|
388
|
+
tone: 79,
|
|
389
|
+
saturation: 0.66,
|
|
390
|
+
inherit: false,
|
|
391
|
+
},
|
|
392
|
+
});
|
|
239
393
|
```
|
|
240
394
|
|
|
241
|
-
|
|
395
|
+
Since tone steps now invert consistently across schemes, the same ramp keeps its
|
|
396
|
+
spacing in light and dark without involving the contrast solver. Use an HC tone
|
|
397
|
+
pair only when the animation should become more pronounced in high contrast.
|
|
242
398
|
|
|
243
|
-
##
|
|
399
|
+
## Effects
|
|
244
400
|
|
|
245
|
-
|
|
401
|
+
Define one neutral shadow system:
|
|
246
402
|
|
|
247
403
|
```ts
|
|
248
|
-
|
|
249
|
-
'shadow-
|
|
250
|
-
|
|
404
|
+
defaultTheme.colors({
|
|
405
|
+
'shadow-sm': {
|
|
406
|
+
type: 'shadow',
|
|
407
|
+
bg: 'surface',
|
|
408
|
+
fg: 'surface-text',
|
|
409
|
+
intensity: 5,
|
|
410
|
+
inherit: false,
|
|
411
|
+
},
|
|
412
|
+
'shadow-md': {
|
|
413
|
+
type: 'shadow',
|
|
414
|
+
bg: 'surface',
|
|
415
|
+
fg: 'surface-text',
|
|
416
|
+
intensity: [10, 20],
|
|
417
|
+
inherit: false,
|
|
418
|
+
},
|
|
419
|
+
'shadow-lg': {
|
|
420
|
+
type: 'shadow',
|
|
421
|
+
bg: 'surface',
|
|
422
|
+
fg: 'surface-text',
|
|
423
|
+
intensity: [15, 30],
|
|
424
|
+
inherit: false,
|
|
425
|
+
},
|
|
426
|
+
});
|
|
251
427
|
```
|
|
252
428
|
|
|
253
|
-
Including `fg`
|
|
429
|
+
Including `fg` lets shadow strength follow the resolved foreground/background
|
|
430
|
+
gap. Use an HC pair for shadows that should deepen in high contrast.
|
|
254
431
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
## Overlay (fixed opacity)
|
|
432
|
+
Use `opacity` for one fixed-alpha color:
|
|
258
433
|
|
|
259
434
|
```ts
|
|
260
|
-
|
|
435
|
+
defaultTheme.colors({
|
|
436
|
+
overlay: { tone: 10, opacity: 0.5, inherit: false },
|
|
437
|
+
});
|
|
261
438
|
```
|
|
262
439
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
## Mixes for hover / tint
|
|
266
|
-
|
|
267
|
-
Reach for mix tokens when you want one color to "tint through" another:
|
|
440
|
+
Use mixes when one color should tint through another:
|
|
268
441
|
|
|
269
442
|
```ts
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
443
|
+
defaultTheme.colors({
|
|
444
|
+
hover: {
|
|
445
|
+
type: 'mix',
|
|
446
|
+
base: 'surface',
|
|
447
|
+
target: 'accent-surface',
|
|
448
|
+
value: 8,
|
|
449
|
+
blend: 'transparent',
|
|
450
|
+
},
|
|
451
|
+
tint: {
|
|
452
|
+
type: 'mix',
|
|
453
|
+
base: 'surface',
|
|
454
|
+
target: 'accent-surface',
|
|
455
|
+
value: 20,
|
|
456
|
+
},
|
|
457
|
+
});
|
|
280
458
|
```
|
|
281
459
|
|
|
282
|
-
|
|
283
|
-
|
|
460
|
+
Transparent mixes are good for hover overlays. Opaque mixes are good for solid
|
|
461
|
+
tints. Mix colors can also use `contrast`; the solver adjusts the value or
|
|
462
|
+
opacity to hit the floor.
|
|
284
463
|
|
|
285
|
-
|
|
464
|
+
## Extend into Status Themes
|
|
286
465
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
One shared `TINTED_SURFACE_OVERRIDE`, applied to every colored theme, with only the `hue` changing per status:
|
|
466
|
+
Once the default theme is shaped, create colored siblings by replacing hue and
|
|
467
|
+
overriding only the root surface that should become visibly tinted:
|
|
290
468
|
|
|
291
469
|
```ts
|
|
292
|
-
const TINTED_SURFACE_OVERRIDE
|
|
470
|
+
const TINTED_SURFACE_OVERRIDE = {
|
|
293
471
|
surface: { tone: 96, saturation: 0.8 },
|
|
294
472
|
};
|
|
295
473
|
|
|
296
|
-
const primaryTheme = defaultTheme.extend({
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
474
|
+
const primaryTheme = defaultTheme.extend({
|
|
475
|
+
colors: TINTED_SURFACE_OVERRIDE,
|
|
476
|
+
});
|
|
477
|
+
const successTheme = defaultTheme.extend({
|
|
478
|
+
hue: SUCCESS_HUE,
|
|
479
|
+
colors: TINTED_SURFACE_OVERRIDE,
|
|
480
|
+
});
|
|
481
|
+
const dangerTheme = defaultTheme.extend({
|
|
482
|
+
hue: DANGER_HUE,
|
|
483
|
+
colors: TINTED_SURFACE_OVERRIDE,
|
|
484
|
+
});
|
|
485
|
+
const warningTheme = defaultTheme.extend({
|
|
486
|
+
hue: WARNING_HUE,
|
|
487
|
+
colors: TINTED_SURFACE_OVERRIDE,
|
|
488
|
+
});
|
|
489
|
+
const noteTheme = defaultTheme.extend({
|
|
490
|
+
hue: NOTE_HUE,
|
|
491
|
+
colors: TINTED_SURFACE_OVERRIDE,
|
|
492
|
+
});
|
|
301
493
|
```
|
|
302
494
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
`primaryTheme` keeps the default hue but gets the tinted surface — useful for places that want a brand-tinted banner without semantic status meaning.
|
|
495
|
+
The inherited accent and disabled tokens now resolve in each status hue. Tokens
|
|
496
|
+
marked `inherit: false` stay default-only, so sibling themes remain small.
|
|
306
497
|
|
|
307
|
-
## Palette
|
|
498
|
+
## Export the Palette
|
|
308
499
|
|
|
309
|
-
Compose
|
|
500
|
+
Compose the themes once:
|
|
310
501
|
|
|
311
502
|
```ts
|
|
312
503
|
const palette = glaze.palette({
|
|
313
504
|
default: defaultTheme,
|
|
314
505
|
primary: primaryTheme,
|
|
315
506
|
success: successTheme,
|
|
316
|
-
danger:
|
|
507
|
+
danger: dangerTheme,
|
|
317
508
|
warning: warningTheme,
|
|
318
|
-
note:
|
|
509
|
+
note: noteTheme,
|
|
319
510
|
});
|
|
320
511
|
```
|
|
321
512
|
|
|
322
|
-
The
|
|
513
|
+
The usual export shape is default unprefixed and status themes prefixed:
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
palette.tasty({
|
|
517
|
+
prefix: {
|
|
518
|
+
default: '',
|
|
519
|
+
primary: 'primary-',
|
|
520
|
+
success: 'success-',
|
|
521
|
+
danger: 'danger-',
|
|
522
|
+
warning: 'warning-',
|
|
523
|
+
note: 'note-',
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
See [migration.md](migration.md) for export shapes, prefix maps, Tasty wiring,
|
|
529
|
+
CSS variables, and JSON integration.
|
|
530
|
+
|
|
531
|
+
## High Contrast
|
|
323
532
|
|
|
324
|
-
|
|
533
|
+
High contrast is not a separate palette. Any value that accepts an HC pair can
|
|
534
|
+
tighten the HC variant: `tone`, `contrast`, shadow `intensity`, and mix `value`.
|
|
325
535
|
|
|
326
|
-
|
|
536
|
+
Use HC pairs where users should actually get more separation:
|
|
327
537
|
|
|
328
|
-
- Text
|
|
329
|
-
-
|
|
330
|
-
-
|
|
331
|
-
-
|
|
332
|
-
-
|
|
538
|
+
- Text/content contrast: `{ apca: ['content', 'body'] }`.
|
|
539
|
+
- Accent fills: `{ apca: ['content', 'body'] }` or stronger.
|
|
540
|
+
- Borders: `tone: ['-10', '-20']`.
|
|
541
|
+
- Shadows: `intensity: [10, 20]`.
|
|
542
|
+
- Decorative ramps that must stay perceivable.
|
|
333
543
|
|
|
334
|
-
In HC
|
|
544
|
+
In HC variants, Glaze bypasses the normal tone window and uses the full
|
|
545
|
+
`[0, 100]` range. Edge tones can reach the edge; contrast floors have more room
|
|
546
|
+
to solve.
|
|
335
547
|
|
|
336
|
-
##
|
|
548
|
+
## Checklist
|
|
337
549
|
|
|
338
550
|
Before shipping a palette, verify:
|
|
339
551
|
|
|
340
|
-
-
|
|
341
|
-
-
|
|
342
|
-
-
|
|
343
|
-
|
|
344
|
-
-
|
|
345
|
-
-
|
|
346
|
-
|
|
552
|
+
- Text, icon, and content tokens either have APCA/WCAG contrast or are
|
|
553
|
+
deliberately edge-anchored.
|
|
554
|
+
- Accent fills use `mode: 'fixed'`; accent foregrounds on neutral UI stay
|
|
555
|
+
`mode: 'auto'` and are based on `surface`.
|
|
556
|
+
- Ambiguous APCA tokens have an explicit `role`; obvious names rely on inference.
|
|
557
|
+
- Low-stakes visual relationships use relative `tone` instead of fake contrast
|
|
558
|
+
floors.
|
|
559
|
+
- `inherit: false` is set on default-only tokens so status themes stay focused.
|
|
560
|
+
- HC pairs exist where high contrast should visibly tighten.
|
|
561
|
+
- `glaze.configure({ states, modes })` matches the states registered in the app.
|