@tenphi/glaze 0.15.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/migration.md CHANGED
@@ -68,7 +68,31 @@ Relative `tone` offsets that overshoot `[0, 100]` now **mirror to the other side
68
68
 
69
69
  ### Resolved variants store tone
70
70
 
71
- `ResolvedColorVariant` now exposes `t` (tone, 0–1) instead of `l`. If you read resolved internals, convert with `variantToOkhsl(variant).l`. Token/CSS/JSON output is unchanged Glaze still emits `okhsl(...)` / `rgb(...)` etc. (`okhst` is input-only and is never emitted).
71
+ `ResolvedColorVariant` now exposes `t` (tone, 0–1) instead of `l`. If you read resolved internals, convert with `variantToOkhsl(variant).l`. Token/CSS/JSON output uses native formats by default (`oklch` for `tokens()` / `json()`, `okhsl` for `tasty()`); use `tasty({ format: 'okhsl' })` or `tasty({ format: 'okhst' })` for Glaze-native spaces.
72
+
73
+ ### 0.16.0 — output format defaults and Tasty-only spaces
74
+
75
+ **Breaking changes in 0.16.0:**
76
+
77
+ | Export | Old default | New default |
78
+ |---|---|---|
79
+ | `tokens()` / `json()` (theme + palette) | `okhsl` | `oklch` |
80
+ | Standalone `.json()` | `okhsl` | `oklch` |
81
+ | `tasty()` | `okhsl` (unchanged) | `okhsl` |
82
+
83
+ `'okhsl'` and the new `'okhst'` output format are **Tasty-only**. Passing either to `css()`, `tailwind()`, `tokens()`, or `json()` throws. Migrate:
84
+
85
+ ```ts
86
+ // before
87
+ theme.tokens({ format: 'okhsl' })
88
+ theme.json()
89
+
90
+ // after — pick one
91
+ theme.tasty({ format: 'okhsl' }) // Tasty #name keys, okhsl strings
92
+ theme.tokens({ format: 'oklch' }) // native CSS (new default)
93
+ ```
94
+
95
+ **New:** `splitHue` on `css()` / `tasty()` (theme + palette) and standalone `color.css()` emits hue as a separate custom property for runtime re-skinning. Requires `format: 'oklch'` and every color to be pastel. See [api.md → Hue channel splitting](api.md#hue-channel-splitting-splithue).
72
96
 
73
97
  ### Export snapshots
74
98
 
@@ -76,16 +100,19 @@ Relative `tone` offsets that overshoot `[0, 100]` now **mirror to the other side
76
100
 
77
101
  ## Choosing an export
78
102
 
79
- Glaze emits the same resolved colors in four shapes. Pick one based on your renderer.
103
+ Glaze emits the same resolved colors in six shapes. Pick one based on your renderer / tooling.
80
104
 
81
105
  | Method | Output shape | Use it for |
82
106
  |---|---|---|
83
107
  | `palette.tasty(options?)` | `{ '#name': { '': value, '@dark': value, '@hc': value } }` | The [Tasty](https://tasty.style/docs) style system. Single object, state aliases keyed inside each token. |
84
108
  | `palette.tokens(options?)` | `{ light: { name: value }, dark: { name: value }, ... }` | Most CSS-in-JS systems. Per-variant flat maps, easy to feed into a `:root { ... }` selector via your framework's globals. |
85
109
  | `palette.css(options?)` | `{ light: '--name-color: rgb(...);', dark: '...', ... }` | Framework-free CSS / static stylesheets. Variant-grouped CSS custom property strings ready to wrap in `:root` and `prefers-color-scheme` queries. |
86
- | `palette.json(options?)` | `{ themeName: { name: { light, dark, ... } } }` | Tooling, JSON pipelines, design-token exporters (Style Dictionary, etc.). |
110
+ | `palette.json(options?)` | `{ themeName: { name: { light, dark, ... } } }` | Tooling, JSON pipelines. |
111
+ | `palette.dtcg(options?)` | `{ light: { name: { $type, $value } }, dark: { ... }, ... }` | W3C [DTCG 2025.10](https://www.designtokens.org/) `.tokens.json` — Figma, Tokens Studio, Style Dictionary, Terrazzo, Penpot. One document per scheme. |
112
+ | `palette.dtcgResolver(options?)` | `{ version, sets, modifiers, resolutionOrder }` | W3C DTCG **Resolver-Module** — a single document describing every scheme variant as `sets` + a `scheme` modifier with a context per variant. For resolver tools such as Dispersa. |
113
+ | `palette.tailwind(options?)` | `'@theme { --color-*: ... } .dark { ... } ...'` | Tailwind CSS v4. A single ready-to-paste `@theme` block plus dark / high-contrast overrides. |
87
114
 
88
- All four accept `format` (`'okhsl' \| 'rgb' \| 'hsl' \| 'oklch'`). `tokens()`, `tasty()`, and `json()` also accept `modes` (`{ dark, highContrast }`). `css()` always returns all four strings (`light`, `dark`, `lightContrast`, `darkContrast`). See [api.md → Palette](api.md#palette) for full options.
115
+ `tasty()`, `tokens()`, `json()`, `dtcg()`, `dtcgResolver()`, and `tailwind()` accept `modes` (`{ dark, highContrast }`). `css()` always returns all four strings (`light`, `dark`, `lightContrast`, `darkContrast`). The CSS-string exports accept `format` (`'rgb' \| 'hsl' \| 'oklch'` on `tokens`/`json`/`css`/`tailwind`; `'okhsl' \| 'okhst'` on `tasty()` only); `dtcg()` and `dtcgResolver()` use `colorSpace` (`'srgb' \| 'oklch'`) instead. See [api.md → Palette](api.md#palette) for full options.
89
116
 
90
117
  ## Wiring exports into the app
91
118
 
@@ -164,9 +191,47 @@ const data = palette.json();
164
191
 
165
192
  Feed into your tooling pipeline. Each color is grouped by theme name, then by token name, then by variant — no prefix logic to undo.
166
193
 
194
+ ### W3C DTCG (`.tokens.json`)
195
+
196
+ ```ts
197
+ import { writeFileSync } from 'node:fs';
198
+ const dtcg = palette.dtcg();
199
+
200
+ writeFileSync('tokens.light.tokens.json', JSON.stringify(dtcg.light, null, 2));
201
+ if (dtcg.dark) {
202
+ writeFileSync('tokens.dark.tokens.json', JSON.stringify(dtcg.dark, null, 2));
203
+ }
204
+ ```
205
+
206
+ Each document is a spec-conformant token tree. One file per scheme is the most tool-compatible convention — Style Dictionary treats them as themes, Tokens Studio as sets, and Figma as variable modes. Use `colorSpace: 'oklch'` for wide-gamut, Glaze-native values (no `hex`); the default `'srgb'` emits components plus a `hex` hint that every reader understands. Feed the files straight into Style Dictionary v4+, Tokens Studio, or any DTCG-compatible tool — no Glaze-specific transform needed.
207
+
208
+ ### W3C DTCG Resolver-Module (single document)
209
+
210
+ When you want **one file** describing every scheme variant — for a resolver tool such as [Dispersa](https://github.com/dispersa-core/dispersa) — use `dtcgResolver()` instead of `dtcg()`:
211
+
212
+ ```ts
213
+ import { writeFileSync } from 'node:fs';
214
+ const resolver = palette.dtcgResolver({ modes: { highContrast: true } });
215
+
216
+ writeFileSync('resolver.json', JSON.stringify(resolver, null, 2));
217
+ ```
218
+
219
+ The document places the light tokens in `sets.base.sources[0]` (the default context) and emits a single `scheme` modifier with a context per variant (`light` / `dark` / `lightContrast` / `darkContrast`). Each non-default context holds that variant's exact resolved tokens — Glaze resolves `darkContrast` independently, so the four-context shape keeps every value correct (two independent modifiers would compose additively and produce wrong dark + high-contrast values). Rename the set, modifier, or contexts via `setName` / `modifierName` / `contextNames` if your resolver expects different labels. Prefer `dtcg()` when you need maximum per-file tool compatibility; prefer `dtcgResolver()` when a resolver tool consumes the document for you.
220
+
221
+ ### Tailwind CSS v4
222
+
223
+ ```ts
224
+ const css = palette.tailwind();
225
+ // Write to your CSS entry, or paste into a `@import "tailwindcss"` stylesheet:
226
+ // @theme { --color-primary-surface: oklch(...); ... }
227
+ // .dark { --color-primary-surface: oklch(...); ... }
228
+ ```
229
+
230
+ The `--color-*` namespace makes every color available as `bg-*` / `text-*` / `border-*`. Drive dark mode from the OS preference instead of a class with `darkSelector: '@media (prefers-color-scheme: dark)'` (it nests `:root` automatically). High-contrast overrides land under `.high-contrast` and `.dark.high-contrast` by default; both are omitted unless `modes.highContrast` is enabled.
231
+
167
232
  ## Prefix map strategies
168
233
 
169
- `palette.tokens()` / `tasty()` / `css()` accept a `prefix` option:
234
+ `palette.tokens()` / `tasty()` / `css()` / `dtcg()` / `tailwind()` accept a `prefix` option:
170
235
 
171
236
  | Value | Result |
172
237
  |---|---|
package/docs/okhst.md CHANGED
@@ -169,14 +169,14 @@ type ContrastSpec =
169
169
  | number // bare WCAG ratio
170
170
  | ContrastPreset // 'AA' | 'AAA' | 'AA-large' | 'AAA-large' (WCAG)
171
171
  | { wcag: HCPair<number | ContrastPreset> }
172
- | { apca: HCPair<number> };
172
+ | { apca: HCPair<number | ApcaPreset> };
173
173
 
174
174
  contrast?: HCPair<ContrastSpec>;
175
175
  ```
176
176
 
177
177
  A bare number or preset means WCAG. The `[normal, highContrast]` pair may live at
178
178
  the outer level (`[4.5, 7]`, `[{ wcag: 4.5 }, { wcag: 7 }]`) **or** inside the
179
- metric (`{ wcag: [4.5, 7] }`, `{ apca: [45, 60] }`). `resolveContrastForMode`
179
+ metric (`{ wcag: [4.5, 7] }`, `{ apca: [45, 60] }`, `{ apca: ['content', 'body'] }`). `resolveContrastForMode`
180
180
  peels the outer pair by mode, then the inner metric pair by the same mode, then
181
181
  resolves presets, returning `{ metric, target }`.
182
182
 
@@ -196,6 +196,49 @@ luminance — the soft-clamp constants are calibrated against `Ys`, so the solve
196
196
  feeds it the matching basis. This is a faithful-but-simplified APCA (it omits
197
197
  the spatial/font-size lookup that maps Lc to a usable text size).
198
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
+
199
242
  ## Verification (APCA / WCAG drift)
200
243
 
201
244
  Because chromatic swatches inherit gray's tone-derived lightness but drift in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/glaze",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "OKHST-based color theme generator with WCAG contrast solving for light, dark, and high-contrast schemes",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",