@tenphi/glaze 1.5.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +132 -0
- package/dist/index.cjs +49 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -6
- package/dist/index.d.mts +11 -6
- package/dist/index.mjs +49 -18
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +38 -9
- package/docs/migration.md +36 -0
- package/package.json +3 -2
package/AGENTS.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# AGENTS.md — @tenphi/glaze
|
|
2
|
+
|
|
3
|
+
## Package Overview
|
|
4
|
+
|
|
5
|
+
- **Package:** `@tenphi/glaze`
|
|
6
|
+
- **Repository:** [https://github.com/tenphi/glaze](https://github.com/tenphi/glaze)
|
|
7
|
+
- **Description:** Zero-dependency color theme generator built on OKHST (OKHSL with a contrast-uniform **tone** axis), with WCAG + APCA contrast solving for light, dark, and high-contrast schemes.
|
|
8
|
+
- **Runtime:** Node >= 20, browsers, edge. Pure math, no I/O.
|
|
9
|
+
- **Output formats:** ESM + CJS, with full `.d.ts`.
|
|
10
|
+
|
|
11
|
+
## Project Structure
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
glaze/
|
|
15
|
+
├── src/ Source
|
|
16
|
+
├── docs/ Long-form docs (okhst / api / methodology / migration)
|
|
17
|
+
├── assets/ Logo + brand assets
|
|
18
|
+
├── scripts/ Repo helpers
|
|
19
|
+
├── README.md Compact intro
|
|
20
|
+
├── AGENTS.md This file
|
|
21
|
+
├── CHANGELOG.md Auto-generated by Changesets
|
|
22
|
+
├── package.json
|
|
23
|
+
├── tsconfig.json
|
|
24
|
+
├── tsdown.config.ts Build config
|
|
25
|
+
├── vitest.config.ts Test config
|
|
26
|
+
└── eslint.config.js
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### `src/` files
|
|
30
|
+
|
|
31
|
+
| File | Description |
|
|
32
|
+
|---|---|
|
|
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`. 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. |
|
|
39
|
+
| [src/serialize.ts](src/serialize.ts) | Authoring-export helpers: `GLAZE_EXPORT_VERSION`, `assertExportKind` / `assertExportVersion`, and `isThemeExport` / `isColorTokenExport` / `isPaletteExport` type guards. |
|
|
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`), OKHST↔OKHSL conversions, `variantToOkhsl` (tone→lightness at render), `normalizeToneWindow` (`[lo,hi]` / `{lo,hi,eps}` / `false` → `{lo,hi,eps}`), `mapToneForScheme` (scheme inversion + window remap, HC bypass), `mapSaturationDark` (the `darkDesaturation` reducer the resolver skips when a dark saturation is authored), and `schemeToneRange` for the solver. `activeWindow` owns the HC window bypass and its continuous form under `contrastLevel`. Only tone adapts here — hue/saturation are the resolver's business. |
|
|
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`, `resolveContrastForMode`, `resolveContrastForLevel` (both ends resolved, target interpolated for a manual `contrastLevel`; throws on a metric switch), `contrastMetricOf`, `resolveMinContrast`, `apcaContrast`. Closed-form WCAG seed + tone search. |
|
|
43
|
+
| [src/shadow.ts](src/shadow.ts) | Shadow + mix def predicates (`isShadowDef`, `isMixDef`), default `ShadowTuning`, tuning merge, the actual `computeShadow` math (hue blend, saturation cap, lightness clamp, `tanh` alpha curve) operating on OKHSL lightness at the edge, and `circularLerp` for hue. |
|
|
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/formatters.ts](src/formatters.ts) | Variant→string dispatch (`okhsl` / `rgb` / `hsl` / `oklch`) and the four 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). For `splitHue` it emits the light hue declarations plus, via `darkHueDeclarations` in `channels.ts`, the whole dark set when a `darkHue` makes it differ. Also the `resolveModes` helper used everywhere. |
|
|
46
|
+
| [src/okhsl-color-math.ts](src/okhsl-color-math.ts) | OKHSL ↔ linear-sRGB ↔ gamma-sRGB ↔ OKLab conversions, hex parsing (3/6/8 digits), gamut clamping, and the `formatOkhsl` / `formatOkhst` / `formatRgb` / `formatHsl` / `formatOklch` writers. 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. |
|
|
47
|
+
| [src/config.ts](src/config.ts) | Global config singleton. `defaultConfig()` is the one source of truth for defaults; `configure()` mutates the live object and bumps a monotonic `configVersion` so theme caches invalidate. `snapshotConfig()` powers `glaze.getConfig()`. `freezeConfigForExport()` freezes resolve-relevant fields for authoring export (`pastel` is instance-only; `contrastLevel` is frozen only when instance-authored, since a global level is a live preference). `normalizeContrastLevel()` validates the manual contrast level. |
|
|
48
|
+
| [src/validation.ts](src/validation.ts) | `validateColorDefs` (missing references, shadow-bg-cannot-be-shadow, mix-cannot-target-shadow, contrast-without-base, relative-without-base, WCAG-vs-APCA metric switch in a `contrast` pair, etc.) + `topoSort` so the resolver processes each color after its dependencies. |
|
|
49
|
+
| [src/warnings.ts](src/warnings.ts) | Deduped contrast-unmet warnings. Caps cache at 256 entries to keep dev-server output bounded. |
|
|
50
|
+
| [src/types.ts](src/types.ts) | All public TypeScript types: `HCPair`, `MinContrast`, `ContrastSpec`, `RelativeValue`, `ExtremeValue` (`'max'`/`'min'`), `ToneValue`, `AdaptationMode`, `OkhstColor`, `ToneWindow` (`[lo,hi]` \| `{lo,hi,eps}` \| `false`), `ColorDef` discriminated union (with `tone` + `flip` + `darkHue` / `darkSaturation`), `GlazeConfig` (with `lightTone` / `darkTone` / `autoFlip` / `contrastLevel`), `GlazeThemeSeed` (`hue` / `saturation` + optional dark pair), `GlazeTheme`, `GlazePalette`, `GlazeColorToken`, the `*Export` shapes, etc. `ResolvedColorVariant` stores `{ h, s, t, alpha }`. |
|
|
51
|
+
| `src/glaze.test.ts` | Main test suite — covers the factory surface, resolver behavior, palette composition, shadow/mix algorithms, the manual contrast level, and the standalone color token round-trip. |
|
|
52
|
+
| `src/contrast-solver.test.ts` | Tests for the binary-search solver in isolation. |
|
|
53
|
+
| `src/hc-pair.test.ts` | Tests for the pure HC-pair selection and interpolation helpers. |
|
|
54
|
+
| `src/okhsl-color-math.test.ts` | Tests for the format writers' 0–1 scale contract, the percentage-scale guard, and producer→writer composition. |
|
|
55
|
+
|
|
56
|
+
### `docs/` files
|
|
57
|
+
|
|
58
|
+
| File | Description |
|
|
59
|
+
|---|---|
|
|
60
|
+
| [docs/okhst.md](docs/okhst.md) | The OKHST color model spec — tone math, `REF_EPS`, 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. |
|
|
61
|
+
| [docs/api.md](docs/api.md) | Full API reference. Every method, every option, every type. Reads as a lookup, not a tutorial. |
|
|
62
|
+
| [docs/methodology.md](docs/methodology.md) | Palette design methodology — how to build a palette from scratch. Covers naming conventions, surface ladders, the accent anchor pattern, contrast-driven disabled chips, shadows, and `extend` composition. |
|
|
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. |
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
| Command | Description |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `pnpm build` | Build ESM + CJS via tsdown (with `.d.ts` and sourcemaps). |
|
|
70
|
+
| `pnpm test` | Run all tests (Vitest 4). |
|
|
71
|
+
| `pnpm test -- <pattern>` | Run a subset of tests. |
|
|
72
|
+
| `pnpm test:watch` | Tests in watch mode. |
|
|
73
|
+
| `pnpm test:coverage` | Tests with coverage report. |
|
|
74
|
+
| `pnpm typecheck` | Type-check without emitting. |
|
|
75
|
+
| `pnpm lint` | ESLint over `src/`. |
|
|
76
|
+
| `pnpm lint:fix` | ESLint with `--fix`. |
|
|
77
|
+
| `pnpm format` | Prettier write `src/**/*.ts`. |
|
|
78
|
+
| `pnpm format:check` | Prettier check (no writes). |
|
|
79
|
+
| `pnpm changeset` | Create a changeset entry. |
|
|
80
|
+
| `pnpm release` | Build and publish (used by CI). |
|
|
81
|
+
|
|
82
|
+
## Stack
|
|
83
|
+
|
|
84
|
+
- **Language:** TypeScript (strict mode, `consistent-type-imports` enforced)
|
|
85
|
+
- **Build:** [tsdown](https://github.com/rolldown/tsdown) — ESM + CJS, unbundled, dts + sourcemaps
|
|
86
|
+
- **Test:** [Vitest 4](https://vitest.dev/) with globals enabled
|
|
87
|
+
- **Lint:** ESLint 10 + typescript-eslint + `eslint-config-prettier`
|
|
88
|
+
- **Format:** Prettier — single quotes, semicolons, trailing commas, 80 cols
|
|
89
|
+
- **Versioning:** [Changesets](https://github.com/changesets/changesets)
|
|
90
|
+
- **Package manager:** pnpm 11
|
|
91
|
+
|
|
92
|
+
## Code Conventions
|
|
93
|
+
|
|
94
|
+
- TypeScript strict mode; `consistent-type-imports` enforced.
|
|
95
|
+
- Test files use `*.test.ts` and live next to the module they test in `src/`.
|
|
96
|
+
- Unused vars prefixed with `_` are allowed.
|
|
97
|
+
- **No runtime dependencies** — every bit of math is implemented from scratch.
|
|
98
|
+
- Functional API pattern: `glaze()` factory with attached static methods; no classes.
|
|
99
|
+
- Each module starts with a JSDoc block describing its responsibility — keep these in sync when behavior changes.
|
|
100
|
+
|
|
101
|
+
## Adding a New Color Type
|
|
102
|
+
|
|
103
|
+
If you need to add a fourth `ColorDef` variant (alongside regular / shadow / mix), touch these files in order:
|
|
104
|
+
|
|
105
|
+
1. [src/types.ts](src/types.ts) — add the new interface and extend the `ColorDef` discriminated union.
|
|
106
|
+
2. [src/shadow.ts](src/shadow.ts) — add a discriminator predicate (`isFooDef`).
|
|
107
|
+
3. [src/validation.ts](src/validation.ts) — extend `validateColorDefs` (missing-reference / cycle handling) and `topoSort` (record the new dependency edges).
|
|
108
|
+
4. [src/resolver.ts](src/resolver.ts) — add a per-scheme `resolveFooForScheme` branch and dispatch from `resolveColorForScheme`.
|
|
109
|
+
5. [src/formatters.ts](src/formatters.ts) — only if the output shape differs (it usually doesn't; the resolver returns a normal `ResolvedColorVariant`).
|
|
110
|
+
6. `src/glaze.test.ts` — add coverage for the new def, including HC pairs, dark-mode behavior, and inheritance through `extend`.
|
|
111
|
+
|
|
112
|
+
For a new public method on `glaze` (e.g. another statics entry), the wiring lives entirely in [src/glaze.ts](src/glaze.ts); the implementation should still go in a focused module.
|
|
113
|
+
|
|
114
|
+
## CI/CD & Release
|
|
115
|
+
|
|
116
|
+
- **CI:** Runs lint, format check, typecheck, build, and tests on push to `main` and PRs.
|
|
117
|
+
- **Release:** On push to `main`, `changesets/action` either creates a version PR or publishes to npm.
|
|
118
|
+
- **Snapshot:** Comment `/snapshot` on a PR to publish a `0.0.0-snapshot.<sha>` release.
|
|
119
|
+
- **npm trusted publishing:** Uses OIDC provenance via the `release` GitHub environment; the workflow requires `environment: release` for npm to accept the OIDC token.
|
|
120
|
+
- **`RELEASE_TOKEN` secret:** Fine-grained GitHub PAT (repo-scoped, `contents: write` + `pull-requests: write`) used by `changesets/action` for Git pushes and PR creation. Rotate before expiry.
|
|
121
|
+
- **Repo settings required:** Settings → Actions → General → Workflow permissions → "Allow GitHub Actions to create and approve pull requests" must be enabled.
|
|
122
|
+
|
|
123
|
+
## Before Pushing Changes
|
|
124
|
+
|
|
125
|
+
Follow [`.cursor/commands/submit-changes.md`](.cursor/commands/submit-changes.md) for the full release-oriented workflow. At minimum:
|
|
126
|
+
|
|
127
|
+
1. **Typecheck** — `pnpm typecheck`. Stop and fix errors before continuing.
|
|
128
|
+
2. **Lint** — `pnpm lint`. Stop and fix errors before continuing.
|
|
129
|
+
3. **Format** — `pnpm format` so the diff matches Prettier output.
|
|
130
|
+
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).
|
|
131
|
+
5. **Commit** — Conventional Commits (`feat`, `fix`, `refactor`, `test`, `docs`, `chore`, `perf`, `ci`). Optional scope. Keep the subject short. Include the changeset file in the same commit.
|
|
132
|
+
6. **Push** — never push to `main`. Confirm the current branch, then `git push -u origin HEAD`.
|
package/dist/index.cjs
CHANGED
|
@@ -372,6 +372,7 @@ const sRGBGammaToLinear = (val) => {
|
|
|
372
372
|
};
|
|
373
373
|
/**
|
|
374
374
|
* Convert OKHSL to gamma-encoded sRGB (clamped to 0–1).
|
|
375
|
+
* h: 0–360, s: 0–1, l: 0–1. Returns [r, g, b] with each channel 0–1.
|
|
375
376
|
*/
|
|
376
377
|
function okhslToSrgb(h, s, l, pastel = false) {
|
|
377
378
|
const lin = okhslToLinearSrgb(h, s, l, pastel);
|
|
@@ -581,47 +582,72 @@ function fmt$1(value, decimals) {
|
|
|
581
582
|
return parseFloat(value.toFixed(decimals)).toString();
|
|
582
583
|
}
|
|
583
584
|
/**
|
|
585
|
+
* Every `format*` writer takes `s` / `l` / `t` on the **0–1 factor scale** —
|
|
586
|
+
* the same scale every producer in the library hands back (`resolve()`,
|
|
587
|
+
* `variantToOkhsl`, `srgbToOkhsl`, `oklabToOkhsl`, `okhslToOklab`,
|
|
588
|
+
* `okhslToSrgb`). The percentages are an output detail: the writers multiply
|
|
589
|
+
* by 100 themselves when the CSS syntax asks for a percentage.
|
|
590
|
+
*
|
|
591
|
+
* Before 2.0 the writers took 0–100 while everything producing those values
|
|
592
|
+
* returned 0–1, so the obvious composition was off by 100x and failed
|
|
593
|
+
* silently — `0.7` is a legal percentage, so the result was a valid CSS string
|
|
594
|
+
* naming a near-black color. A value above 1 can only be that old
|
|
595
|
+
* percentage-scale input, so warn about it loudly rather than emit a wrong
|
|
596
|
+
* color quietly.
|
|
597
|
+
*/
|
|
598
|
+
const scaleWarnCache = /* @__PURE__ */ new Set();
|
|
599
|
+
function warnPercentScale(fn, ...values) {
|
|
600
|
+
if (!values.some((v) => v > 1.000001)) return;
|
|
601
|
+
if (!scaleWarnCache.has(fn)) {
|
|
602
|
+
scaleWarnCache.add(fn);
|
|
603
|
+
console.warn(`glaze: ${fn}() got a value above 1 — it takes the 0-1 factors every Glaze producer returns (resolve(), variantToOkhsl, srgbToOkhsl), not 0-100 percentages. Divide by 100, or drop the "* 100" left over from the pre-2.0 signature.`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
584
607
|
* Format OKHSL values as a CSS `okhsl(H S% L%)` string.
|
|
585
|
-
* h: 0–360, s: 0–
|
|
608
|
+
* h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
|
|
586
609
|
*/
|
|
587
610
|
function formatOkhsl(h, s, l, pastel = false) {
|
|
611
|
+
warnPercentScale("formatOkhsl", s, l);
|
|
588
612
|
let outS = s;
|
|
589
|
-
if (pastel) outS = oklabToOkhsl(okhslToOklab(h, s
|
|
590
|
-
return `okhsl(${fmt$1(h, 2)} ${fmt$1(outS, 2)}% ${fmt$1(l, 2)}%)`;
|
|
613
|
+
if (pastel) outS = oklabToOkhsl(okhslToOklab(h, s, l, true), false)[1];
|
|
614
|
+
return `okhsl(${fmt$1(h, 2)} ${fmt$1(outS * 100, 2)}% ${fmt$1(l * 100, 2)}%)`;
|
|
591
615
|
}
|
|
592
616
|
/**
|
|
593
617
|
* Format OKHST values as a CSS `okhst(H S% T%)` string.
|
|
594
|
-
* h: 0–360, s: 0–
|
|
618
|
+
* h: 0–360, s: 0–1, t: 0–1 (factor scale, matching `ResolvedColorVariant`).
|
|
595
619
|
*
|
|
596
620
|
* Pastel recompute matches `formatOkhsl`: convert via OKLab so external
|
|
597
621
|
* parsers that only understand non-pastel OKHST render identically.
|
|
598
622
|
*/
|
|
599
623
|
function formatOkhst(h, s, t, pastel = false) {
|
|
624
|
+
warnPercentScale("formatOkhst", s, t);
|
|
600
625
|
let outS = s;
|
|
601
626
|
if (pastel) {
|
|
602
627
|
const REF_EPS = .05;
|
|
603
628
|
const den = Math.log(1 + REF_EPS) - Math.log(REF_EPS);
|
|
604
|
-
const y = Math.exp(t
|
|
605
|
-
|
|
606
|
-
outS = oklabToOkhsl(okhslToOklab(h, s / 100, l, true), false)[1] * 100;
|
|
629
|
+
const y = Math.exp(t * den + Math.log(REF_EPS)) - REF_EPS;
|
|
630
|
+
outS = oklabToOkhsl(okhslToOklab(h, s, toe(Math.cbrt(Math.max(0, y))), true), false)[1];
|
|
607
631
|
}
|
|
608
|
-
return `okhst(${fmt$1(h, 2)} ${fmt$1(outS, 2)}% ${fmt$1(t, 2)}%)`;
|
|
632
|
+
return `okhst(${fmt$1(h, 2)} ${fmt$1(outS * 100, 2)}% ${fmt$1(t * 100, 2)}%)`;
|
|
609
633
|
}
|
|
610
634
|
/**
|
|
611
635
|
* Format OKHSL values as a CSS `rgb(R G B)` string.
|
|
612
636
|
* Uses 2 decimal places to avoid 8-bit quantization contrast loss.
|
|
613
|
-
* h: 0–360, s: 0–
|
|
637
|
+
* h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
|
|
614
638
|
*/
|
|
615
639
|
function formatRgb(h, s, l, pastel = false) {
|
|
616
|
-
|
|
640
|
+
warnPercentScale("formatRgb", s, l);
|
|
641
|
+
const [r, g, b] = okhslToSrgb(h, s, l, pastel);
|
|
617
642
|
return `rgb(${parseFloat((r * 255).toFixed(2))} ${parseFloat((g * 255).toFixed(2))} ${parseFloat((b * 255).toFixed(2))})`;
|
|
618
643
|
}
|
|
619
644
|
/**
|
|
620
645
|
* Format OKHSL values as a CSS `hsl(H S% L%)` string.
|
|
621
|
-
* h: 0–360, s: 0–
|
|
646
|
+
* h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
|
|
622
647
|
*/
|
|
623
648
|
function formatHsl(h, s, l, pastel = false) {
|
|
624
|
-
|
|
649
|
+
warnPercentScale("formatHsl", s, l);
|
|
650
|
+
const [r, g, b] = okhslToSrgb(h, s, l, pastel);
|
|
625
651
|
const max = Math.max(r, g, b);
|
|
626
652
|
const min = Math.min(r, g, b);
|
|
627
653
|
const delta = max - min;
|
|
@@ -638,10 +664,11 @@ function formatHsl(h, s, l, pastel = false) {
|
|
|
638
664
|
}
|
|
639
665
|
/**
|
|
640
666
|
* Format OKHSL values as a CSS `oklch(L C H)` string.
|
|
641
|
-
* h: 0–360, s: 0–
|
|
667
|
+
* h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
|
|
642
668
|
*/
|
|
643
669
|
function formatOklch(h, s, l, pastel = false) {
|
|
644
|
-
|
|
670
|
+
warnPercentScale("formatOklch", s, l);
|
|
671
|
+
const [L, C, hh] = okhslToOklch(h, s, l, pastel);
|
|
645
672
|
return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 2)})`;
|
|
646
673
|
}
|
|
647
674
|
/**
|
|
@@ -1119,7 +1146,11 @@ function okhslToOkhst(c) {
|
|
|
1119
1146
|
}
|
|
1120
1147
|
/**
|
|
1121
1148
|
* Edge adapter: a resolved variant stores canonical tone `t` (0–1). Convert
|
|
1122
|
-
* it to the OKHSL `{ h, s, l }` the formatters and luminance pipeline expect
|
|
1149
|
+
* it to the OKHSL `{ h, s, l }` the formatters and luminance pipeline expect —
|
|
1150
|
+
* `s` and `l` on the same 0–1 factor scale, so `const { h, s, l } =
|
|
1151
|
+
* variantToOkhsl(v)` destructures straight into `formatOkhsl` / `formatRgb` /
|
|
1152
|
+
* `okhslToLinearSrgb` with no rescaling. `alpha` stays on the variant; use
|
|
1153
|
+
* `glaze.format(v, format)` for a string that carries it.
|
|
1123
1154
|
*/
|
|
1124
1155
|
function variantToOkhsl(v) {
|
|
1125
1156
|
return {
|
|
@@ -3026,10 +3057,10 @@ function fmt(value, decimals) {
|
|
|
3026
3057
|
function formatVariant(v, format = "oklch", pastel = false) {
|
|
3027
3058
|
const effectivePastel = v.pastel ?? pastel;
|
|
3028
3059
|
let base;
|
|
3029
|
-
if (format === "okhst") base = formatOkhst(v.h, v.s
|
|
3060
|
+
if (format === "okhst") base = formatOkhst(v.h, v.s, v.t, effectivePastel);
|
|
3030
3061
|
else {
|
|
3031
3062
|
const { l } = variantToOkhsl(v);
|
|
3032
|
-
base = formatters[format](v.h, v.s
|
|
3063
|
+
base = formatters[format](v.h, v.s, l, effectivePastel);
|
|
3033
3064
|
}
|
|
3034
3065
|
if (v.alpha >= 1) return base;
|
|
3035
3066
|
const closing = base.lastIndexOf(")");
|
|
@@ -3045,7 +3076,7 @@ function formatVariantHue(v, plan, pastel = false) {
|
|
|
3045
3076
|
const [L, C] = okhslToOklch(v.h, v.s, l, effectivePastel);
|
|
3046
3077
|
let base;
|
|
3047
3078
|
if (plan.inline) if (v.s <= 1e-6) base = `oklch(${fmt(L, 4)} 0 0)`;
|
|
3048
|
-
else base = formatOklch(v.h, v.s
|
|
3079
|
+
else base = formatOklch(v.h, v.s, l, effectivePastel);
|
|
3049
3080
|
else base = `oklch(${fmt(L, 4)} ${fmt(C, 4)} ${plan.hueVar})`;
|
|
3050
3081
|
if (v.alpha >= 1) return base;
|
|
3051
3082
|
const closing = base.lastIndexOf(")");
|