@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,77 @@
1
+ /**
2
+ * Radix-like preset aligned with common component library semantics.
3
+ * Expands coverage for subtle/surface/solid layers and semantic text.
4
+ */
5
+ export const radixLikeUiTokens = {
6
+ tokens: {
7
+ "bg.app": {
8
+ name: "bg.app",
9
+ description: "Application background for the overall canvas.",
10
+ category: "background",
11
+ query: { role: "bg.app", usage: "bg", surface: "app" },
12
+ },
13
+ "bg.surface": {
14
+ name: "bg.surface",
15
+ description: "Base surface background for cards and panels.",
16
+ category: "background",
17
+ query: { role: "bg.surface", usage: "bg", surface: "surface" },
18
+ },
19
+ "bg.subtle": {
20
+ name: "bg.subtle",
21
+ description: "Subtle background for secondary sections.",
22
+ category: "background",
23
+ query: { role: "bg.subtle", usage: "bg", surface: "subtle" },
24
+ },
25
+ "bg.solid": {
26
+ name: "bg.solid",
27
+ description: "Solid background for high-emphasis elements.",
28
+ category: "background",
29
+ query: { role: "bg.solid", usage: "bg", surface: "solid" },
30
+ states: { hover: true, active: true },
31
+ },
32
+ "text.primary": {
33
+ name: "text.primary",
34
+ description: "Primary text on standard surfaces.",
35
+ category: "text",
36
+ query: { role: "text.primary", usage: "text", surface: "surface" },
37
+ },
38
+ "text.secondary": {
39
+ name: "text.secondary",
40
+ description: "Secondary text for supporting content.",
41
+ category: "text",
42
+ query: { role: "text.secondary", usage: "text", surface: "surface", emphasis: "muted" },
43
+ },
44
+ "text.inverse": {
45
+ name: "text.inverse",
46
+ description: "Inverse text for solid backgrounds.",
47
+ category: "text",
48
+ query: { role: "text.inverse", usage: "text", surface: "solid", emphasis: "inverted" },
49
+ },
50
+ "border.default": {
51
+ name: "border.default",
52
+ description: "Default border for layout and surfaces.",
53
+ category: "border",
54
+ query: { role: "border.default", usage: "border", surface: "surface" },
55
+ },
56
+ "border.subtle": {
57
+ name: "border.subtle",
58
+ description: "Subtle border for separators.",
59
+ category: "border",
60
+ query: { role: "border.subtle", usage: "border", surface: "subtle" },
61
+ },
62
+ "icon.default": {
63
+ name: "icon.default",
64
+ description: "Default icon color on surfaces.",
65
+ category: "icon",
66
+ query: { role: "icon.default", usage: "icon", surface: "surface" },
67
+ states: { hover: true },
68
+ },
69
+ "ring.default": {
70
+ name: "ring.default",
71
+ description: "Base ring color (derive focus via state operator).",
72
+ category: "ring",
73
+ query: { role: "ring.default", usage: "ring", surface: "surface", emphasis: "strong" },
74
+ states: { focus: true },
75
+ },
76
+ },
77
+ };
@@ -0,0 +1 @@
1
+ export { serializeColor, serializeResolved } from "./serializeColor.js";
@@ -0,0 +1 @@
1
+ export { serializeColor, serializeResolved } from "./serializeColor.js";
@@ -0,0 +1,6 @@
1
+ import type { OutputOptions } from "../types/index.js";
2
+ export type NormalizedOutput = Required<Pick<OutputOptions, "preferSpace" | "includeSpaces" | "gamutMapping" | "strict" | "srgbFormat">> & {
3
+ precision: Required<NonNullable<OutputOptions["precision"]>>;
4
+ includeMeta: boolean;
5
+ };
6
+ export declare const normalizeOutput: (output?: OutputOptions) => NormalizedOutput;
@@ -0,0 +1,45 @@
1
+ const gamutMappings = [
2
+ "clip",
3
+ "compressChroma",
4
+ "preferP3ThenCompress",
5
+ ];
6
+ const srgbFormats = ["hex", "rgb", "rgba"];
7
+ const formatString = (value) => (value ? value.trim() : undefined);
8
+ const assertOneOf = (value, options, label) => {
9
+ if (!options.includes(value)) {
10
+ throw new Error(`${label} must be one of: ${options.join(", ")} (received "${value}")`);
11
+ }
12
+ return value;
13
+ };
14
+ export const normalizeOutput = (output) => {
15
+ const preferSpaceValue = formatString(output?.preferSpace);
16
+ const gamutMappingValue = formatString(output?.gamutMapping);
17
+ const srgbFormatValue = formatString(output?.srgbFormat);
18
+ const preferSpace = preferSpaceValue
19
+ ? assertOneOf(preferSpaceValue, ["oklch", "srgb", "p3"], "output preferSpace")
20
+ : "oklch";
21
+ const gamutMapping = gamutMappingValue
22
+ ? assertOneOf(gamutMappingValue, gamutMappings, "output gamutMapping")
23
+ : "preferP3ThenCompress";
24
+ const srgbFormat = srgbFormatValue
25
+ ? assertOneOf(srgbFormatValue, srgbFormats, "output srgbFormat")
26
+ : "hex";
27
+ if (output?.strict !== undefined && typeof output.strict !== "boolean") {
28
+ throw new Error("Output strict must be a boolean");
29
+ }
30
+ return {
31
+ preferSpace,
32
+ includeSpaces: output?.includeSpaces ?? [],
33
+ gamutMapping,
34
+ precision: {
35
+ l: 1,
36
+ c: 3,
37
+ h: 1,
38
+ alpha: 2,
39
+ ...output?.precision,
40
+ },
41
+ strict: output?.strict ?? false,
42
+ includeMeta: output?.includeMeta ?? false,
43
+ srgbFormat,
44
+ };
45
+ };
@@ -0,0 +1,21 @@
1
+ import type { OkLchColor } from "../engine/generateScale.js";
2
+ import type { BaseResolvedColor } from "../engine/resolveBaseColor.js";
3
+ import type { ColorMeta, OutputOptions, ResolvedColor } from "../types/index.js";
4
+ type SerializedColorJson = ResolvedColor;
5
+ /**
6
+ * Serialize raw OKLCH channels into CSS-ready strings for the requested spaces.
7
+ *
8
+ * @param color - OKLCH channels resolved by the core resolver.
9
+ * @param output - Serialization options for precision, gamut mapping, and output spaces.
10
+ * @param meta - Optional metadata to include when `includeMeta` is enabled.
11
+ */
12
+ export declare const serializeColor: (color: OkLchColor, output?: OutputOptions, meta?: ColorMeta) => ResolvedColor;
13
+ /**
14
+ * Serialize a resolver output while preserving resolver metadata.
15
+ *
16
+ * @param color - Result of the core resolver.
17
+ * @param output - Serialization options for precision, gamut mapping, and output spaces.
18
+ */
19
+ export declare const serializeResolved: (color: BaseResolvedColor, output?: OutputOptions) => ResolvedColor;
20
+ export declare const serializeColorJson: (color: OkLchColor, output?: OutputOptions, meta?: ColorMeta) => SerializedColorJson;
21
+ export {};
@@ -0,0 +1,178 @@
1
+ import { formatHex, formatHex8 } from "culori";
2
+ import { isInGamut, mapToGamut, toGamutRgb } from "../engine/gamut.js";
3
+ import { clamp } from "../utils/clamp.js";
4
+ import { normalizeOutput } from "./normalizeOutput.js";
5
+ const roundTo = (value, digits) => {
6
+ const factor = 10 ** digits;
7
+ return Math.round(value * factor) / factor;
8
+ };
9
+ const formatNumber = (value, digits) => {
10
+ const rounded = roundTo(value, digits);
11
+ if (digits === 0) {
12
+ return String(Math.round(rounded));
13
+ }
14
+ const fixed = rounded.toFixed(digits);
15
+ return fixed.replace(/\.?0+$/, "");
16
+ };
17
+ const normalizeHue = (hue) => ((hue % 360) + 360) % 360;
18
+ const formatOklch = (color, precision) => {
19
+ const alpha = clamp(color.alpha ?? 1, 0, 1);
20
+ const hasAlpha = alpha < 1;
21
+ const l = formatNumber(clamp(color.l, 0, 100), precision.l);
22
+ const c = formatNumber(Math.max(0, color.c), precision.c);
23
+ const h = formatNumber(normalizeHue(color.h), precision.h);
24
+ const alphaPart = hasAlpha ? ` / ${formatNumber(alpha, precision.alpha)}` : "";
25
+ return `oklch(${l}% ${c} ${h}${alphaPart})`;
26
+ };
27
+ const formatDisplayP3 = (rgb, alpha, precision) => {
28
+ const r = formatNumber(clamp(rgb.r, 0, 1), precision.c);
29
+ const g = formatNumber(clamp(rgb.g, 0, 1), precision.c);
30
+ const b = formatNumber(clamp(rgb.b, 0, 1), precision.c);
31
+ const alphaPart = alpha < 1 ? ` / ${formatNumber(alpha, precision.alpha)}` : "";
32
+ return `color(display-p3 ${r} ${g} ${b}${alphaPart})`;
33
+ };
34
+ const formatSrgbHex = (rgb, alpha) => {
35
+ const rgbColor = {
36
+ mode: "rgb",
37
+ r: clamp(rgb.r, 0, 1),
38
+ g: clamp(rgb.g, 0, 1),
39
+ b: clamp(rgb.b, 0, 1),
40
+ alpha,
41
+ };
42
+ return alpha < 1 ? formatHex8(rgbColor) : formatHex(rgbColor);
43
+ };
44
+ const formatSrgbFunction = (rgb, alpha, precision, forceAlpha) => {
45
+ const toChannel = (value) => String(Math.round(clamp(value, 0, 1) * 255));
46
+ const r = toChannel(rgb.r);
47
+ const g = toChannel(rgb.g);
48
+ const b = toChannel(rgb.b);
49
+ if (forceAlpha || alpha < 1) {
50
+ return `rgba(${r} ${g} ${b} / ${formatNumber(alpha, precision.alpha)})`;
51
+ }
52
+ return `rgb(${r} ${g} ${b})`;
53
+ };
54
+ const formatSrgb = (rgb, alpha, output) => {
55
+ switch (output.srgbFormat) {
56
+ case "rgb":
57
+ return formatSrgbFunction(rgb, alpha, output.precision, false);
58
+ case "rgba":
59
+ return formatSrgbFunction(rgb, alpha, output.precision, true);
60
+ default:
61
+ return formatSrgbHex(rgb, alpha);
62
+ }
63
+ };
64
+ const buildDiagnostics = (spaceUsed, clipped, compressed) => ({
65
+ spaceUsed,
66
+ ...(clipped ? { clipped } : {}),
67
+ ...(compressed ? { compressed } : {}),
68
+ });
69
+ const buildMeta = (meta, output, diagnostics) => output.includeMeta
70
+ ? {
71
+ ...meta,
72
+ spaceUsed: diagnostics.spaceUsed,
73
+ gamutMapping: output.gamutMapping,
74
+ ...(diagnostics.clipped ? { clipped: diagnostics.clipped } : {}),
75
+ ...(diagnostics.compressed ? { compressed: diagnostics.compressed } : {}),
76
+ }
77
+ : undefined;
78
+ const resolveAlpha = (baseAlpha, mapped) => clamp(mapped?.alpha ?? baseAlpha, 0, 1);
79
+ const requirePreferredSpace = (prefer, strict, value, fallback) => {
80
+ if (!strict)
81
+ return value ?? fallback;
82
+ // In strict mode, do not silently fall back for explicit non-oklch preferences.
83
+ if (prefer !== "oklch" && !value) {
84
+ throw new Error(`Unable to serialize preferred space: ${prefer}`);
85
+ }
86
+ return value ?? fallback;
87
+ };
88
+ const resolveUsedSpace = (prefer, preferredValue) => {
89
+ if (prefer === "oklch") {
90
+ return "oklch";
91
+ }
92
+ return preferredValue ? prefer : "oklch";
93
+ };
94
+ /**
95
+ * Serialize raw OKLCH channels into CSS-ready strings for the requested spaces.
96
+ *
97
+ * @param color - OKLCH channels resolved by the core resolver.
98
+ * @param output - Serialization options for precision, gamut mapping, and output spaces.
99
+ * @param meta - Optional metadata to include when `includeMeta` is enabled.
100
+ */
101
+ export const serializeColor = (color, output, meta) => {
102
+ const normalized = normalizeOutput(output);
103
+ const alpha = clamp(color.alpha ?? 1, 0, 1);
104
+ const spaces = new Set([normalized.preferSpace, ...normalized.includeSpaces]);
105
+ const oklchText = formatOklch(color, normalized.precision);
106
+ const needsSrgb = spaces.has("srgb") || normalized.preferSpace === "srgb";
107
+ const srgbInGamut = needsSrgb ? isInGamut(color, "srgb") : false;
108
+ const srgbColor = needsSrgb
109
+ ? mapToGamut(color, "srgb", normalized.gamutMapping, normalized.strict)
110
+ : undefined;
111
+ const srgbRgb = srgbColor ? toGamutRgb(srgbColor, "srgb") : null;
112
+ const srgbAlpha = resolveAlpha(alpha, srgbColor);
113
+ const srgbText = srgbRgb ? formatSrgb(srgbRgb, srgbAlpha, normalized) : undefined;
114
+ const srgbClipped = needsSrgb && normalized.gamutMapping === "clip" && !srgbInGamut;
115
+ const srgbCompressed = needsSrgb && normalized.gamutMapping !== "clip" && !srgbInGamut ? true : undefined;
116
+ const needsP3 = spaces.has("p3") || normalized.preferSpace === "p3";
117
+ const p3InGamut = needsP3 ? isInGamut(color, "p3") : false;
118
+ const p3Color = needsP3
119
+ ? normalized.gamutMapping === "preferP3ThenCompress" && p3InGamut
120
+ ? color
121
+ : mapToGamut(color, "p3", normalized.gamutMapping, normalized.strict)
122
+ : undefined;
123
+ const p3Rgb = p3Color ? toGamutRgb(p3Color, "p3") : null;
124
+ const p3Alpha = resolveAlpha(alpha, p3Color);
125
+ const p3Text = p3Rgb ? formatDisplayP3(p3Rgb, p3Alpha, normalized.precision) : undefined;
126
+ const p3Clipped = needsP3 && normalized.gamutMapping === "clip" && !p3InGamut;
127
+ const p3Compressed = needsP3 && normalized.gamutMapping !== "clip" && !p3InGamut ? true : undefined;
128
+ const preferredValue = normalized.preferSpace === "srgb"
129
+ ? srgbText
130
+ : normalized.preferSpace === "p3"
131
+ ? p3Text
132
+ : oklchText;
133
+ const value = (() => {
134
+ if (normalized.preferSpace === "srgb") {
135
+ return requirePreferredSpace("srgb", normalized.strict, srgbText, oklchText);
136
+ }
137
+ if (normalized.preferSpace === "p3") {
138
+ return requirePreferredSpace("p3", normalized.strict, p3Text, oklchText);
139
+ }
140
+ return oklchText;
141
+ })();
142
+ const spaceUsed = resolveUsedSpace(normalized.preferSpace, preferredValue);
143
+ const diagnostics = (() => {
144
+ if (spaceUsed === "srgb") {
145
+ return buildDiagnostics(spaceUsed, srgbClipped, srgbCompressed);
146
+ }
147
+ if (spaceUsed === "p3") {
148
+ return buildDiagnostics(spaceUsed, p3Clipped, p3Compressed);
149
+ }
150
+ return buildDiagnostics(spaceUsed);
151
+ })();
152
+ return {
153
+ value,
154
+ srgb: spaces.has("srgb") ? srgbText : undefined,
155
+ p3: spaces.has("p3") ? p3Text : undefined,
156
+ oklch: spaces.has("oklch") ? oklchText : undefined,
157
+ alpha,
158
+ meta: buildMeta(meta, normalized, diagnostics),
159
+ };
160
+ };
161
+ /**
162
+ * Serialize a resolver output while preserving resolver metadata.
163
+ *
164
+ * @param color - Result of the core resolver.
165
+ * @param output - Serialization options for precision, gamut mapping, and output spaces.
166
+ */
167
+ export const serializeResolved = (color, output) => {
168
+ const meta = {
169
+ step: color.step,
170
+ variantUsed: color.variantUsed,
171
+ seedUsed: color.seedUsed,
172
+ };
173
+ return serializeColor(color.oklch, output, meta);
174
+ };
175
+ export const serializeColorJson = (color, output, meta) => {
176
+ // JSON export is string-based and mirrors CSS serialization intentionally.
177
+ return serializeColor(color, output, meta);
178
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createTheme } from "../core/createTheme.js";
3
+ import { serializeColor, serializeResolved } from "./serializeColor.js";
4
+ describe("serializeResolved", () => {
5
+ it("includes resolver metadata when includeMeta is true", () => {
6
+ const resolved = {
7
+ oklch: { l: 60, c: 0.2, h: 40, alpha: 0.5 },
8
+ step: 7,
9
+ variantUsed: "accent",
10
+ seedUsed: "#123456",
11
+ };
12
+ const result = serializeResolved(resolved, { includeMeta: true, preferSpace: "oklch" });
13
+ expect(result.meta?.step).toBe(7);
14
+ expect(result.meta?.variantUsed).toBe("accent");
15
+ expect(result.meta?.seedUsed).toBe("#123456");
16
+ expect(result.meta?.spaceUsed).toBe("oklch");
17
+ expect(result.meta?.gamutMapping).toBe("preferP3ThenCompress");
18
+ });
19
+ });
20
+ describe("serializeColor", () => {
21
+ it("uses rgba output when explicitly requested", () => {
22
+ const color = { l: 60, c: 0.2, h: 40, alpha: 1 };
23
+ const result = serializeColor(color, { preferSpace: "srgb", srgbFormat: "rgba" });
24
+ expect(result.value.startsWith("rgba(")).toBe(true);
25
+ });
26
+ });
27
+ describe("theme.serialize", () => {
28
+ it("serializes a resolved query using the theme", () => {
29
+ const theme = createTheme({
30
+ seeds: {
31
+ light: { neutral: "#8B8D98", accent: "#3D63DD" },
32
+ dark: { neutral: "#8B8D98", accent: "#3D63DD" },
33
+ },
34
+ preset: "modern",
35
+ });
36
+ const result = theme.serialize({
37
+ role: "bg.solid",
38
+ usage: "bg",
39
+ surface: "solid",
40
+ context: "light",
41
+ }, { preferSpace: "oklch" });
42
+ expect(result.value.startsWith("oklch(")).toBe(true);
43
+ expect(result.alpha).toBeGreaterThanOrEqual(0);
44
+ });
45
+ });
@@ -0,0 +1 @@
1
+ export * from "./serialize/index.js";
@@ -0,0 +1 @@
1
+ export * from "./serialize/index.js";
@@ -0,0 +1,187 @@
1
+ export type CssColorString = string;
2
+ export type ColorSpace = "srgb" | "p3" | "oklch";
3
+ export type ColorContext = "light" | "dark" | "highContrast" | "dimmed";
4
+ export type SurfaceIntent = "app" | "surface" | "subtle" | "solid" | "overlay" | "data" | "transparent";
5
+ export type ColorState = "default" | "hover" | "active" | "selected" | "focus" | "disabled";
6
+ export type ColorEmphasis = "muted" | "subtle" | "default" | "strong" | "inverted";
7
+ export type SemanticVariant = "neutral" | "accent" | "success" | "warning" | "danger" | "info" | "highlight" | "premium" | `category:${string}` | `chart:${string}`;
8
+ export type ColorRole = string;
9
+ export type ColorUsage = "bg" | "border" | "text" | "icon" | "ring" | "shadow" | "stroke" | "fill";
10
+ /**
11
+ * Token-safe background hints.
12
+ * Tokens must not embed literal colors.
13
+ */
14
+ export type TokenBackgroundHint = {
15
+ kind: "auto";
16
+ } | {
17
+ kind: "role";
18
+ role: ColorRole;
19
+ };
20
+ /**
21
+ * Token-safe query shape (Phase 3).
22
+ * - `output` is forbidden (export/serializer concern)
23
+ * - `on.kind: "color"` is forbidden (no embedded colors)
24
+ * - `state` must not be encoded (states are operators; declare via TokenDefinition.states)
25
+ */
26
+ export type TokenQuery = Omit<ColorQuery, "output" | "on" | "state"> & {
27
+ output?: never;
28
+ on?: TokenBackgroundHint;
29
+ state?: never;
30
+ };
31
+ /**
32
+ * Token-supported interactive states.
33
+ *
34
+ * Note: `"default"` is the base token, so it is intentionally excluded here.
35
+ */
36
+ export type TokenState = Exclude<ColorState, "default">;
37
+ /**
38
+ * Declarative set of supported states for a token.
39
+ * Use `true` to mark a state as supported.
40
+ */
41
+ export type TokenStates = Partial<Record<TokenState, true>>;
42
+ export type BackgroundHint = {
43
+ kind: "auto";
44
+ } | {
45
+ kind: "role";
46
+ role: ColorRole;
47
+ } | {
48
+ kind: "color";
49
+ value: CssColorString;
50
+ };
51
+ export type ContrastRequirement = {
52
+ model: "apca";
53
+ targetLc: number;
54
+ minLc?: number;
55
+ maxLc?: number;
56
+ } | {
57
+ model: "wcag2";
58
+ minRatio: number;
59
+ } | {
60
+ model: "none";
61
+ };
62
+ export type AlphaStrategy = {
63
+ mode: "none";
64
+ } | {
65
+ mode: "fixed";
66
+ alpha: number;
67
+ } | {
68
+ mode: "solveOnBackground";
69
+ };
70
+ export interface OutputOptions {
71
+ preferSpace?: ColorSpace;
72
+ includeSpaces?: ColorSpace[];
73
+ gamutMapping?: "clip" | "compressChroma" | "preferP3ThenCompress";
74
+ strict?: boolean;
75
+ precision?: {
76
+ l?: number;
77
+ c?: number;
78
+ h?: number;
79
+ alpha?: number;
80
+ };
81
+ includeMeta?: boolean;
82
+ srgbFormat?: "hex" | "rgb" | "rgba";
83
+ }
84
+ export interface RawColor {
85
+ space: ColorSpace;
86
+ channels: number[];
87
+ alpha: number;
88
+ }
89
+ export interface ColorMeta {
90
+ role?: ColorRole;
91
+ variant?: SemanticVariant;
92
+ usage?: ColorUsage;
93
+ context?: ColorContext;
94
+ surface?: SurfaceIntent;
95
+ state?: ColorState;
96
+ emphasis?: ColorEmphasis;
97
+ on?: BackgroundHint;
98
+ contrast?: ContrastRequirement;
99
+ step?: number;
100
+ variantUsed?: string;
101
+ seedUsed?: CssColorString;
102
+ gamutMapping?: OutputOptions["gamutMapping"];
103
+ spaceUsed?: ColorSpace;
104
+ clipped?: boolean;
105
+ compressed?: boolean;
106
+ provenance?: string;
107
+ }
108
+ /**
109
+ * Declarative token definition consumed by registries, exporters, CLI and codegen.
110
+ *
111
+ * Rules:
112
+ * - Tokens never carry actual color values.
113
+ * - `query.output` is forbidden; output formatting is decided by serializers/exporters.
114
+ * - Do not encode interactive state in `query.state`; declare supported states via `states`.
115
+ * - Do not embed literal background colors via `query.on: { kind: "color" }`.
116
+ */
117
+ export interface TokenDefinition {
118
+ name: string;
119
+ description?: string;
120
+ query: TokenQuery;
121
+ category?: string;
122
+ states?: TokenStates;
123
+ }
124
+ /**
125
+ * Collection of base token definitions keyed by token name.
126
+ */
127
+ export interface TokenRegistry {
128
+ tokens: Record<string, TokenDefinition>;
129
+ }
130
+ export interface ResolvedColor {
131
+ /**
132
+ * Serialized string corresponding to `preferSpace`.
133
+ * Always present.
134
+ */
135
+ value: CssColorString;
136
+ /**
137
+ * Auxiliary sRGB representation.
138
+ * Only present if included via `includeSpaces`.
139
+ */
140
+ srgb?: CssColorString;
141
+ /**
142
+ * Auxiliary Display-P3 representation.
143
+ * Only present if included via `includeSpaces`.
144
+ */
145
+ p3?: CssColorString;
146
+ /**
147
+ * Auxiliary OKLCH representation.
148
+ * Only present if included via `includeSpaces`.
149
+ */
150
+ oklch?: CssColorString;
151
+ alpha: number;
152
+ meta?: ColorMeta;
153
+ }
154
+ export interface ColorQuery {
155
+ role: ColorRole;
156
+ variant?: SemanticVariant;
157
+ usage?: ColorUsage;
158
+ context?: ColorContext;
159
+ surface?: SurfaceIntent;
160
+ state?: ColorState;
161
+ emphasis?: ColorEmphasis;
162
+ on?: BackgroundHint;
163
+ contrast?: ContrastRequirement;
164
+ alpha?: AlphaStrategy;
165
+ output?: OutputOptions;
166
+ }
167
+ export interface OnSolidQuery {
168
+ bgRole: ColorRole;
169
+ usage: "text" | "icon";
170
+ context?: ColorContext;
171
+ state?: ColorState;
172
+ emphasis?: ColorEmphasis;
173
+ alpha?: AlphaStrategy;
174
+ contrast?: ContrastRequirement;
175
+ output?: OutputOptions;
176
+ }
177
+ export interface SemanticColorTheme {
178
+ resolve(query: ColorQuery): ResolvedColor;
179
+ resolveMany(queries: ColorQuery[]): ResolvedColor[];
180
+ color(role: ColorRole, options?: Omit<ColorQuery, "role">): ResolvedColor;
181
+ onSolid(query: OnSolidQuery): ResolvedColor;
182
+ withContext(context: ColorContext): SemanticColorTheme;
183
+ export: {
184
+ cssVars(): string;
185
+ json(): unknown;
186
+ };
187
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare const clamp: (value: number, min: number, max: number) => number;
@@ -0,0 +1 @@
1
+ export const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
@@ -0,0 +1 @@
1
+ export { parseColor } from "./parseColor.js";
@@ -0,0 +1 @@
1
+ export { parseColor } from "./parseColor.js";
@@ -0,0 +1 @@
1
+ export declare const lerp: (start: number, end: number, t: number) => number;
@@ -0,0 +1 @@
1
+ export const lerp = (start, end, t) => start + (end - start) * t;
@@ -0,0 +1,6 @@
1
+ import type { CssColorString, RawColor } from "../types/index.js";
2
+ export declare function parseColor(input: CssColorString): {
3
+ input: CssColorString;
4
+ okLch: RawColor;
5
+ srgb: RawColor;
6
+ };
@@ -0,0 +1,67 @@
1
+ import { converter } from "culori";
2
+ const hexPattern = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
3
+ const toOklch = converter("oklch");
4
+ const OKLCH_L_MAX = 100;
5
+ const OKLCH_H_MAX = 360;
6
+ const MAX_OKLCH_CHROMA = 0.4;
7
+ const NEGATIVE_CHROMA_EPSILON = 1e-6;
8
+ const toChannel = (hex) => Number.parseInt(hex, 16) / 255;
9
+ const normalizeHue = (hue) => ((hue % OKLCH_H_MAX) + OKLCH_H_MAX) % OKLCH_H_MAX;
10
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
11
+ export function parseColor(input) {
12
+ const normalizedInput = input.trim();
13
+ if (!hexPattern.test(normalizedInput)) {
14
+ throw new Error(`Invalid color input: "${input}"`);
15
+ }
16
+ const hex = normalizedInput.slice(1);
17
+ let r = 0;
18
+ let g = 0;
19
+ let b = 0;
20
+ let alpha = 1;
21
+ if (hex.length === 3) {
22
+ r = toChannel(`${hex[0]}${hex[0]}`);
23
+ g = toChannel(`${hex[1]}${hex[1]}`);
24
+ b = toChannel(`${hex[2]}${hex[2]}`);
25
+ }
26
+ else if (hex.length === 6 || hex.length === 8) {
27
+ r = toChannel(hex.slice(0, 2));
28
+ g = toChannel(hex.slice(2, 4));
29
+ b = toChannel(hex.slice(4, 6));
30
+ if (hex.length === 8) {
31
+ alpha = toChannel(hex.slice(6, 8));
32
+ }
33
+ }
34
+ else {
35
+ throw new Error(`Invalid hex length: "${normalizedInput}"`);
36
+ }
37
+ const oklchValue = toOklch({ mode: "rgb", r, g, b });
38
+ if (!oklchValue) {
39
+ throw new Error(`Unable to convert color input: "${normalizedInput}" (r=${r}, g=${g}, b=${b})`);
40
+ }
41
+ const l = typeof oklchValue.l === "number" && Number.isFinite(oklchValue.l) ? oklchValue.l : 0;
42
+ const c = typeof oklchValue.c === "number" && Number.isFinite(oklchValue.c) ? oklchValue.c : 0;
43
+ const h = typeof oklchValue.h === "number" && Number.isFinite(oklchValue.h) ? oklchValue.h : 0;
44
+ const scaledLightness = l * 100;
45
+ if (c < -NEGATIVE_CHROMA_EPSILON) {
46
+ throw new Error(`Invalid OKLCH chroma value: ${c} for "${input}"`);
47
+ }
48
+ const okLch = {
49
+ space: "oklch",
50
+ channels: [
51
+ clamp(scaledLightness, 0, OKLCH_L_MAX),
52
+ clamp(Math.max(0, c), 0, MAX_OKLCH_CHROMA),
53
+ normalizeHue(h),
54
+ ],
55
+ alpha,
56
+ };
57
+ const srgb = {
58
+ space: "srgb",
59
+ channels: [r, g, b],
60
+ alpha,
61
+ };
62
+ return {
63
+ input: normalizedInput,
64
+ okLch,
65
+ srgb,
66
+ };
67
+ }