- Package:
@tenphi/glaze - Repository: https://github.com/tenphi/glaze
- 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.
- Runtime: Node >= 20, browsers, edge. Pure math, no I/O.
- Output formats: ESM + CJS, with full
.d.ts.
glaze/
├── src/ Source
├── docs/ Long-form docs (okhst / api / methodology / migration)
├── assets/ Logo + brand assets
├── scripts/ Repo helpers
├── README.md Compact intro
├── AGENTS.md This file
├── CHANGELOG.md Auto-generated by Changesets
├── package.json
├── tsconfig.json
├── tsdown.config.ts Build config
├── vitest.config.ts Test config
└── eslint.config.js
| File | Description |
|---|---|
| src/index.ts | Public barrel; re-exports glaze, every public type, the contrast-solver utilities, and the OKHSL/sRGB/OKLab math helpers. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| src/serialize.ts | Authoring-export helpers: GLAZE_EXPORT_VERSION, assertExportKind / assertExportVersion, and isThemeExport / isColorTokenExport / isPaletteExport type guards. |
| 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 }). |
| 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. |
| 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. |
| 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. |
| 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. |
| src/formatters.ts | Variant→string dispatch (okhsl / rgb / hsl / oklch) and the four token-map shapes Glaze emits: buildTokenMap (Tasty #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. |
| 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. |
| 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. |
| 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. |
| src/warnings.ts | Deduped contrast-unmet warnings. Caps cache at 256 entries to keep dev-server output bounded. |
| 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 }. |
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. |
src/contrast-solver.test.ts |
Tests for the binary-search solver in isolation. |
src/hc-pair.test.ts |
Tests for the pure HC-pair selection and interpolation helpers. |
src/okhsl-color-math.test.ts |
Tests for the format writers' 0–1 scale contract, the percentage-scale guard, and producer→writer composition. |
| File | Description |
|---|---|
| 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. |
| docs/api.md | Full API reference. Every method, every option, every type. Reads as a lookup, not a tutorial. |
| 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. |
| docs/migration.md | Migration & integration guide — choosing an export shape, wiring tokens into Tasty / CSS / JSON, prefix-map strategies, migrating from a legacy color system, and common pitfalls. |
| Command | Description |
|---|---|
pnpm build |
Build ESM + CJS via tsdown (with .d.ts and sourcemaps). |
pnpm test |
Run all tests (Vitest 4). |
pnpm test -- <pattern> |
Run a subset of tests. |
pnpm test:watch |
Tests in watch mode. |
pnpm test:coverage |
Tests with coverage report. |
pnpm typecheck |
Type-check without emitting. |
pnpm lint |
ESLint over src/. |
pnpm lint:fix |
ESLint with --fix. |
pnpm format |
Prettier write src/**/*.ts. |
pnpm format:check |
Prettier check (no writes). |
pnpm changeset |
Create a changeset entry. |
pnpm release |
Build and publish (used by CI). |
- Language: TypeScript (strict mode,
consistent-type-importsenforced) - Build: tsdown — ESM + CJS, unbundled, dts + sourcemaps
- Test: Vitest 4 with globals enabled
- Lint: ESLint 10 + typescript-eslint +
eslint-config-prettier - Format: Prettier — single quotes, semicolons, trailing commas, 80 cols
- Versioning: Changesets
- Package manager: pnpm 11
- TypeScript strict mode;
consistent-type-importsenforced. - Test files use
*.test.tsand live next to the module they test insrc/. - Unused vars prefixed with
_are allowed. - No runtime dependencies — every bit of math is implemented from scratch.
- Functional API pattern:
glaze()factory with attached static methods; no classes. - Each module starts with a JSDoc block describing its responsibility — keep these in sync when behavior changes.
If you need to add a fourth ColorDef variant (alongside regular / shadow / mix), touch these files in order:
- src/types.ts — add the new interface and extend the
ColorDefdiscriminated union. - src/shadow.ts — add a discriminator predicate (
isFooDef). - src/validation.ts — extend
validateColorDefs(missing-reference / cycle handling) andtopoSort(record the new dependency edges). - src/resolver.ts — add a per-scheme
resolveFooForSchemebranch and dispatch fromresolveColorForScheme. - src/formatters.ts — only if the output shape differs (it usually doesn't; the resolver returns a normal
ResolvedColorVariant). src/glaze.test.ts— add coverage for the new def, including HC pairs, dark-mode behavior, and inheritance throughextend.
For a new public method on glaze (e.g. another statics entry), the wiring lives entirely in src/glaze.ts; the implementation should still go in a focused module.
- CI: Runs lint, format check, typecheck, build, and tests on push to
mainand PRs. - Release: On push to
main,changesets/actioneither creates a version PR or publishes to npm. - Snapshot: Comment
/snapshoton a PR to publish a0.0.0-snapshot.<sha>release. - npm trusted publishing: Uses OIDC provenance via the
releaseGitHub environment; the workflow requiresenvironment: releasefor npm to accept the OIDC token. RELEASE_TOKENsecret: Fine-grained GitHub PAT (repo-scoped,contents: write+pull-requests: write) used bychangesets/actionfor Git pushes and PR creation. Rotate before expiry.- Repo settings required: Settings → Actions → General → Workflow permissions → "Allow GitHub Actions to create and approve pull requests" must be enabled.
Follow .cursor/commands/submit-changes.md for the full release-oriented workflow. At minimum:
- Typecheck —
pnpm typecheck. Stop and fix errors before continuing. - Lint —
pnpm lint. Stop and fix errors before continuing. - Format —
pnpm formatso the diff matches Prettier output. - Changeset — for any user-visible change (features, fixes, refactors, perf, docs that affect published behavior), add a markdown file in
.changeset/with apatch/minor/majorbump and a concise user-facing summary. Skip only for purely internal changes (CI, repo churn, tests with no behavior change). - 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. - Push — never push to
main. Confirm the current branch, thengit push -u origin HEAD.