@clhaas/palette-kit 0.1.7 → 0.3.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 (293) hide show
  1. package/.codex/skills/color-pipeline-implementer/SKILL.md +23 -0
  2. package/.codex/skills/commit-message-crafter/SKILL.md +63 -0
  3. package/.codex/skills/commit-message-crafter/references/benchmarks.md +20 -0
  4. package/.codex/skills/contrast-solver-helper/SKILL.md +20 -0
  5. package/.codex/skills/exporters-builder/SKILL.md +20 -0
  6. package/.codex/skills/markdownlint-writer/SKILL.md +32 -0
  7. package/.codex/skills/phase-implementation-runbook/SKILL.md +92 -0
  8. package/.codex/skills/type-contract-auditor/SKILL.md +21 -0
  9. package/.github/skills/review-guide/SKILL.md +23 -0
  10. package/.github/skills/review-guide/references/review-guide-v0.3.md +629 -0
  11. package/.markdownlint.json +4 -0
  12. package/AGENTS.md +16 -0
  13. package/CHANGELOG.md +34 -0
  14. package/README.md +79 -169
  15. package/biome.json +43 -0
  16. package/dist/cli/args.d.ts +12 -0
  17. package/dist/cli/args.js +56 -0
  18. package/dist/cli/args.test.js +22 -0
  19. package/dist/cli/codegen/__snapshots__/tokens.test.js.snap +87 -0
  20. package/dist/cli/codegen/tokens.d.ts +12 -0
  21. package/dist/cli/codegen/tokens.js +139 -0
  22. package/dist/cli/codegen/tokens.test.d.ts +1 -0
  23. package/dist/cli/codegen/tokens.test.js +51 -0
  24. package/dist/cli/config.d.ts +40 -0
  25. package/dist/cli/config.js +34 -0
  26. package/dist/cli/validate.d.ts +2 -0
  27. package/dist/cli/validate.js +33 -0
  28. package/dist/cli/validate.test.d.ts +1 -0
  29. package/dist/cli/validate.test.js +40 -0
  30. package/dist/cli.js +138 -140
  31. package/dist/contrast/apca.d.ts +2 -2
  32. package/dist/contrast/apca.js +14 -4
  33. package/dist/contrast/apca.test.d.ts +1 -0
  34. package/dist/contrast/apca.test.js +16 -0
  35. package/dist/contrast/index.d.ts +4 -0
  36. package/dist/contrast/index.js +4 -0
  37. package/dist/contrast/scoring.d.ts +4 -0
  38. package/dist/contrast/scoring.js +31 -0
  39. package/dist/contrast/scoring.test.d.ts +1 -0
  40. package/dist/contrast/scoring.test.js +148 -0
  41. package/dist/contrast/solver.d.ts +13 -0
  42. package/dist/contrast/solver.js +170 -0
  43. package/dist/contrast/solver.test.d.ts +1 -0
  44. package/dist/contrast/solver.test.js +75 -0
  45. package/dist/contrast/types.d.ts +17 -0
  46. package/dist/contrast/types.js +1 -0
  47. package/dist/contrast/utils.d.ts +4 -0
  48. package/dist/contrast/utils.js +18 -0
  49. package/dist/contrast/wcag2.d.ts +3 -0
  50. package/dist/contrast/wcag2.js +19 -0
  51. package/dist/contrast/wcag2.test.d.ts +1 -0
  52. package/dist/contrast/wcag2.test.js +17 -0
  53. package/dist/core/createTheme.d.ts +35 -0
  54. package/dist/core/createTheme.js +24 -0
  55. package/dist/core/dx-helpers.test.d.ts +1 -0
  56. package/dist/core/dx-helpers.test.js +61 -0
  57. package/dist/core/index.d.ts +2 -0
  58. package/dist/core/index.js +2 -0
  59. package/dist/core/onSolid.test.d.ts +1 -0
  60. package/dist/core/onSolid.test.js +118 -0
  61. package/dist/core/qa.v1.test.d.ts +1 -0
  62. package/dist/core/qa.v1.test.js +112 -0
  63. package/dist/core/resolve.d.ts +3 -0
  64. package/dist/core/resolve.js +8 -0
  65. package/dist/core/resolve.test.d.ts +1 -0
  66. package/dist/core/resolve.test.js +89 -0
  67. package/dist/core/resolveMany.d.ts +8 -0
  68. package/dist/core/resolveMany.js +17 -0
  69. package/dist/core/tokenRegistry.d.ts +23 -0
  70. package/dist/core/tokenRegistry.js +83 -0
  71. package/dist/core/tokenRegistry.test.d.ts +1 -0
  72. package/dist/core/tokenRegistry.test.js +133 -0
  73. package/dist/engine/applyOperators.d.ts +3 -0
  74. package/dist/engine/applyOperators.js +23 -0
  75. package/dist/engine/context.d.ts +4 -0
  76. package/dist/engine/context.js +1 -0
  77. package/dist/engine/gamut.d.ts +13 -0
  78. package/dist/engine/gamut.js +101 -0
  79. package/dist/engine/gamut.test.d.ts +1 -0
  80. package/dist/engine/gamut.test.js +23 -0
  81. package/dist/engine/generateScale.d.ts +15 -0
  82. package/dist/engine/generateScale.js +29 -0
  83. package/dist/engine/generateScale.test.d.ts +1 -0
  84. package/dist/engine/generateScale.test.js +32 -0
  85. package/dist/engine/index.d.ts +8 -0
  86. package/dist/engine/index.js +4 -0
  87. package/dist/engine/normalize.d.ts +43 -0
  88. package/dist/engine/normalize.js +403 -0
  89. package/dist/engine/normalize.test.d.ts +1 -0
  90. package/dist/engine/normalize.test.js +136 -0
  91. package/dist/engine/onSolid.d.ts +3 -0
  92. package/dist/engine/onSolid.js +110 -0
  93. package/dist/engine/resolveBaseColor.d.ts +25 -0
  94. package/dist/engine/resolveBaseColor.js +127 -0
  95. package/dist/engine/resolveBaseColor.test.d.ts +1 -0
  96. package/dist/engine/resolveBaseColor.test.js +97 -0
  97. package/dist/export/__snapshots__/exportTheme.test.js.snap +74 -0
  98. package/dist/export/exportTheme.d.ts +47 -0
  99. package/dist/export/exportTheme.js +170 -0
  100. package/dist/export/exportTheme.test.d.ts +1 -0
  101. package/dist/export/exportTheme.test.js +118 -0
  102. package/dist/export/index.d.ts +1 -0
  103. package/dist/export/index.js +1 -0
  104. package/dist/export/serializeColor.d.ts +1 -0
  105. package/dist/export/serializeColor.js +1 -0
  106. package/dist/export/serializeColor.test.d.ts +1 -0
  107. package/dist/export/serializeColor.test.js +54 -0
  108. package/dist/export.d.ts +1 -0
  109. package/dist/export.js +1 -0
  110. package/dist/index.d.ts +3 -20
  111. package/dist/index.js +2 -17
  112. package/dist/operators/emphasis.d.ts +3 -0
  113. package/dist/operators/emphasis.js +113 -0
  114. package/dist/operators/emphasis.test.d.ts +1 -0
  115. package/dist/operators/emphasis.test.js +69 -0
  116. package/dist/operators/index.d.ts +3 -0
  117. package/dist/operators/index.js +2 -0
  118. package/dist/operators/state.d.ts +3 -0
  119. package/dist/operators/state.js +102 -0
  120. package/dist/operators/state.test.d.ts +1 -0
  121. package/dist/operators/state.test.js +48 -0
  122. package/dist/operators/types.d.ts +13 -0
  123. package/dist/operators/types.js +1 -0
  124. package/dist/operators/utils.d.ts +16 -0
  125. package/dist/operators/utils.js +23 -0
  126. package/dist/presets/curves.d.ts +28 -0
  127. package/dist/presets/curves.js +145 -0
  128. package/dist/presets/index.d.ts +2 -0
  129. package/dist/presets/index.js +1 -0
  130. package/dist/presets/tokens/index.d.ts +3 -0
  131. package/dist/presets/tokens/index.js +3 -0
  132. package/dist/presets/tokens/minimal-ui.d.ts +6 -0
  133. package/dist/presets/tokens/minimal-ui.js +53 -0
  134. package/dist/presets/tokens/modern-ui.d.ts +5 -0
  135. package/dist/presets/tokens/modern-ui.js +83 -0
  136. package/dist/presets/tokens/presets.test.d.ts +1 -0
  137. package/dist/presets/tokens/presets.test.js +31 -0
  138. package/dist/presets/tokens/radixLike-ui.d.ts +6 -0
  139. package/dist/presets/tokens/radixLike-ui.js +77 -0
  140. package/dist/serialize/index.d.ts +1 -0
  141. package/dist/serialize/index.js +1 -0
  142. package/dist/serialize/normalizeOutput.d.ts +6 -0
  143. package/dist/serialize/normalizeOutput.js +45 -0
  144. package/dist/serialize/serializeColor.d.ts +21 -0
  145. package/dist/serialize/serializeColor.js +178 -0
  146. package/dist/serialize/serializeResolved.test.d.ts +1 -0
  147. package/dist/serialize/serializeResolved.test.js +45 -0
  148. package/dist/serialize.d.ts +1 -0
  149. package/dist/serialize.js +1 -0
  150. package/dist/types/index.d.ts +187 -0
  151. package/dist/types/index.js +1 -0
  152. package/dist/utils/clamp.d.ts +1 -0
  153. package/dist/utils/clamp.js +1 -0
  154. package/dist/utils/index.d.ts +1 -0
  155. package/dist/utils/index.js +1 -0
  156. package/dist/utils/lerp.d.ts +1 -0
  157. package/dist/utils/lerp.js +1 -0
  158. package/dist/utils/parseColor.d.ts +6 -0
  159. package/dist/utils/parseColor.js +67 -0
  160. package/dist/utils/parseColor.test.d.ts +1 -0
  161. package/dist/utils/parseColor.test.js +51 -0
  162. package/dist/utils/smoothstep.d.ts +1 -0
  163. package/dist/utils/smoothstep.js +5 -0
  164. package/package.json +19 -12
  165. package/planning/phase-10-review.md +550 -0
  166. package/planning/phase-7-review.md +411 -0
  167. package/planning/phase-8-review.md +669 -0
  168. package/planning/phase-9-review.md +564 -0
  169. package/planning/roadmap-v0.3.md +284 -0
  170. package/planning/spec-serializer-v0.3.md +324 -0
  171. package/planning/spec-v0.3.md +305 -0
  172. package/src/cli/args.test.ts +28 -0
  173. package/src/cli/args.ts +66 -0
  174. package/src/cli/codegen/__snapshots__/tokens.test.ts.snap +87 -0
  175. package/src/cli/codegen/tokens.test.ts +61 -0
  176. package/src/cli/codegen/tokens.ts +191 -0
  177. package/src/cli/config.ts +71 -0
  178. package/src/cli/validate.test.ts +49 -0
  179. package/src/cli/validate.ts +38 -0
  180. package/src/cli.ts +183 -0
  181. package/src/contrast/apca.test.ts +20 -0
  182. package/src/contrast/apca.ts +26 -0
  183. package/src/contrast/index.ts +4 -0
  184. package/src/contrast/scoring.test.ts +188 -0
  185. package/src/contrast/scoring.ts +48 -0
  186. package/src/contrast/solver.test.ts +147 -0
  187. package/src/contrast/solver.ts +235 -0
  188. package/src/contrast/types.ts +20 -0
  189. package/src/contrast/utils.ts +28 -0
  190. package/src/contrast/wcag2.test.ts +21 -0
  191. package/src/contrast/wcag2.ts +24 -0
  192. package/src/core/createTheme.ts +78 -0
  193. package/src/core/dx-helpers.test.ts +82 -0
  194. package/src/core/index.ts +7 -0
  195. package/src/core/onSolid.test.ts +146 -0
  196. package/src/core/qa.v1.test.ts +149 -0
  197. package/src/core/resolve.test.ts +99 -0
  198. package/src/core/resolve.ts +11 -0
  199. package/src/core/resolveMany.ts +22 -0
  200. package/src/core/tokenRegistry.test.ts +153 -0
  201. package/src/core/tokenRegistry.ts +114 -0
  202. package/src/engine/applyOperators.ts +32 -0
  203. package/src/engine/context.ts +8 -0
  204. package/src/engine/gamut.test.ts +30 -0
  205. package/src/engine/gamut.ts +144 -0
  206. package/src/engine/generateScale.test.ts +46 -0
  207. package/src/engine/generateScale.ts +48 -0
  208. package/src/engine/index.ts +8 -0
  209. package/src/engine/normalize.test.ts +222 -0
  210. package/src/engine/normalize.ts +550 -0
  211. package/src/engine/onSolid.ts +178 -0
  212. package/src/engine/resolveBaseColor.test.ts +117 -0
  213. package/src/engine/resolveBaseColor.ts +203 -0
  214. package/src/export/__snapshots__/exportTheme.test.ts.snap +74 -0
  215. package/src/export/exportTheme.test.ts +144 -0
  216. package/src/export/exportTheme.ts +251 -0
  217. package/src/export/index.ts +1 -0
  218. package/src/export/serializeColor.test.ts +73 -0
  219. package/src/export/serializeColor.ts +1 -0
  220. package/src/export.ts +1 -0
  221. package/src/index.ts +3 -0
  222. package/src/operators/emphasis.test.ts +85 -0
  223. package/src/operators/emphasis.ts +132 -0
  224. package/src/operators/index.ts +3 -0
  225. package/src/operators/state.test.ts +66 -0
  226. package/src/operators/state.ts +122 -0
  227. package/src/operators/types.ts +14 -0
  228. package/src/operators/utils.ts +44 -0
  229. package/src/presets/curves.ts +168 -0
  230. package/src/presets/index.ts +2 -0
  231. package/src/presets/tokens/index.ts +3 -0
  232. package/src/presets/tokens/minimal-ui.ts +55 -0
  233. package/src/presets/tokens/modern-ui.ts +85 -0
  234. package/src/presets/tokens/presets.test.ts +46 -0
  235. package/src/presets/tokens/radixLike-ui.ts +79 -0
  236. package/src/serialize/index.ts +1 -0
  237. package/src/serialize/normalizeOutput.ts +63 -0
  238. package/src/serialize/serializeColor.ts +260 -0
  239. package/src/serialize/serializeResolved.test.ts +57 -0
  240. package/src/serialize.ts +1 -0
  241. package/src/types/index.ts +207 -0
  242. package/src/utils/clamp.ts +2 -0
  243. package/src/utils/index.ts +1 -0
  244. package/src/utils/lerp.ts +1 -0
  245. package/src/utils/parseColor.test.ts +66 -0
  246. package/src/utils/parseColor.ts +87 -0
  247. package/src/utils/smoothstep.ts +6 -0
  248. package/tsconfig.build.json +11 -0
  249. package/tsconfig.json +15 -0
  250. package/dist/alpha/generateAlphaScale.d.ts +0 -5
  251. package/dist/alpha/generateAlphaScale.js +0 -34
  252. package/dist/contrast/onSolid.d.ts +0 -6
  253. package/dist/contrast/onSolid.js +0 -28
  254. package/dist/contrast/solveText.d.ts +0 -2
  255. package/dist/contrast/solveText.js +0 -31
  256. package/dist/createTheme.d.ts +0 -38
  257. package/dist/createTheme.js +0 -137
  258. package/dist/data/radixSeeds.d.ts +0 -3
  259. package/dist/data/radixSeeds.js +0 -34
  260. package/dist/diagnostics/analyzeScale.d.ts +0 -2
  261. package/dist/diagnostics/analyzeScale.js +0 -7
  262. package/dist/diagnostics/analyzeTheme.d.ts +0 -2
  263. package/dist/diagnostics/analyzeTheme.js +0 -35
  264. package/dist/diagnostics/warnings.d.ts +0 -2
  265. package/dist/diagnostics/warnings.js +0 -20
  266. package/dist/engine/curves.d.ts +0 -9
  267. package/dist/engine/curves.js +0 -48
  268. package/dist/engine/oklch.d.ts +0 -8
  269. package/dist/engine/oklch.js +0 -40
  270. package/dist/engine/templates.d.ts +0 -14
  271. package/dist/engine/templates.js +0 -45
  272. package/dist/exporters/selectColorMode.d.ts +0 -2
  273. package/dist/exporters/selectColorMode.js +0 -19
  274. package/dist/exporters/toCssVars.d.ts +0 -13
  275. package/dist/exporters/toCssVars.js +0 -108
  276. package/dist/exporters/toJson.d.ts +0 -3
  277. package/dist/exporters/toJson.js +0 -25
  278. package/dist/exporters/toReactNative.d.ts +0 -54
  279. package/dist/exporters/toReactNative.js +0 -33
  280. package/dist/exporters/toTailwind.d.ts +0 -17
  281. package/dist/exporters/toTailwind.js +0 -111
  282. package/dist/exporters/toTs.d.ts +0 -3
  283. package/dist/exporters/toTs.js +0 -43
  284. package/dist/generateScale.d.ts +0 -48
  285. package/dist/generateScale.js +0 -274
  286. package/dist/overlays/generateOverlayScale.d.ts +0 -2
  287. package/dist/overlays/generateOverlayScale.js +0 -34
  288. package/dist/text/generateTextScale.d.ts +0 -8
  289. package/dist/text/generateTextScale.js +0 -18
  290. package/dist/tokens/presetRadixLikeUi.d.ts +0 -5
  291. package/dist/tokens/presetRadixLikeUi.js +0 -55
  292. package/dist/types.d.ts +0 -69
  293. /package/dist/{types.js → cli/args.test.d.ts} +0 -0
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { generateTokenArtifacts } from "./tokens.js";
3
+ describe("codegen tokens", () => {
4
+ const registry = {
5
+ tokens: {
6
+ "bg.app": {
7
+ name: "bg.app",
8
+ description: "App background.",
9
+ category: "bg",
10
+ query: { role: "bg.app", usage: "bg", surface: "app" },
11
+ },
12
+ "text.primary": {
13
+ name: "text.primary",
14
+ description: "Primary text on standard surfaces.",
15
+ category: "text",
16
+ query: { role: "text.primary", usage: "text", surface: "surface" },
17
+ states: { hover: true },
18
+ },
19
+ "border.default": {
20
+ name: "border.default",
21
+ description: "Default border color.",
22
+ category: "border",
23
+ query: { role: "border.default", usage: "border", surface: "surface" },
24
+ },
25
+ },
26
+ };
27
+ it("generates navigable tokens.ts and tokens.d.ts", () => {
28
+ const { tokensTs, tokensDts } = generateTokenArtifacts(registry);
29
+ expect(tokensTs).toContain('export const tokens = {');
30
+ expect(tokensTs).toContain('bg: {');
31
+ expect(tokensTs).toContain('app: "bg.app"');
32
+ expect(tokensTs).toContain('text: {');
33
+ expect(tokensTs).toContain('primary: "text.primary"');
34
+ expect(tokensTs).toMatchSnapshot();
35
+ expect(tokensDts).toMatchSnapshot();
36
+ });
37
+ it("is deterministic regardless of registry object order", () => {
38
+ const shuffled = {
39
+ tokens: {
40
+ "text.primary": registry.tokens["text.primary"],
41
+ "bg.app": registry.tokens["bg.app"],
42
+ "border.default": registry.tokens["border.default"],
43
+ },
44
+ };
45
+ const a = generateTokenArtifacts(registry);
46
+ const b = generateTokenArtifacts(shuffled);
47
+ expect(b.tokenNames).toEqual(a.tokenNames);
48
+ expect(b.tokensTs).toBe(a.tokensTs);
49
+ expect(b.tokensDts).toBe(a.tokensDts);
50
+ });
51
+ });
@@ -0,0 +1,40 @@
1
+ import type { ThemeConfig } from "../engine/resolveBaseColor.js";
2
+ import type { OutputOptions } from "../types/index.js";
3
+ /**
4
+ * Official token preset names supported by Palette Kit tooling.
5
+ *
6
+ * These map to `src/presets/tokens/*` registries.
7
+ */
8
+ export type TokenPresetName = "minimal-ui" | "radixLike-ui" | "modern-ui";
9
+ /**
10
+ * Configuration contract for `palette-kit build`.
11
+ *
12
+ * This file is consumed by the CLI and is intended to be used from
13
+ * `palette.config.ts` created by `palette-kit init`.
14
+ */
15
+ export type PaletteConfig = {
16
+ /**
17
+ * Theme inputs for palette generation (seed colors + optional variants).
18
+ */
19
+ theme: ThemeConfig;
20
+ /**
21
+ * Token preset selection for exporters/CLI.
22
+ */
23
+ tokens: {
24
+ preset: TokenPresetName;
25
+ };
26
+ /**
27
+ * Exporter options (formatting, gamut mapping, precision).
28
+ */
29
+ output?: OutputOptions;
30
+ };
31
+ /**
32
+ * Build a `palette.config.ts` template for `palette-kit init`.
33
+ *
34
+ * @param packageName - Import path used for `PaletteConfig` typing (usually the package name).
35
+ */
36
+ export declare const buildConfigTemplate: (packageName: string) => string;
37
+ /**
38
+ * Runtime guard for token preset strings coming from user config.
39
+ */
40
+ export declare const isTokenPresetName: (value: string) => value is TokenPresetName;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Build a `palette.config.ts` template for `palette-kit init`.
3
+ *
4
+ * @param packageName - Import path used for `PaletteConfig` typing (usually the package name).
5
+ */
6
+ export const buildConfigTemplate = (packageName) => `import type { PaletteConfig } from "${packageName}";
7
+
8
+ /**
9
+ * Palette Kit configuration.
10
+ * Update seeds, preset, and output options to match your design system.
11
+ */
12
+ const config: PaletteConfig = {
13
+ theme: {
14
+ seeds: {
15
+ light: { neutral: "#8B8D98", accent: "#3D63DD" },
16
+ dark: { neutral: "#8B8D98", accent: "#3D63DD" },
17
+ },
18
+ preset: "modern",
19
+ },
20
+ tokens: {
21
+ preset: "modern-ui",
22
+ },
23
+ output: {
24
+ preferSpace: "oklch",
25
+ includeSpaces: ["srgb", "p3"],
26
+ },
27
+ };
28
+
29
+ export default config;
30
+ `;
31
+ /**
32
+ * Runtime guard for token preset strings coming from user config.
33
+ */
34
+ export const isTokenPresetName = (value) => value === "minimal-ui" || value === "radixLike-ui" || value === "modern-ui";
@@ -0,0 +1,2 @@
1
+ import { type PaletteConfig } from "./config.js";
2
+ export declare const validateConfig: (config: PaletteConfig) => void;
@@ -0,0 +1,33 @@
1
+ import { isTokenPresetName } from "./config.js";
2
+ const validateSeed = (context, value) => {
3
+ if (!value || typeof value !== "object") {
4
+ throw new Error(`Config.theme.seeds.${context} must be an object`);
5
+ }
6
+ const seed = value;
7
+ if (typeof seed.neutral !== "string") {
8
+ throw new Error(`Config.theme.seeds.${context}.neutral must be a string color`);
9
+ }
10
+ if (typeof seed.accent !== "string") {
11
+ throw new Error(`Config.theme.seeds.${context}.accent must be a string color`);
12
+ }
13
+ };
14
+ export const validateConfig = (config) => {
15
+ if (!config || typeof config !== "object") {
16
+ throw new Error("Config must export a default object");
17
+ }
18
+ if (!config.theme) {
19
+ throw new Error("Config.theme is required");
20
+ }
21
+ const seeds = config.theme.seeds;
22
+ if (!seeds?.light || !seeds?.dark) {
23
+ throw new Error("Config.theme.seeds.light and .dark are required");
24
+ }
25
+ validateSeed("light", seeds.light);
26
+ validateSeed("dark", seeds.dark);
27
+ if (!config.tokens?.preset) {
28
+ throw new Error("Config.tokens.preset is required");
29
+ }
30
+ if (!isTokenPresetName(config.tokens.preset)) {
31
+ throw new Error(`Unsupported token preset: ${config.tokens.preset}`);
32
+ }
33
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { validateConfig } from "./validate.js";
3
+ describe("cli config validation", () => {
4
+ it("accepts a valid config", () => {
5
+ const config = {
6
+ theme: {
7
+ seeds: {
8
+ light: { neutral: "#111827", accent: "#3d63dd" },
9
+ dark: { neutral: "#111827", accent: "#3d63dd" },
10
+ },
11
+ },
12
+ tokens: { preset: "modern-ui" },
13
+ };
14
+ expect(() => validateConfig(config)).not.toThrow();
15
+ });
16
+ it("rejects missing neutral/accent", () => {
17
+ const config = {
18
+ theme: {
19
+ seeds: {
20
+ light: { neutral: "#111827" },
21
+ dark: { accent: "#3d63dd" },
22
+ },
23
+ },
24
+ tokens: { preset: "modern-ui" },
25
+ };
26
+ expect(() => validateConfig(config)).toThrow(/must be a string color/);
27
+ });
28
+ it("rejects invalid preset", () => {
29
+ const config = {
30
+ theme: {
31
+ seeds: {
32
+ light: { neutral: "#111827", accent: "#3d63dd" },
33
+ dark: { neutral: "#111827", accent: "#3d63dd" },
34
+ },
35
+ },
36
+ tokens: { preset: "nope" },
37
+ };
38
+ expect(() => validateConfig(config)).toThrow(/Unsupported token preset/);
39
+ });
40
+ });
package/dist/cli.js CHANGED
@@ -1,150 +1,148 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { createRequire } from "node:module";
4
- import path from "node:path";
2
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
+ import { basename, join, resolve } from "node:path";
5
4
  import { pathToFileURL } from "node:url";
6
- import { createTheme } from "./createTheme.js";
7
- import { toTs, toTsWithMode } from "./exporters/toTs.js";
8
- const DEFAULT_CONFIG_FILES = [
9
- "palette.config.mjs",
10
- "palette.config.js",
11
- "palette.config.cjs",
12
- "palette.config.json",
13
- ];
14
- const args = process.argv.slice(2);
15
- const command = args[0];
16
- if (!command || command === "--help" || command === "-h") {
17
- printHelp();
18
- process.exit(0);
19
- }
20
- if (command !== "generate") {
21
- console.error(`Unknown command: ${command}`);
22
- printHelp();
23
- process.exit(1);
24
- }
25
- const options = parseArgs(args.slice(1));
26
- const configPath = resolveConfigPath(options.configPath);
27
- const outPath = path.resolve(process.cwd(), options.outPath ?? "src/theme.ts");
28
- const mode = options.mode;
29
- const config = await loadConfig(configPath);
30
- if (!config || typeof config !== "object") {
31
- console.error("Config must export a plain object with createTheme options.");
32
- process.exit(1);
33
- }
34
- if (!("neutral" in config) || !("accent" in config)) {
35
- console.error("Config must include at least `neutral` and `accent` sources.");
36
- process.exit(1);
37
- }
38
- const theme = createTheme(config);
39
- const ts = mode ? toTsWithMode(theme, mode) : toTs(theme);
40
- mkdirSync(path.dirname(outPath), { recursive: true });
41
- writeFileSync(outPath, ts);
42
- console.log(`Generated ${path.relative(process.cwd(), outPath)}`);
43
- function parseArgs(input) {
44
- const options = {};
45
- for (let i = 0; i < input.length; i += 1) {
46
- const arg = input[i];
47
- if (arg === "--config" || arg === "-c") {
48
- options.configPath = input[i + 1];
49
- i += 1;
50
- continue;
51
- }
52
- if (arg?.startsWith("--config=")) {
53
- options.configPath = arg.split("=")[1];
54
- continue;
55
- }
56
- if (arg === "--out" || arg === "-o") {
57
- options.outPath = input[i + 1];
58
- i += 1;
59
- continue;
60
- }
61
- if (arg?.startsWith("--out=")) {
62
- options.outPath = arg.split("=")[1];
63
- continue;
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;
64
121
  }
65
- if (arg === "--mode" || arg === "-m") {
66
- const value = input[i + 1];
67
- if (!value) {
68
- console.error('Missing value for --mode. Use "srgb" or "p3".');
69
- process.exit(1);
70
- }
71
- if (value !== "srgb" && value !== "p3") {
72
- console.error(`Invalid mode: ${value}. Use "srgb" or "p3".`);
73
- process.exit(1);
74
- }
75
- options.mode = value;
76
- i += 1;
77
- continue;
122
+ if (parsed.help || !parsed.command) {
123
+ printHelp();
124
+ return;
78
125
  }
79
- if (arg?.startsWith("--mode=")) {
80
- const value = arg.split("=")[1];
81
- if (!value) {
82
- console.error('Missing value for --mode. Use "srgb" or "p3".');
83
- process.exit(1);
84
- }
85
- if (value !== "srgb" && value !== "p3") {
86
- console.error(`Invalid mode: ${value}. Use "srgb" or "p3".`);
87
- process.exit(1);
88
- }
89
- options.mode = value;
90
- continue;
126
+ if (!COMMANDS.includes(parsed.command)) {
127
+ throw new CliUsageError(`Unknown command: ${parsed.command}`);
91
128
  }
92
- if (arg === "--help" || arg === "-h") {
93
- printHelp();
94
- process.exit(0);
129
+ if (parsed.command === "init") {
130
+ await runInit(parsed.flags);
131
+ return;
95
132
  }
96
- console.error(`Unknown argument: ${arg}`);
97
- printHelp();
98
- process.exit(1);
99
- }
100
- return options;
101
- }
102
- function resolveConfigPath(configPath) {
103
- if (configPath) {
104
- const resolved = path.resolve(process.cwd(), configPath);
105
- if (!existsSync(resolved)) {
106
- console.error(`Config not found: ${configPath}`);
107
- process.exit(1);
133
+ if (parsed.command === "build") {
134
+ await runBuild(parsed.flags);
135
+ return;
108
136
  }
109
- return resolved;
110
137
  }
111
- for (const filename of DEFAULT_CONFIG_FILES) {
112
- const candidate = path.resolve(process.cwd(), filename);
113
- if (existsSync(candidate)) {
114
- return candidate;
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();
115
144
  }
145
+ process.exitCode = 1;
116
146
  }
117
- console.error("No config file found. Expected one of:");
118
- for (const filename of DEFAULT_CONFIG_FILES) {
119
- console.error(` - ${filename}`);
120
- }
121
- process.exit(1);
122
- throw new Error("No config file found.");
123
- }
124
- async function loadConfig(filePath) {
125
- const ext = path.extname(filePath).toLowerCase();
126
- if (ext === ".json") {
127
- const raw = readFileSync(filePath, "utf8");
128
- return JSON.parse(raw);
129
- }
130
- if (ext === ".cjs") {
131
- const require = createRequire(import.meta.url);
132
- return require(filePath);
133
- }
134
- const module = await import(pathToFileURL(filePath).href);
135
- return (module.default ?? module.config ?? module.theme ?? module);
136
- }
137
- function printHelp() {
138
- console.log([
139
- "palette-kit generate [options]",
140
- "",
141
- "Options:",
142
- " -c, --config <file> Config file (default: palette.config.*)",
143
- " -o, --out <file> Output file (default: src/theme.ts)",
144
- " -m, --mode <srgb|p3> Export mode (default: theme as-is)",
145
- " -h, --help Show help",
146
- "",
147
- "Example:",
148
- " palette-kit generate --config palette.config.mjs --out src/theme.ts",
149
- ].join("\n"));
150
- }
147
+ };
148
+ void main();
@@ -1,2 +1,2 @@
1
- import type { ColorHex } from "../types.js";
2
- export declare function apcaContrast(foreground: ColorHex, background: ColorHex): number;
1
+ import type { SrgbColor } from "./types.js";
2
+ export declare function computeApcaLc(fg: SrgbColor, bg: SrgbColor): number;
@@ -1,5 +1,15 @@
1
- import { calcAPCA } from "apca-w3";
2
- export function apcaContrast(foreground, background) {
3
- const contrast = Number(calcAPCA(foreground, background));
4
- return Number(contrast.toFixed(2));
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;
5
15
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
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
+ });
@@ -0,0 +1,4 @@
1
+ export * from "./apca.js";
2
+ export * from "./solver.js";
3
+ export * from "./types.js";
4
+ export * from "./wcag2.js";
@@ -0,0 +1,4 @@
1
+ export * from "./apca.js";
2
+ export * from "./solver.js";
3
+ export * from "./types.js";
4
+ export * from "./wcag2.js";
@@ -0,0 +1,4 @@
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;
@@ -0,0 +1,31 @@
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
+ };
@@ -0,0 +1 @@
1
+ export {};