@tenphi/glaze 2.0.2 → 3.0.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/AGENTS.md +69 -32
- package/README.md +45 -25
- package/dist/index.cjs +3091 -1318
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +564 -119
- package/dist/index.d.mts +564 -119
- package/dist/index.mjs +3087 -1319
- package/dist/index.mjs.map +1 -1
- package/docs/contrast.mdx +751 -0
- package/docs/exporting.mdx +635 -0
- package/docs/getting-started.mdx +487 -0
- package/docs/hdr.mdx +523 -0
- package/docs/methodology.mdx +976 -0
- package/docs/migration.md +476 -285
- package/docs/mixes.mdx +348 -0
- package/docs/okhst.mdx +692 -0
- package/docs/reference/color-definitions.md +552 -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 +225 -0
- package/docs/reference/output.md +714 -0
- package/docs/reference/palette.md +435 -0
- package/docs/reference/theme.md +332 -0
- package/docs/reference/utilities.md +646 -0
- package/docs/schemes.mdx +636 -0
- package/docs/shadows-and-glows.mdx +519 -0
- package/package.json +4 -7
- package/docs/api.md +0 -2021
- package/docs/methodology.md +0 -686
- package/docs/okhst.md +0 -207
package/AGENTS.md
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
```
|
|
14
14
|
glaze/
|
|
15
15
|
├── src/ Source
|
|
16
|
-
├── docs/ Long-form docs
|
|
16
|
+
├── docs/ Long-form docs: concepts, guides, reference (md/mdx, rendered by the site)
|
|
17
|
+
├── docs-site/ Site-only files: home page + live-example components
|
|
17
18
|
├── assets/ Logo + brand assets
|
|
18
19
|
├── scripts/ Repo helpers
|
|
19
20
|
├── README.md Compact intro
|
|
@@ -21,8 +22,11 @@ glaze/
|
|
|
21
22
|
├── CHANGELOG.md Auto-generated by Changesets
|
|
22
23
|
├── package.json
|
|
23
24
|
├── tsconfig.json
|
|
25
|
+
├── tsconfig.test.json Typecheck config for src/**/*.test.ts (Vitest globals)
|
|
24
26
|
├── tsdown.config.ts Build config
|
|
25
27
|
├── vitest.config.ts Test config
|
|
28
|
+
├── astro.config.ts Docs site (Astro + Cookbook)
|
|
29
|
+
├── docs.config.ts Docs site config: navigation, theme, `@hc` state
|
|
26
30
|
└── eslint.config.js
|
|
27
31
|
```
|
|
28
32
|
|
|
@@ -30,37 +34,67 @@ glaze/
|
|
|
30
34
|
|
|
31
35
|
| File | Description |
|
|
32
36
|
|---|---|
|
|
33
|
-
| [src/index.ts](src/index.ts) | Public barrel; re-exports `glaze`, every public type, the contrast-solver utilities, and the OKHSL/sRGB/OKLab math helpers. |
|
|
34
|
-
| [src/glaze.ts](src/glaze.ts) | `glaze()` factory + attached statics: `palette`, `paletteFrom`, `color`, `colorFrom`, `themeFrom`, `from` (alias), `fromHex`, `fromRgb`, `shadow`, `format`, `configure`, `getConfig`, `resetConfig`, `isThemeExport` / `isColorTokenExport` / `isPaletteExport`. Thin wiring layer over the focused modules below. |
|
|
35
|
-
| [src/theme.ts](src/theme.ts) | Single-theme factory (`createTheme`). Owns the mutable `ColorMap`, the `resolve()` cache (versioned against `getConfigVersion()`), and the `tokens` / `tasty` / `json` / `css` / `extend` / `export` methods. `export(override?)` deep-clones defs and freezes effective config with `kind`/`version`. |
|
|
36
|
-
| [src/palette.ts](src/palette.ts) | Multi-theme composition (`createPalette` / `createPaletteFromExport`). Shared per-theme driver `buildPaletteOutput` handles prefix resolution, primary duplication, collision filtering. Authoring: `export(override?)`, `theme` / `themes` / `list` / `primary`. |
|
|
37
|
-
| [src/color-token.ts](src/color-token.ts) | Standalone `glaze.color()` tokens. Parses values through `color-value.ts`; owns the structured-input validator, the two factory paths, sparse local config + live resolve, and the JSON-safe export / `glaze.colorFrom` rehydrate round-trip. |
|
|
38
|
-
| [src/color-value.ts](src/color-value.ts) | `GlazeColorValue` parsing + validation: hex (3/6/8), the `rgb()` / `hsl()` / `okhsl()` / `okhst()` / `oklch()` functions, and the four value-object shapes, all normalized to OKHSL by `extractOkhslFromValue
|
|
39
|
-
| [src/serialize.ts](src/serialize.ts) | Authoring-export helpers: `GLAZE_EXPORT_VERSION
|
|
40
|
-
| [src/resolver.ts](src/resolver.ts) | Four-pass solver (light → light-HC → dark → dark-HC), dropping to two-pass (light → dark) with mirrored HC slots only at `contrastLevel: 100`, where the normal passes already are the HC ones (`hcMirrorsNormal`). Under any other manual level `passTone` / `passNumber` / `resolveContrastSpec` feed interpolated inputs into the two *normal* passes and a probe solve at the nearer endpoint pins which side of its base a contrast-solved color sits on; the HC passes bypass the level and the probe entirely, so the HC tier is bit-identical to `'auto'`. Stores canonical tone (`t`) in variants; per-scheme branches for regular, shadow, and mix defs; integrates the contrast solver, and the OKHST tone helpers. `resolveChannels` owns per-scheme hue/saturation (incl. the `darkHue` / `darkSaturation` seed + def overrides, the `darkDesaturation` bypass, and the absolute hue/saturation a `from` color carries in place of the seed factor) and feeds both the emitted variant and the contrast solver. `configForColor` gives a `from` color a local `lightTone: false` so its light variant reproduces the authored value. Converts to/from OKHSL lightness only at the mix/shadow edges. Pre-seeds externally-resolved bases for `glaze.color({ base })`. |
|
|
41
|
-
| [src/okhst.ts](src/okhst.ts) | The OKHST tone layer. `REF_EPS`, tone↔lightness transfers (`toTone`/`fromTone`, `toneFromY`/`yFromTone
|
|
42
|
-
| [src/contrast-solver.ts](src/contrast-solver.ts) | Tone-based binary-search solver for WCAG **and** APCA. Public API: `findToneForContrast` (incl. the `preferInitial` tie-break the manual level uses on its normal passes to keep a color's side stable), `findValueForMixContrast
|
|
43
|
-
| [src/
|
|
44
|
-
| [src/hc-pair.ts](src/hc-pair.ts) | Tiny shared helpers: HC-pair selection (`pairNormal` / `pairHC`), HC-pair interpolation for the manual contrast level (`contrastFraction`, `hcMirrorsNormal` — the level-100 "HC slots are duplicates" predicate, `levelFraction`, `numberAt`, `parseToneValueAt`, `PAIR_SWITCH`), tone-value parsing (`parseToneValue` for absolute / relative / `'max'`/`'min'` extremes, `isExtremeTone`), root-tone discrimination (`isAbsoluteTone`, now incl. extremes), `clamp` / `lerp`, hue resolution (`resolveEffectiveHue`), relative-value parsing. Leaf module — types only, so every other module can import it. |
|
|
45
|
-
| [src/
|
|
46
|
-
| [src/
|
|
47
|
-
| [src/
|
|
48
|
-
| [src/
|
|
49
|
-
| [src/
|
|
50
|
-
| [src/
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
| `src/
|
|
37
|
+
| [src/index.ts](src/index.ts) | Public barrel; re-exports `glaze`, `GLAZE_EXPORT_VERSION`, the snapshot type guards, every public type (incl. the HDR ones: `ResolvedHdr`, `GlazeHdrFormat`, `GlazeCssSchemeBlocks`, `GlazeDtcgHdr`, `GlazeDtcgExtensions`), the `splitHue` planning types, the export guards, the contrast-solver utilities and constants, the role helpers, the OKHST tone utilities, and the OKHSL/sRGB/OKLab math helpers (incl. `apcaLuminanceFromLinearRgb`, `formatSrgb` and `formatSrgbLinear`), and the HDR channel helpers `hdrToSrgb` / `hdrToLinearSrgb`. |
|
|
38
|
+
| [src/glaze.ts](src/glaze.ts) | `glaze()` factory (`glaze(hue, saturation?, config?)` or `glaze(seed, config?)`; the old `glaze(seed, undefined, config)` still works, deprecated) + attached statics: `palette`, `paletteFrom`, `color`, `colorFrom`, `themeFrom`, `from` (alias), `fromHex`, `fromRgb`, `shadow`, `glow` (returns `hdr` when its step passes white with `headroom` > 0), `format`, `formatHdr`, `configure`, `getConfig`, `resetConfig`, `isThemeExport` / `isColorTokenExport` / `isPaletteExport`. `shadow` / `glow` require a `bg` and a finite `intensity` (the parser reads a `bg` / `fg` past white or black as white or black), validate their `tuning` with `normalizeLightingTuning`, and warn through `warnLightingShortfall`. The untyped `glaze(hue, config)` takes the config when no third argument does, and a `null` saturation or config reads as unset. The snapshot guards are cast to explicit guard types so the declaration bundler can't turn them into `any`. Thin wiring layer over the focused modules below. |
|
|
39
|
+
| [src/theme.ts](src/theme.ts) | Single-theme factory (`createTheme`; `assertValidSeed` rejects a non-finite seed hue / saturation). Owns the mutable `ColorMap`, the `resolve()` cache (versioned against `getConfigVersion()`), and the `tokens` / `tasty` / `json` / `css` / `dtcg` / `dtcgResolver` / `tailwind` / `extend` / `export` methods. The config override is copied once at creation (`copyConfigOverride`); `extend()` merges the parent's copy with a copy of `options.config`; `getConfig()` returns `copyResolvedConfig(effectiveConfig)`, a deep copy. Exports carry the HDR tier only with `modes.hdr` (`css()`: `options.hdr ?? modes.hdr`); `tasty()` keys it with `states.hdr`. `tasty()` / `css()` with `splitHue` run its guards first (`assertSplitHueFormat`, `assertAllPastel`, `claimHueVars`), and `css()` also claims each color's value property (`claimValueProps`). `export(override?)` deep-clones defs and freezes effective config with `kind`/`version`. |
|
|
40
|
+
| [src/palette.ts](src/palette.ts) | Multi-theme composition (`createPalette` / `createPaletteFromExport`, plus `createThemeFromExport` behind `glaze.themeFrom`). Shared per-theme driver `buildPaletteOutput` handles prefix resolution, primary duplication, collision filtering for `tokens` / `tasty` / `css` / `dtcg` / `dtcgResolver` / `tailwind`; `json()` keys by theme name with no prefix, primary or collisions. The HDR tier is built from the same filtered, prefixed maps and the primary alias pass (`css()` accumulates `{ sdr, hdr }` lines; `tailwind()` merges with `mergeTailwindLines` / `emptyTailwindLines`). `tasty()` / `css()` with `splitHue` share one owners map per call, so hue vars and (`css()`) value properties, primary aliases included, are claimed across every pass. Authoring: `export(override?)`, `theme` / `themes` / `list` / `primary`. |
|
|
41
|
+
| [src/color-token.ts](src/color-token.ts) | Standalone `glaze.color()` tokens. Parses values through `color-value.ts`; owns the structured-input validator, the two factory paths, sparse local config (copied at creation with `copyConfigOverride`, in both factories) + live resolve, and the JSON-safe export / `glaze.colorFrom` rehydrate round-trip. Pins the role: an explicit `role`, else `'text'` (the token is the foreground against its base or its own input color; `name` is a label and never drives inference). A link resolves against its base token's own variants in every scheme (what the base paints, under its own config), so a light `'+0'` lands on it and a light floor holds against it. The token copies its input by property access, so getters and proxies copy too (`copyTokenOptions` reads each `TOKEN_OPTION_KEYS` field, then `copyColorValue` for a value and a raw `base`, `copyPair` for tone pairs, `copyContrast`; the structured validator runs on the copy), and `export()` builds every object fresh the same way, so a snapshot and the live token never share one. A `base` is exported as its token's own snapshot — a raw one as the token it was wrapped in at creation — config frozen. The hidden `static` seed is marked with `anchorAtAuthoredTone`, so a delta on it inverts in dark like one on the same color in a theme. A structured `darkHue` must be a finite number or a `'+N'` / `'-N'` offset. Every token registers an internal dark-delta anchor hook (a `WeakMap` keyed by token), cached per pass next to its resolve cache, and resolves its base inline, so a `'+N'` link steps from where the base sits under the base's own windows and a chain of links adds one resolve frame per link. `css()` with `splitHue` claims the token's `--{name}-hue` and its value property in one namespace. Tokens are never glows, so HDR options don't change their output. |
|
|
42
|
+
| [src/color-value.ts](src/color-value.ts) | `GlazeColorValue` parsing + validation: hex (3/4/6/8 digits, every digit checked), the `rgb()` / `rgba()` / `hsl()` / `hsla()` / `okhsl()` / `okhst()` / `oklch()` functions (each component read strictly by `readComponent`: a number with an optional unit, `none` as 0, anything else throws; a hue takes `deg` / `turn` / `rad` / `grad`; out-of-range channels, lightness and saturation clamp, and a negative chroma to 0, as CSS clamps them; alpha — `/ A` or a fourth component — is dropped with a warning, once per string), and the four value-object shapes (a negative component rejected, naming it; a 0–1 field up to 1.5 clamps to 1), all copied by `copyColorValue`, all normalized to OKHSL by `extractOkhslFromValue` (an sRGB gray to saturation 0, by `srgbToOkhsl`). Split out of `color-token.ts` so the resolver can reach it for a theme color's `from` without closing an import cycle. Leaf module — depends only on the color math. |
|
|
43
|
+
| [src/serialize.ts](src/serialize.ts) | Authoring-export helpers: `GLAZE_EXPORT_VERSION` (2 since Glaze 3; HDR `headroom` rides in `glowTuning` / defs without a bump), `assertExportKind` / `assertExportVersion`, `assertNoLegacyShadows` (rejects a version-1 theme snapshot with shadow defs or `shadowTuning`, since shadows changed meaning in 3.0; palettes pass their version down to each theme), and `isThemeExport` / `isColorTokenExport` / `isPaletteExport` type guards. |
|
|
44
|
+
| [src/resolver.ts](src/resolver.ts) | Four-pass solver (light → light-HC → dark → dark-HC), dropping to two-pass (light → dark) with mirrored HC slots only at `contrastLevel: 100`, where the normal passes already are the HC ones (`hcMirrorsNormal`). Under any other manual level `passTone` / `passNumber` / `resolveContrastSpec` feed interpolated inputs into the two *normal* passes and a probe solve at the nearer endpoint pins which side of its base a contrast-solved color sits on (a probe that lands on its base pins no side); the HC passes bypass the level and the probe entirely, so the HC tier is bit-identical to `'auto'`. `sideOfBase` treats a preferred tone within 1e-9 of its base as on neither side, in the main solve and the probe, so float noise never picks the side. Stores canonical tone (`t`) in variants; per-scheme branches for regular, shadow / glow (one shared `resolveLightingForScheme`, which attaches a glow's `variant.hdr` via `toResolvedHdr` and warns through `warnLightingShortfall`), and mix defs; integrates the contrast solver, and the OKHST tone helpers. A dark `auto` tone delta steps from `darkAnchor` (memoized per pass on `ResolveContext.anchors`; a chain of tone-less aliases is walked in a loop, not recursed): the authored tone for an `auto` base placed by a tone or for a def marked with `anchorAtAuthoredTone` (a standalone token's hidden `static` seed), its base's anchor plus the delta for an `auto` delta, and for any other base — `fixed` / `static`, or an `auto` contrast-solved, mix, or `'max'` / `'min'`-on-a-base one — its own dark tone read back through the dark window (`unmapDarkTone`), so an `auto` dependent mirrors around wherever the base sits. `stepFromAnchor` clamps the step to 0–100 widened to reach an anchor past the window, and `readsStoredLightTone` reads a light-side anchor's stored light tone in HC and at level 100 at the REF light eps, so those stay bit-identical. External `glaze.color()` bases supply their anchor through a `DarkAnchorHook` (`{ tone, config }`, passed as the `externalAnchors` argument of `resolveAllColors`; `darkAnchorOf` is what the hook calls); an external base under another dark window (`sameDarkWindow`), or one with no hook, is read back through this pass's. `resolveChannels` owns per-scheme hue/saturation (incl. the `darkHue` / `darkSaturation` seed + def overrides, the `darkDesaturation` bypass, and the absolute hue/saturation a `from` color carries in place of the seed factor) and feeds both the emitted variant and the contrast solver. `configForColor` gives a `from` color a local `lightTone: false` so its light variant reproduces the authored value. Converts to/from OKHSL lightness only at the mix / shadow / glow edges. A mix re-reads the saturation of a base or target whose `pastel` flag differs from its own in its own gamut, from the same OKLab color (hue and lightness carry over and a pastel mix caps a vivid one at the pastel limit; a gray, by `mixHue`'s `MIX_SAT_EPSILON`, is left as is, and a white or black is re-read just inside the lightness range (`MIX_EXTREME_L`), so it keeps its hue and the mix stays continuous at tone 0 / 100), and one `mixAt` / `paintAt` pair per blend feeds both the emitted variant and the contrast search, so the floor is measured on the painted color (an `srgb` lerp is measured raw until the pastel cap clamps it). Pre-seeds externally-resolved bases for `glaze.color({ base })`. |
|
|
45
|
+
| [src/okhst.ts](src/okhst.ts) | The OKHST tone layer. `REF_EPS`, tone↔lightness transfers (`toTone`/`fromTone`, `toneFromY`/`yFromTone`, unclamped, so an HDR value's tone can pass 100), OKHST↔OKHSL conversions, `variantToOkhsl` (tone→lightness at render), `normalizeToneWindow` (`[lo,hi]` / `{lo,hi,eps}` / `false` → `{lo,hi,eps}`; throws on any other shape, `lo > hi`, a non-finite endpoint, or an `eps` outside (0, 1e6] (`MAX_WINDOW_EPS`), then clamps endpoints to 0–100), `mapToneForScheme` (scheme inversion + window remap, HC bypass) and its dark inverse `unmapDarkTone` (`darkAnchor` reads a `fixed` / `static`, solved, mix, or extreme base back through the dark window, which comes back outside 0–100 for a tone past the window), `mapDarkToneUnclamped` (the dark `auto` map without the authored-scale clamp, bit-identical to `mapToneForScheme` inside 0–100, for a step from a base past the window), `sameDarkWindow` (whether two configs give a pass the same active dark window), `mapSaturationDark` (the `darkDesaturation` reducer the resolver skips when a dark saturation is authored), and `schemeToneRange` (the window in tone, used only to clamp a contrast-solved color's preferred tone; the solver itself searches the full range). `activeWindow` owns the HC window bypass and its continuous form under `contrastLevel`. Only tone adapts here — hue/saturation are the resolver's business. |
|
|
46
|
+
| [src/contrast-solver.ts](src/contrast-solver.ts) | Tone-based binary-search solver for WCAG **and** APCA. Public API: `findToneForContrast` (incl. the `preferInitial` tie-break the manual level uses on its normal passes to keep a color's side stable), `findValueForMixContrast` (no `targetLinearRgb`: the mix is described entirely by `luminanceAtValue`), `resolveContrastForMode`, `resolveContrastForLevel` (both ends resolved, target interpolated for a manual `contrastLevel`; throws on a metric switch), `resolveMinContrast`, `resolveApcaTarget`, `apcaContrast`, plus the `APCA_PRESETS` / `APCA_HC_ENHANCEMENT` / `APCA_MAX_LC` constants (the HC boost stops at `APCA_MAX_LC` but never lowers a normal target already past it). A spec is `{ apca }` only when its `apca` is set (`isApcaSpec`: a key holding `undefined` is unset). Internal: `isContrastPreset` / `isApcaPreset` (validation.ts's spec check), `metricLuminance` (the resolver's mix closures and drift check), `metricScore` (the resolver's pastel-cap check) and `contrastMetricOf` (validation.ts's WCAG-vs-APCA pair check). Searches aim 1% (WCAG) / 0.5 Lc (APCA) past the floor; the closed-form WCAG gray seed is the initial branch's first probe, never a bound, so a chromatic color lands on its own crossing. The two branches meet at the preferred position, so each reaches only its own side of the base (a branch that misses reports its outer extreme, never the position next to the other branch), and a nearest-wins gap within twice the search resolution is a tie that keeps the initial side. When no searched side meets the floor but the preferred position clears the bare target, that position is returned, met (tone solver: `branch: 'preferred'`, also for a met starting side whose only position is the preferred tone), so a value-100 mix keeps a passing pure target; an unmet result names the side whose bound it pinned to. With no `initialDirection`, a search starts toward the end that scores higher, and a float-noise tie between them (a gray base at tone 50) starts darker / lower. |
|
|
47
|
+
| [src/lighting.ts](src/lighting.ts) | Shadow / glow / mix def predicates (`isShadowDef`, `isGlowDef`, `isLightingDef`, `isMixDef`), `DEFAULT_LIGHTING_TUNING` + `resolveLightingTuning` (defaults ← global `shadowTuning` / `glowTuning` ← per-color, field by field with `??`, so a field set to `undefined` is unset, into a `ResolvedLightingTuning` whose `headroom` defaults to 0 and clamps to 0–`MAX_HEADROOM` (2)), `circularLerp`, and the exact solver `computeLighting`: object factor `k = max(0, 1 + β·d·(t_bg − t_fg)/100)` (`objectFactor`), the pigment (pure black / white unless `reach` / `tint` / `hueBleed` say otherwise; a neutral surface — saturation ≤ 1e-4, which a gray given as a value always is — gives a tint the object's hue with `hueBleed`, else none; a pigment that moves the surface by no more than float noise stays transparent), and `solveOpacity`, a bisection on opacity against browser compositing so the composite's luminance tone lands exactly `intensity × k` tones darker (shadow) / lighter (glow). Out-of-reach steps pin to the pigment's strongest composite (a golden-section search covers tinted shadow pigments whose channels diverge) and report `reached` for the warning. HDR glows (`exposeGlow`, closed form after the untouched SDR solve): with `headroom` > 0 and a target tone `T* = surface tone + step` past 100, a glow whose SDR is pinned at its brightest (`pinnedBase`: the pigment at alpha 1, or a white surface — tone ≥ 100 − 1e-6 — at alpha 0) gets `hdr`: that opaque base, taken exactly from its OKHSL source as standard (non-pastel) OKHSL, times `2^exposure`, with gain `min(Y(T*) / Y(base), 2^headroom / max channel)`. A pigment that cannot lift a non-white surface gets none and keeps the SDR shortfall. `toResolvedHdr` stores it in OKHST (`ResolvedHdr`); `hdrToLinearSrgb` / `hdrToSrgb` (both public) rebuild the unclamped linear / extended-sRGB channels the writers use. |
|
|
48
|
+
| [src/hc-pair.ts](src/hc-pair.ts) | Tiny shared helpers: HC-pair selection (`pairNormal` / `pairHC`), HC-pair interpolation for the manual contrast level (`contrastFraction`, `hcMirrorsNormal` — the level-100 "HC slots are duplicates" predicate, `levelFraction`, `numberAt`, `parseToneValueAt`, `PAIR_SWITCH`), tone-value parsing (`parseToneValue` for absolute / relative / `'max'`/`'min'` extremes, `isExtremeTone`), root-tone discrimination (`isAbsoluteTone`, now incl. extremes), `clamp` / `lerp`, hue resolution (`resolveEffectiveHue`), relative-value parsing, and `showValue` (writes a rejected input into a validation error, an `undefined` or `NaN` inside an array or object included — via a private-use marker, skipped when the caller's value holds it; shared by `normalizeToneWindow`, `normalizeHeadroom`, `normalizeContrastLevel`, and `normalizeDarkDesaturation`). Leaf module — types only, so every other module can import it. |
|
|
49
|
+
| [src/channels.ts](src/channels.ts) | Hue-channel planning for `splitHue` (`buildHuePlan` / `buildHuePlans` / `buildHueDeclarations`): per-color `var()` hue references and the `--*-hue` declarations behind them, collected per scheme with the dark set emitted only when it differs. A `mode: 'static'` color is pinned at its one hue when the dark seed hue differs, and otherwise ignores its `darkHue` (as the resolver does), keeping its light reference in both schemes. Every scheme renders with the light plan, so a color inline in light (achromatic there) is inline everywhere and declares no var in any scheme. Hue values are rounded to 2 decimals, like `oklch()` output. Also the `splitHue` guards: `assertSplitHueFormat` (only `oklch` takes a `var()` hue), and `claimHueVars` + `claimValueProps`, which share one `SplitHueOwners` map so the theme var, each color's own hue var, and (`css()` only) each value property keep one owner, throwing with fix text that names both sides. Public types `ChannelCtx`, `HuePlan`, `HueDeclaration`. |
|
|
50
|
+
| [src/format-guard.ts](src/format-guard.ts) | Export-time guards: `assertNativeFormat` (`okhsl` / `okhst` are `tasty()`-only) and `assertAllPastel` (`splitHue` needs every emitted color pastel). Both public. |
|
|
51
|
+
| [src/roles.ts](src/roles.ts) | Semantic roles for APCA polarity: alias keyword sets, name tokenization (`inferRoleFromName`), `normalizeRole`, `roleToPolarity`, `oppositeRole`. The per-color resolution chain (explicit → name inference → opposite of the base's → `'text'`) runs in the resolver's `resolveRole`. No dependencies. |
|
|
52
|
+
| [src/formatters.ts](src/formatters.ts) | Variant→string dispatch (`okhsl` / `rgb` / `hsl` / `oklch`), the HDR writer `formatHdrVariant` (extended `color(srgb R G B)` whatever the SDR format, or `color(srgb-linear R G B)` on request), and the token-map shapes Glaze emits: `buildTokenMap` ([Tasty](https://tasty.style) `#name` keys + state aliases), `buildFlatTokenMap` (per-variant maps), `buildJsonMap` (per-color JSON), `buildCssMap` (CSS custom-property declarations), `buildDtcgMap` / `buildDtcgResolver` (DTCG 2025.10 documents and the Resolver-Module document), and `buildTailwindLines` / `buildTailwindMap` / `emitTailwindCss` (Tailwind v4 `@theme` + dark / HC overrides). For `splitHue` it emits the light hue declarations plus, via `buildHueDeclarations` in `channels.ts`, the whole dark set when a `darkHue` makes it differ. The HDR tier (`variant.hdr`) is emitted only with `modes.hdr` (`css()`: its `hdr` option), always as a literal extended `color(srgb …)`, even under `splitHue`: Tasty gets a sparse `states.hdr` key after each SDR key that has one (Tasty gives later keys priority); `json` sparse `<scheme>Hdr` keys; `tokens` dense `<scheme>Hdr` maps; `css` dense `hdr` blocks over all four schemes; Tailwind dense blocks after all SDR blocks under `hdrSelector` (`DEFAULT_TAILWIND_HDR_SELECTOR`); DTCG `$extensions['me.tenphi.glaze'].hdr` (`dtcgHdrValue`) next to an unchanged SDR `$value`. Shared helpers: `SCHEMES`, `emittedSchemes`, `hasHdrIn`, `joinSchemeLines` / `emptySchemeLines`, `emptyTailwindLines` / `mergeTailwindLines`. Also the `resolveModes` helper used everywhere (`{ dark, highContrast, hdr }`, override ?? global). |
|
|
53
|
+
| [src/okhsl-color-math.ts](src/okhsl-color-math.ts) | OKHSL ↔ linear-sRGB ↔ gamma-sRGB ↔ OKLab conversions, strict hex parsing (`parseHex` / `parseHexAlpha`: 3/4/6/8 digits, every digit checked), `srgbToOkhsl` reading a gray (OKLab chroma under `GRAY_CHROMA`, 1e-6, above an exact gray's float residue) as saturation 0 with its hue kept, gamut clamping, the two luminances the contrast solver scores (WCAG `gamutClampedLuminance`, APCA `Ys` via `apcaLuminanceFromLinearRgb`, both public), browser compositing (`toDisplayGamma` / `compositeOverGamma` — `source-over` on gamma-encoded sRGB, shared by transparent mixes and shadows / glows), and the `formatOkhsl` / `formatOkhst` / `formatRgb` / `formatHsl` / `formatOklch` writers plus the unclamped HDR writers `formatSrgb` (extended sRGB channels, the sRGB curve continued past 1) and `formatSrgbLinear` (linear channels), both 4 decimals. Converters and writers share one scale — `h` 0–360, `s` / `l` / `t` 0–1 — so any converter composes with any writer (the 0–100 tone axis lives in `okhst.ts`'s `toTone` / `fromTone`, not here); `warnPercentScale` catches pre-2.0 percentage-scale input instead of emitting a plausible wrong color. The only file with the actual color science. |
|
|
54
|
+
| [src/config.ts](src/config.ts) | Global config singleton. `defaultConfig()` is the one source of truth for defaults (incl. `states.hdr` `'@media(dynamic-range: high)'` and `modes.hdr: false`); `configure()` validates every field into a new config (merging `states` / `modes` field by field; a call that throws changes nothing) and bumps a monotonic `configVersion` so theme caches invalidate. No config object is shared with a caller: `configure()` copies what it keeps, themes and tokens copy their override once where they store it (`copyConfigOverride`, which reads each `GlazeConfigOverride` key by property access, so getters, inherited fields and proxies work, and reads `null` as no override; also used for `extend({ config })` and `export(override)`), and `snapshotConfig()` (`glaze.getConfig()`) / `copyResolvedConfig()` (`theme.getConfig()`, a cheap field-wise copy) hand out deep copies. `mergeConfig()` merges an override over the live global at resolve (`pastel` is instance-only; an override's `shadowTuning` / `glowTuning` replaces the global one wholesale). `freezeConfigForExport()` freezes resolve-relevant fields for authoring export (`contrastLevel` only when instance-authored, since a global level is a live preference; never `modes` / `states`). `normalizeContrastLevel()` / `normalizeDarkDesaturation()` / `normalizeToneWindowInput()` validate and clamp the manual contrast level (0–100), `darkDesaturation` (0–1; non-finite throws), and tone windows, in `configure()` and (for per-instance overrides, at resolve) `mergeConfig()`; they and the window check treat only `undefined` as "keep", so `null` throws. `normalizeHeadroom()` / `normalizeLightingTuning()` (shared with validation.ts and `glaze.shadow()` / `glow()`) reject a non-finite `objectInfluence` / `tint` / `reach` / `hueBleed`, a non-finite or negative glow `headroom`, and any shadow `headroom`, and clamp one above `MAX_HEADROOM` with a warning that `configure()` / `mergeConfig()` queue (`DeferredWarnings`) until every field has passed. Imports `lighting.ts` and `warnings.ts`. |
|
|
55
|
+
| [src/validation.ts](src/validation.ts) | `validateColorDefs` (missing references, a shadow / glow without `bg`, a shadow / glow `bg` / `fg` cannot be a shadow or glow, a malformed `contrast` spec (`assertValidContrast`: a misspelled preset, a non-finite number, a bad shape; only `wcag` / `apca` keys that are set count, and `isPairOf` reads both entries by index, so a sparse `[, 7]` fails), a numeric field the resolver can't place (`assertField`: non-finite `tone` / `hue` / `darkHue` / `saturation` / `darkSaturation` / mix `value` / shadow `intensity`, a malformed `'+N'` — `isRelativeValue`, which allows surrounding whitespace and also checks a token's structured `darkHue` —, an `opacity` outside 0–1), mixes and regular bases cannot reference one, contrast-without-base, relative-without-base — `hasRelativeTone` checks both entries of an HC pair, so `tone: [50, '+20']` on a root throws — WCAG-vs-APCA metric switch in a `contrast` pair, and per-color lighting tuning through `normalizeLightingTuning`: a shadow `headroom` throws, a bad glow one throws, and clamp warnings are flushed only after every def and the cycle check pass) + `topoSort` so the resolver processes each color after its dependencies. |
|
|
56
|
+
| [src/warnings.ts](src/warnings.ts) | Deduped resolve-time warnings: contrast unmet (`warnContrastUnmet`, and `warnMixContrastUnmet` for mixes, whose hint names the pastel cap when it is why the floor is missed: the target painted in full would meet it), chromatic contrast drift (`warnContrastDrift`, which the resolver skips for unmet and achromatic variants), out-of-reach shadow / glow steps (`warnLightingShortfall` dispatches to the SDR `warnLightingUnreachable` or, when a glow has an HDR value, `warnHdrUnreachable`, which fires only when the headroom cap leaves the step short), and a glow `headroom` clamped to its maximum (`warnHeadroomClamped`, deduped per field and value). The three contrast warnings share one slack check (`missesFloor`: 2% under a WCAG ratio, 1.5 Lc under an APCA target); the lighting ones use a 0.05-tone slack (`LIGHTING_WARN_SLACK`). All share one dedupe cache, cleared at 256 entries to keep dev-server output bounded. `DeferredWarnings` / `flushWarnings` let a validator hold a warning until the whole input has passed. |
|
|
57
|
+
| [src/types.ts](src/types.ts) | All public TypeScript types: `HCPair`, `MinContrast`, `ContrastSpec`, `Role` / `RoleInput`, `RelativeValue`, `ExtremeValue` (`'max'`/`'min'`), `ToneValue`, `AdaptationMode`, `OkhstColor`, `ToneWindow` (`[lo,hi]` \| `{lo,hi,eps}` \| `false`), `ColorDef` discriminated union (regular with `tone` + `autoFlip` + `darkHue` / `darkSaturation`; `ShadowColorDef` / `GlowColorDef`; `MixColorDef`), `LightingTuning` (`ShadowTuning` is an alias; `GlowTuning` extends it with `headroom`), `GlazeConfig` (with `lightTone` / `darkTone` / `autoFlip` / `contrastLevel` / `shadowTuning` / `glowTuning`, and `states.hdr` / `modes.hdr`, always present on `GlazeConfigResolved`), `GlazeOutputModes` (`dark` / `highContrast` / `hdr`), `GlazeThemeSeed` (`hue` / `saturation` + optional dark pair), `GlazeTheme`, `GlazePalette`, `GlazeColorToken`, the per-method option types (`GlazeTastyOptions`, `GlazeCssOptions` with `hdr`, `GlazeTailwindOptions` with `hdrSelector`, and their palette / token variants), the `*Export` shapes, the DTCG shapes (`DtcgColorToken.$extensions` → `GlazeDtcgExtensions` / `GlazeDtcgHdr`), etc. `ResolvedColorVariant` stores `{ h, s, t, alpha }`, plus `pastel?` (the effective flag, which says which saturation space `s` is in) and `hdr?: ResolvedHdr` (`{ h, s, t, exposure }`, `s` standard OKHSL) on a glow past SDR white; `GlazeHdrFormat` is `'srgb'` \| `'srgb-linear'` (`'srgb'`, extended `color(srgb …)`, is the default and what every exporter writes; `GlazeDtcgHdr.value` is extended `srgb` too); `GlazeCssResult` has an optional `hdr: GlazeCssSchemeBlocks`; `GlazeGlowInput` takes a `GlowTuning`. |
|
|
58
|
+
| `src/glaze.test.ts` | Main test suite — covers the factory surface, resolver behavior, palette composition, every export format, shadow / glow (per-surface step uniformity, object influence, unreachable steps, v1 snapshot rejection) and mix algorithms (incl. transparent-mix contrast against browser compositing), gray values and sRGB blends of grays as mix endpoints, mixes whose base or target has another `pastel` flag (exact endpoints, a gray endpoint staying gray, the pastel cap, floors measured on the painted color), shadow / glow tuning (a tint on a neutral surface, a pigment that can't move the surface) and the standalone `glaze.shadow()` / `glaze.glow()` input checks, the manual contrast level, `glaze(hue, config)` and a `null` saturation, and the standalone color token round-trip. |
|
|
59
|
+
| `src/resolver.test.ts` | Dark tone deltas: the dark window's scale × the delta on every base (chains, `from` bases, solved / mixed / extreme bases, `fixed` / `static` bases incl. a `fixed` delta riding an `auto` base past the window, token links incl. a fixed base and a base with its own dark window), exact HC steps (custom light eps included), level 100 = HC, the identity-window and stored-light-tone cases, the low-base clamp, steps from a base past the window, one anchor read per linked token per pass, a token delta with no base stepping like one on its own color linked as a base, and long chains (of deltas, and of tone-less aliases) without deep recursion; plus `unmapDarkTone` / `mapDarkToneUnclamped` and the zero-delta / level-probe side ties. |
|
|
60
|
+
| `src/contrast-solver.test.ts` | Tests for the binary-search solver: spec resolution (`resolveMinContrast` / `resolveContrastForMode` / `resolveContrastForLevel`), `preferInitial`, `apcaContrast`, WCAG and APCA solves, branch-side sweeps (gray WCAG; APCA fg / bg and saturated hues, sided by metric luminance), float-noise side stability and the tone-50 start, the preferred tone a search can't improve on (kept, labelled `'preferred'`, met when there's no room), the gray-seed probe against a fine scan, and `findValueForMixContrast` (incl. value-0 / value-1 keeps and a value-100 mix theme check); the APCA HC boost never lowering a target past its cap, and a non-finite level throwing. |
|
|
61
|
+
| `src/hdr.test.ts` | HDR glows: SDR bit-identity at headroom 0 / 1 (variants and every exporter), exact steps (tinted and pastel bases to 1e-9), the trigger (white theme surface, standalone `'#ffffff'`, never for a pigment that can't lift a non-white surface), the headroom cap and its warnings, continuity at T* → 100 and the reach < 1 jump, theme behavior (HC, `contrastLevel`, `extend()`, palettes, `export()` round-trips, `resetConfig()`), validation and deferred clamp warnings, the writers (extended `color(srgb …)` and `srgb-linear` as one color, `hdrToSrgb` / `hdrToLinearSrgb`); then every exporter for themes and palettes behind `modes.hdr` — incl. a `@tenphi/tasty` compile test that evaluates the compiled `@media` rules in each dark / HC / HDR context — and tokens staying untouched. |
|
|
62
|
+
| `src/config.test.ts` | Config isolation and validation: deep-copy `glaze.getConfig()` / `theme.getConfig()`, overrides copied where stored (incl. getters, proxies, `extend()`, both token forms), `darkDesaturation` clamping and errors (incl. `null` for it and `contrastLevel`), lighting tuning fields (non-finite throws, `undefined` is unset), tone-window validation and clamping (global and per-instance at resolve, `null`, the 1e6 eps cap, frozen into exports), `normalizeToneWindow` / `normalizeToneWindowInput`. |
|
|
63
|
+
| `src/color-token.test.ts` | Standalone `glaze.color()` roles (the `'text'` default with and without a base, explicit roles and aliases, `name` as a label only, roles through export / `colorFrom`), value links on a structured base landing on and solving against what the base paints under its own config, the component scales of value strings (incl. hue units, `none`, clamping, rejected components, and the alpha forms that warn, once per string), value objects and structured / override inputs read by property access (getters, inherited fields, non-enumerable fields, proxies), value-object fields clamped between 1 and 1.5, and detached input and snapshots (a raw `base` exported with its config frozen, a proxy `contrast`, a 1000-link chain exported in linear time). |
|
|
64
|
+
| `src/formatters.test.ts` | Output options: removed options rejected at the type level (`@ts-expect-error`, enforced by `pnpm typecheck`), `splitHue` requiring `oklch`, hue-var and value-property collision guards across themes / palettes / primary aliases / tokens, the light-plan rules for colors achromatic in light, hue rounding, and a `static` color with a `darkHue` keeping its light hue reference. |
|
|
65
|
+
| `src/validation.test.ts` | `hasRelativeTone` over both HC-pair entries and the relative-tone-without-base error (a relative HC entry on a root throws at every level; a token's is anchored to its own color); malformed `contrast` specs (and set-vs-`undefined` metric keys), non-finite or malformed numeric fields (relative strings with whitespace allowed), a shadow / glow without `bg`, and non-finite theme seeds. |
|
|
66
|
+
| `src/warnings.test.ts` | Mix contrast-unmet warnings (opaque and transparent, once per scheme, the value-100 fallback, the pastel-cap hint, slack) and the chromatic drift advisory (skipped for unmet and achromatic colors). |
|
|
67
|
+
| `src/hc-pair.test.ts` | Tests for the pure HC-pair selection and interpolation helpers, and `showValue`. |
|
|
68
|
+
| `src/okhsl-color-math.test.ts` | Tests for the format writers' 0–1 scale contract, the percentage-scale guard, producer→writer composition, strict hex parsing (every digit in all four forms; bad input returns `null` from `parseHex` / `parseHexAlpha` and throws from the public entry points), `srgbToOkhsl` reading every gray as saturation 0 and keeping the faintest tints, and `apcaLuminanceFromLinearRgb` reproducing the solver's APCA Lc. |
|
|
55
69
|
|
|
56
70
|
### `docs/` files
|
|
57
71
|
|
|
72
|
+
The documentation site (Cookbook on Astro) renders `docs/**/*.{md,mdx}` at the
|
|
73
|
+
path without the extension, plus `docs-site/index.mdx` as the home page.
|
|
74
|
+
Navigation lives in `docs.config.ts`. Concept and guide pages are `.mdx` with
|
|
75
|
+
live examples; reference pages are plain Markdown.
|
|
76
|
+
|
|
77
|
+
| File | Description |
|
|
78
|
+
|---|---|
|
|
79
|
+
| [docs/getting-started.mdx](docs/getting-started.mdx) | Install, a first theme built step by step, four schemes, and export. |
|
|
80
|
+
| [docs/okhst.mdx](docs/okhst.mdx) | The OKHST color model spec — tone math, `REF_EPS`, tone above 100 (HDR), the T↔L invariant, the scheme pipeline, calibrated window defaults, the contrast metric, and the APCA verification step. Source of truth for the color model. Also hosts the ramp explorer (a Cookbook `Preview` built by `docs-site/components/okhst/ramp-explorer.ts`), which replaced the standalone playground; `astro.config.ts` redirects `/playground` to it. |
|
|
81
|
+
| [docs/schemes.mdx](docs/schemes.mdx) | Light, dark, and high contrast: inversion + tone windows, adaptation modes, dark seed, HC pairs, the manual contrast level. |
|
|
82
|
+
| [docs/contrast.mdx](docs/contrast.mdx) | Contrast solving: WCAG / APCA floors, roles and polarity, `autoFlip`, HC escalation, warnings. |
|
|
83
|
+
| [docs/shadows-and-glows.mdx](docs/shadows-and-glows.mdx) | `shadow` / `glow`: exact tone steps, object influence, state layers, elevation, tuning. SDR only; it links to the HDR page once. |
|
|
84
|
+
| [docs/hdr.mdx](docs/hdr.mdx) | HDR glows (`headroom`): when a glow gets an HDR value, the extended `color(srgb …)` format (and why not `rec2100-*`), opting in and every exporter's HDR tier, where it shows (browsers, WebGPU, native apps), and a WebGPU showcase (a Cookbook `Preview` built by `docs-site/components/shadows-and-glows/hdr-preview.ts`). The one HDR guide: other guide pages only link to it, and the reference pages keep their exact API entries (output, color-definitions, configuration, utilities, palette). |
|
|
85
|
+
| [docs/mixes.mdx](docs/mixes.mdx) | Opaque and transparent mixes, blend spaces, contrast on mixes. |
|
|
86
|
+
| [docs/methodology.mdx](docs/methodology.mdx) | Palette design methodology — building a complete palette step by step, with the palette shown live as it grows. |
|
|
87
|
+
| [docs/exporting.mdx](docs/exporting.mdx) | Choosing an export shape, wiring tokens into [Tasty](https://tasty.style) / CSS / JSON / DTCG / Tailwind, prefix maps, the `primary` alias, and `splitHue` (HDR output lives in `docs/hdr.mdx`). |
|
|
88
|
+
| [docs/migration.md](docs/migration.md) | Migrating an existing color system, upgrading between majors (one `3.0 — …` section per breaking change; `3.0 — shadows and glows` is referenced by `src/serialize.ts` errors), common pitfalls. |
|
|
89
|
+
| [docs/reference/](docs/reference/index.md) | API reference: `index` (map of every export + conventions), `theme`, `color-definitions`, `contrast`, `color-tokens`, `palette`, `output`, `configuration`, `utilities`. Lookup, not tutorial. |
|
|
90
|
+
|
|
91
|
+
### `docs-site/` files
|
|
92
|
+
|
|
58
93
|
| File | Description |
|
|
59
94
|
|---|---|
|
|
60
|
-
| [docs/
|
|
61
|
-
| [docs/
|
|
62
|
-
| [docs/
|
|
63
|
-
| [docs/migration.md](docs/migration.md) | Migration & integration guide — choosing an export shape, wiring tokens into [Tasty](https://tasty.style) / CSS / JSON, prefix-map strategies, migrating from a legacy color system, and common pitfalls. |
|
|
95
|
+
| [docs-site/index.mdx](docs-site/index.mdx) | Home page. Opens with the theme playground: a Cookbook `Preview` built by `docs-site/components/home/theme-playground.ts` that re-solves the `UiPreview` card's theme in the browser as the reader moves hue, saturation, and contrast-level sliders. |
|
|
96
|
+
| [docs-site/components/](docs-site/components/README.md) | Live-example components (`Example`, `Swatches`, `UiPreview`, `SchemeGrid`, `ToneRamp`, page-specific folders). They resolve themes from `src/` at build time and pass colors through Tasty `styles` — Cookbook rejects `style` attributes and non-Tasty CSS. `glaze-preview.ts` holds what the `Preview` frames that run Glaze share. See the folder's README before adding one. |
|
|
97
|
+
| [docs-site/demo-bundle.ts](docs-site/demo-bundle.ts) | Astro integration that builds `src/index.ts` into `public/demo/glaze.iife.js` (a `Glaze` global; generated and git-ignored) when the site starts or builds, and again in dev on a source change. A `Preview` that runs Glaze inlines it through `scripts`. |
|
|
64
98
|
|
|
65
99
|
## Commands
|
|
66
100
|
|
|
@@ -71,19 +105,22 @@ glaze/
|
|
|
71
105
|
| `pnpm test -- <pattern>` | Run a subset of tests. |
|
|
72
106
|
| `pnpm test:watch` | Tests in watch mode. |
|
|
73
107
|
| `pnpm test:coverage` | Tests with coverage report. |
|
|
74
|
-
| `pnpm typecheck` | Type-check without emitting. |
|
|
108
|
+
| `pnpm typecheck` | Type-check the library (`tsconfig.json`) and the test files (`tsconfig.test.json`, Vitest globals) without emitting. |
|
|
75
109
|
| `pnpm lint` | ESLint over `src/`. |
|
|
76
110
|
| `pnpm lint:fix` | ESLint with `--fix`. |
|
|
77
111
|
| `pnpm format` | Prettier write `src/**/*.ts`. |
|
|
78
112
|
| `pnpm format:check` | Prettier check (no writes). |
|
|
79
113
|
| `pnpm changeset` | Create a changeset entry. |
|
|
80
114
|
| `pnpm release` | Build and publish (used by CI). |
|
|
115
|
+
| `pnpm docs` | Docs site dev server (Astro + Cookbook). |
|
|
116
|
+
| `pnpm build:docs` | Build the docs site into `dist/` (overwrites the library build there), as deployed to GitHub Pages. |
|
|
117
|
+
| `pnpm docs:doctor` | Cookbook configuration / content checks. |
|
|
81
118
|
|
|
82
119
|
## Stack
|
|
83
120
|
|
|
84
121
|
- **Language:** TypeScript (strict mode, `consistent-type-imports` enforced)
|
|
85
122
|
- **Build:** [tsdown](https://github.com/rolldown/tsdown) — ESM + CJS, unbundled, dts + sourcemaps
|
|
86
|
-
- **Test:** [Vitest 4](https://vitest.dev/) with globals enabled
|
|
123
|
+
- **Test:** [Vitest 4](https://vitest.dev/) with globals enabled; `@tenphi/tasty` is a devDependency only, for the compile test in `src/hdr.test.ts` that renders real `tasty()` output and checks which value each dark / high-contrast / HDR context gets
|
|
87
124
|
- **Lint:** ESLint 10 + typescript-eslint + `eslint-config-prettier`
|
|
88
125
|
- **Format:** Prettier — single quotes, semicolons, trailing commas, 80 cols
|
|
89
126
|
- **Versioning:** [Changesets](https://github.com/changesets/changesets)
|
|
@@ -100,10 +137,10 @@ glaze/
|
|
|
100
137
|
|
|
101
138
|
## Adding a New Color Type
|
|
102
139
|
|
|
103
|
-
If you need to add
|
|
140
|
+
If you need to add another `ColorDef` variant (alongside regular / shadow / glow / mix), touch these files in order:
|
|
104
141
|
|
|
105
142
|
1. [src/types.ts](src/types.ts) — add the new interface and extend the `ColorDef` discriminated union.
|
|
106
|
-
2. [src/
|
|
143
|
+
2. [src/lighting.ts](src/lighting.ts) — add a discriminator predicate (`isFooDef`).
|
|
107
144
|
3. [src/validation.ts](src/validation.ts) — extend `validateColorDefs` (missing-reference / cycle handling) and `topoSort` (record the new dependency edges).
|
|
108
145
|
4. [src/resolver.ts](src/resolver.ts) — add a per-scheme `resolveFooForScheme` branch and dispatch from `resolveColorForScheme`.
|
|
109
146
|
5. [src/formatters.ts](src/formatters.ts) — only if the output shape differs (it usually doesn't; the resolver returns a normal `ResolvedColorVariant`).
|
|
@@ -124,7 +161,7 @@ For a new public method on `glaze` (e.g. another statics entry), the wiring live
|
|
|
124
161
|
|
|
125
162
|
Follow [`.cursor/commands/submit-changes.md`](.cursor/commands/submit-changes.md) for the full release-oriented workflow. At minimum:
|
|
126
163
|
|
|
127
|
-
1. **Typecheck** — `pnpm typecheck
|
|
164
|
+
1. **Typecheck** — `pnpm typecheck` (the library and the test files). Stop and fix errors before continuing.
|
|
128
165
|
2. **Lint** — `pnpm lint`. Stop and fix errors before continuing.
|
|
129
166
|
3. **Format** — `pnpm format` so the diff matches Prettier output.
|
|
130
167
|
4. **Changeset** — for any user-visible change (features, fixes, refactors, perf, docs that affect published behavior), add a markdown file in `.changeset/` with a `patch` / `minor` / `major` bump and a concise user-facing summary. Skip only for purely internal changes (CI, repo churn, tests with no behavior change).
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<h1 align="center">Glaze</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
OKHST
|
|
8
|
+
<a href="https://github.com/tenphi/okhst">OKHST</a>-based color theme generator with WCAG and APCA contrast solving
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -25,12 +25,13 @@ contrast floor.
|
|
|
25
25
|
|
|
26
26
|
- **OKHST color space** — OKHSL with a contrast-shaped **tone** axis. Equal
|
|
27
27
|
steps give equal WCAG contrast for neutrals and a useful approximation for
|
|
28
|
-
chromatic colors. See [
|
|
28
|
+
chromatic colors. See [Tone and OKHST](docs/okhst.mdx) and the
|
|
29
29
|
[full OKHST specification](https://github.com/tenphi/okhst).
|
|
30
30
|
- **WCAG 2 + APCA contrast solving** — automatic tone adjustment to meet a WCAG ratio or APCA Lc floor
|
|
31
31
|
- **Unified dark mode** — one tone space for light, dark, and high-contrast; dark is a single `100 − t` inversion, no fitted curve
|
|
32
32
|
- **Mix colors** — blend two colors with OKHSL or sRGB interpolation, opaque or transparent, with optional contrast solving
|
|
33
|
-
- **
|
|
33
|
+
- **Shadows and glows** — translucent overlays whose opacity is solved per surface, so `intensity` is an exact tone step wherever the surface has room for it (a step past black or white is pinned and warns), with object-aware strength
|
|
34
|
+
- **HDR glows** — set `glowTuning.headroom` and a glow whose step passes white also gets an exact HDR value: extended sRGB `color(srgb …)` with channels past 1, plus a base and an exposure in DTCG for native apps, exported when you turn on `modes.hdr`. See [HDR glows](docs/hdr.mdx).
|
|
34
35
|
- **Light + Dark + High-Contrast** — all schemes from one definition
|
|
35
36
|
- **Manual contrast level** — put your normal palette on a `0–100` slider between itself and the high-contrast one; every level in between is solved, not approximated, and the high-contrast tier stays available on top
|
|
36
37
|
- **Per-color hue override** — absolute or relative hue shifts within a theme
|
|
@@ -43,7 +44,7 @@ contrast floor.
|
|
|
43
44
|
- **Import/Export** — serialize and restore themes, color tokens, and entire palettes as JSON
|
|
44
45
|
- **Create from hex/RGB** — start from an existing brand color
|
|
45
46
|
- **Zero dependencies** — pure math, runs anywhere (Node.js, browser, edge)
|
|
46
|
-
- **
|
|
47
|
+
- **ESM + CJS** — dual-format package
|
|
47
48
|
- **TypeScript-first** — full type definitions included
|
|
48
49
|
|
|
49
50
|
## Installation
|
|
@@ -69,62 +70,69 @@ glaze.configure({
|
|
|
69
70
|
modes: { dark: true, highContrast: true },
|
|
70
71
|
});
|
|
71
72
|
|
|
72
|
-
const
|
|
73
|
+
const theme = glaze(280, 80);
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
theme.colors({
|
|
75
76
|
surface: { tone: 97, saturation: 0.15 },
|
|
76
77
|
'surface-text': {
|
|
77
78
|
base: 'surface',
|
|
78
79
|
tone: '-1',
|
|
80
|
+
saturation: 0.4,
|
|
79
81
|
contrast: { apca: ['content', 'body'] },
|
|
80
82
|
},
|
|
81
83
|
border: {
|
|
82
84
|
base: 'surface',
|
|
83
85
|
tone: ['-8', '-16'],
|
|
86
|
+
saturation: 0.2,
|
|
84
87
|
inherit: false,
|
|
85
88
|
},
|
|
86
|
-
'accent-surface': { tone:
|
|
87
|
-
'accent-surface
|
|
88
|
-
base: 'accent-surface',
|
|
89
|
-
tone: '
|
|
89
|
+
'accent-surface-text': { tone: 100, mode: 'fixed' },
|
|
90
|
+
'accent-surface': {
|
|
91
|
+
base: 'accent-surface-text',
|
|
92
|
+
tone: '-1',
|
|
90
93
|
contrast: 'AA',
|
|
91
94
|
mode: 'fixed',
|
|
92
95
|
},
|
|
93
96
|
});
|
|
94
97
|
|
|
95
|
-
const
|
|
96
|
-
const palette = glaze.palette({ default:
|
|
98
|
+
const danger = theme.extend({ hue: 23 });
|
|
99
|
+
const palette = glaze.palette({ default: theme, danger });
|
|
97
100
|
|
|
98
101
|
const tokens = palette.tokens({
|
|
99
102
|
prefix: { default: '', danger: 'danger-' },
|
|
100
103
|
});
|
|
101
104
|
// → {
|
|
102
105
|
// light: { surface: 'oklch(...)', 'danger-surface': 'oklch(...)', ... },
|
|
103
|
-
// dark: {
|
|
106
|
+
// dark: { ... },
|
|
104
107
|
// lightContrast: { ... },
|
|
105
108
|
// darkContrast: { ... },
|
|
106
109
|
// }
|
|
107
110
|
```
|
|
108
111
|
|
|
112
|
+
The accent fill is solved against its white label, not the reverse: the fill
|
|
113
|
+
darkens until the pair meets `'AA'`, or `'AAA'` in high contrast, where a bare
|
|
114
|
+
`'AA'` rises a tier.
|
|
115
|
+
|
|
109
116
|
## Concepts at a glance
|
|
110
117
|
|
|
111
118
|
1. **A theme has one hue/saturation seed.** Create status themes with
|
|
112
119
|
`extend()` so they inherit the same relationships while changing hue.
|
|
113
|
-
2. **A color is a root or a
|
|
120
|
+
2. **A color is a root or a dependent.** Roots use an absolute `tone` or
|
|
114
121
|
`'max'`/`'min'`. Dependent colors name a `base` and may use a signed
|
|
115
122
|
**tone delta** such as `'-8'` plus a `contrast` floor.
|
|
116
123
|
3. **Tone is an authoring axis, not a contrast guarantee.** Equal tone deltas
|
|
117
124
|
are exact WCAG steps for neutrals. Chromatic colors can drift in rendered
|
|
118
125
|
luminance, so Glaze measures contrast floors against the resolved colors.
|
|
119
126
|
4. **`contrast` is a floor.** A bare number or preset means WCAG;
|
|
120
|
-
`{ wcag }` and `{ apca }` select the metric explicitly. The solver
|
|
121
|
-
the requested tone whenever it already
|
|
127
|
+
`{ wcag }` and `{ apca }` select the metric explicitly. The solver keeps
|
|
128
|
+
the requested tone whenever it already clears the floor with a small
|
|
129
|
+
margin (1% over a WCAG ratio, 0.5 Lc over an APCA target).
|
|
122
130
|
5. **Each color chooses its dark adaptation.** `mode: 'auto'` uses dark tone
|
|
123
131
|
inversion (`100 - t`), `fixed` keeps the authored side of the tone scale,
|
|
124
132
|
and `static` skips scheme adaptation.
|
|
125
133
|
6. **Tone windows bound ordinary schemes.** `lightTone` and `darkTone`
|
|
126
|
-
configure the light
|
|
127
|
-
range. Values that accept `[normal, highContrast]` pairs can tighten
|
|
134
|
+
configure the light and dark tone windows. High-contrast variants use the
|
|
135
|
+
full range. Values that accept `[normal, highContrast]` pairs can tighten
|
|
128
136
|
deliberately in HC.
|
|
129
137
|
|
|
130
138
|
In Glaze, a **scheme variant** is one of `light`, `dark`, `lightContrast`, or
|
|
@@ -143,13 +151,25 @@ In Glaze, a **scheme variant** is one of `light`, `dark`, `lightContrast`, or
|
|
|
143
151
|
|
|
144
152
|
## Documentation
|
|
145
153
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
-
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
The full documentation, with live examples that follow your light / dark and
|
|
155
|
+
contrast preferences, is at [glaze.tenphi.me](https://glaze.tenphi.me). The
|
|
156
|
+
same pages live in [`docs/`](docs):
|
|
157
|
+
|
|
158
|
+
- [Getting started](docs/getting-started.mdx) — install, build a first theme,
|
|
159
|
+
and export it.
|
|
160
|
+
- Concepts: [light, dark, and high contrast](docs/schemes.mdx),
|
|
161
|
+
[contrast solving](docs/contrast.mdx),
|
|
162
|
+
[shadows and glows](docs/shadows-and-glows.mdx),
|
|
163
|
+
[mixes and transparency](docs/mixes.mdx), and the color model under them,
|
|
164
|
+
[tone and OKHST](docs/okhst.mdx).
|
|
165
|
+
- Guides: [designing a palette](docs/methodology.mdx),
|
|
166
|
+
[exporting and integration](docs/exporting.mdx), and
|
|
167
|
+
[migration and upgrading](docs/migration.md) (including Glaze 2 → 3).
|
|
168
|
+
- The [API reference](docs/reference/index.md) for every method, option, and
|
|
169
|
+
type.
|
|
170
|
+
- Explore the tone axis in the
|
|
171
|
+
[ramp explorer](https://glaze.tenphi.me/okhst/#ramp-explorer), or read the
|
|
172
|
+
[full OKHST specification](https://github.com/tenphi/okhst) for its math.
|
|
153
173
|
- [`AGENTS.md`](https://github.com/tenphi/glaze/blob/main/AGENTS.md) — source-tree orientation for contributors.
|
|
154
174
|
|
|
155
175
|
## License
|