@clhaas/palette-kit 0.1.8 → 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 (295) 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 -22
  111. package/dist/index.js +2 -18
  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 -148
  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/text/resolveOnBgText.d.ts +0 -9
  291. package/dist/text/resolveOnBgText.js +0 -28
  292. package/dist/tokens/presetRadixLikeUi.d.ts +0 -5
  293. package/dist/tokens/presetRadixLikeUi.js +0 -55
  294. package/dist/types.d.ts +0 -69
  295. /package/dist/{types.js → cli/args.test.d.ts} +0 -0
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ import { parseColor } from "./parseColor.js";
4
+
5
+ describe("parseColor", () => {
6
+ it("parses #fff with alpha 1", () => {
7
+ const result = parseColor("#fff");
8
+
9
+ expect(result.input).toBe("#fff");
10
+ expect(result.srgb.space).toBe("srgb");
11
+ expect(result.srgb.alpha).toBe(1);
12
+ expect(result.srgb.channels[0]).toBeCloseTo(1, 6);
13
+ expect(result.srgb.channels[1]).toBeCloseTo(1, 6);
14
+ expect(result.srgb.channels[2]).toBeCloseTo(1, 6);
15
+ });
16
+
17
+ it("parses #112233", () => {
18
+ const result = parseColor("#112233");
19
+
20
+ expect(result.srgb.channels[0]).toBeCloseTo(0x11 / 255, 6);
21
+ expect(result.srgb.channels[1]).toBeCloseTo(0x22 / 255, 6);
22
+ expect(result.srgb.channels[2]).toBeCloseTo(0x33 / 255, 6);
23
+ expect(result.srgb.alpha).toBe(1);
24
+ });
25
+
26
+ it("parses #11223380 with alpha", () => {
27
+ const result = parseColor("#11223380");
28
+
29
+ expect(result.srgb.alpha).toBeCloseTo(0x80 / 255, 3);
30
+ });
31
+
32
+ it("normalizes okLch channels", () => {
33
+ const result = parseColor("#ffffff");
34
+ const [l, c, h] = result.okLch.channels;
35
+
36
+ expect(result.okLch.space).toBe("oklch");
37
+ expect(result.okLch.alpha).toBe(1);
38
+ expect(l).toBeGreaterThanOrEqual(0);
39
+ expect(l).toBeLessThanOrEqual(100);
40
+ expect(c).toBeGreaterThanOrEqual(0);
41
+ expect(h).toBeGreaterThanOrEqual(0);
42
+ expect(h).toBeLessThan(360);
43
+ });
44
+
45
+ it("preserves okLch alpha", () => {
46
+ const result = parseColor("#33669980");
47
+
48
+ expect(result.okLch.alpha).toBeCloseTo(0x80 / 255, 3);
49
+ });
50
+
51
+ it("throws on invalid input", () => {
52
+ expect(() => parseColor("#12")).toThrowError(/#12/);
53
+ expect(() => parseColor("#GGGGGG")).toThrowError(/#GGGGGG/);
54
+ });
55
+
56
+ it("includes rgb values when conversion fails", async () => {
57
+ vi.resetModules();
58
+ vi.doMock("culori", () => ({
59
+ converter: () => () => undefined,
60
+ }));
61
+
62
+ const { parseColor: mockedParseColor } = await import("./parseColor.js");
63
+
64
+ expect(() => mockedParseColor("#ffffff")).toThrowError(/r=1, g=1, b=1/);
65
+ });
66
+ });
@@ -0,0 +1,87 @@
1
+ import { converter } from "culori";
2
+
3
+ import type { CssColorString, RawColor } from "../types/index.js";
4
+
5
+ const hexPattern = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
6
+ const toOklch = converter("oklch");
7
+
8
+ const OKLCH_L_MAX = 100;
9
+ const OKLCH_H_MAX = 360;
10
+ const MAX_OKLCH_CHROMA = 0.4;
11
+ const NEGATIVE_CHROMA_EPSILON = 1e-6;
12
+
13
+ const toChannel = (hex: string) => Number.parseInt(hex, 16) / 255;
14
+
15
+ const normalizeHue = (hue: number) => ((hue % OKLCH_H_MAX) + OKLCH_H_MAX) % OKLCH_H_MAX;
16
+ const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
17
+
18
+ export function parseColor(input: CssColorString): {
19
+ input: CssColorString;
20
+ okLch: RawColor;
21
+ srgb: RawColor;
22
+ } {
23
+ const normalizedInput = input.trim();
24
+
25
+ if (!hexPattern.test(normalizedInput)) {
26
+ throw new Error(`Invalid color input: "${input}"`);
27
+ }
28
+
29
+ const hex = normalizedInput.slice(1);
30
+ let r = 0;
31
+ let g = 0;
32
+ let b = 0;
33
+ let alpha = 1;
34
+
35
+ if (hex.length === 3) {
36
+ r = toChannel(`${hex[0]}${hex[0]}`);
37
+ g = toChannel(`${hex[1]}${hex[1]}`);
38
+ b = toChannel(`${hex[2]}${hex[2]}`);
39
+ } else if (hex.length === 6 || hex.length === 8) {
40
+ r = toChannel(hex.slice(0, 2));
41
+ g = toChannel(hex.slice(2, 4));
42
+ b = toChannel(hex.slice(4, 6));
43
+
44
+ if (hex.length === 8) {
45
+ alpha = toChannel(hex.slice(6, 8));
46
+ }
47
+ } else {
48
+ throw new Error(`Invalid hex length: "${normalizedInput}"`);
49
+ }
50
+
51
+ const oklchValue = toOklch({ mode: "rgb", r, g, b });
52
+
53
+ if (!oklchValue) {
54
+ throw new Error(`Unable to convert color input: "${normalizedInput}" (r=${r}, g=${g}, b=${b})`);
55
+ }
56
+
57
+ const l = typeof oklchValue.l === "number" && Number.isFinite(oklchValue.l) ? oklchValue.l : 0;
58
+ const c = typeof oklchValue.c === "number" && Number.isFinite(oklchValue.c) ? oklchValue.c : 0;
59
+ const h = typeof oklchValue.h === "number" && Number.isFinite(oklchValue.h) ? oklchValue.h : 0;
60
+ const scaledLightness = l * 100;
61
+
62
+ if (c < -NEGATIVE_CHROMA_EPSILON) {
63
+ throw new Error(`Invalid OKLCH chroma value: ${c} for "${input}"`);
64
+ }
65
+
66
+ const okLch: RawColor = {
67
+ space: "oklch",
68
+ channels: [
69
+ clamp(scaledLightness, 0, OKLCH_L_MAX),
70
+ clamp(Math.max(0, c), 0, MAX_OKLCH_CHROMA),
71
+ normalizeHue(h),
72
+ ],
73
+ alpha,
74
+ };
75
+
76
+ const srgb: RawColor = {
77
+ space: "srgb",
78
+ channels: [r, g, b],
79
+ alpha,
80
+ };
81
+
82
+ return {
83
+ input: normalizedInput,
84
+ okLch,
85
+ srgb,
86
+ };
87
+ }
@@ -0,0 +1,6 @@
1
+ import { clamp } from "./clamp.js";
2
+
3
+ export const smoothstep = (t: number) => {
4
+ const x = clamp(t, 0, 1);
5
+ return x * x * (3 - 2 * x);
6
+ };
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": false,
5
+ "declaration": true,
6
+ "emitDeclarationOnly": false,
7
+ "outDir": "dist"
8
+ },
9
+ "include": ["src/**/*.ts"],
10
+ "exclude": ["tests/**", "examples/**", "node_modules/**"]
11
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "declaration": true,
7
+ "strict": true,
8
+ "types": ["node"],
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "noEmit": true
12
+ },
13
+ "include": ["src/**/*.ts", "src/**/*.ts"],
14
+ "exclude": ["examples/**", "tests/**", "node_modules/**"]
15
+ }
@@ -1,5 +0,0 @@
1
- import type { AlphaScale, ColorHex } from "../types.js";
2
- export declare function generateAlphaScale(base: ColorHex, _background: {
3
- light: ColorHex;
4
- dark: ColorHex;
5
- }): AlphaScale;
@@ -1,34 +0,0 @@
1
- import Color from "colorjs.io";
2
- const steps = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
3
- const alphaCurve = {
4
- 1: 0.05,
5
- 2: 0.1,
6
- 3: 0.15,
7
- 4: 0.2,
8
- 5: 0.3,
9
- 6: 0.4,
10
- 7: 0.5,
11
- 8: 0.6,
12
- 9: 0.7,
13
- 10: 0.8,
14
- 11: 0.9,
15
- 12: 0.95,
16
- };
17
- function mixWithAlpha(foreground, alpha) {
18
- const color = new Color(foreground).to("srgb");
19
- const [r, g, b] = color.coords;
20
- const hex = new Color({ space: "srgb", coords: [r, g, b], alpha }).toString({
21
- format: "hex",
22
- });
23
- return hex;
24
- }
25
- export function generateAlphaScale(base, _background) {
26
- const light = {};
27
- const dark = {};
28
- for (const step of steps) {
29
- const alpha = alphaCurve[step];
30
- light[step] = mixWithAlpha(base, alpha);
31
- dark[step] = mixWithAlpha(base, alpha);
32
- }
33
- return { light, dark };
34
- }
@@ -1,6 +0,0 @@
1
- import type { ColorHex } from "../types.js";
2
- export declare function onSolidTextTokens(background: ColorHex): {
3
- primary: ColorHex;
4
- secondary: ColorHex;
5
- disabled: ColorHex;
6
- };
@@ -1,28 +0,0 @@
1
- import { apcaContrast } from "./apca.js";
2
- const white = "#ffffff";
3
- const black = "#000000";
4
- const alphaLevels = {
5
- primary: 0.92,
6
- secondary: 0.72,
7
- disabled: 0.48,
8
- };
9
- function withAlpha(hex, alpha) {
10
- const normalized = hex.replace("#", "");
11
- const alphaHex = Math.round(alpha * 255)
12
- .toString(16)
13
- .padStart(2, "0");
14
- return `#${normalized}${alphaHex}`;
15
- }
16
- function chooseTextColor(background) {
17
- const whiteScore = Math.abs(apcaContrast(white, background));
18
- const blackScore = Math.abs(apcaContrast(black, background));
19
- return whiteScore >= blackScore ? white : black;
20
- }
21
- export function onSolidTextTokens(background) {
22
- const base = chooseTextColor(background);
23
- return {
24
- primary: withAlpha(base, alphaLevels.primary),
25
- secondary: withAlpha(base, alphaLevels.secondary),
26
- disabled: withAlpha(base, alphaLevels.disabled),
27
- };
28
- }
@@ -1,2 +0,0 @@
1
- import type { ColorHex } from "../types.js";
2
- export declare function adjustTextColor(foreground: ColorHex, background: ColorHex, target: number): ColorHex;
@@ -1,31 +0,0 @@
1
- import Color from "colorjs.io";
2
- import { compressToSrgb, oklchToHex } from "../engine/oklch.js";
3
- import { apcaContrast } from "./apca.js";
4
- function clamp(value, min, max) {
5
- return Math.min(max, Math.max(min, value));
6
- }
7
- function adjustLightness(oklch, background, target, maxIterations = 24) {
8
- const bg = new Color(background).to("oklch");
9
- const bgL = bg.coords[0] ?? 0;
10
- const direction = bgL > 0.5 ? -1 : 1;
11
- let current = { ...oklch };
12
- for (let i = 0; i < maxIterations; i += 1) {
13
- const contrast = Math.abs(apcaContrast(oklchToHex(current), background));
14
- if (contrast >= target) {
15
- return current;
16
- }
17
- current = {
18
- ...current,
19
- l: clamp(current.l + direction * 0.02, 0, 1),
20
- };
21
- }
22
- return current;
23
- }
24
- export function adjustTextColor(foreground, background, target) {
25
- const fg = new Color(foreground).to("oklch");
26
- const [l, c, h] = fg.coords;
27
- let candidate = { l: l ?? 0, c: c ?? 0, h: h ?? 0 };
28
- candidate = adjustLightness(candidate, background, target);
29
- candidate = compressToSrgb(candidate);
30
- return oklchToHex(candidate);
31
- }
@@ -1,38 +0,0 @@
1
- import type { GenerateScaleOptions } from "./generateScale.js";
2
- import type { ColorHex, ColorSource, Theme } from "./types.js";
3
- export type TokenOverrides = {
4
- light?: Record<string, ColorHex>;
5
- dark?: Record<string, ColorHex>;
6
- };
7
- export type CreateThemeOptions = {
8
- neutral: ColorSource;
9
- accent: ColorSource;
10
- semantic?: {
11
- success?: ColorSource;
12
- warning?: ColorSource;
13
- danger?: ColorSource;
14
- };
15
- extras?: Record<string, ColorSource>;
16
- tokens?: {
17
- preset?: "radix-like-ui";
18
- overrides?: TokenOverrides;
19
- };
20
- alpha?: {
21
- enabled?: boolean;
22
- background?: {
23
- light?: ColorHex;
24
- dark?: ColorHex;
25
- };
26
- };
27
- text?: {
28
- darkBase?: ColorHex;
29
- lightBase?: ColorHex;
30
- };
31
- contrast?: {
32
- textPrimary?: number;
33
- textSecondary?: number;
34
- };
35
- scale?: Omit<GenerateScaleOptions, "source" | "mode" | "p3">;
36
- p3?: boolean;
37
- };
38
- export declare function createTheme(options: CreateThemeOptions): Theme;
@@ -1,148 +0,0 @@
1
- import { generateAlphaScale } from "./alpha/generateAlphaScale.js";
2
- import { onSolidTextTokens } from "./contrast/onSolid.js";
3
- import { analyzeTheme } from "./diagnostics/analyzeTheme.js";
4
- import { generateScale } from "./generateScale.js";
5
- import { generateOverlayScale } from "./overlays/generateOverlayScale.js";
6
- import { generateTextScale } from "./text/generateTextScale.js";
7
- import { buildPresetTokens } from "./tokens/presetRadixLikeUi.js";
8
- export function createTheme(options) {
9
- const includeP3 = options.p3 ?? false;
10
- const scaleOptions = options.scale ?? {};
11
- const scales = {
12
- neutral: generateScale({ source: options.neutral, ...scaleOptions, p3: includeP3 }),
13
- accent: generateScale({ source: options.accent, ...scaleOptions, p3: includeP3 }),
14
- };
15
- if (options.semantic?.success) {
16
- scales.success = generateScale({
17
- source: options.semantic.success,
18
- ...scaleOptions,
19
- p3: includeP3,
20
- });
21
- }
22
- if (options.semantic?.warning) {
23
- scales.warning = generateScale({
24
- source: options.semantic.warning,
25
- ...scaleOptions,
26
- p3: includeP3,
27
- });
28
- }
29
- if (options.semantic?.danger) {
30
- scales.danger = generateScale({
31
- source: options.semantic.danger,
32
- ...scaleOptions,
33
- p3: includeP3,
34
- });
35
- }
36
- if (options.extras) {
37
- for (const [key, source] of Object.entries(options.extras)) {
38
- scales[key] = generateScale({ source, ...scaleOptions, p3: includeP3 });
39
- }
40
- }
41
- const preset = options.tokens?.preset ?? "radix-like-ui";
42
- const tokens = preset === "radix-like-ui" ? buildPresetTokens(scales) : { light: {}, dark: {} };
43
- const accentScale = scales.accent;
44
- const lightOnSolid = onSolidTextTokens(accentScale.light[9]);
45
- const darkOnSolid = onSolidTextTokens(accentScale.dark[9]);
46
- tokens.light["onSolid.primary"] = lightOnSolid.primary;
47
- tokens.light["onSolid.secondary"] = lightOnSolid.secondary;
48
- tokens.light["onSolid.disabled"] = lightOnSolid.disabled;
49
- tokens.dark["onSolid.primary"] = darkOnSolid.primary;
50
- tokens.dark["onSolid.secondary"] = darkOnSolid.secondary;
51
- tokens.dark["onSolid.disabled"] = darkOnSolid.disabled;
52
- const textScale = generateTextScale({
53
- darkBase: options.text?.darkBase,
54
- lightBase: options.text?.lightBase,
55
- });
56
- const darkTextSteps = {
57
- primary: 12,
58
- secondary: 10,
59
- tertiary: 9,
60
- disabled: 8,
61
- };
62
- const lightTextSteps = {
63
- primary: 1,
64
- secondary: 3,
65
- tertiary: 4,
66
- disabled: 5,
67
- };
68
- const midDarkTextSteps = {
69
- primary: 12,
70
- secondary: 11,
71
- };
72
- const midLightTextSteps = {
73
- primary: 1,
74
- secondary: 3,
75
- };
76
- const onBgLightMode = {
77
- light: { scale: "dark", steps: darkTextSteps },
78
- mid: { scale: "dark", steps: midDarkTextSteps },
79
- dark: { scale: "light", steps: lightTextSteps },
80
- };
81
- const onBgDarkMode = {
82
- light: { scale: "light", steps: lightTextSteps },
83
- mid: { scale: "light", steps: midLightTextSteps },
84
- dark: { scale: "dark", steps: darkTextSteps },
85
- };
86
- const applyOnBgTokens = (target, mapping) => {
87
- for (const [zone, config] of Object.entries(mapping)) {
88
- const scale = config.scale === "dark" ? textScale.dark : textScale.light;
89
- for (const [role, step] of Object.entries(config.steps)) {
90
- target[`text.onBg.${zone}.${role}`] = scale[step];
91
- }
92
- }
93
- };
94
- for (const [step, value] of Object.entries(textScale.dark)) {
95
- tokens.light[`text.dark.${step}`] = value;
96
- tokens.dark[`text.dark.${step}`] = value;
97
- }
98
- for (const [step, value] of Object.entries(textScale.light)) {
99
- tokens.light[`text.light.${step}`] = value;
100
- tokens.dark[`text.light.${step}`] = value;
101
- }
102
- for (const [key, step] of Object.entries(darkTextSteps)) {
103
- const token = `text.dark.${key}`;
104
- tokens.light[token] = textScale.dark[step];
105
- tokens.dark[token] = textScale.dark[step];
106
- }
107
- for (const [key, step] of Object.entries(lightTextSteps)) {
108
- const token = `text.light.${key}`;
109
- tokens.light[token] = textScale.light[step];
110
- tokens.dark[token] = textScale.light[step];
111
- }
112
- applyOnBgTokens(tokens.light, onBgLightMode);
113
- applyOnBgTokens(tokens.dark, onBgDarkMode);
114
- tokens.light["text.primary"] = tokens.light["text.onBg.light.primary"];
115
- tokens.light["text.secondary"] = tokens.light["text.onBg.light.secondary"];
116
- tokens.light["text.tertiary"] = tokens.light["text.onBg.light.tertiary"];
117
- tokens.light["text.disabled"] = tokens.light["text.onBg.light.disabled"];
118
- tokens.dark["text.primary"] = tokens.dark["text.onBg.light.primary"];
119
- tokens.dark["text.secondary"] = tokens.dark["text.onBg.light.secondary"];
120
- tokens.dark["text.tertiary"] = tokens.dark["text.onBg.light.tertiary"];
121
- tokens.dark["text.disabled"] = tokens.dark["text.onBg.light.disabled"];
122
- if (options.tokens?.overrides?.light) {
123
- Object.assign(tokens.light, options.tokens.overrides.light);
124
- }
125
- if (options.tokens?.overrides?.dark) {
126
- Object.assign(tokens.dark, options.tokens.overrides.dark);
127
- }
128
- let alpha;
129
- if (options.alpha?.enabled !== false) {
130
- const background = {
131
- light: options.alpha?.background?.light ?? "#ffffff",
132
- dark: options.alpha?.background?.dark ?? "#111111",
133
- };
134
- alpha = Object.fromEntries(Object.entries(scales).map(([slot, scale]) => [
135
- slot,
136
- generateAlphaScale(scale.light[9], background),
137
- ]));
138
- }
139
- const overlay = generateOverlayScale();
140
- const diagnostics = analyzeTheme({ scales, tokens, alpha, overlay });
141
- return {
142
- scales,
143
- tokens,
144
- alpha,
145
- overlay,
146
- diagnostics,
147
- };
148
- }
@@ -1,3 +0,0 @@
1
- import type { ColorHex, RadixSeedName } from "../types.js";
2
- export declare const radixSeeds: Record<RadixSeedName, ColorHex>;
3
- export declare const radixSeedNames: string[];
@@ -1,34 +0,0 @@
1
- export const radixSeeds = {
2
- amber: "#ffc53d",
3
- blue: "#0090ff",
4
- bronze: "#a18072",
5
- brown: "#ad7f58",
6
- crimson: "#e93d82",
7
- cyan: "#00a2c7",
8
- gold: "#978365",
9
- grass: "#46a758",
10
- gray: "#8d8d8d",
11
- green: "#30a46c",
12
- indigo: "#3e63dd",
13
- iris: "#5b5bd6",
14
- jade: "#29a383",
15
- lime: "#bdee63",
16
- mauve: "#8e8c99",
17
- mint: "#86ead4",
18
- olive: "#898e87",
19
- orange: "#f76b15",
20
- pink: "#d6409f",
21
- plum: "#ab4aba",
22
- purple: "#8e4ec6",
23
- red: "#e5484d",
24
- ruby: "#e54666",
25
- sage: "#868e8b",
26
- sand: "#8d8d86",
27
- sky: "#7ce2fe",
28
- slate: "#8b8d98",
29
- teal: "#12a594",
30
- tomato: "#e54d2e",
31
- violet: "#6e56cf",
32
- yellow: "#ffe629",
33
- };
34
- export const radixSeedNames = Object.keys(radixSeeds).sort();
@@ -1,2 +0,0 @@
1
- import type { Scale, ScaleDiagnostics } from "../types.js";
2
- export declare function analyzeScale(scale: Scale): ScaleDiagnostics;
@@ -1,7 +0,0 @@
1
- export function analyzeScale(scale) {
2
- return {
3
- outOfGamutCount: scale.meta?.outOfGamutCount ?? 0,
4
- outOfP3GamutCount: scale.meta?.outOfP3GamutCount ?? 0,
5
- anchorSteps: scale.meta?.anchorSteps,
6
- };
7
- }
@@ -1,2 +0,0 @@
1
- import type { Theme, ThemeDiagnostics } from "../types.js";
2
- export declare function analyzeTheme(theme: Theme): ThemeDiagnostics;
@@ -1,35 +0,0 @@
1
- import { apcaContrast } from "../contrast/apca.js";
2
- import { analyzeWarnings } from "./warnings.js";
3
- export function analyzeTheme(theme) {
4
- const contrast = {};
5
- const lightBg = theme.tokens.light["bg.app"];
6
- const darkBg = theme.tokens.dark["bg.app"];
7
- if (lightBg) {
8
- if (theme.tokens.light["text.primary"]) {
9
- contrast["light.text.primary"] = apcaContrast(theme.tokens.light["text.primary"], lightBg);
10
- }
11
- if (theme.tokens.light["text.secondary"]) {
12
- contrast["light.text.secondary"] = apcaContrast(theme.tokens.light["text.secondary"], lightBg);
13
- }
14
- }
15
- if (darkBg) {
16
- if (theme.tokens.dark["text.primary"]) {
17
- contrast["dark.text.primary"] = apcaContrast(theme.tokens.dark["text.primary"], darkBg);
18
- }
19
- if (theme.tokens.dark["text.secondary"]) {
20
- contrast["dark.text.secondary"] = apcaContrast(theme.tokens.dark["text.secondary"], darkBg);
21
- }
22
- }
23
- if (theme.tokens.light["onSolid.primary"] && theme.tokens.light["accent.solid"]) {
24
- contrast["light.onSolid.primary"] = apcaContrast(theme.tokens.light["onSolid.primary"], theme.tokens.light["accent.solid"]);
25
- }
26
- if (theme.tokens.dark["onSolid.primary"] && theme.tokens.dark["accent.solid"]) {
27
- contrast["dark.onSolid.primary"] = apcaContrast(theme.tokens.dark["onSolid.primary"], theme.tokens.dark["accent.solid"]);
28
- }
29
- let outOfGamutCount = 0;
30
- for (const scale of Object.values(theme.scales)) {
31
- outOfGamutCount += scale.meta?.outOfGamutCount ?? 0;
32
- }
33
- const warnings = analyzeWarnings(theme);
34
- return { contrast, outOfGamutCount, warnings };
35
- }
@@ -1,2 +0,0 @@
1
- import type { Theme } from "../types.js";
2
- export declare function analyzeWarnings(theme: Theme): string[];
@@ -1,20 +0,0 @@
1
- import Color from "colorjs.io";
2
- export function analyzeWarnings(theme) {
3
- const warnings = [];
4
- const accent = theme.scales.accent?.light?.[9];
5
- if (accent) {
6
- const { coords } = new Color(accent).to("oklch");
7
- const l = coords[0] ?? 0;
8
- const c = coords[1] ?? 0;
9
- if (c < 0.03) {
10
- warnings.push("Accent seed has very low chroma; the palette may look gray.");
11
- }
12
- if (l < 0.2) {
13
- warnings.push("Accent seed is very dark; light mode solids may lack contrast.");
14
- }
15
- if (l > 0.9) {
16
- warnings.push("Accent seed is very light; dark mode solids may lack contrast.");
17
- }
18
- }
19
- return warnings;
20
- }
@@ -1,9 +0,0 @@
1
- import type { Step } from "../types.js";
2
- export type CurveConfig = {
3
- lightness?: Partial<Record<Step, number>>;
4
- chroma?: Partial<Record<Step, number>>;
5
- };
6
- export declare function resolveCurves(curves?: CurveConfig): {
7
- lightness: Record<Step, number>;
8
- chroma: Record<Step, number>;
9
- };
@@ -1,48 +0,0 @@
1
- const steps = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
2
- const defaultLightness = {
3
- 1: 0.3,
4
- 2: 0.3,
5
- 3: 0.6,
6
- 4: 0.6,
7
- 5: 0.6,
8
- 6: 0.85,
9
- 7: 0.85,
10
- 8: 0.85,
11
- 9: 1,
12
- 10: 1,
13
- 11: 1,
14
- 12: 1,
15
- };
16
- const defaultChroma = {
17
- 1: 0.2,
18
- 2: 0.2,
19
- 3: 0.6,
20
- 4: 0.6,
21
- 5: 0.6,
22
- 6: 0.8,
23
- 7: 0.8,
24
- 8: 0.8,
25
- 9: 1,
26
- 10: 1,
27
- 11: 0.7,
28
- 12: 0.7,
29
- };
30
- export function resolveCurves(curves) {
31
- const lightness = { ...defaultLightness };
32
- const chroma = { ...defaultChroma };
33
- if (curves?.lightness) {
34
- for (const step of steps) {
35
- if (curves.lightness[step] !== undefined) {
36
- lightness[step] = curves.lightness[step];
37
- }
38
- }
39
- }
40
- if (curves?.chroma) {
41
- for (const step of steps) {
42
- if (curves.chroma[step] !== undefined) {
43
- chroma[step] = curves.chroma[step];
44
- }
45
- }
46
- }
47
- return { lightness, chroma };
48
- }