@tenphi/glaze 0.0.0-snapshot.d38eee5 → 0.0.0-snapshot.d79cac8
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 +169 -0
- package/README.md +114 -35
- package/dist/index.cjs +4301 -1272
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1176 -154
- package/dist/index.d.mts +1176 -154
- package/dist/index.mjs +4287 -1273
- package/dist/index.mjs.map +1 -1
- package/docs/contrast.mdx +751 -0
- package/docs/exporting.mdx +851 -0
- package/docs/getting-started.mdx +488 -0
- package/docs/methodology.mdx +977 -0
- package/docs/migration.md +509 -274
- package/docs/mixes.mdx +348 -0
- package/docs/okhst.mdx +680 -0
- package/docs/reference/color-definitions.md +554 -0
- package/docs/reference/color-tokens.md +421 -0
- package/docs/reference/configuration.md +527 -0
- package/docs/reference/contrast.md +277 -0
- package/docs/reference/index.md +223 -0
- package/docs/reference/output.md +835 -0
- package/docs/reference/palette.md +435 -0
- package/docs/reference/theme.md +332 -0
- package/docs/reference/utilities.md +629 -0
- package/docs/schemes.mdx +637 -0
- package/docs/shadows-and-glows.mdx +739 -0
- package/package.json +13 -3
- package/docs/api.md +0 -1287
- package/docs/methodology.md +0 -346
- package/docs/okhst.md +0 -259
package/docs/methodology.md
DELETED
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
# Methodology
|
|
2
|
-
|
|
3
|
-
A practical guide for designing a real, production-grade Glaze palette. Recipe-ordered: the sections follow the same sequence you'd actually build a palette in, and each one ties the choice back to a Glaze behavior.
|
|
4
|
-
|
|
5
|
-
## The mental model
|
|
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.
|
|
14
|
-
|
|
15
|
-
## Hue / saturation seeds
|
|
16
|
-
|
|
17
|
-
Declare hues as named constants up top, plus a single shared seed saturation:
|
|
18
|
-
|
|
19
|
-
```ts
|
|
20
|
-
const PURPLE_HUE = 280.3;
|
|
21
|
-
const SUCCESS_HUE = 156.9;
|
|
22
|
-
const DANGER_HUE = 23.1;
|
|
23
|
-
const WARNING_HUE = 84.3;
|
|
24
|
-
const NOTE_HUE = 302.3;
|
|
25
|
-
|
|
26
|
-
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
|
-
|
|
33
|
-
Configure state aliases and output modes once at module load:
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
glaze.configure({
|
|
37
|
-
states: { dark: '@dark', highContrast: '@hc' },
|
|
38
|
-
modes: { dark: true, highContrast: true },
|
|
39
|
-
});
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Match the state alias names to whatever your app wires into the global predefined states (`@dark` / `@hc` is what Tasty expects). Setting `modes.highContrast: true` makes every export emit four variants — HC tokens are then available globally without per-call overrides.
|
|
43
|
-
|
|
44
|
-
## Naming conventions
|
|
45
|
-
|
|
46
|
-
A tight, predictable vocabulary that the rest of the doc relies on:
|
|
47
|
-
|
|
48
|
-
| Pattern | Tokens |
|
|
49
|
-
|---|---|
|
|
50
|
-
| Surface ladder | `surface`, `surface-2`, `surface-3` |
|
|
51
|
-
| Text on surface (decreasing prominence) | `<surface>-text`, `<surface>-text-soft`, `<surface>-text-soft-2` |
|
|
52
|
-
| Misc neutral primitives | `border`, `placeholder`, `focus`, `disabled` |
|
|
53
|
-
| Neutral disabled chip | `disabled-surface`, `disabled-surface-text` |
|
|
54
|
-
| Fixed-mode dark surface | `surface-inverse` |
|
|
55
|
-
| Brand fills | `accent-surface`, `accent-surface-2`, `accent-surface-3`, `accent-surface-hover` |
|
|
56
|
-
| Brand fill anchor | `accent-surface-text` (the fixed white token everything anchors to) |
|
|
57
|
-
| Brand foregrounds on neutrals | `accent-text`, `accent-text-soft`, `accent-icon` |
|
|
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` |
|
|
63
|
-
|
|
64
|
-
**Rule of thumb:** *purpose-name first, variant suffix last* (`-2`, `-text`, `-soft`, `-hover`, `-disabled`).
|
|
65
|
-
|
|
66
|
-
## Surfaces (root colors)
|
|
67
|
-
|
|
68
|
-
`surface` is a root color (absolute `tone`, no `base`) with a low saturation factor. The ladder chains off it via small relative offsets:
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
defaultTheme.colors({
|
|
72
|
-
surface: { tone: 100, saturation: 0.11 },
|
|
73
|
-
'surface-2': { base: 'surface', tone: '-2', saturation: 0.15, inherit: false },
|
|
74
|
-
'surface-3': { base: 'surface', tone: '-4', saturation: 0.19, inherit: false },
|
|
75
|
-
});
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
A factor of `0.11` of the seed gives a barely-noticeable hue shift — enough that light/dark surfaces feel branded, not enough to look tinted. The slight saturation bump on `-2` / `-3` compensates for perceived saturation dropping as tone drops, so the ladder reads as one consistent surface family.
|
|
79
|
-
|
|
80
|
-
`mode: 'auto'` (the default) inverts these in tone (`100 − t`) and remaps into the dark window, so a `tone: 100` light-mode surface lands at the dark window's floor in dark mode. Because tone is contrast-uniform, the relative deltas (`-2`, `-4`) translate to the same contrast steps in both schemes — no fitted curve required. `inherit: false` on `-2` / `-3` keeps colored sibling themes lean — they only need a single tinted `surface`, not the whole ladder.
|
|
81
|
-
|
|
82
|
-
## Text on surfaces (anchor at the edge)
|
|
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`**.
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
'surface-text': {
|
|
88
|
-
base: 'surface', tone: 2, saturation: 0.475,
|
|
89
|
-
},
|
|
90
|
-
'surface-text-soft': {
|
|
91
|
-
base: 'surface', tone: '-1', saturation: 0.375,
|
|
92
|
-
contrast: [9, 11], inherit: false,
|
|
93
|
-
},
|
|
94
|
-
'surface-text-soft-2': {
|
|
95
|
-
base: 'surface', tone: '-1', saturation: 0.24,
|
|
96
|
-
contrast: [4.5, 5.5], inherit: false,
|
|
97
|
-
},
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Repeat the same triple anchored to each subordinate surface (`surface-2-text`, `surface-2-text-soft`, `surface-3-text`, …) so the ladder stays self-consistent.
|
|
101
|
-
|
|
102
|
-
The strong-text `tone: 2` pins the light-mode resolved value near the bottom of the light window (close to black against the near-white surface) and inverts to near-white in dark mode — both yielding very high contrast against their surface. A `contrast: 'AAA'` solver pass would have stopped at the AAA floor (cr = 7) and no further. **Anchoring at the edge** beats the contrast solver because the solver only needs to *meet* the floor, not exceed it.
|
|
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.
|
|
107
|
-
|
|
108
|
-
## Other neutral primitives
|
|
109
|
-
|
|
110
|
-
Borders, placeholders, focus rings, and the floating "muted text" tone — all default-only:
|
|
111
|
-
|
|
112
|
-
```ts
|
|
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'`:
|
|
124
|
-
|
|
125
|
-
```ts
|
|
126
|
-
'disabled-surface': {
|
|
127
|
-
base: 'surface', tone: '-1', saturation: 0.2,
|
|
128
|
-
contrast: [1.5, 2], inherit: false,
|
|
129
|
-
},
|
|
130
|
-
'disabled-surface-text': {
|
|
131
|
-
base: 'disabled-surface', tone: '+1', saturation: 0.3,
|
|
132
|
-
contrast: 3, inherit: false,
|
|
133
|
-
},
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Each token anchors to its immediate parent surface — `*-surface` contrasts against the root `surface`, while `*-surface-text` contrasts against its own chip (`disabled-surface`). This keeps the disabled state self-contained and resolves to consistent ratios in light, dark, and HC (chip ≈ 1.5–2× vs surface, label ≈ 3× on chip). An alpha-tinted overlay would have asymmetric behavior — composited alpha against a near-white light surface produces a much weaker chip than the same overlay against a near-dark dark surface, and the disabled state would stop *looking* disabled in one of the schemes.
|
|
137
|
-
|
|
138
|
-
The general rule: when a color needs to *feel the same across schemes*, anchor it with `mode: 'auto'` + a numeric contrast against a surface, not with a preset.
|
|
139
|
-
|
|
140
|
-
## `surface-inverse` (the fixed-mode escape hatch)
|
|
141
|
-
|
|
142
|
-
```ts
|
|
143
|
-
'surface-inverse': {
|
|
144
|
-
tone: 12, saturation: 0.475, mode: 'fixed', inherit: false,
|
|
145
|
-
},
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
`mode: 'fixed'` skips the dark-scheme tone inversion and only remaps the tone into the dark window, so `surface-inverse` reads as a dark surface in *every* scheme — light, dark, and HC. In high-contrast variants the window is bypassed entirely (identity), so the color stays at its raw tone across all four schemes.
|
|
149
|
-
|
|
150
|
-
Use it for tooltips, code blocks, popovers with their own dark theme. Pair with `#white` for foreground text.
|
|
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'`:
|
|
157
|
-
|
|
158
|
-
```ts
|
|
159
|
-
'accent-surface-text': { tone: 100, mode: 'fixed' },
|
|
160
|
-
|
|
161
|
-
'accent-surface': { base: 'accent-surface-text', tone: '-1', contrast: [4.5, 7], mode: 'fixed' },
|
|
162
|
-
'accent-surface-2': { base: 'accent-surface-text', tone: '-1', contrast: [4.8, 7.5], mode: 'fixed' },
|
|
163
|
-
'accent-surface-3': { base: 'accent-surface-text', tone: '-1', contrast: [5.2, 8], mode: 'fixed' },
|
|
164
|
-
'accent-surface-hover': { base: 'accent-surface-text', tone: '-1', contrast: [6, 8.5], mode: 'fixed' },
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
Three things make this work:
|
|
168
|
-
|
|
169
|
-
- **One anchor, one chain.** All accent surfaces stay in the same hue family because they all derive from `accent-surface-text`.
|
|
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.
|
|
172
|
-
|
|
173
|
-
The hover variant is a dedicated *fixed* token. Reusing `accent-text` (which is `mode: 'auto'` and inverts direction in dark) would break the hover feel.
|
|
174
|
-
|
|
175
|
-
## Adaptive accent foregrounds
|
|
176
|
-
|
|
177
|
-
The opposite of the fills. Brand-colored *foregrounds* are anchored to **`surface`, not `accent-surface`**, with `mode: 'auto'` (default) and full saturation:
|
|
178
|
-
|
|
179
|
-
```ts
|
|
180
|
-
'accent-text': { base: 'surface', tone: '-1', saturation: 1, contrast: [6.4, 10] },
|
|
181
|
-
'accent-text-soft': { base: 'surface', tone: '-1', saturation: 1, contrast: [4.5, 7] },
|
|
182
|
-
'accent-icon': { base: 'surface', tone: '-1', saturation: 0.9375, contrast: [3.2, 5] },
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
Foregrounds need to stay readable on the surface they actually sit on — anchoring to the brand fill would only enforce contrast against that fill, leaving the dark-mode color washed out against the actual surface (e.g. SECONDARY button labels sit on `surface`, not on the brand fill). Anchoring to `surface` + `mode: 'auto'` lets the solver lift the tone in dark mode so the contrast floor holds in both schemes.
|
|
186
|
-
|
|
187
|
-
`accent-text-soft` shares the anchor and saturation but relaxes the contrast floor for a visibly less prominent secondary foreground (link base color, subdued labels). Critically, it stays `mode: 'auto'` — a fixed version would collapse to cr≈3 against the dark surface and break AA.
|
|
188
|
-
|
|
189
|
-
## Brand-tinted disabled
|
|
190
|
-
|
|
191
|
-
Mirrors the neutral disabled pair from above but with higher saturation so the chip reads as a *muted brand color* rather than fully neutral grey. A disabled chip has no contrast *requirement* — it just needs to sit a hair off the surface and carry a faint label. That's a job for **tone**, not the contrast solver:
|
|
192
|
-
|
|
193
|
-
```ts
|
|
194
|
-
'accent-disabled-surface': {
|
|
195
|
-
base: 'surface', tone: '+3', saturation: 0.5,
|
|
196
|
-
},
|
|
197
|
-
'accent-disabled-surface-text': {
|
|
198
|
-
base: 'accent-disabled-surface', tone: '+18', saturation: 0.4,
|
|
199
|
-
flip: false,
|
|
200
|
-
},
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
Tone offsets say what we mean directly: the chip is three tone steps off `surface`, the label eighteen steps off the chip — a deliberately low-contrast pair, no solver involved. Because tone is contrast-uniform, those steps look the same in light and dark, and `mode: 'auto'` (the default) keeps the relationship intact when the scheme inverts.
|
|
204
|
-
|
|
205
|
-
`flip: false` on the label is the safety rail. A relative `tone: '+18'` lands above the chip; if the chip is already near the top of the range (a light surface), `+18` would overshoot 100. With flip on (the default), Glaze mirrors the offset to `-18` so it reflects back into range — handy for contrast tokens, but here it would put the label on the *wrong* side of the chip. Turning flip off clamps to the boundary instead, keeping the label on the side you authored.
|
|
206
|
-
|
|
207
|
-
When a token genuinely needs the extreme — the lightest or darkest tone the scheme allows — reach for `'max'` / `'min'` rather than a large number or a contrast hack:
|
|
208
|
-
|
|
209
|
-
```ts
|
|
210
|
-
'card-floor': { tone: 'min' }, // darkest tone (root, no base needed)
|
|
211
|
-
'card-ceil': { tone: 'max' }, // lightest tone
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
`'max'` resolves to the scheme's highest authored tone (100) and `'min'` to the lowest (0); neither needs a `base`. They flow through scheme mapping like any absolute tone, so under `mode: 'auto'` they invert in dark — `'max'` is the lightest tone in light mode and, after inversion, the *darkest* in dark. Use `mode: 'static'` if you want the same extreme pinned across every scheme, or `mode: 'fixed'` to keep it on the same end without inverting. Either way: no magic numbers, no contrast floor standing in for "push it all the way".
|
|
215
|
-
|
|
216
|
-
These are inherited (no `inherit: false`), so each colored sibling theme automatically emits `<theme>-accent-disabled-surface` and `<theme>-accent-disabled-surface-text`. PRIMARY-style disabled buttons stay tinted with the active theme's hue (danger-tinted danger button, success-tinted success button), preserving brand identity even in the disabled state.
|
|
217
|
-
|
|
218
|
-
## Per-color hue overrides (code highlighting)
|
|
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:
|
|
221
|
-
|
|
222
|
-
```ts
|
|
223
|
-
'code-comment': { base: 'surface', hue: 280, saturation: 0.1, tone: '-1', contrast: [4.5, 7], inherit: false },
|
|
224
|
-
'code-keyword': { base: 'surface', hue: 348, saturation: 1, tone: '-1', contrast: [5, 7.5], inherit: false },
|
|
225
|
-
'code-string': { base: 'surface', hue: SUCCESS_HUE, saturation: 1, tone: '-1', contrast: [4.5, 7], inherit: false },
|
|
226
|
-
// …code-punctuation, code-number, code-function, code-attribute follow the same shape
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
The canonical pattern for "I want a color from a different hue family but the same adaptive behavior". Absolute `hue` overrides the theme seed for a single color; everything else (contrast against `surface`, dark adaptation, HC tightening) still works. `inherit: false` because syntax highlighting is a default-only concern.
|
|
230
|
-
|
|
231
|
-
## Loading-animation faces
|
|
232
|
-
|
|
233
|
-
A 3-step ramp using *absolute* tones with high saturation factors and tight numeric contrasts:
|
|
234
|
-
|
|
235
|
-
```ts
|
|
236
|
-
'loading-face-1': { base: 'surface', tone: 98, saturation: 0.3, contrast: [1.04, 1.5], inherit: false },
|
|
237
|
-
'loading-face-2': { base: 'surface', tone: 91, saturation: 0.62, contrast: [1.24, 2.5], inherit: false },
|
|
238
|
-
'loading-face-3': { base: 'surface', tone: 79, saturation: 0.66, contrast: [1.75, 4], inherit: false },
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
Combines absolute tone positioning (so the ramp is deterministic in light mode) with a numeric contrast floor (so the ramp still reads in dark and HC). The HC contrast jumps significantly (`1.04 → 1.5`, `1.24 → 2.5`, `1.75 → 4`) so the animation stays perceivable for low-vision users.
|
|
242
|
-
|
|
243
|
-
## Shadows
|
|
244
|
-
|
|
245
|
-
Three sizes, all sharing `bg: 'surface'` and `fg: 'surface-text'`, varying only `intensity`:
|
|
246
|
-
|
|
247
|
-
```ts
|
|
248
|
-
'shadow-sm': { type: 'shadow', bg: 'surface', fg: 'surface-text', intensity: 5, inherit: false },
|
|
249
|
-
'shadow-md': { type: 'shadow', bg: 'surface', fg: 'surface-text', intensity: 10, inherit: false },
|
|
250
|
-
'shadow-lg': { type: 'shadow', bg: 'surface', fg: 'surface-text', intensity: 15, inherit: false },
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
Including `fg` matters: shadow strength scales with `|l_bg − l_fg|`, so anchoring `fg` to `surface-text` (which is anchored at the edge of the window) makes shadows *automatically deeper* in dark mode where the bg/fg gap is larger. All shadows are `inherit: false` — there's only one shadow system for the whole UI, and colored sibling themes don't carry their own.
|
|
254
|
-
|
|
255
|
-
For HC, pass `intensity: [normal, hc]` (e.g. `[10, 20]`) to deepen shadows in high-contrast mode. The full algorithm and tuning knobs are in [api.md → Shadows](api.md#shadows).
|
|
256
|
-
|
|
257
|
-
## Overlay (fixed opacity)
|
|
258
|
-
|
|
259
|
-
```ts
|
|
260
|
-
overlay: { tone: 10, opacity: 0.5, inherit: false },
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
The shortcut for *one solid color with a fixed alpha* — no shadow algorithm, no mix. `opacity` on a regular color attaches an alpha component to every variant. Use it for backdrops, scrims, modal overlays. (Combining `opacity` with `contrast` is not recommended — perceived lightness becomes unpredictable when alpha is fixed; Glaze emits a `console.warn`.)
|
|
264
|
-
|
|
265
|
-
## Mixes for hover / tint
|
|
266
|
-
|
|
267
|
-
Reach for mix tokens when you want one color to "tint through" another:
|
|
268
|
-
|
|
269
|
-
```ts
|
|
270
|
-
hover: {
|
|
271
|
-
type: 'mix', base: 'surface', target: 'accent-surface',
|
|
272
|
-
value: 8, blend: 'transparent',
|
|
273
|
-
},
|
|
274
|
-
// hover → accent-surface with alpha = 0.08
|
|
275
|
-
|
|
276
|
-
tint: {
|
|
277
|
-
type: 'mix', base: 'surface', target: 'accent-surface',
|
|
278
|
-
value: 20,
|
|
279
|
-
},
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
- **Transparent mix** — *the target color with controlled alpha*. Useful for hover overlays.
|
|
283
|
-
- **Opaque mix** — solid blend of two colors. Good for subtle tints.
|
|
284
|
-
|
|
285
|
-
Choose `space: 'okhsl'` (default) for design tokens — perceptually uniform, consistent with the rest of Glaze. Choose `space: 'srgb'` to match what the browser would render with a plain CSS overlay. Mix colors support the same `contrast` prop as regular colors; the solver adjusts the mix ratio (opaque) or opacity (transparent) to meet the target.
|
|
286
|
-
|
|
287
|
-
## Colored sibling themes via `extend()`
|
|
288
|
-
|
|
289
|
-
One shared `TINTED_SURFACE_OVERRIDE`, applied to every colored theme, with only the `hue` changing per status:
|
|
290
|
-
|
|
291
|
-
```ts
|
|
292
|
-
const TINTED_SURFACE_OVERRIDE: ColorMap = {
|
|
293
|
-
surface: { tone: 96, saturation: 0.8 },
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
const primaryTheme = defaultTheme.extend({ colors: TINTED_SURFACE_OVERRIDE });
|
|
297
|
-
const successTheme = defaultTheme.extend({ hue: SUCCESS_HUE, colors: TINTED_SURFACE_OVERRIDE });
|
|
298
|
-
const dangerTheme = defaultTheme.extend({ hue: DANGER_HUE, colors: TINTED_SURFACE_OVERRIDE });
|
|
299
|
-
const warningTheme = defaultTheme.extend({ hue: WARNING_HUE, colors: TINTED_SURFACE_OVERRIDE });
|
|
300
|
-
const noteTheme = defaultTheme.extend({ hue: NOTE_HUE, colors: TINTED_SURFACE_OVERRIDE });
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
Colored themes need a visibly tinted surface for status banners — saturation jumps from the neutral `0.11` (default theme) to `0.8`. The `inherit: false` discipline pays off here: because most neutrals (`surface-2`, `surface-3`, `border`, `placeholder`, `disabled-*`, `code-*`, `loading-*`, `shadow-*`) are flagged default-only, each colored theme inherits *only* the accent + tinted surface chain and emits a small, focused token set.
|
|
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.
|
|
306
|
-
|
|
307
|
-
## Palette composition
|
|
308
|
-
|
|
309
|
-
Compose all themes into a palette so they can be exported as one token set:
|
|
310
|
-
|
|
311
|
-
```ts
|
|
312
|
-
const palette = glaze.palette({
|
|
313
|
-
default: defaultTheme,
|
|
314
|
-
primary: primaryTheme,
|
|
315
|
-
success: successTheme,
|
|
316
|
-
danger: dangerTheme,
|
|
317
|
-
warning: warningTheme,
|
|
318
|
-
note: noteTheme,
|
|
319
|
-
});
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
The default theme is conventionally exported unprefixed (its tokens land as `#surface`, `#border`); colored themes are prefixed with their name. See [migration.md](migration.md) for the prefix map shape, alias patterns, and how to wire the resulting tokens into Tasty / CSS / framework-agnostic JSON.
|
|
323
|
-
|
|
324
|
-
## High-contrast strategy
|
|
325
|
-
|
|
326
|
-
Glaze's high-contrast mode is opt-in per token: anywhere `tone`, `contrast`, `intensity`, or `value` accepts an HC pair, you can pass `[normal, hc]` to tighten the HC variant. The heuristic is to pair anything that's already contrast-driven:
|
|
327
|
-
|
|
328
|
-
- Text-against-surface contrasts (`[9, 11]`, `[4.5, 5.5]`, `[6.4, 10]`).
|
|
329
|
-
- The accent surface ladder (`[4.5, 7]` → `[5.2, 8]` → `[6, 8.5]`).
|
|
330
|
-
- The loading ramp's contrasts.
|
|
331
|
-
- Shadow `intensity` (e.g. `intensity: [10, 20]`).
|
|
332
|
-
- `border` tone (e.g. `tone: ['-10', '-20']`).
|
|
333
|
-
|
|
334
|
-
In HC the tone window is **bypassed entirely** — light HC and dark HC operate on the full `[0, 100]` range. That's why edge-anchored absolute tones like `surface-text: { tone: 2 }` reach close to black in light HC and close to white in dark HC, exactly what you want for maximum contrast.
|
|
335
|
-
|
|
336
|
-
## Closing checklist
|
|
337
|
-
|
|
338
|
-
Before shipping a palette, verify:
|
|
339
|
-
|
|
340
|
-
- [ ] Every text token has an explicit `contrast` *or* an edge-anchored absolute `tone`.
|
|
341
|
-
- [ ] Every accent surface uses `mode: 'fixed'` + numeric `contrast` (not preset `'AA'` / `'AAA'`).
|
|
342
|
-
- [ ] Every brand foreground (`accent-text*`, `accent-icon`) is anchored to `surface`, **not** to `accent-surface`.
|
|
343
|
-
- [ ] Every `inherit: false` is intentional — colored sibling themes only carry the tokens they actually need.
|
|
344
|
-
- [ ] HC pairs are present on every contrast-driven token, not just the strong ones.
|
|
345
|
-
- [ ] Shadow `fg` is set when you want shadows to deepen in dark mode.
|
|
346
|
-
- [ ] `glaze.configure({ states, modes })` matches the global predefined states wired in your app's root.
|
package/docs/okhst.md
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
# OKHST — the contrast-uniform tone space
|
|
2
|
-
|
|
3
|
-
This is the canonical specification for the color model Glaze uses internally
|
|
4
|
-
and accepts as input. It is the source of truth that [api.md](api.md),
|
|
5
|
-
[methodology.md](methodology.md), and [migration.md](migration.md) reference.
|
|
6
|
-
|
|
7
|
-
## What OKHST is
|
|
8
|
-
|
|
9
|
-
**OKHST is OKHSL with its lightness axis replaced by a contrast-uniform _tone_
|
|
10
|
-
axis.** It shares OKHSL's hue (`h`, 0–360) and saturation (`s`, 0–1) verbatim;
|
|
11
|
-
only the third coordinate changes:
|
|
12
|
-
|
|
13
|
-
| Space | Coords | Third axis |
|
|
14
|
-
|---|---|---|
|
|
15
|
-
| OKHSL | `h, s, l` | `l` — perceptual lightness (toe-adjusted OKLab L) |
|
|
16
|
-
| OKHST | `h, s, t` | `t` — tone: a normalized log of luminance |
|
|
17
|
-
|
|
18
|
-
OKHST exists for one reason: in OKHSL, _equal lightness steps_ are perceptually
|
|
19
|
-
even but produce _uneven contrast_ (the ratio between adjacent steps drifts).
|
|
20
|
-
OKHST's tone axis is shaped so that _equal tone steps_ produce _even WCAG
|
|
21
|
-
contrast_ between steps. Authoring ramps in tone gives you contrast-even ladders
|
|
22
|
-
for free, and dark-mode inversion becomes a single subtraction (`100 - t`)
|
|
23
|
-
instead of a fitted curve.
|
|
24
|
-
|
|
25
|
-
OKHST is an **input space only**. It is parseable as an `okhst(H S% T%)` string
|
|
26
|
-
and an `{ h, s, t }` object, but it is **never emitted** — there is no CSS
|
|
27
|
-
`okhst()` function, so output formats stay `okhsl | rgb | hsl | oklch`.
|
|
28
|
-
|
|
29
|
-
## The tone transfer
|
|
30
|
-
|
|
31
|
-
For a gray (s = 0) at OKHSL lightness `l`, luminance is closed-form through the
|
|
32
|
-
OKHSL toe and OKLab cube:
|
|
33
|
-
|
|
34
|
-
```
|
|
35
|
-
Y = toeInv(l) ** 3 // OKLab L = toeInv(l); luminance ≈ L³
|
|
36
|
-
l = toe(cbrt(Y)) // exact inverse
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
(`toe` / `toeInv` already exist in [okhsl-color-math.ts](../src/okhsl-color-math.ts).)
|
|
40
|
-
|
|
41
|
-
Tone is a normalized natural-log of `Y`, offset by a small `eps`:
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
toTone(Y, eps) = (ln(Y + eps) - ln(eps)) / (ln(1 + eps) - ln(eps)) * 100
|
|
45
|
-
fromTone(T, eps) = exp( (T / 100) * (ln(1 + eps) - ln(eps)) + ln(eps) ) - eps
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
`toTone` and `fromTone` are exact analytic inverses, so a round-trip is lossless
|
|
49
|
-
to ~1e-15. `toTone(0) = 0` and `toTone(1) = 100` for any `eps`, so tone is always
|
|
50
|
-
a clean 0–100 scale.
|
|
51
|
-
|
|
52
|
-
### Why `eps ≈ 0.05` makes tone contrast-uniform
|
|
53
|
-
|
|
54
|
-
WCAG 2 contrast is `(Y_hi + 0.05) / (Y_lo + 0.05)`. Pick `eps = 0.05` and the
|
|
55
|
-
tone transfer becomes a normalized `ln(Y + 0.05)`. Two colors that differ by a
|
|
56
|
-
fixed tone delta `ΔT` then differ by a fixed _ratio_ of `(Y + 0.05)` — i.e. a
|
|
57
|
-
fixed WCAG contrast ratio — regardless of where on the scale they sit:
|
|
58
|
-
|
|
59
|
-
```
|
|
60
|
-
cr(T2, T1) = (Y2 + 0.05) / (Y1 + 0.05)
|
|
61
|
-
= exp( (T2 - T1)/100 * (ln(1.05) - ln(0.05)) )
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Empirically (gray, `eps = 0.05`), each `+10` tone multiplies contrast-vs-black by
|
|
65
|
-
a near-constant factor:
|
|
66
|
-
|
|
67
|
-
| tone | cr vs black |
|
|
68
|
-
|---|---|
|
|
69
|
-
| 10 | 1.36 |
|
|
70
|
-
| 30 | 2.49 |
|
|
71
|
-
| 50 | 4.58 |
|
|
72
|
-
| 70 | 8.43 |
|
|
73
|
-
| 90 | 15.49 |
|
|
74
|
-
| 100 | 21.00 |
|
|
75
|
-
|
|
76
|
-
So a tone ramp `[20, 40, 60, 80]` has _constant_ contrast between adjacent
|
|
77
|
-
stops. That is the whole point.
|
|
78
|
-
|
|
79
|
-
## Core invariant: `T → L` is independent of `H` and `S`
|
|
80
|
-
|
|
81
|
-
`okhstToOkhsl({ h, s, t })` passes `h` and `s` through unchanged and sets
|
|
82
|
-
`l = fromTone(t)`. `fromTone` is a pure function of `(t, eps)`; OKHSL's
|
|
83
|
-
`l → OKLab L = toeInv(l)` map has no hue/saturation term — `h`/`s` enter only
|
|
84
|
-
the chroma/cusp math. Therefore:
|
|
85
|
-
|
|
86
|
-
> **A given tone yields the same OKHSL lightness for every hue and saturation.**
|
|
87
|
-
|
|
88
|
-
OKHST inherits OKHSL's gamut and reversibility exactly: every `(h, s, t)` is
|
|
89
|
-
realizable and round-trips.
|
|
90
|
-
|
|
91
|
-
**This uniformity is in lightness, not luminance.** Equal tone gives equal
|
|
92
|
-
OKHSL `L` for all `h`/`s`, but equal _WCAG/APCA contrast_ only for grays.
|
|
93
|
-
A saturated yellow and a saturated blue at the same tone share a lightness yet
|
|
94
|
-
differ in real luminance `Y`. This chromatic drift is the one honest
|
|
95
|
-
approximation in the design — see [§10 Verification](#verification-apca--wcag-drift).
|
|
96
|
-
The single deliberate exception to the invariant is the optional `contrast`
|
|
97
|
-
solver, which shifts a stop's tone per `h`/`s` to meet a luminance-based floor.
|
|
98
|
-
|
|
99
|
-
## Reference eps vs per-mode eps
|
|
100
|
-
|
|
101
|
-
Two distinct roles, kept separate on purpose:
|
|
102
|
-
|
|
103
|
-
- **Reference eps (`0.05`, fixed).** Defines the OKHST _color space_ and the
|
|
104
|
-
canonical stored tone. `okhst()` input, `{ h, s, t }` input, the internal
|
|
105
|
-
`ResolvedColorVariant.t`, relative `tone` offsets, and the contrast solver all
|
|
106
|
-
use the reference eps. This is what makes OKHST stable and scheme-independent.
|
|
107
|
-
- **Per-mode eps (`lightTone.eps`, `darkTone.eps`).** A _rendering_ curvature
|
|
108
|
-
knob per scheme. It only affects how authored tone is mapped through a scheme
|
|
109
|
-
window before the result is stored. Defaults to the reference value, so by
|
|
110
|
-
default the two coincide and there is nothing to reconcile.
|
|
111
|
-
|
|
112
|
-
When a mode's eps differs from the reference, `mapToneForScheme` maps using the
|
|
113
|
-
mode eps to land a final OKHSL `l`, then stores `toTone(l, REF_EPS)` so offsets
|
|
114
|
-
and contrast stay comparable across schemes.
|
|
115
|
-
|
|
116
|
-
## Scheme pipeline (no Möbius)
|
|
117
|
-
|
|
118
|
-
```
|
|
119
|
-
author tone T (0–100)
|
|
120
|
-
→ mode branch:
|
|
121
|
-
auto + dark : invert T' = 100 - T
|
|
122
|
-
fixed / light: keep T' = T
|
|
123
|
-
static : identity, skip window
|
|
124
|
-
→ window remap: T' into the scheme window [lo, hi] (tone units)
|
|
125
|
-
→ render curvature (mode eps) → OKHSL l
|
|
126
|
-
→ store canonical tone t = toTone(l, REF_EPS) // variant {h, s, t, alpha}
|
|
127
|
-
→ (edge only) fromTone(t, REF_EPS) → l → sRGB / luminance
|
|
128
|
-
optional: contrast floor (wcag/apca) searches in tone, overriding t
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
High-contrast is **not** a separate curve. It reuses the same math with the
|
|
132
|
-
window forced to the full range `[0, 100]`, keeping the mode's eps. There is no
|
|
133
|
-
`darkCurve` and no separate HC curve.
|
|
134
|
-
|
|
135
|
-
`fixed` mode remaps into the window but does **not** invert (brand colors stay
|
|
136
|
-
recognizable). `static` skips the window entirely (identity) so the same tone
|
|
137
|
-
renders in every scheme.
|
|
138
|
-
|
|
139
|
-
## Calibrated constants (defaults)
|
|
140
|
-
|
|
141
|
-
Chosen as clean defaults that keep light mode close to the previous pipeline
|
|
142
|
-
while the axis stays contrast-uniform (the old Möbius curve was intentionally
|
|
143
|
-
non-uniform, which is what we are replacing). The light floor sits at `lo = 10`
|
|
144
|
-
and the dark floor at `lo = 15`, so neither scheme bottoms out darker than the
|
|
145
|
-
legacy pipeline produced. `eps` is pinned to the reference value `0.05` so the
|
|
146
|
-
tone axis stays WCAG-uniform.
|
|
147
|
-
|
|
148
|
-
| Config | lo | hi | eps |
|
|
149
|
-
|---|---|---|---|
|
|
150
|
-
| `lightTone` | 10 | 100 | 0.05 |
|
|
151
|
-
| `darkTone` | 15 | 95 | 0.05 |
|
|
152
|
-
|
|
153
|
-
A window is authored as `[lo, hi]` (reference eps — the common form),
|
|
154
|
-
`{ lo, hi, eps }` (advanced: explicit per-mode render curvature), or `false`
|
|
155
|
-
to disable clamping. `false` is the full range `[0, 100]` at the reference eps —
|
|
156
|
-
it removes the **boundaries**, not the tone curve.
|
|
157
|
-
|
|
158
|
-
Other defaults: `darkDesaturation = 0.1` (unchanged),
|
|
159
|
-
`autoFlip = true`.
|
|
160
|
-
|
|
161
|
-
Reference: `REF_EPS = 0.05`.
|
|
162
|
-
|
|
163
|
-
## Contrast metric (unified)
|
|
164
|
-
|
|
165
|
-
`contrast` is a single prop with a pluggable metric:
|
|
166
|
-
|
|
167
|
-
```ts
|
|
168
|
-
type ContrastSpec =
|
|
169
|
-
| number // bare WCAG ratio
|
|
170
|
-
| ContrastPreset // 'AA' | 'AAA' | 'AA-large' | 'AAA-large' (WCAG)
|
|
171
|
-
| { wcag: HCPair<number | ContrastPreset> }
|
|
172
|
-
| { apca: HCPair<number | ApcaPreset> };
|
|
173
|
-
|
|
174
|
-
contrast?: HCPair<ContrastSpec>;
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
A bare number or preset means WCAG. The `[normal, highContrast]` pair may live at
|
|
178
|
-
the outer level (`[4.5, 7]`, `[{ wcag: 4.5 }, { wcag: 7 }]`) **or** inside the
|
|
179
|
-
metric (`{ wcag: [4.5, 7] }`, `{ apca: [45, 60] }`, `{ apca: ['content', 'body'] }`). `resolveContrastForMode`
|
|
180
|
-
peels the outer pair by mode, then the inner metric pair by the same mode, then
|
|
181
|
-
resolves presets, returning `{ metric, target }`.
|
|
182
|
-
|
|
183
|
-
The solver searches in **tone** (contrast-uniform → fast convergence and a
|
|
184
|
-
closed-form WCAG seed). For WCAG, the seed is the tone whose gray luminance hits
|
|
185
|
-
`Y = R·(Y_base + 0.05) − 0.05`; chromatic drift is then refined by binary search.
|
|
186
|
-
For APCA, it binary-searches tone against the APCA Lc target.
|
|
187
|
-
|
|
188
|
-
### APCA
|
|
189
|
-
|
|
190
|
-
`apcaContrast(yText, yBg)` implements SAPC/APCA Lc (soft-clamp of low luminances
|
|
191
|
-
plus the polarity exponents for normal vs reverse contrast), returning a signed
|
|
192
|
-
Lc whose magnitude the solver compares against the target. Its inputs are APCA
|
|
193
|
-
*screen* luminances `Ys = 0.2126·R^2.4 + 0.7152·G^2.4 + 0.0722·B^2.4` over the
|
|
194
|
-
gamma-encoded channels (`apcaLuminanceFromLinearRgb`), **not** WCAG relative
|
|
195
|
-
luminance — the soft-clamp constants are calibrated against `Ys`, so the solver
|
|
196
|
-
feeds it the matching basis. This is a faithful-but-simplified APCA (it omits
|
|
197
|
-
the spatial/font-size lookup that maps Lc to a usable text size).
|
|
198
|
-
|
|
199
|
-
#### Polarity (roles)
|
|
200
|
-
|
|
201
|
-
APCA is **asymmetric**: `|apcaContrast(a, b)| ≠ |apcaContrast(b, a)|`, because
|
|
202
|
-
the normal-polarity exponents (dark text on light bg) differ from the
|
|
203
|
-
reverse-polarity exponents (light text on dark bg). Glaze picks the argument
|
|
204
|
-
order from each color's semantic **role** against its base:
|
|
205
|
-
|
|
206
|
-
| Role | Polarity | Argument order | Aliases (name inference) |
|
|
207
|
-
| --------- | -------- | ------------------------------ | ------------------------------------------- |
|
|
208
|
-
| `text` | `fg` | `apcaContrast(candidate, base)`| `text`, `fg`, `foreground`, `content`, `ink`, `label`, `stroke` |
|
|
209
|
-
| `border` | `fg` | `apcaContrast(candidate, base)`| `border`, `divider`, `outline`, `separator`, `hairline`, `rule` |
|
|
210
|
-
| `surface` | `bg` | `apcaContrast(base, candidate)`| `surface`, `bg`, `background`, `fill`, `canvas`, `paper`, `layer` |
|
|
211
|
-
|
|
212
|
-
A color's role is resolved per the chain: explicit `role` → inferred from the
|
|
213
|
-
color name (last recognized token wins, so `button-text` → `text`,
|
|
214
|
-
`input-bg` → `surface`) → the opposite of the base's role → `'text'` (foreground)
|
|
215
|
-
default. Name inference is on by default (`config.inferRole: true`) and can be
|
|
216
|
-
disabled. WCAG is symmetric, so role never changes WCAG results — it only fixes
|
|
217
|
-
APCA argument order.
|
|
218
|
-
|
|
219
|
-
#### APCA presets
|
|
220
|
-
|
|
221
|
-
APCA targets may be a raw Lc number or a named preset (APCA Bronze Simple Mode
|
|
222
|
-
conformance levels), independent of role:
|
|
223
|
-
|
|
224
|
-
```ts
|
|
225
|
-
type ApcaPreset = 'preferred' | 'body' | 'content' | 'large' | 'non-text' | 'min';
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
| Preset | Lc | Use case |
|
|
229
|
-
| ------------- | --- | ---------------------------------------------------- |
|
|
230
|
-
| `'preferred'` | 90 | Preferred body / column text |
|
|
231
|
-
| `'body'` | 75 | Minimum body / column text |
|
|
232
|
-
| `'content'` | 60 | Readable non-body content (~WCAG AA 4.5:1) |
|
|
233
|
-
| `'large'` | 45 | Large/bold headlines; fine icons/outlines (~3:1) |
|
|
234
|
-
| `'non-text'` | 30 | Solid icons/controls; placeholder/disabled text |
|
|
235
|
-
| `'min'` | 15 | Dividers/decorative; APCA "point of invisibility" |
|
|
236
|
-
|
|
237
|
-
```ts
|
|
238
|
-
contrast: { apca: 'content' } // Lc 60
|
|
239
|
-
contrast: { apca: ['content', 'body'] } // HC pair: 60 normal, 75 high-contrast
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
## Verification (APCA / WCAG drift)
|
|
243
|
-
|
|
244
|
-
Because chromatic swatches inherit gray's tone-derived lightness but drift in
|
|
245
|
-
real luminance, a color resolved with a `base` + `contrast` may land slightly
|
|
246
|
-
under the contrast its tone implies. After resolving such a color, Glaze
|
|
247
|
-
computes the actual WCAG ratio and APCA Lc of the chromatic result against its
|
|
248
|
-
base and emits a deduped `console.warn` when it drifts below the gray-tone
|
|
249
|
-
expectation. This is advisory: it surfaces the one approximation rather than
|
|
250
|
-
hiding it. The dedupe cache is the existing 256-entry cache in
|
|
251
|
-
[warnings.ts](../src/warnings.ts).
|
|
252
|
-
|
|
253
|
-
## Migration from `lightness`
|
|
254
|
-
|
|
255
|
-
`lightness` (OKHSL `l`, 0–100) is replaced by `tone` (0–100). They are **not**
|
|
256
|
-
the same number — tone is the contrast-uniform reparameterization. To convert an
|
|
257
|
-
old absolute `lightness: L` to the equivalent `tone`, use
|
|
258
|
-
`toTone(L/100, 0.05)`. See [migration.md](migration.md) for the full guide and
|
|
259
|
-
a conversion table.
|