@clhaas/palette-kit 0.3.0 → 0.4.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.
Files changed (312) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +80 -87
  3. package/dist/contrast/contrast.d.ts +16 -0
  4. package/dist/contrast/contrast.js +102 -0
  5. package/dist/core/intent-registry.d.ts +11 -0
  6. package/dist/core/intent-registry.js +70 -0
  7. package/dist/core/oklch.d.ts +16 -0
  8. package/dist/core/oklch.js +56 -0
  9. package/dist/create-palette-kit.d.ts +9 -0
  10. package/dist/create-palette-kit.js +67 -0
  11. package/dist/engine/context/context.d.ts +13 -0
  12. package/dist/engine/context/context.js +37 -0
  13. package/dist/engine/level/curves.d.ts +17 -0
  14. package/dist/engine/level/curves.js +49 -0
  15. package/dist/engine/level/level.d.ts +4 -0
  16. package/dist/engine/level/level.js +13 -0
  17. package/dist/engine/relation/relation.d.ts +105 -0
  18. package/dist/engine/relation/relation.js +137 -0
  19. package/dist/engine/resolve/resolve.d.ts +36 -0
  20. package/dist/engine/resolve/resolve.js +116 -0
  21. package/dist/engine/state/state.d.ts +46 -0
  22. package/dist/engine/state/state.js +68 -0
  23. package/dist/engine/usage/fill.d.ts +9 -0
  24. package/dist/engine/usage/fill.js +9 -0
  25. package/dist/engine/usage/lines.d.ts +9 -0
  26. package/dist/engine/usage/lines.js +9 -0
  27. package/dist/engine/usage/overlays.d.ts +9 -0
  28. package/dist/engine/usage/overlays.js +9 -0
  29. package/dist/engine/usage/strategy.d.ts +56 -0
  30. package/dist/engine/usage/strategy.js +30 -0
  31. package/dist/engine/usage/visualVocabulary.d.ts +9 -0
  32. package/dist/engine/usage/visualVocabulary.js +9 -0
  33. package/dist/export/serialize.d.ts +14 -0
  34. package/dist/export/serialize.js +89 -0
  35. package/dist/export/types.d.ts +37 -0
  36. package/dist/export/types.js +31 -0
  37. package/dist/index.d.ts +3 -3
  38. package/dist/index.js +2 -2
  39. package/dist/operators/convert.d.ts +32 -0
  40. package/dist/operators/convert.js +80 -0
  41. package/dist/presets/presets.d.ts +95 -0
  42. package/dist/presets/presets.js +308 -0
  43. package/dist/types/index.d.ts +111 -187
  44. package/dist/utils/errors/errors.d.ts +17 -0
  45. package/dist/utils/errors/errors.js +22 -0
  46. package/docs/API.md +167 -0
  47. package/docs/Alpha.md +14 -0
  48. package/docs/Architecture.md +56 -0
  49. package/docs/CLI.md +22 -0
  50. package/docs/Concepts.md +73 -0
  51. package/docs/Config.md +144 -0
  52. package/docs/Diagnostics.md +22 -0
  53. package/docs/Exporters.md +33 -0
  54. package/docs/FAQ.md +59 -0
  55. package/docs/Migration.md +61 -0
  56. package/docs/Overlays.md +33 -0
  57. package/docs/README.md +60 -0
  58. package/docs/Text.md +41 -0
  59. package/docs/Tokens.md +42 -0
  60. package/docs/Usage-JSON.md +39 -0
  61. package/docs/Usage-ReactNative.md +63 -0
  62. package/docs/Usage-Web.md +66 -0
  63. package/docs/Validation.md +97 -0
  64. package/docs/Why.md +37 -0
  65. package/docs/_api-surface.md +53 -0
  66. package/docs/snippets/serialize-oklch.md +9 -0
  67. package/docs/spec.md +98 -0
  68. package/package.json +74 -59
  69. package/.codex/skills/color-pipeline-implementer/SKILL.md +0 -23
  70. package/.codex/skills/commit-message-crafter/SKILL.md +0 -63
  71. package/.codex/skills/commit-message-crafter/references/benchmarks.md +0 -20
  72. package/.codex/skills/contrast-solver-helper/SKILL.md +0 -20
  73. package/.codex/skills/exporters-builder/SKILL.md +0 -20
  74. package/.codex/skills/markdownlint-writer/SKILL.md +0 -32
  75. package/.codex/skills/phase-implementation-runbook/SKILL.md +0 -92
  76. package/.codex/skills/type-contract-auditor/SKILL.md +0 -21
  77. package/.github/skills/review-guide/SKILL.md +0 -23
  78. package/.github/skills/review-guide/references/review-guide-v0.3.md +0 -629
  79. package/.markdownlint.json +0 -4
  80. package/AGENTS.md +0 -16
  81. package/biome.json +0 -43
  82. package/dist/cli/args.d.ts +0 -12
  83. package/dist/cli/args.js +0 -56
  84. package/dist/cli/args.test.d.ts +0 -1
  85. package/dist/cli/args.test.js +0 -22
  86. package/dist/cli/codegen/__snapshots__/tokens.test.js.snap +0 -87
  87. package/dist/cli/codegen/tokens.d.ts +0 -12
  88. package/dist/cli/codegen/tokens.js +0 -139
  89. package/dist/cli/codegen/tokens.test.d.ts +0 -1
  90. package/dist/cli/codegen/tokens.test.js +0 -51
  91. package/dist/cli/config.d.ts +0 -40
  92. package/dist/cli/config.js +0 -34
  93. package/dist/cli/validate.d.ts +0 -2
  94. package/dist/cli/validate.js +0 -33
  95. package/dist/cli/validate.test.d.ts +0 -1
  96. package/dist/cli/validate.test.js +0 -40
  97. package/dist/cli.d.ts +0 -2
  98. package/dist/cli.js +0 -148
  99. package/dist/contrast/apca.d.ts +0 -2
  100. package/dist/contrast/apca.js +0 -15
  101. package/dist/contrast/apca.test.d.ts +0 -1
  102. package/dist/contrast/apca.test.js +0 -16
  103. package/dist/contrast/index.d.ts +0 -4
  104. package/dist/contrast/index.js +0 -4
  105. package/dist/contrast/scoring.d.ts +0 -4
  106. package/dist/contrast/scoring.js +0 -31
  107. package/dist/contrast/scoring.test.d.ts +0 -1
  108. package/dist/contrast/scoring.test.js +0 -148
  109. package/dist/contrast/solver.d.ts +0 -13
  110. package/dist/contrast/solver.js +0 -170
  111. package/dist/contrast/solver.test.d.ts +0 -1
  112. package/dist/contrast/solver.test.js +0 -75
  113. package/dist/contrast/types.d.ts +0 -17
  114. package/dist/contrast/types.js +0 -1
  115. package/dist/contrast/utils.d.ts +0 -4
  116. package/dist/contrast/utils.js +0 -18
  117. package/dist/contrast/wcag2.d.ts +0 -3
  118. package/dist/contrast/wcag2.js +0 -19
  119. package/dist/contrast/wcag2.test.d.ts +0 -1
  120. package/dist/contrast/wcag2.test.js +0 -17
  121. package/dist/core/createTheme.d.ts +0 -35
  122. package/dist/core/createTheme.js +0 -24
  123. package/dist/core/dx-helpers.test.d.ts +0 -1
  124. package/dist/core/dx-helpers.test.js +0 -61
  125. package/dist/core/index.d.ts +0 -2
  126. package/dist/core/index.js +0 -2
  127. package/dist/core/onSolid.test.d.ts +0 -1
  128. package/dist/core/onSolid.test.js +0 -118
  129. package/dist/core/qa.v1.test.d.ts +0 -1
  130. package/dist/core/qa.v1.test.js +0 -112
  131. package/dist/core/resolve.d.ts +0 -3
  132. package/dist/core/resolve.js +0 -8
  133. package/dist/core/resolve.test.d.ts +0 -1
  134. package/dist/core/resolve.test.js +0 -89
  135. package/dist/core/resolveMany.d.ts +0 -8
  136. package/dist/core/resolveMany.js +0 -17
  137. package/dist/core/tokenRegistry.d.ts +0 -23
  138. package/dist/core/tokenRegistry.js +0 -83
  139. package/dist/core/tokenRegistry.test.d.ts +0 -1
  140. package/dist/core/tokenRegistry.test.js +0 -133
  141. package/dist/engine/applyOperators.d.ts +0 -3
  142. package/dist/engine/applyOperators.js +0 -23
  143. package/dist/engine/context.d.ts +0 -4
  144. package/dist/engine/context.js +0 -1
  145. package/dist/engine/gamut.d.ts +0 -13
  146. package/dist/engine/gamut.js +0 -101
  147. package/dist/engine/gamut.test.d.ts +0 -1
  148. package/dist/engine/gamut.test.js +0 -23
  149. package/dist/engine/generateScale.d.ts +0 -15
  150. package/dist/engine/generateScale.js +0 -29
  151. package/dist/engine/generateScale.test.d.ts +0 -1
  152. package/dist/engine/generateScale.test.js +0 -32
  153. package/dist/engine/index.d.ts +0 -8
  154. package/dist/engine/index.js +0 -4
  155. package/dist/engine/normalize.d.ts +0 -43
  156. package/dist/engine/normalize.js +0 -403
  157. package/dist/engine/normalize.test.d.ts +0 -1
  158. package/dist/engine/normalize.test.js +0 -136
  159. package/dist/engine/onSolid.d.ts +0 -3
  160. package/dist/engine/onSolid.js +0 -110
  161. package/dist/engine/resolveBaseColor.d.ts +0 -25
  162. package/dist/engine/resolveBaseColor.js +0 -127
  163. package/dist/engine/resolveBaseColor.test.d.ts +0 -1
  164. package/dist/engine/resolveBaseColor.test.js +0 -97
  165. package/dist/export/__snapshots__/exportTheme.test.js.snap +0 -74
  166. package/dist/export/exportTheme.d.ts +0 -47
  167. package/dist/export/exportTheme.js +0 -170
  168. package/dist/export/exportTheme.test.d.ts +0 -1
  169. package/dist/export/exportTheme.test.js +0 -118
  170. package/dist/export/index.d.ts +0 -1
  171. package/dist/export/index.js +0 -1
  172. package/dist/export/serializeColor.d.ts +0 -1
  173. package/dist/export/serializeColor.js +0 -1
  174. package/dist/export/serializeColor.test.d.ts +0 -1
  175. package/dist/export/serializeColor.test.js +0 -54
  176. package/dist/export.d.ts +0 -1
  177. package/dist/export.js +0 -1
  178. package/dist/operators/emphasis.d.ts +0 -3
  179. package/dist/operators/emphasis.js +0 -113
  180. package/dist/operators/emphasis.test.d.ts +0 -1
  181. package/dist/operators/emphasis.test.js +0 -69
  182. package/dist/operators/index.d.ts +0 -3
  183. package/dist/operators/index.js +0 -2
  184. package/dist/operators/state.d.ts +0 -3
  185. package/dist/operators/state.js +0 -102
  186. package/dist/operators/state.test.d.ts +0 -1
  187. package/dist/operators/state.test.js +0 -48
  188. package/dist/operators/types.d.ts +0 -13
  189. package/dist/operators/types.js +0 -1
  190. package/dist/operators/utils.d.ts +0 -16
  191. package/dist/operators/utils.js +0 -23
  192. package/dist/presets/curves.d.ts +0 -28
  193. package/dist/presets/curves.js +0 -145
  194. package/dist/presets/index.d.ts +0 -2
  195. package/dist/presets/index.js +0 -1
  196. package/dist/presets/tokens/index.d.ts +0 -3
  197. package/dist/presets/tokens/index.js +0 -3
  198. package/dist/presets/tokens/minimal-ui.d.ts +0 -6
  199. package/dist/presets/tokens/minimal-ui.js +0 -53
  200. package/dist/presets/tokens/modern-ui.d.ts +0 -5
  201. package/dist/presets/tokens/modern-ui.js +0 -83
  202. package/dist/presets/tokens/presets.test.d.ts +0 -1
  203. package/dist/presets/tokens/presets.test.js +0 -31
  204. package/dist/presets/tokens/radixLike-ui.d.ts +0 -6
  205. package/dist/presets/tokens/radixLike-ui.js +0 -77
  206. package/dist/serialize/index.d.ts +0 -1
  207. package/dist/serialize/index.js +0 -1
  208. package/dist/serialize/normalizeOutput.d.ts +0 -6
  209. package/dist/serialize/normalizeOutput.js +0 -45
  210. package/dist/serialize/serializeColor.d.ts +0 -21
  211. package/dist/serialize/serializeColor.js +0 -178
  212. package/dist/serialize/serializeResolved.test.d.ts +0 -1
  213. package/dist/serialize/serializeResolved.test.js +0 -45
  214. package/dist/serialize.d.ts +0 -1
  215. package/dist/serialize.js +0 -1
  216. package/dist/utils/clamp.d.ts +0 -1
  217. package/dist/utils/clamp.js +0 -1
  218. package/dist/utils/index.d.ts +0 -1
  219. package/dist/utils/index.js +0 -1
  220. package/dist/utils/lerp.d.ts +0 -1
  221. package/dist/utils/lerp.js +0 -1
  222. package/dist/utils/parseColor.d.ts +0 -6
  223. package/dist/utils/parseColor.js +0 -67
  224. package/dist/utils/parseColor.test.d.ts +0 -1
  225. package/dist/utils/parseColor.test.js +0 -51
  226. package/dist/utils/smoothstep.d.ts +0 -1
  227. package/dist/utils/smoothstep.js +0 -5
  228. package/planning/phase-10-review.md +0 -550
  229. package/planning/phase-7-review.md +0 -411
  230. package/planning/phase-8-review.md +0 -669
  231. package/planning/phase-9-review.md +0 -564
  232. package/planning/roadmap-v0.3.md +0 -284
  233. package/planning/spec-serializer-v0.3.md +0 -324
  234. package/planning/spec-v0.3.md +0 -305
  235. package/src/cli/args.test.ts +0 -28
  236. package/src/cli/args.ts +0 -66
  237. package/src/cli/codegen/__snapshots__/tokens.test.ts.snap +0 -87
  238. package/src/cli/codegen/tokens.test.ts +0 -61
  239. package/src/cli/codegen/tokens.ts +0 -191
  240. package/src/cli/config.ts +0 -71
  241. package/src/cli/validate.test.ts +0 -49
  242. package/src/cli/validate.ts +0 -38
  243. package/src/cli.ts +0 -183
  244. package/src/contrast/apca.test.ts +0 -20
  245. package/src/contrast/apca.ts +0 -26
  246. package/src/contrast/index.ts +0 -4
  247. package/src/contrast/scoring.test.ts +0 -188
  248. package/src/contrast/scoring.ts +0 -48
  249. package/src/contrast/solver.test.ts +0 -147
  250. package/src/contrast/solver.ts +0 -235
  251. package/src/contrast/types.ts +0 -20
  252. package/src/contrast/utils.ts +0 -28
  253. package/src/contrast/wcag2.test.ts +0 -21
  254. package/src/contrast/wcag2.ts +0 -24
  255. package/src/core/createTheme.ts +0 -78
  256. package/src/core/dx-helpers.test.ts +0 -82
  257. package/src/core/index.ts +0 -7
  258. package/src/core/onSolid.test.ts +0 -146
  259. package/src/core/qa.v1.test.ts +0 -149
  260. package/src/core/resolve.test.ts +0 -99
  261. package/src/core/resolve.ts +0 -11
  262. package/src/core/resolveMany.ts +0 -22
  263. package/src/core/tokenRegistry.test.ts +0 -153
  264. package/src/core/tokenRegistry.ts +0 -114
  265. package/src/engine/applyOperators.ts +0 -32
  266. package/src/engine/context.ts +0 -8
  267. package/src/engine/gamut.test.ts +0 -30
  268. package/src/engine/gamut.ts +0 -144
  269. package/src/engine/generateScale.test.ts +0 -46
  270. package/src/engine/generateScale.ts +0 -48
  271. package/src/engine/index.ts +0 -8
  272. package/src/engine/normalize.test.ts +0 -222
  273. package/src/engine/normalize.ts +0 -550
  274. package/src/engine/onSolid.ts +0 -178
  275. package/src/engine/resolveBaseColor.test.ts +0 -117
  276. package/src/engine/resolveBaseColor.ts +0 -203
  277. package/src/export/__snapshots__/exportTheme.test.ts.snap +0 -74
  278. package/src/export/exportTheme.test.ts +0 -144
  279. package/src/export/exportTheme.ts +0 -251
  280. package/src/export/index.ts +0 -1
  281. package/src/export/serializeColor.test.ts +0 -73
  282. package/src/export/serializeColor.ts +0 -1
  283. package/src/export.ts +0 -1
  284. package/src/index.ts +0 -3
  285. package/src/operators/emphasis.test.ts +0 -85
  286. package/src/operators/emphasis.ts +0 -132
  287. package/src/operators/index.ts +0 -3
  288. package/src/operators/state.test.ts +0 -66
  289. package/src/operators/state.ts +0 -122
  290. package/src/operators/types.ts +0 -14
  291. package/src/operators/utils.ts +0 -44
  292. package/src/presets/curves.ts +0 -168
  293. package/src/presets/index.ts +0 -2
  294. package/src/presets/tokens/index.ts +0 -3
  295. package/src/presets/tokens/minimal-ui.ts +0 -55
  296. package/src/presets/tokens/modern-ui.ts +0 -85
  297. package/src/presets/tokens/presets.test.ts +0 -46
  298. package/src/presets/tokens/radixLike-ui.ts +0 -79
  299. package/src/serialize/index.ts +0 -1
  300. package/src/serialize/normalizeOutput.ts +0 -63
  301. package/src/serialize/serializeColor.ts +0 -260
  302. package/src/serialize/serializeResolved.test.ts +0 -57
  303. package/src/serialize.ts +0 -1
  304. package/src/types/index.ts +0 -207
  305. package/src/utils/clamp.ts +0 -2
  306. package/src/utils/index.ts +0 -1
  307. package/src/utils/lerp.ts +0 -1
  308. package/src/utils/parseColor.test.ts +0 -66
  309. package/src/utils/parseColor.ts +0 -87
  310. package/src/utils/smoothstep.ts +0 -6
  311. package/tsconfig.build.json +0 -11
  312. package/tsconfig.json +0 -15
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ <!-- markdownlint-disable MD024 -->
4
+
5
+ ## v0.4.0
6
+
7
+ ### Breaking changes
8
+
9
+ - Replaces the previous experimental `createTheme` API with `createPaletteKit`.
10
+ - Publishes only the package root export. CLI commands, token exporters, and
11
+ serializer subpaths are not part of v0.4.
12
+ - Resolver options are modeled as explicit axes: `usage`, `intent`, `level`,
13
+ relation, `state`, `context`, and `output`.
14
+
15
+ ### Features
16
+
17
+ - Public `createPaletteKit` factory and `palette.resolve`.
18
+ - Public resolver presets: `soft`, `neutral`, and `strong`.
19
+ - Explicit `resolverConfig` overrides for level curves, state deltas, relation
20
+ parameters, and chroma limits.
21
+ - APCA contrast enforcement for `on` relations with a default Lc 60 target.
22
+ - Functional `over` and `under` overlay relations with configured alpha and
23
+ depth behavior.
24
+ - Supported outputs: `oklch`, `oklab`, `srgb`, `p3`, `hex`, and `rgba`.
25
+ - Strict public TypeScript option types for invalid usage/level/relation/state
26
+ combinations where possible.
27
+
3
28
  ## v0.3.0
4
29
 
5
30
  ### Breaking changes
package/README.md CHANGED
@@ -1,127 +1,120 @@
1
1
  # Palette Kit
2
2
 
3
- Palette Kit is a **runtime-first color engine** for generating OKLCH-based palettes from semantic queries, with optional build-time tooling (serializer, exporters, CLI, codegen).
3
+ Palette Kit v0.4 is a deterministic OKLCH color resolution engine. It resolves
4
+ semantic color requests from explicit axes and serializes the result only after
5
+ resolution.
4
6
 
5
7
  ## Install
6
8
 
7
- ```bash
9
+ ```sh
8
10
  npm install @clhaas/palette-kit
9
11
  ```
10
12
 
11
- ## Runtime quick start
13
+ ## Quick Start
12
14
 
13
15
  ```ts
14
- import { createTheme } from "@clhaas/palette-kit";
16
+ import { createPaletteKit } from "@clhaas/palette-kit";
15
17
 
16
- const theme = createTheme({
17
- seeds: {
18
- light: { neutral: "#111827", accent: "#3d63dd" },
19
- dark: { neutral: "#111827", accent: "#3d63dd" },
18
+ const palette = createPaletteKit({
19
+ context: "light",
20
+ output: "oklch",
21
+ preset: "neutral",
22
+ intents: {
23
+ brand: { hue: 260, chroma: 0.14 },
24
+ neutral: { hue: 0, chroma: 0 },
20
25
  },
21
- preset: "modern",
22
26
  });
23
27
 
24
- const bg = theme.resolve({
25
- role: "bg.app",
26
- usage: "bg",
27
- surface: "app",
28
- context: "light",
28
+ const surface = palette.resolve({
29
+ usage: "fill",
30
+ intent: "neutral",
31
+ level: 2,
29
32
  });
30
33
 
31
- const onSolidText = theme.onSolid({
32
- bgRole: "bg.app",
33
- usage: "text",
34
- context: "light",
35
- contrast: { model: "apca", targetLc: 75 },
34
+ const text = palette.resolve({
35
+ usage: "visualVocabulary",
36
+ intent: "brand",
37
+ on: surface,
36
38
  });
37
39
  ```
38
40
 
39
- ## Serializer (public)
41
+ ## Public Runtime API
40
42
 
41
- Use the serializer to turn resolved OKLCH into CSS/RN-ready strings:
42
-
43
- ```ts
44
- import { createTheme } from "@clhaas/palette-kit";
45
- import { serializeResolved } from "@clhaas/palette-kit/serialize";
43
+ The package root exports:
46
44
 
47
- const theme = createTheme({
48
- seeds: {
49
- light: { neutral: "#111827", accent: "#3d63dd" },
50
- dark: { neutral: "#111827", accent: "#3d63dd" },
51
- },
52
- });
45
+ - `createPaletteKit`
46
+ - `softResolverConfig`
47
+ - `neutralResolverConfig`
48
+ - `strongResolverConfig`
49
+ - `defaultResolverConfig`
53
50
 
54
- const resolved = theme.resolve({ role: "bg.app", usage: "bg", surface: "app" });
55
- const color = serializeResolved(resolved, { preferSpace: "srgb", srgbFormat: "hex" });
51
+ Public TypeScript types are also exported from the package root.
56
52
 
57
- console.log(color.value);
58
- ```
53
+ There are no public subpath exports, CLI commands, token exporters, or codegen
54
+ APIs in v0.4.
59
55
 
60
- ## Exporters (public, build-time)
56
+ ## Resolver Rules
61
57
 
62
- Export deterministic CSS variables and JSON tokens:
58
+ | Usage | Level | Relations |
59
+ | --- | --- | --- |
60
+ | `fill` | Required | `on` optional |
61
+ | `visualVocabulary` | Forbidden | `on` required |
62
+ | `lines` | Required | `on` optional |
63
+ | `overlays` | Required | `over` or `under` optional |
63
64
 
64
- ```ts
65
- import { createTheme } from "@clhaas/palette-kit";
66
- import { exportThemeCss, exportThemeJson } from "@clhaas/palette-kit/export";
65
+ `state` defaults to `"default"`. Non-default states require explicit
66
+ `stateDirection`; Palette Kit never infers whether state should increase or
67
+ decrease lightness.
67
68
 
68
- const theme = createTheme({
69
- seeds: {
70
- light: { neutral: "#111827", accent: "#3d63dd" },
71
- dark: { neutral: "#111827", accent: "#3d63dd" },
72
- },
73
- });
69
+ Context is explicit. Provide palette-level `context`, resolver-level `context`,
70
+ or host-injected `systemDefaultContext`. Context affects default level curves;
71
+ for example, dark context inverts the structural lightness scale while
72
+ preserving intent hue and chroma.
74
73
 
75
- const tokens = {
76
- "bg.app": { usage: "bg", surface: "app" },
77
- "text.primary": { usage: "text", surface: "surface" },
78
- };
74
+ ## Output
79
75
 
80
- const { css } = exportThemeCss(theme, tokens, { includeSpaces: ["oklch", "p3"] });
81
- const json = exportThemeJson(theme, tokens, { includeSpaces: ["srgb"] });
82
- ```
76
+ Supported outputs:
83
77
 
84
- ## CLI
78
+ - `oklch`: normalized OKLCH object
79
+ - `oklab`: OKLab object
80
+ - `srgb`: `{ r, g, b, alpha }`
81
+ - `p3`: Display-P3 `{ r, g, b, alpha }`
82
+ - `hex`: `#rrggbb`
83
+ - `rgba`: `{ r, g, b, a }`
85
84
 
86
- - `palette-kit init` creates `palette.config.ts`
87
- - `palette-kit build` writes `dist/palette/` artifacts (`tokens.css`, `tokens.json`, `tokens.ts`, `tokens.d.ts`)
85
+ RGB-like outputs use clipped 8-bit channels. Output never changes semantic
86
+ resolution.
88
87
 
89
- See `docs/CLI.md` for flags and config details.
88
+ Output precedence is resolver-level `output`, then palette-level `output`, then
89
+ host-injected `systemDefaultOutput`, then the explicit `oklch` default.
90
90
 
91
- ## Docs
91
+ ## Configuration
92
92
 
93
- - `docs/README.md`
94
- - `docs/_api-surface.md`
95
- - `docs/Exporters.md`
96
- - `docs/CLI.md`
97
- - `docs/Migration.md`
93
+ `createPaletteKit` accepts `preset` and explicit `resolverConfig` overrides.
98
94
 
99
- ## Compatibility
95
+ ```ts
96
+ const palette = createPaletteKit({
97
+ context: "light",
98
+ preset: "soft",
99
+ intents,
100
+ resolverConfig: {
101
+ relationParams: {
102
+ on: { contrastTarget: 75 },
103
+ },
104
+ },
105
+ });
106
+ ```
100
107
 
101
- - ESM package (`"type": "module"`).
102
- - CJS is not supported in v0.3; use ESM or dynamic `import()` in CJS environments.
103
- - Subpath imports:
104
- - `@clhaas/palette-kit/serialize`
105
- - `@clhaas/palette-kit/export`
106
- - `@clhaas/palette-kit/cli`
107
- - Tree-shaking: runtime imports (`@clhaas/palette-kit`) do not bundle build-time tools (exporters/CLI).
108
+ The default preset is `neutral`.
108
109
 
109
- ### CommonJS note
110
+ ## Documentation
110
111
 
111
- ```js
112
- // CJS workaround (dynamic import)
113
- (async () => {
114
- const { createTheme } = await import("@clhaas/palette-kit");
115
- const theme = createTheme({
116
- seeds: {
117
- light: { neutral: "#111827", accent: "#3d63dd" },
118
- dark: { neutral: "#111827", accent: "#3d63dd" },
119
- },
120
- });
121
- console.log(theme);
122
- })();
123
- ```
112
+ - [Docs index](docs/README.md)
113
+ - [API](docs/API.md)
114
+ - [Configuration](docs/Config.md)
115
+ - [Specification summary](docs/spec.md)
116
+ - [Migration](docs/Migration.md)
124
117
 
125
- ## License
118
+ ## Module Format
126
119
 
127
- MIT see `LICENSE`.
120
+ Palette Kit is ESM-only.
@@ -0,0 +1,16 @@
1
+ import { type OklchColor } from '../core/oklch.js';
2
+ import type { Context } from '../engine/context/context.js';
3
+ import type { ChromaConfig, RelationParamsConfig } from '../presets/presets.js';
4
+ export type ContrastResolutionConfig = Readonly<{
5
+ on: RelationParamsConfig['on'];
6
+ chromaLimits: ChromaConfig;
7
+ }>;
8
+ export type ContrastResolutionInput = Readonly<{
9
+ color: OklchColor;
10
+ target: OklchColor;
11
+ context: Context;
12
+ config: ContrastResolutionConfig;
13
+ }>;
14
+ export declare function measureApcaContrast(foreground: OklchColor, background: OklchColor): number;
15
+ export declare function measureWcagContrast(foreground: OklchColor, background: OklchColor): number;
16
+ export declare function resolveOnContrast({ color, config, context, target, }: ContrastResolutionInput): OklchColor;
@@ -0,0 +1,102 @@
1
+ import { APCAcontrast, sRGBtoY } from 'apca-w3';
2
+ import { normalizeOklch } from '../core/oklch.js';
3
+ import { serializeOklchToSrgb } from '../export/serialize.js';
4
+ import { createContrastUnsatisfiableError } from '../utils/errors/errors.js';
5
+ const LIGHTNESS_STEP = 0.5;
6
+ const CONTRAST_PRECISION = 2;
7
+ const clampLightness = (value) => Math.min(100, Math.max(0, value));
8
+ const roundContrast = (contrast) => Number(contrast.toFixed(CONTRAST_PRECISION));
9
+ export function measureApcaContrast(foreground, background) {
10
+ const foregroundRgb = serializeOklchToSrgb(foreground);
11
+ const backgroundRgb = serializeOklchToSrgb(background);
12
+ const contrast = APCAcontrast(sRGBtoY([foregroundRgb.r, foregroundRgb.g, foregroundRgb.b]), sRGBtoY([backgroundRgb.r, backgroundRgb.g, backgroundRgb.b]));
13
+ const numericContrast = typeof contrast === 'number' ? contrast : Number(contrast);
14
+ if (Number.isFinite(numericContrast)) {
15
+ return numericContrast;
16
+ }
17
+ const wcagContrast = measureWcagContrast(foreground, background);
18
+ const polarity = foreground.l <= background.l ? 1 : -1;
19
+ return polarity * wcagContrast * 10;
20
+ }
21
+ const channelToLinear = (channel) => {
22
+ const normalized = channel / 255;
23
+ return normalized <= 0.03928
24
+ ? normalized / 12.92
25
+ : ((normalized + 0.055) / 1.055) ** 2.4;
26
+ };
27
+ const relativeLuminance = (color) => {
28
+ const rgb = serializeOklchToSrgb(color);
29
+ return (0.2126 * channelToLinear(rgb.r) +
30
+ 0.7152 * channelToLinear(rgb.g) +
31
+ 0.0722 * channelToLinear(rgb.b));
32
+ };
33
+ export function measureWcagContrast(foreground, background) {
34
+ const foregroundLuminance = relativeLuminance(foreground);
35
+ const backgroundLuminance = relativeLuminance(background);
36
+ const lighter = Math.max(foregroundLuminance, backgroundLuminance);
37
+ const darker = Math.min(foregroundLuminance, backgroundLuminance);
38
+ return (lighter + 0.05) / (darker + 0.05);
39
+ }
40
+ const hasTargetContrast = (contrast, target) => Math.abs(contrast) >= target;
41
+ const createCandidate = (color, lightness, chroma) => normalizeOklch({
42
+ alpha: color.alpha,
43
+ c: Math.max(0, chroma),
44
+ h: color.h,
45
+ l: clampLightness(lightness),
46
+ });
47
+ const selectContrastDirections = (target, context) => {
48
+ if (target.l < 45) {
49
+ return ['increase', 'decrease'];
50
+ }
51
+ if (target.l > 55) {
52
+ return ['decrease', 'increase'];
53
+ }
54
+ return context === 'dark'
55
+ ? ['increase', 'decrease']
56
+ : ['decrease', 'increase'];
57
+ };
58
+ const scanLightness = (color, target, chroma, direction, maxLuminanceShift, contrastTarget) => {
59
+ const signedStep = direction === 'increase' ? LIGHTNESS_STEP : -LIGHTNESS_STEP;
60
+ const iterations = Math.ceil(maxLuminanceShift / LIGHTNESS_STEP);
61
+ let bestColor = createCandidate(color, color.l, chroma);
62
+ let bestContrast = measureApcaContrast(bestColor, target);
63
+ for (let index = 0; index <= iterations; index += 1) {
64
+ const lightness = color.l + signedStep * index;
65
+ const shift = Math.abs(lightness - color.l);
66
+ if (shift > maxLuminanceShift) {
67
+ break;
68
+ }
69
+ const candidate = createCandidate(color, lightness, chroma);
70
+ const contrast = measureApcaContrast(candidate, target);
71
+ if (Math.abs(contrast) > Math.abs(bestContrast)) {
72
+ bestColor = candidate;
73
+ bestContrast = contrast;
74
+ }
75
+ if (hasTargetContrast(contrast, contrastTarget)) {
76
+ break;
77
+ }
78
+ }
79
+ return { bestColor, bestContrast };
80
+ };
81
+ export function resolveOnContrast({ color, config, context, target, }) {
82
+ const contrastTarget = config.on.contrastTarget;
83
+ const maxReduction = Math.min(color.c, color.c * config.chromaLimits.maxReduction);
84
+ const chromaStep = config.chromaLimits.reductionStep;
85
+ let bestContrast = measureApcaContrast(color, target);
86
+ if (hasTargetContrast(bestContrast, contrastTarget)) {
87
+ return Object.freeze(color);
88
+ }
89
+ for (let reduction = 0; reduction <= maxReduction + chromaStep / 2; reduction += chromaStep) {
90
+ const chroma = Math.max(0, color.c - reduction);
91
+ for (const direction of selectContrastDirections(target, context)) {
92
+ const result = scanLightness(color, target, chroma, direction, config.on.maxLuminanceShift, contrastTarget);
93
+ if (Math.abs(result.bestContrast) > Math.abs(bestContrast)) {
94
+ bestContrast = result.bestContrast;
95
+ }
96
+ if (hasTargetContrast(result.bestContrast, contrastTarget)) {
97
+ return Object.freeze(result.bestColor);
98
+ }
99
+ }
100
+ }
101
+ throw createContrastUnsatisfiableError(roundContrast(Math.abs(bestContrast)), contrastTarget);
102
+ }
@@ -0,0 +1,11 @@
1
+ export type IntentName = string;
2
+ export type IntentDefinition = {
3
+ hue: number;
4
+ chroma: number;
5
+ };
6
+ export type IntentRegistry<I extends string = string> = Readonly<{
7
+ intents: Readonly<Record<I, Readonly<IntentDefinition>>>;
8
+ }>;
9
+ export declare function createIntentRegistry<const I extends string>(intents: Record<I, IntentDefinition>): IntentRegistry<I>;
10
+ export declare function hasIntent<I extends string>(registry: IntentRegistry<I>, intent: IntentName): intent is I;
11
+ export declare function getIntent<I extends string>(registry: IntentRegistry<I>, intent: IntentName): Readonly<IntentDefinition>;
@@ -0,0 +1,70 @@
1
+ import { createUnknownIntentError } from '../utils/errors/errors.js';
2
+ const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
3
+ const normalizeHue = (hue) => {
4
+ const normalized = ((hue % 360) + 360) % 360;
5
+ return Object.is(normalized, -0) ? 0 : normalized;
6
+ };
7
+ const forbiddenIntentTokens = Object.freeze({
8
+ level: new Set(['strong', 'subtle', 'weak', 'muted', 'heavy']),
9
+ relation: new Set(['on', 'over', 'under', 'overlay']),
10
+ state: new Set(['hover', 'active', 'focus', 'selected', 'disabled']),
11
+ usage: new Set(['text', 'border', 'icon', 'fill', 'line', 'lines']),
12
+ visual: new Set(['green', 'red', 'blue', 'dark', 'light']),
13
+ });
14
+ const splitIntentName = (name) => name
15
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
16
+ .split(/[-_]+|\s+/)
17
+ .map((part) => part.toLowerCase())
18
+ .filter((part) => part.length > 0);
19
+ const validateIntentName = (name) => {
20
+ if (name.length === 0) {
21
+ throw new Error('Intent name must not be empty.');
22
+ }
23
+ if (/\s/.test(name)) {
24
+ throw new Error(`Intent name "${name}" must not contain whitespace.`);
25
+ }
26
+ if (name.includes('.')) {
27
+ throw new Error(`Intent name "${name}" must use a flat namespace.`);
28
+ }
29
+ const tokens = splitIntentName(name);
30
+ for (const [category, forbiddenTokens] of Object.entries(forbiddenIntentTokens)) {
31
+ if (tokens.some((token) => forbiddenTokens.has(token))) {
32
+ throw new Error(`Intent name "${name}" must describe meaning only and must not encode ${category}.`);
33
+ }
34
+ }
35
+ };
36
+ const normalizeIntentDefinition = (name, definition) => {
37
+ if (!isFiniteNumber(definition.hue)) {
38
+ throw new Error(`Intent "${name}" hue must be a finite number.`);
39
+ }
40
+ if (!isFiniteNumber(definition.chroma)) {
41
+ throw new Error(`Intent "${name}" chroma must be a finite number.`);
42
+ }
43
+ if (definition.chroma < 0) {
44
+ throw new Error(`Intent "${name}" chroma must be greater than or equal to 0.`);
45
+ }
46
+ return Object.freeze({
47
+ chroma: definition.chroma,
48
+ hue: normalizeHue(definition.hue),
49
+ });
50
+ };
51
+ export function createIntentRegistry(intents) {
52
+ const entries = Object.entries(intents);
53
+ const normalized = {};
54
+ for (const [name, definition] of entries) {
55
+ validateIntentName(name);
56
+ normalized[name] = normalizeIntentDefinition(name, definition);
57
+ }
58
+ return Object.freeze({
59
+ intents: Object.freeze(normalized),
60
+ });
61
+ }
62
+ export function hasIntent(registry, intent) {
63
+ return Object.hasOwn(registry.intents, intent);
64
+ }
65
+ export function getIntent(registry, intent) {
66
+ if (!hasIntent(registry, intent)) {
67
+ throw createUnknownIntentError(intent);
68
+ }
69
+ return registry.intents[intent];
70
+ }
@@ -0,0 +1,16 @@
1
+ export type OklchColor = {
2
+ space: 'oklch';
3
+ l: number;
4
+ c: number;
5
+ h: number;
6
+ alpha: number;
7
+ };
8
+ export type OklchInput = {
9
+ l: number;
10
+ c: number;
11
+ h: number;
12
+ alpha?: number;
13
+ };
14
+ export declare function normalizeOklch(input: OklchInput): OklchColor;
15
+ export declare function isOklchColor(value: unknown): value is OklchColor;
16
+ export declare function assertOklchColor(value: unknown): asserts value is OklchColor;
@@ -0,0 +1,56 @@
1
+ const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
2
+ const normalizeHue = (hue) => {
3
+ const normalized = ((hue % 360) + 360) % 360;
4
+ return Object.is(normalized, -0) ? 0 : normalized;
5
+ };
6
+ const validateFiniteChannel = (name, value) => {
7
+ if (!isFiniteNumber(value)) {
8
+ throw new Error(`OKLCH ${name} must be a finite number.`);
9
+ }
10
+ };
11
+ export function normalizeOklch(input) {
12
+ validateFiniteChannel('l', input.l);
13
+ validateFiniteChannel('c', input.c);
14
+ validateFiniteChannel('h', input.h);
15
+ if (input.l < 0 || input.l > 100) {
16
+ throw new Error('OKLCH l must be between 0 and 100.');
17
+ }
18
+ if (input.c < 0) {
19
+ throw new Error('OKLCH c must be greater than or equal to 0.');
20
+ }
21
+ const alpha = input.alpha ?? 1;
22
+ validateFiniteChannel('alpha', alpha);
23
+ if (alpha < 0 || alpha > 1) {
24
+ throw new Error('OKLCH alpha must be between 0 and 1.');
25
+ }
26
+ return {
27
+ alpha,
28
+ c: input.c,
29
+ h: normalizeHue(input.h),
30
+ l: input.l,
31
+ space: 'oklch',
32
+ };
33
+ }
34
+ export function isOklchColor(value) {
35
+ if (typeof value !== 'object' || value === null) {
36
+ return false;
37
+ }
38
+ const candidate = value;
39
+ return (candidate.space === 'oklch' &&
40
+ isFiniteNumber(candidate.l) &&
41
+ candidate.l >= 0 &&
42
+ candidate.l <= 100 &&
43
+ isFiniteNumber(candidate.c) &&
44
+ candidate.c >= 0 &&
45
+ isFiniteNumber(candidate.h) &&
46
+ candidate.h >= 0 &&
47
+ candidate.h < 360 &&
48
+ isFiniteNumber(candidate.alpha) &&
49
+ candidate.alpha >= 0 &&
50
+ candidate.alpha <= 1);
51
+ }
52
+ export function assertOklchColor(value) {
53
+ if (!isOklchColor(value)) {
54
+ throw new Error('Expected a normalized OKLCH color.');
55
+ }
56
+ }
@@ -0,0 +1,9 @@
1
+ import { type ColorOutput } from './export/types.js';
2
+ import type { PaletteDefaultOutput, PaletteKit, PaletteKitConfig } from './types/index.js';
3
+ /**
4
+ * Creates an immutable Palette Kit resolver instance.
5
+ *
6
+ * The factory normalizes the provided intent registry once, keeps context and
7
+ * output defaults explicit, and never reads ambient platform state.
8
+ */
9
+ export declare function createPaletteKit<const I extends string, const PaletteOutput extends ColorOutput | undefined = undefined, const SystemDefaultOutput extends ColorOutput | undefined = undefined>(config: PaletteKitConfig<I, PaletteOutput, SystemDefaultOutput>): PaletteKit<I, PaletteDefaultOutput<PaletteOutput, SystemDefaultOutput>>;
@@ -0,0 +1,67 @@
1
+ import { createIntentRegistry, } from './core/intent-registry.js';
2
+ import { assertContext } from './engine/context/context.js';
3
+ import { resolveColor } from './engine/resolve/resolve.js';
4
+ import { serializeColor } from './export/serialize.js';
5
+ import { assertColorOutput, resolveOutput, } from './export/types.js';
6
+ import { defaultResolverConfig, getResolverPresetConfig, mergeResolverConfig, } from './presets/presets.js';
7
+ function resolveSerializedOutput(color, output) {
8
+ if (output === 'oklch') {
9
+ return color;
10
+ }
11
+ return serializeColor(color, output);
12
+ }
13
+ function validateOptionalContext(context) {
14
+ if (context !== undefined) {
15
+ assertContext(context);
16
+ }
17
+ }
18
+ function validateOptionalOutput(output) {
19
+ if (output !== undefined) {
20
+ assertColorOutput(output);
21
+ }
22
+ }
23
+ function createResolveFunction(intentRegistry, paletteContext, systemDefaultContext, paletteOutput, systemDefaultOutput, resolverConfig) {
24
+ return (options) => {
25
+ const output = resolveOutput({
26
+ paletteOutput,
27
+ resolverOutput: options.output,
28
+ systemDefaultOutput,
29
+ });
30
+ const resolved = resolveColor({
31
+ intent: options.intent,
32
+ intentRegistry,
33
+ level: options.level,
34
+ on: options.on,
35
+ over: options.over,
36
+ paletteContext,
37
+ resolverConfig,
38
+ resolverContext: options.context,
39
+ state: options.state,
40
+ stateDirection: options.stateDirection,
41
+ systemDefaultContext,
42
+ under: options.under,
43
+ usage: options.usage,
44
+ });
45
+ return resolveSerializedOutput(resolved.color, output);
46
+ };
47
+ }
48
+ /**
49
+ * Creates an immutable Palette Kit resolver instance.
50
+ *
51
+ * The factory normalizes the provided intent registry once, keeps context and
52
+ * output defaults explicit, and never reads ambient platform state.
53
+ */
54
+ export function createPaletteKit(config) {
55
+ validateOptionalContext(config.context);
56
+ validateOptionalContext(config.systemDefaultContext);
57
+ validateOptionalOutput(config.output);
58
+ validateOptionalOutput(config.systemDefaultOutput);
59
+ const presetConfig = config.preset === undefined
60
+ ? defaultResolverConfig
61
+ : getResolverPresetConfig(config.preset);
62
+ const resolverConfig = mergeResolverConfig(presetConfig, config.resolverConfig);
63
+ const intentRegistry = createIntentRegistry(config.intents);
64
+ return Object.freeze({
65
+ resolve: createResolveFunction(intentRegistry, config.context, config.systemDefaultContext, config.output, config.systemDefaultOutput, resolverConfig),
66
+ });
67
+ }
@@ -0,0 +1,13 @@
1
+ export declare const CONTEXTS: readonly ["light", "dark"];
2
+ export type Context = (typeof CONTEXTS)[number];
3
+ export type ContextResolutionInput = Readonly<{
4
+ resolverContext?: unknown;
5
+ paletteContext?: unknown;
6
+ systemDefaultContext?: unknown;
7
+ }>;
8
+ export type ContextCurveValues<T> = Readonly<Record<Context, T>>;
9
+ export type ContextCurveHook<T> = (context: Context) => T;
10
+ export declare function isContext(value: unknown): value is Context;
11
+ export declare function assertContext(value: unknown): asserts value is Context;
12
+ export declare function resolveContext({ resolverContext, paletteContext, systemDefaultContext, }: ContextResolutionInput): Context;
13
+ export declare function createContextCurveHook<T>(values: ContextCurveValues<T>): ContextCurveHook<T>;
@@ -0,0 +1,37 @@
1
+ import { createUnresolvedContextError } from '../../utils/errors/errors.js';
2
+ export const CONTEXTS = Object.freeze(['light', 'dark']);
3
+ const contextList = CONTEXTS.join(', ');
4
+ const formatInvalidContextError = (value) => `Invalid context "${String(value)}". Expected one of: ${contextList}.`;
5
+ export function isContext(value) {
6
+ return (typeof value === 'string' && CONTEXTS.includes(value));
7
+ }
8
+ export function assertContext(value) {
9
+ if (!isContext(value)) {
10
+ throw new Error(formatInvalidContextError(value));
11
+ }
12
+ }
13
+ export function resolveContext({ resolverContext, paletteContext, systemDefaultContext, }) {
14
+ if (resolverContext !== undefined) {
15
+ assertContext(resolverContext);
16
+ return resolverContext;
17
+ }
18
+ if (paletteContext !== undefined) {
19
+ assertContext(paletteContext);
20
+ return paletteContext;
21
+ }
22
+ if (systemDefaultContext !== undefined) {
23
+ assertContext(systemDefaultContext);
24
+ return systemDefaultContext;
25
+ }
26
+ throw createUnresolvedContextError();
27
+ }
28
+ export function createContextCurveHook(values) {
29
+ const frozenValues = Object.freeze({
30
+ dark: values.dark,
31
+ light: values.light,
32
+ });
33
+ return Object.freeze((context) => {
34
+ assertContext(context);
35
+ return frozenValues[context];
36
+ });
37
+ }
@@ -0,0 +1,17 @@
1
+ import type { Context } from '../context/context.js';
2
+ import type { Usage } from '../usage/strategy.js';
3
+ import { type Level } from './level.js';
4
+ export type LevelDrivenUsage = Exclude<Usage, 'visualVocabulary'>;
5
+ export type LevelCurve<T> = (level: Level, context: Context) => T;
6
+ export type FillLevelCurve = LevelCurve<number>;
7
+ export type LinesLevelCurve = LevelCurve<number>;
8
+ export type OverlayLevelResult = Readonly<{
9
+ luminanceDelta: number;
10
+ }>;
11
+ export type OverlaysLevelCurve = LevelCurve<OverlayLevelResult>;
12
+ export type LevelCurveConfig = Readonly<{
13
+ fill: FillLevelCurve;
14
+ lines: LinesLevelCurve;
15
+ overlays: OverlaysLevelCurve;
16
+ }>;
17
+ export declare const defaultLevelCurves: LevelCurveConfig;