@tenphi/glaze 0.0.0-snapshot.f3bb46b → 0.0.0-snapshot.fdc17e3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,561 @@
1
+ # Methodology
2
+
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.
6
+
7
+ For the full API surface, see [api.md](api.md). For the color model details, see
8
+ [okhst.md](okhst.md).
9
+
10
+ ## The mental model
11
+
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:
57
+
58
+ ```ts
59
+ const PURPLE_HUE = 280.3;
60
+ const SUCCESS_HUE = 156.9;
61
+ const DANGER_HUE = 23.1;
62
+ const WARNING_HUE = 84.3;
63
+ const NOTE_HUE = 302.3;
64
+
65
+ const SEED_SATURATION = 80;
66
+
67
+ glaze.configure({
68
+ states: { dark: '@dark', highContrast: '@hc' },
69
+ modes: { dark: true, highContrast: true },
70
+ });
71
+ ```
72
+
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.
76
+
77
+ ## Naming
78
+
79
+ Prefer purpose first and variant last:
80
+
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`.
91
+
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.
95
+
96
+ ## Build the Default Theme
97
+
98
+ Start with the surface family. It is mostly tone, with small saturation changes
99
+ to keep the ladder visually coherent:
100
+
101
+ ```ts
102
+ const defaultTheme = glaze(PURPLE_HUE, SEED_SATURATION);
103
+
104
+ defaultTheme.colors({
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
+ },
118
+ });
119
+ ```
120
+
121
+ Because tone shifts are consistent across schemes, these small relative offsets
122
+ are enough. There is no separate dark-mode curve to tune.
123
+
124
+ ### Text and Borders
125
+
126
+ Use a hard edge tone for maximum-prominence text, and APCA floors for softer
127
+ content:
128
+
129
+ ```ts
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
+ });
157
+ ```
158
+
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.
163
+
164
+ Repeat the same pattern for `surface-2` and `surface-3` only if components need
165
+ text directly on those surfaces.
166
+
167
+ ### Neutral Utility Tokens
168
+
169
+ Keep neutral-only primitives local to the default theme:
170
+
171
+ ```ts
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
+ });
191
+ ```
192
+
193
+ Absolute tones are fine for primitives whose job is visual placement rather than
194
+ a strict relationship to a specific surface.
195
+
196
+ ## Chips and Disabled States
197
+
198
+ For subtle fills, tone is usually clearer than contrast:
199
+
200
+ ```ts
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
+ });
216
+ ```
217
+
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.
221
+
222
+ Use contrast instead when the chip must hit an explicit accessibility floor:
223
+
224
+ ```ts
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
+ });
234
+ ```
235
+
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.
239
+
240
+ ## Fixed Surfaces and Accent Fills
241
+
242
+ Use `mode: 'fixed'` when the authored color should stay recognizable across
243
+ schemes.
244
+
245
+ ```ts
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
+ });
277
+ ```
278
+
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.
282
+
283
+ ## Adaptive Accent Foregrounds
284
+
285
+ Brand foregrounds that sit on neutral surfaces should stay adaptive:
286
+
287
+ ```ts
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
+ });
308
+ ```
309
+
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.
313
+
314
+ Brand-tinted disabled states can usually be pure tone:
315
+
316
+ ```ts
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
+ });
330
+ ```
331
+
332
+ These are inherited, so status themes automatically get
333
+ `success-accent-disabled-surface`, `danger-accent-disabled-surface`, and the
334
+ matching text tokens.
335
+
336
+ ## Special Purpose Colors
337
+
338
+ Use absolute `hue` overrides for tokens that should come from another hue family
339
+ but keep the same adaptation behavior:
340
+
341
+ ```ts
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
+ });
368
+ ```
369
+
370
+ Use small tone ramps for decorative motion:
371
+
372
+ ```ts
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
+ });
393
+ ```
394
+
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.
398
+
399
+ ## Effects
400
+
401
+ Define one neutral shadow system:
402
+
403
+ ```ts
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
+ });
427
+ ```
428
+
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.
431
+
432
+ Use `opacity` for one fixed-alpha color:
433
+
434
+ ```ts
435
+ defaultTheme.colors({
436
+ overlay: { tone: 10, opacity: 0.5, inherit: false },
437
+ });
438
+ ```
439
+
440
+ Use mixes when one color should tint through another:
441
+
442
+ ```ts
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
+ });
458
+ ```
459
+
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.
463
+
464
+ ## Extend into Status Themes
465
+
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:
468
+
469
+ ```ts
470
+ const TINTED_SURFACE_OVERRIDE = {
471
+ surface: { tone: 96, saturation: 0.8 },
472
+ };
473
+
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
+ });
493
+ ```
494
+
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.
497
+
498
+ ## Export the Palette
499
+
500
+ Compose the themes once:
501
+
502
+ ```ts
503
+ const palette = glaze.palette({
504
+ default: defaultTheme,
505
+ primary: primaryTheme,
506
+ success: successTheme,
507
+ danger: dangerTheme,
508
+ warning: warningTheme,
509
+ note: noteTheme,
510
+ });
511
+ ```
512
+
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
532
+
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`.
535
+
536
+ Use HC pairs where users should actually get more separation:
537
+
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.
543
+
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.
547
+
548
+ ## Checklist
549
+
550
+ Before shipping a palette, verify:
551
+
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.