@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/dist/cli.js DELETED
@@ -1,148 +0,0 @@
1
- #!/usr/bin/env node
2
- import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
- import { basename, join, resolve } from "node:path";
4
- import { pathToFileURL } from "node:url";
5
- import { CliUsageError, COMMANDS, HELP_TEXT, parseArgs } from "./cli/args.js";
6
- import { generateTokenArtifacts } from "./cli/codegen/tokens.js";
7
- import { buildConfigTemplate } from "./cli/config.js";
8
- import { validateConfig } from "./cli/validate.js";
9
- import { createTheme } from "./core/createTheme.js";
10
- import { validateTokenRegistry } from "./core/tokenRegistry.js";
11
- import { exportThemeCss, exportThemeJson } from "./export/exportTheme.js";
12
- import { minimalUiTokens, modernUiTokens, radixLikeUiTokens } from "./presets/tokens/index.js";
13
- const readPackageJson = async () => {
14
- const url = new URL("../package.json", import.meta.url);
15
- const content = await readFile(url, "utf8");
16
- return JSON.parse(content);
17
- };
18
- const printHelp = () => {
19
- console.log(HELP_TEXT);
20
- };
21
- const printVersion = async () => {
22
- const pkg = await readPackageJson();
23
- console.log(pkg.version);
24
- };
25
- const ensureDir = async (dir) => {
26
- await mkdir(dir, { recursive: true });
27
- };
28
- const exists = async (filePath) => {
29
- try {
30
- await stat(filePath);
31
- return true;
32
- }
33
- catch {
34
- return false;
35
- }
36
- };
37
- const loadConfig = async (configPath) => {
38
- const resolved = resolve(configPath);
39
- try {
40
- const module = await import(pathToFileURL(resolved).href);
41
- const config = (module.default ?? module);
42
- return config;
43
- }
44
- catch (error) {
45
- const suffix = resolved.endsWith(".ts")
46
- ? "\nTip: Use a .mjs config or run Node with a TS loader (e.g. tsx)."
47
- : "";
48
- const message = error instanceof Error ? error.message : String(error);
49
- throw new Error(`Unable to load config at ${resolved}. ${message}${suffix}`);
50
- }
51
- };
52
- const tokenPresetMap = {
53
- "minimal-ui": minimalUiTokens,
54
- "radixLike-ui": radixLikeUiTokens,
55
- "modern-ui": modernUiTokens,
56
- };
57
- const toThemeTokens = (registry) => Object.fromEntries(Object.entries(registry.tokens).map(([name, token]) => [name, token.query]));
58
- const writeTokensCodegen = async (outDir, registry) => {
59
- const generated = generateTokenArtifacts(registry);
60
- await writeFile(join(outDir, "tokens.ts"), generated.tokensTs, "utf8");
61
- await writeFile(join(outDir, "tokens.d.ts"), generated.tokensDts, "utf8");
62
- return generated.tokenNames;
63
- };
64
- const writeReport = async (outDir, config, tokenCount, outputFiles) => {
65
- const lines = [
66
- "# Palette Kit Report",
67
- "",
68
- `- preset: ${config.tokens.preset}`,
69
- `- tokens: ${tokenCount}`,
70
- `- output: preferSpace=${config.output?.preferSpace ?? "oklch"}`,
71
- `- includeSpaces: ${(config.output?.includeSpaces ?? []).join(", ") || "(none)"}`,
72
- `- files: ${outputFiles.join(", ")}`,
73
- ];
74
- await writeFile(join(outDir, "report.md"), `${lines.join("\n")}\n`, "utf8");
75
- };
76
- const runInit = async (flags) => {
77
- const targetPath = typeof flags.path === "string" ? flags.path : ".";
78
- const force = Boolean(flags.force);
79
- const outDir = resolve(targetPath);
80
- await ensureDir(outDir);
81
- const filePath = join(outDir, "palette.config.ts");
82
- if (!force && (await exists(filePath))) {
83
- throw new Error(`Config already exists at ${filePath}. Use --force to overwrite`);
84
- }
85
- const pkg = await readPackageJson();
86
- await writeFile(filePath, buildConfigTemplate(pkg.name), "utf8");
87
- console.log(`Created ${filePath}`);
88
- };
89
- const runBuild = async (flags) => {
90
- const configPath = typeof flags.config === "string" ? flags.config : "palette.config.ts";
91
- const outDir = resolve(typeof flags.outDir === "string" ? flags.outDir : "dist/palette");
92
- const report = Boolean(flags.report);
93
- const config = await loadConfig(configPath);
94
- validateConfig(config);
95
- const registry = tokenPresetMap[config.tokens.preset];
96
- validateTokenRegistry(registry);
97
- const theme = createTheme(config.theme);
98
- const tokens = toThemeTokens(registry);
99
- const css = exportThemeCss(theme, tokens, config.output).css;
100
- const json = exportThemeJson(theme, tokens, config.output);
101
- await ensureDir(outDir);
102
- const tokenNames = await writeTokensCodegen(outDir, registry);
103
- await writeFile(join(outDir, "tokens.css"), css, "utf8");
104
- await writeFile(join(outDir, "tokens.json"), `${JSON.stringify(json, null, 2)}\n`, "utf8");
105
- if (report) {
106
- await writeReport(outDir, config, tokenNames.length, [
107
- "tokens.css",
108
- "tokens.json",
109
- "tokens.ts",
110
- "tokens.d.ts",
111
- ]);
112
- }
113
- console.log(`Wrote ${basename(outDir)}/tokens.css, tokens.json, tokens.ts, tokens.d.ts`);
114
- };
115
- const main = async () => {
116
- try {
117
- const parsed = parseArgs(process.argv.slice(2));
118
- if (parsed.version) {
119
- await printVersion();
120
- return;
121
- }
122
- if (parsed.help || !parsed.command) {
123
- printHelp();
124
- return;
125
- }
126
- if (!COMMANDS.includes(parsed.command)) {
127
- throw new CliUsageError(`Unknown command: ${parsed.command}`);
128
- }
129
- if (parsed.command === "init") {
130
- await runInit(parsed.flags);
131
- return;
132
- }
133
- if (parsed.command === "build") {
134
- await runBuild(parsed.flags);
135
- return;
136
- }
137
- }
138
- catch (error) {
139
- const message = error instanceof Error ? error.message : String(error);
140
- console.error(message);
141
- if (error instanceof CliUsageError) {
142
- console.error("");
143
- printHelp();
144
- }
145
- process.exitCode = 1;
146
- }
147
- };
148
- void main();
@@ -1,2 +0,0 @@
1
- import type { SrgbColor } from "./types.js";
2
- export declare function computeApcaLc(fg: SrgbColor, bg: SrgbColor): number;
@@ -1,15 +0,0 @@
1
- import { APCAcontrast, sRGBtoY } from "apca-w3";
2
- const clamp01 = (value) => Math.min(1, Math.max(0, value));
3
- const toApcaInput = (color) => [
4
- clamp01(color.r) * 255,
5
- clamp01(color.g) * 255,
6
- clamp01(color.b) * 255,
7
- ];
8
- export function computeApcaLc(fg, bg) {
9
- const fgY = sRGBtoY(toApcaInput(fg));
10
- const bgY = sRGBtoY(toApcaInput(bg));
11
- const raw = APCAcontrast(fgY, bgY);
12
- const value = typeof raw === "number" ? raw : Number(raw);
13
- // Fail-soft to avoid propagating NaN into solver scoring.
14
- return Number.isFinite(value) ? value : 0;
15
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,16 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { computeApcaLc } from "./apca.js";
3
- describe("computeApcaLc", () => {
4
- it("reports high magnitude for black on white", () => {
5
- const lc = computeApcaLc({ r: 0, g: 0, b: 0 }, { r: 1, g: 1, b: 1 });
6
- expect(Math.abs(lc)).toBeGreaterThan(60);
7
- });
8
- it("reports high magnitude for white on black", () => {
9
- const lc = computeApcaLc({ r: 1, g: 1, b: 1 }, { r: 0, g: 0, b: 0 });
10
- expect(Math.abs(lc)).toBeGreaterThan(60);
11
- });
12
- it("does not return NaN", () => {
13
- const lc = computeApcaLc({ r: 0.2, g: 0.4, b: 0.6 }, { r: 0.1, g: 0.1, b: 0.1 });
14
- expect(Number.isNaN(lc)).toBe(false);
15
- });
16
- });
@@ -1,4 +0,0 @@
1
- export * from "./apca.js";
2
- export * from "./solver.js";
3
- export * from "./types.js";
4
- export * from "./wcag2.js";
@@ -1,4 +0,0 @@
1
- export * from "./apca.js";
2
- export * from "./solver.js";
3
- export * from "./types.js";
4
- export * from "./wcag2.js";
@@ -1,4 +0,0 @@
1
- import type { ContrastRequirement } from "../types/index.js";
2
- import type { ContrastCheckResult } from "./types.js";
3
- export declare const scoreApca: (value: number, target: number, min: number, max: number, hasMax: boolean) => number;
4
- export declare const scoreContrast: (result: ContrastCheckResult, req: ContrastRequirement) => number;
@@ -1,31 +0,0 @@
1
- export const scoreApca = (value, target, min, max, hasMax) => {
2
- if (Number.isNaN(value)) {
3
- return Number.NEGATIVE_INFINITY;
4
- }
5
- if (hasMax) {
6
- if (value >= min && value <= max) {
7
- return 1000 - Math.abs(value - target);
8
- }
9
- if (value < min) {
10
- return -(min - value);
11
- }
12
- return -(value - max);
13
- }
14
- if (value < min) {
15
- const distance = min - value;
16
- return -distance * 10 - Math.abs(value - target);
17
- }
18
- return -Math.abs(value - target);
19
- };
20
- export const scoreContrast = (result, req) => {
21
- if (!Number.isFinite(result.value)) {
22
- return Number.NEGATIVE_INFINITY;
23
- }
24
- if (req.model === "apca") {
25
- const min = req.minLc ?? req.targetLc;
26
- const hasMax = req.maxLc !== undefined;
27
- const max = req.maxLc ?? Number.POSITIVE_INFINITY;
28
- return scoreApca(result.value, req.targetLc, min, max, hasMax);
29
- }
30
- return result.value;
31
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,148 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { scoreApca, scoreContrast } from "./scoring.js";
3
- describe("scoreApca", () => {
4
- it("prefers values closer to target within range", () => {
5
- const target = 60;
6
- const min = 50;
7
- const max = 80;
8
- const hasMax = true;
9
- expect(scoreApca(60, target, min, max, hasMax)).toBeGreaterThan(scoreApca(80, target, min, max, hasMax));
10
- });
11
- it("penalizes values outside the range", () => {
12
- const target = 60;
13
- const min = 50;
14
- const max = 80;
15
- const hasMax = true;
16
- expect(scoreApca(40, target, min, max, hasMax)).toBeLessThan(0);
17
- expect(scoreApca(90, target, min, max, hasMax)).toBeLessThan(0);
18
- });
19
- it("penalizes below min and prefers target when no max", () => {
20
- const target = 60;
21
- const min = 50;
22
- const max = Number.POSITIVE_INFINITY;
23
- const hasMax = false;
24
- expect(scoreApca(40, target, min, max, hasMax)).toBeLessThan(scoreApca(55, target, min, max, hasMax));
25
- expect(scoreApca(60, target, min, max, hasMax)).toBeGreaterThan(scoreApca(70, target, min, max, hasMax));
26
- });
27
- it("returns NEGATIVE_INFINITY for NaN values", () => {
28
- const target = 60;
29
- const min = 50;
30
- const max = 80;
31
- const hasMax = true;
32
- expect(scoreApca(Number.NaN, target, min, max, hasMax)).toBe(Number.NEGATIVE_INFINITY);
33
- });
34
- });
35
- describe("scoreContrast", () => {
36
- describe("with APCA contrast model", () => {
37
- it("delegates to scoreApca with correct parameters when maxLc is defined", () => {
38
- const result = {
39
- model: "apca",
40
- target: 60,
41
- value: 65,
42
- pass: true,
43
- };
44
- const req = {
45
- model: "apca",
46
- targetLc: 60,
47
- minLc: 50,
48
- maxLc: 80,
49
- };
50
- const score = scoreContrast(result, req);
51
- const expectedScore = scoreApca(65, 60, 50, 80, true);
52
- expect(score).toBe(expectedScore);
53
- });
54
- it("delegates to scoreApca without max when maxLc is undefined", () => {
55
- const result = {
56
- model: "apca",
57
- target: 60,
58
- value: 65,
59
- pass: true,
60
- };
61
- const req = {
62
- model: "apca",
63
- targetLc: 60,
64
- minLc: 50,
65
- };
66
- const score = scoreContrast(result, req);
67
- const expectedScore = scoreApca(65, 60, 50, Number.POSITIVE_INFINITY, false);
68
- expect(score).toBe(expectedScore);
69
- });
70
- it("uses targetLc as min when minLc is undefined", () => {
71
- const result = {
72
- model: "apca",
73
- target: 60,
74
- value: 65,
75
- pass: true,
76
- };
77
- const req = {
78
- model: "apca",
79
- targetLc: 60,
80
- };
81
- const score = scoreContrast(result, req);
82
- const expectedScore = scoreApca(65, 60, 60, Number.POSITIVE_INFINITY, false);
83
- expect(score).toBe(expectedScore);
84
- });
85
- });
86
- describe("with WCAG2 contrast model", () => {
87
- it("returns the raw contrast value", () => {
88
- const result = {
89
- model: "wcag2",
90
- target: 4.5,
91
- value: 7.2,
92
- pass: true,
93
- };
94
- const req = {
95
- model: "wcag2",
96
- minRatio: 4.5,
97
- };
98
- const score = scoreContrast(result, req);
99
- expect(score).toBe(7.2);
100
- });
101
- });
102
- describe("with NaN or non-finite values", () => {
103
- it("returns NEGATIVE_INFINITY for NaN", () => {
104
- const result = {
105
- model: "apca",
106
- target: 60,
107
- value: Number.NaN,
108
- pass: false,
109
- };
110
- const req = {
111
- model: "apca",
112
- targetLc: 60,
113
- minLc: 50,
114
- };
115
- const score = scoreContrast(result, req);
116
- expect(score).toBe(Number.NEGATIVE_INFINITY);
117
- });
118
- it("returns NEGATIVE_INFINITY for positive infinity", () => {
119
- const result = {
120
- model: "apca",
121
- target: 60,
122
- value: Number.POSITIVE_INFINITY,
123
- pass: false,
124
- };
125
- const req = {
126
- model: "apca",
127
- targetLc: 60,
128
- minLc: 50,
129
- };
130
- const score = scoreContrast(result, req);
131
- expect(score).toBe(Number.NEGATIVE_INFINITY);
132
- });
133
- it("returns NEGATIVE_INFINITY for negative infinity", () => {
134
- const result = {
135
- model: "wcag2",
136
- target: 4.5,
137
- value: Number.NEGATIVE_INFINITY,
138
- pass: false,
139
- };
140
- const req = {
141
- model: "wcag2",
142
- minRatio: 4.5,
143
- };
144
- const score = scoreContrast(result, req);
145
- expect(score).toBe(Number.NEGATIVE_INFINITY);
146
- });
147
- });
148
- });
@@ -1,13 +0,0 @@
1
- import type { OkLchColor } from "../engine/generateScale.js";
2
- import type { CurvePresetName } from "../presets/index.js";
3
- import type { ContrastRequirement, SurfaceIntent } from "../types/index.js";
4
- import type { ContrastCheckResult, SolveOptions } from "./types.js";
5
- export declare function solveContrast(fg: OkLchColor, bg: OkLchColor | undefined, req: ContrastRequirement, ctx: {
6
- preset?: CurvePresetName;
7
- surface: SurfaceIntent;
8
- context: "light" | "dark";
9
- }, opts?: SolveOptions): {
10
- color: OkLchColor;
11
- result: ContrastCheckResult;
12
- iterations: number;
13
- };
@@ -1,170 +0,0 @@
1
- import { converter } from "culori";
2
- import { getSurfaceRange } from "../operators/utils.js";
3
- import { computeApcaLc } from "./apca.js";
4
- import { scoreContrast } from "./scoring.js";
5
- import { contrastRatio } from "./wcag2.js";
6
- const toSrgb = converter("rgb");
7
- const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
8
- const clampOkLch = (color, cMax) => ({
9
- l: clamp(color.l, 0, 100),
10
- c: clamp(color.c, 0, cMax),
11
- h: color.h,
12
- alpha: color.alpha,
13
- });
14
- const clampOkLchLoose = (color) => ({
15
- l: clamp(color.l, 0, 100),
16
- c: Math.max(0, color.c),
17
- h: color.h,
18
- alpha: color.alpha,
19
- });
20
- const toSrgbColor = (color) => {
21
- const rgb = toSrgb({ mode: "oklch", l: clamp(color.l, 0, 100) / 100, c: color.c, h: color.h });
22
- if (!rgb) {
23
- return null;
24
- }
25
- const r = typeof rgb.r === "number" && Number.isFinite(rgb.r) ? clamp(rgb.r, 0, 1) : 0;
26
- const g = typeof rgb.g === "number" && Number.isFinite(rgb.g) ? clamp(rgb.g, 0, 1) : 0;
27
- const b = typeof rgb.b === "number" && Number.isFinite(rgb.b) ? clamp(rgb.b, 0, 1) : 0;
28
- return { r, g, b };
29
- };
30
- const getTarget = (req) => {
31
- if (req.model === "apca") {
32
- return req.targetLc;
33
- }
34
- if (req.model === "wcag2") {
35
- return req.minRatio;
36
- }
37
- return 0;
38
- };
39
- const checkContrast = (fg, bg, req, epsilon) => {
40
- if (req.model === "none") {
41
- return { model: "none", target: 0, value: 0, pass: true };
42
- }
43
- const fgSrgb = toSrgbColor(fg);
44
- const bgSrgb = toSrgbColor(bg);
45
- const target = getTarget(req);
46
- if (!fgSrgb || !bgSrgb) {
47
- return { model: req.model, target, value: Number.NaN, pass: false };
48
- }
49
- if (req.model === "apca") {
50
- const value = Math.abs(computeApcaLc(fgSrgb, bgSrgb));
51
- const minTarget = req.minLc ?? req.targetLc;
52
- const maxTarget = req.maxLc ?? Number.POSITIVE_INFINITY;
53
- const pass = Number.isFinite(value) && value >= minTarget - epsilon && value <= maxTarget + epsilon;
54
- return { model: "apca", target, value, pass };
55
- }
56
- const value = contrastRatio(fgSrgb, bgSrgb);
57
- const pass = Number.isFinite(value) && value + epsilon >= target;
58
- return { model: "wcag2", target, value, pass };
59
- };
60
- const pickBetter = (current, candidate, req) => {
61
- const currentScore = scoreContrast(current.result, req);
62
- const candidateScore = scoreContrast(candidate.result, req);
63
- return candidateScore > currentScore ? candidate : current;
64
- };
65
- export function solveContrast(fg, bg, req, ctx, opts) {
66
- const options = {
67
- strict: false,
68
- maxIterations: 24,
69
- epsilon: 0.01,
70
- ...opts,
71
- };
72
- if (req.model === "none") {
73
- return {
74
- color: fg,
75
- result: { model: "none", target: 0, value: 0, pass: true },
76
- iterations: 0,
77
- };
78
- }
79
- if (!bg) {
80
- if (options.strict) {
81
- throw new Error("Contrast solver requires background");
82
- }
83
- return {
84
- color: fg,
85
- result: { model: req.model, target: getTarget(req), value: Number.NaN, pass: false },
86
- iterations: 0,
87
- };
88
- }
89
- const range = getSurfaceRange(ctx.preset, ctx.surface, ctx.context);
90
- const clamped = clampOkLch(fg, range.cMax);
91
- const background = clampOkLchLoose(bg);
92
- let iterations = 0;
93
- const evaluate = (color) => {
94
- const result = checkContrast(color, background, req, options.epsilon);
95
- iterations += 1;
96
- return result;
97
- };
98
- let best = { color: clamped, result: evaluate(clamped) };
99
- if (best.result.pass) {
100
- return { ...best, iterations };
101
- }
102
- const lMin = 0;
103
- const lMax = 100;
104
- const sampleT = 0.25;
105
- const sampleDown = clamp(clamped.l + (lMin - clamped.l) * sampleT, lMin, lMax);
106
- const sampleUp = clamp(clamped.l + (lMax - clamped.l) * sampleT, lMin, lMax);
107
- let preferredBound = lMin;
108
- if (iterations < options.maxIterations) {
109
- const downCandidate = {
110
- color: { ...clamped, l: sampleDown },
111
- result: evaluate({ ...clamped, l: sampleDown }),
112
- };
113
- best = pickBetter(best, downCandidate, req);
114
- if (best.result.pass) {
115
- return { ...best, iterations };
116
- }
117
- if (iterations < options.maxIterations) {
118
- const upCandidate = {
119
- color: { ...clamped, l: sampleUp },
120
- result: evaluate({ ...clamped, l: sampleUp }),
121
- };
122
- best = pickBetter(best, upCandidate, req);
123
- if (best.result.pass) {
124
- return { ...best, iterations };
125
- }
126
- preferredBound =
127
- scoreContrast(upCandidate.result, req) > scoreContrast(downCandidate.result, req)
128
- ? lMax
129
- : lMin;
130
- }
131
- }
132
- const remainingAfterSamples = Math.max(0, options.maxIterations - iterations);
133
- const lSteps = Math.max(4, remainingAfterSamples);
134
- for (let step = 1; step <= lSteps && iterations < options.maxIterations; step += 1) {
135
- const t = step / lSteps;
136
- const l = clamp(clamped.l + (preferredBound - clamped.l) * t, lMin, lMax);
137
- const candidateColor = { ...clamped, l };
138
- const result = evaluate(candidateColor);
139
- const candidate = { color: candidateColor, result };
140
- best = pickBetter(best, candidate, req);
141
- if (result.pass) {
142
- return { color: candidateColor, result, iterations };
143
- }
144
- }
145
- let current = { ...best.color };
146
- while (iterations < options.maxIterations && current.c > 0) {
147
- const nextC = clamp(current.c * 0.9, 0, range.cMax);
148
- current = { ...current, c: nextC };
149
- const result = evaluate(current);
150
- best = pickBetter(best, { color: current, result }, req);
151
- if (result.pass) {
152
- return { color: current, result, iterations };
153
- }
154
- const sweepSteps = Math.min(3, options.maxIterations - iterations);
155
- for (let step = 1; step <= sweepSteps && iterations < options.maxIterations; step += 1) {
156
- const t = step / sweepSteps;
157
- const l = clamp(current.l + (preferredBound - current.l) * t, lMin, lMax);
158
- const candidateColor = { ...current, l };
159
- const candidateResult = evaluate(candidateColor);
160
- best = pickBetter(best, { color: candidateColor, result: candidateResult }, req);
161
- if (candidateResult.pass) {
162
- return { color: candidateColor, result: candidateResult, iterations };
163
- }
164
- }
165
- }
166
- if (options.strict) {
167
- throw new Error(`Contrast solver failed (${best.result.model}) target=${best.result.target} value=${best.result.value} iterations=${iterations}`);
168
- }
169
- return { color: best.color, result: best.result, iterations };
170
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,75 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { parseColor } from "../utils/parseColor.js";
3
- import { solveContrast } from "./solver.js";
4
- const toOkLch = (hex) => {
5
- const parsed = parseColor(hex);
6
- return {
7
- l: parsed.okLch.channels[0] * 100,
8
- c: parsed.okLch.channels[1],
9
- h: parsed.okLch.channels[2],
10
- alpha: parsed.okLch.alpha,
11
- };
12
- };
13
- describe("solveContrast", () => {
14
- it("raises contrast on light backgrounds", () => {
15
- const fg = toOkLch("#777777");
16
- const bg = toOkLch("#ffffff");
17
- const result = solveContrast(fg, bg, { model: "wcag2", minRatio: 4.5 }, { surface: "surface", context: "light" });
18
- expect(result.result.pass).toBe(true);
19
- expect(result.result.value).toBeGreaterThanOrEqual(4.5);
20
- expect(result.color.l).toBeGreaterThanOrEqual(0);
21
- expect(result.color.l).toBeLessThanOrEqual(100);
22
- expect(result.color.c).toBeGreaterThanOrEqual(0);
23
- });
24
- it("returns original color when contrast model is none", () => {
25
- const fg = toOkLch("#777777");
26
- const bg = toOkLch("#ffffff");
27
- const result = solveContrast(fg, bg, { model: "none" }, { surface: "surface", context: "light" });
28
- expect(result.result.pass).toBe(true);
29
- expect(result.iterations).toBe(0);
30
- expect(result.color).toEqual(fg);
31
- });
32
- it("skips when background is missing and strict is false", () => {
33
- const fg = toOkLch("#777777");
34
- const result = solveContrast(fg, undefined, { model: "wcag2", minRatio: 4.5 }, { surface: "surface", context: "light" }, { strict: false });
35
- expect(result.result.pass).toBe(false);
36
- expect(Number.isNaN(result.result.value)).toBe(true);
37
- });
38
- it("throws when background is missing and strict is true", () => {
39
- const fg = toOkLch("#777777");
40
- expect(() => solveContrast(fg, undefined, { model: "wcag2", minRatio: 4.5 }, { surface: "surface", context: "light" }, { strict: true })).toThrowError(/requires background/i);
41
- });
42
- it("raises contrast on dark backgrounds", () => {
43
- const fg = toOkLch("#777777");
44
- const bg = toOkLch("#111111");
45
- const result = solveContrast(fg, bg, { model: "wcag2", minRatio: 4.5 }, { surface: "surface", context: "dark" });
46
- expect(result.result.pass).toBe(true);
47
- expect(result.result.value).toBeGreaterThanOrEqual(4.5);
48
- });
49
- it("keeps hue stable", () => {
50
- const fg = toOkLch("#3366ff");
51
- const bg = toOkLch("#ffffff");
52
- const result = solveContrast(fg, bg, { model: "wcag2", minRatio: 4.5 }, { surface: "surface", context: "light" });
53
- expect(result.color.h).toBeCloseTo(fg.h, 6);
54
- });
55
- it("throws in strict mode when target is unattainable", () => {
56
- const fg = toOkLch("#ffffff");
57
- const bg = toOkLch("#ffffff");
58
- expect(() => solveContrast(fg, bg, { model: "wcag2", minRatio: 30 }, { surface: "surface", context: "light" }, { strict: true })).toThrowError(/contrast solver failed/i);
59
- });
60
- it("prefers APCA values closer to target within the allowed range", () => {
61
- const fg = toOkLch("#777777");
62
- const bg = toOkLch("#ffffff");
63
- const minLc = 50;
64
- const maxLc = 80;
65
- const targetLc = 60;
66
- const result = solveContrast(fg, bg, { model: "apca", targetLc, minLc, maxLc }, { surface: "surface", context: "light" });
67
- const value = result.result.value;
68
- const targetDistance = Math.abs(value - targetLc);
69
- const maxDistance = Math.abs(maxLc - targetLc);
70
- expect(result.result.pass).toBe(true);
71
- expect(value).toBeGreaterThanOrEqual(minLc);
72
- expect(value).toBeLessThanOrEqual(maxLc);
73
- expect(targetDistance).toBeLessThanOrEqual(maxDistance);
74
- });
75
- });
@@ -1,17 +0,0 @@
1
- export type ContrastModel = "apca" | "wcag2" | "none";
2
- export type ContrastCheckResult = {
3
- model: ContrastModel;
4
- target: number;
5
- value: number;
6
- pass: boolean;
7
- };
8
- export type SolveOptions = {
9
- strict?: boolean;
10
- maxIterations?: number;
11
- epsilon?: number;
12
- };
13
- export type SrgbColor = {
14
- r: number;
15
- g: number;
16
- b: number;
17
- };
@@ -1 +0,0 @@
1
- export {};