@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,3 @@
1
+ export { applyEmphasisOperator } from "./emphasis.js";
2
+ export { applyStateOperator } from "./state.js";
3
+ export type { OperatorInput } from "./types.js";
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { applyStateOperator } from "./state.js";
4
+
5
+ const base = {
6
+ oklch: { l: 60, c: 0.12, h: 210 },
7
+ context: "light" as const,
8
+ surface: "surface" as const,
9
+ usage: "bg" as const,
10
+ state: "default" as const,
11
+ emphasis: "default" as const,
12
+ preset: "modern" as const,
13
+ step: 9,
14
+ };
15
+
16
+ describe("applyStateOperator", () => {
17
+ it("darkens hover in light context", () => {
18
+ const result = applyStateOperator({ ...base, state: "hover" });
19
+
20
+ expect(result.l).toBeLessThan(base.oklch.l);
21
+ });
22
+
23
+ it("lightens hover in dark context", () => {
24
+ const result = applyStateOperator({ ...base, context: "dark", state: "hover" });
25
+
26
+ expect(result.l).toBeGreaterThan(base.oklch.l);
27
+ });
28
+
29
+ it("active is stronger than hover", () => {
30
+ const hover = applyStateOperator({ ...base, state: "hover" });
31
+ const active = applyStateOperator({ ...base, state: "active" });
32
+
33
+ expect(Math.abs(active.l - base.oklch.l)).toBeGreaterThan(Math.abs(hover.l - base.oklch.l));
34
+ });
35
+
36
+ it("selected is less aggressive than active", () => {
37
+ const selected = applyStateOperator({ ...base, state: "selected" });
38
+ const active = applyStateOperator({ ...base, state: "active" });
39
+
40
+ expect(Math.abs(selected.l - base.oklch.l)).toBeLessThan(Math.abs(active.l - base.oklch.l));
41
+ });
42
+
43
+ it("disabled reduces chroma", () => {
44
+ const disabled = applyStateOperator({ ...base, state: "disabled" });
45
+
46
+ expect(disabled.c).toBeLessThan(base.oklch.c * 0.6);
47
+ });
48
+
49
+ it("returns the original color for default state", () => {
50
+ const result = applyStateOperator({ ...base, state: "default" });
51
+
52
+ expect(result).toEqual(base.oklch);
53
+ });
54
+
55
+ it("focus only affects ring and border usage", () => {
56
+ const bgFocus = applyStateOperator({ ...base, usage: "bg", state: "focus" });
57
+ const ringFocus = applyStateOperator({ ...base, usage: "ring", state: "focus" });
58
+ const borderFocus = applyStateOperator({ ...base, usage: "border", state: "focus" });
59
+
60
+ expect(bgFocus.l).toBeCloseTo(base.oklch.l, 6);
61
+ expect(ringFocus.l).not.toBeCloseTo(base.oklch.l, 6);
62
+ expect(Math.abs(borderFocus.l - base.oklch.l)).toBeLessThan(
63
+ Math.abs(ringFocus.l - base.oklch.l),
64
+ );
65
+ });
66
+ });
@@ -0,0 +1,122 @@
1
+ import type { OkLchColor } from "../engine/generateScale.js";
2
+ import { clamp } from "../utils/clamp.js";
3
+ import type { OperatorInput } from "./types.js";
4
+ import { getNeutralL, getSurfaceRange } from "./utils.js";
5
+
6
+ type StateTuning = {
7
+ hover: number;
8
+ active: number;
9
+ selected: number;
10
+ focus: number;
11
+ disabledChroma: number;
12
+ disabledNeutralPull: number;
13
+ };
14
+
15
+ const STATE_TUNING: Record<OperatorInput["surface"], StateTuning> = {
16
+ app: {
17
+ hover: 2,
18
+ active: 4,
19
+ selected: 3,
20
+ focus: 2,
21
+ disabledChroma: 0.45,
22
+ disabledNeutralPull: 0.45,
23
+ },
24
+ surface: {
25
+ hover: 2.5,
26
+ active: 4.5,
27
+ selected: 3.5,
28
+ focus: 2.5,
29
+ disabledChroma: 0.45,
30
+ disabledNeutralPull: 0.45,
31
+ },
32
+ subtle: {
33
+ hover: 3,
34
+ active: 5.5,
35
+ selected: 4,
36
+ focus: 3,
37
+ disabledChroma: 0.45,
38
+ disabledNeutralPull: 0.5,
39
+ },
40
+ solid: {
41
+ hover: 4,
42
+ active: 7,
43
+ selected: 5,
44
+ focus: 4,
45
+ disabledChroma: 0.4,
46
+ disabledNeutralPull: 0.55,
47
+ },
48
+ overlay: {
49
+ hover: 3,
50
+ active: 5.5,
51
+ selected: 4,
52
+ focus: 3,
53
+ disabledChroma: 0.45,
54
+ disabledNeutralPull: 0.5,
55
+ },
56
+ data: {
57
+ hover: 4,
58
+ active: 7,
59
+ selected: 5,
60
+ focus: 4,
61
+ disabledChroma: 0.4,
62
+ disabledNeutralPull: 0.55,
63
+ },
64
+ transparent: {
65
+ hover: 2,
66
+ active: 4,
67
+ selected: 3,
68
+ focus: 2,
69
+ disabledChroma: 0.45,
70
+ disabledNeutralPull: 0.45,
71
+ },
72
+ };
73
+
74
+ const FOCUS_BORDER_SCALE = 0.6;
75
+
76
+ const clampOkLch = (color: OkLchColor, maxChroma: number): OkLchColor => ({
77
+ ...color,
78
+ l: clamp(color.l, 0, 100),
79
+ c: clamp(color.c, 0, maxChroma),
80
+ });
81
+
82
+ export const applyStateOperator = (input: OperatorInput): OkLchColor => {
83
+ const { state, context, surface, usage } = input;
84
+ const tuning = STATE_TUNING[surface];
85
+ const range = getSurfaceRange(input.preset, surface, context);
86
+ const neutralL = getNeutralL(input.preset, surface, context);
87
+ const direction = context === "light" ? -1 : 1;
88
+
89
+ if (state === "default") {
90
+ return input.oklch;
91
+ }
92
+
93
+ if (state === "focus" && usage !== "ring" && usage !== "border") {
94
+ return input.oklch;
95
+ }
96
+
97
+ const next: OkLchColor = { ...input.oklch };
98
+
99
+ switch (state) {
100
+ case "hover":
101
+ next.l += direction * tuning.hover;
102
+ break;
103
+ case "active":
104
+ next.l += direction * tuning.active;
105
+ break;
106
+ case "selected":
107
+ next.l += direction * tuning.selected;
108
+ break;
109
+ case "focus":
110
+ next.l += direction * tuning.focus * (usage === "border" ? FOCUS_BORDER_SCALE : 1);
111
+ break;
112
+ case "disabled":
113
+ next.c *= tuning.disabledChroma;
114
+ next.l += (neutralL - next.l) * tuning.disabledNeutralPull;
115
+ break;
116
+ default:
117
+ // Allow unknown/future states to be no-ops to avoid unexpected shifts.
118
+ return input.oklch;
119
+ }
120
+
121
+ return clampOkLch(next, range.cMax);
122
+ };
@@ -0,0 +1,14 @@
1
+ import type { OkLchColor } from "../engine/generateScale.js";
2
+ import type { CurvePresetName } from "../presets/index.js";
3
+ import type { ColorEmphasis, ColorState, ColorUsage, SurfaceIntent } from "../types/index.js";
4
+
5
+ export type OperatorInput = {
6
+ oklch: OkLchColor;
7
+ context: "light" | "dark";
8
+ surface: SurfaceIntent;
9
+ usage: ColorUsage;
10
+ state: ColorState;
11
+ emphasis: ColorEmphasis;
12
+ preset?: CurvePresetName;
13
+ step: number;
14
+ };
@@ -0,0 +1,44 @@
1
+ import type { CurvePresetName } from "../presets/index.js";
2
+ import { curvePresets } from "../presets/index.js";
3
+ import type { SurfaceIntent } from "../types/index.js";
4
+ import { clamp } from "../utils/clamp.js";
5
+ import { lerp } from "../utils/lerp.js";
6
+
7
+ // TODO: deduplicate with generateScale constants and helpers.
8
+ const STEPS = 12;
9
+ const OKLCH_L_MIN = 0;
10
+ const OKLCH_L_MAX = 100;
11
+
12
+ export type OperatorContext = "light" | "dark";
13
+
14
+ export const getSurfaceRange = (
15
+ presetName: CurvePresetName | undefined,
16
+ surface: SurfaceIntent,
17
+ context: OperatorContext,
18
+ ) => {
19
+ const preset = curvePresets[presetName ?? "modern"];
20
+ return preset.surfaces[surface].ranges[context];
21
+ };
22
+
23
+ export const getNeutralL = (
24
+ presetName: CurvePresetName | undefined,
25
+ surface: SurfaceIntent,
26
+ context: OperatorContext,
27
+ ) => {
28
+ const range = getSurfaceRange(presetName, surface, context);
29
+ return (range.l[0] + range.l[1]) / 2;
30
+ };
31
+
32
+ export const getStepLightness = (
33
+ presetName: CurvePresetName | undefined,
34
+ surface: SurfaceIntent,
35
+ context: OperatorContext,
36
+ step: number,
37
+ ) => {
38
+ const preset = curvePresets[presetName ?? "modern"];
39
+ const surfaceCurve = preset.surfaces[surface];
40
+ const range = surfaceCurve.ranges[context];
41
+ const t = clamp((step - 1) / (STEPS - 1), 0, 1);
42
+ const lightnessT = surfaceCurve.l(t);
43
+ return clamp(lerp(range.l[0], range.l[1], lightnessT), OKLCH_L_MIN, OKLCH_L_MAX);
44
+ };
@@ -0,0 +1,168 @@
1
+ import type { SurfaceIntent } from "../types/index.js";
2
+ import { clamp } from "../utils/clamp.js";
3
+ import { smoothstep } from "../utils/smoothstep.js";
4
+
5
+ export type CurvePresetName = "modern" | "radixLike";
6
+
7
+ export type CurveFn = (t: number) => number;
8
+
9
+ export type SurfaceCurve = {
10
+ l: CurveFn;
11
+ c: CurveFn;
12
+ ranges: {
13
+ light: { l: [number, number]; cMin: number; cMax: number; chromaBoost?: number };
14
+ dark: { l: [number, number]; cMin: number; cMax: number; chromaBoost?: number };
15
+ };
16
+ };
17
+
18
+ export type CurvePreset = {
19
+ name: CurvePresetName;
20
+ surfaces: Record<SurfaceIntent, SurfaceCurve>;
21
+ };
22
+
23
+ const normalizeT = (t: number) => clamp(t, 0, 1);
24
+ const lightnessCurve: CurveFn = (t) => smoothstep(normalizeT(t));
25
+ const chromaCurve: CurveFn = (t) => Math.sin(Math.PI * normalizeT(t));
26
+
27
+ export const modern: CurvePreset = {
28
+ name: "modern",
29
+ surfaces: {
30
+ // app: fundo do app (separação mínima, chroma baixíssimo)
31
+ app: {
32
+ l: lightnessCurve,
33
+ c: chromaCurve,
34
+ ranges: {
35
+ light: { l: [90, 99], cMin: 0.004, cMax: 0.05 },
36
+ dark: { l: [6, 22], cMin: 0.004, cMax: 0.07 },
37
+ },
38
+ },
39
+ // surface: cards/panels (separação leve do app bg, chroma baixo)
40
+ surface: {
41
+ l: lightnessCurve,
42
+ c: chromaCurve,
43
+ ranges: {
44
+ light: { l: [84, 97], cMin: 0.008, cMax: 0.08 },
45
+ dark: { l: [8, 28], cMin: 0.008, cMax: 0.09 },
46
+ },
47
+ },
48
+ // subtle: tints/hover backgrounds (um pouco mais de chroma, ainda calmo)
49
+ subtle: {
50
+ l: lightnessCurve,
51
+ c: chromaCurve,
52
+ ranges: {
53
+ light: { l: [78, 95], cMin: 0.012, cMax: 0.1 },
54
+ dark: { l: [10, 32], cMin: 0.012, cMax: 0.11 },
55
+ },
56
+ },
57
+ // solid: backgrounds sólidos (chroma mais alto, pensado para onSolid)
58
+ solid: {
59
+ l: lightnessCurve,
60
+ c: chromaCurve,
61
+ ranges: {
62
+ light: { l: [46, 90], cMin: 0.03, cMax: 0.18, chromaBoost: 1.25 },
63
+ dark: { l: [12, 42], cMin: 0.03, cMax: 0.2, chromaBoost: 1.25 },
64
+ },
65
+ },
66
+ // overlay: modal surfaces/scrims (chroma baixo, controle forte de L)
67
+ overlay: {
68
+ l: lightnessCurve,
69
+ c: chromaCurve,
70
+ ranges: {
71
+ light: { l: [72, 96], cMin: 0.01, cMax: 0.09 },
72
+ dark: { l: [14, 40], cMin: 0.01, cMax: 0.1 },
73
+ },
74
+ },
75
+ // data: charts/heatmaps (pode usar chroma maior sem “gritar”)
76
+ data: {
77
+ l: lightnessCurve,
78
+ c: chromaCurve,
79
+ ranges: {
80
+ light: { l: [38, 86], cMin: 0.04, cMax: 0.22, chromaBoost: 1.3 },
81
+ dark: { l: [18, 48], cMin: 0.04, cMax: 0.24, chromaBoost: 1.3 },
82
+ },
83
+ },
84
+ // transparent: base quase neutra (chroma quase zero; usado para ghost)
85
+ transparent: {
86
+ l: lightnessCurve,
87
+ c: chromaCurve,
88
+ ranges: {
89
+ light: { l: [62, 96], cMin: 0, cMax: 0.06 },
90
+ dark: { l: [8, 30], cMin: 0, cMax: 0.07 },
91
+ },
92
+ },
93
+ },
94
+ };
95
+
96
+ export const radixLike: CurvePreset = {
97
+ name: "radixLike",
98
+ surfaces: {
99
+ // app: fundo do app (separação mínima, chroma baixíssimo)
100
+ app: {
101
+ l: lightnessCurve,
102
+ c: chromaCurve,
103
+ ranges: {
104
+ light: { l: [92, 99], cMin: 0.003, cMax: 0.06 },
105
+ dark: { l: [4, 18], cMin: 0.003, cMax: 0.08 },
106
+ },
107
+ },
108
+ // surface: cards/panels (separação leve do app bg, chroma baixo)
109
+ surface: {
110
+ l: lightnessCurve,
111
+ c: chromaCurve,
112
+ ranges: {
113
+ light: { l: [86, 97], cMin: 0.006, cMax: 0.1 },
114
+ dark: { l: [6, 24], cMin: 0.006, cMax: 0.11 },
115
+ },
116
+ },
117
+ // subtle: tints/hover backgrounds (um pouco mais de chroma, ainda calmo)
118
+ subtle: {
119
+ l: lightnessCurve,
120
+ c: chromaCurve,
121
+ ranges: {
122
+ light: { l: [80, 94], cMin: 0.01, cMax: 0.12 },
123
+ dark: { l: [8, 30], cMin: 0.01, cMax: 0.13 },
124
+ },
125
+ },
126
+ // solid: backgrounds sólidos (chroma mais alto, pensado para onSolid)
127
+ solid: {
128
+ l: lightnessCurve,
129
+ c: chromaCurve,
130
+ ranges: {
131
+ light: { l: [50, 88], cMin: 0.035, cMax: 0.2, chromaBoost: 1.25 },
132
+ dark: { l: [10, 38], cMin: 0.035, cMax: 0.22, chromaBoost: 1.25 },
133
+ },
134
+ },
135
+ // overlay: modal surfaces/scrims (chroma baixo, controle forte de L)
136
+ overlay: {
137
+ l: lightnessCurve,
138
+ c: chromaCurve,
139
+ ranges: {
140
+ light: { l: [74, 96], cMin: 0.01, cMax: 0.11 },
141
+ dark: { l: [12, 38], cMin: 0.01, cMax: 0.12 },
142
+ },
143
+ },
144
+ // data: charts/heatmaps (pode usar chroma maior sem “gritar”)
145
+ data: {
146
+ l: lightnessCurve,
147
+ c: chromaCurve,
148
+ ranges: {
149
+ light: { l: [40, 84], cMin: 0.05, cMax: 0.24, chromaBoost: 1.3 },
150
+ dark: { l: [16, 46], cMin: 0.05, cMax: 0.26, chromaBoost: 1.3 },
151
+ },
152
+ },
153
+ // transparent: base quase neutra (chroma quase zero; usado para ghost)
154
+ transparent: {
155
+ l: lightnessCurve,
156
+ c: chromaCurve,
157
+ ranges: {
158
+ light: { l: [60, 94], cMin: 0, cMax: 0.08 },
159
+ dark: { l: [6, 28], cMin: 0, cMax: 0.09 },
160
+ },
161
+ },
162
+ },
163
+ };
164
+
165
+ export const curvePresets: Record<CurvePresetName, CurvePreset> = {
166
+ modern,
167
+ radixLike,
168
+ };
@@ -0,0 +1,2 @@
1
+ export type { CurveFn, CurvePreset, CurvePresetName, SurfaceCurve } from "./curves.js";
2
+ export { curvePresets, modern, radixLike } from "./curves.js";
@@ -0,0 +1,3 @@
1
+ export { minimalUiTokens } from "./minimal-ui.js";
2
+ export { modernUiTokens } from "./modern-ui.js";
3
+ export { radixLikeUiTokens } from "./radixLike-ui.js";
@@ -0,0 +1,55 @@
1
+ import type { TokenRegistry } from "../../types/index.js";
2
+
3
+ /**
4
+ * Minimal token preset aimed at quick adoption.
5
+ * Focuses on a small, coherent base set for app surfaces and text.
6
+ */
7
+ export const minimalUiTokens: TokenRegistry = {
8
+ tokens: {
9
+ "bg.app": {
10
+ name: "bg.app",
11
+ description: "Base application background.",
12
+ category: "background",
13
+ query: { role: "bg.app", usage: "bg", surface: "app" },
14
+ },
15
+ "bg.surface": {
16
+ name: "bg.surface",
17
+ description: "Default surface background for containers.",
18
+ category: "background",
19
+ query: { role: "bg.surface", usage: "bg", surface: "surface" },
20
+ },
21
+ "text.primary": {
22
+ name: "text.primary",
23
+ description: "Primary text on standard surfaces.",
24
+ category: "text",
25
+ query: { role: "text.primary", usage: "text", surface: "surface" },
26
+ },
27
+ "text.secondary": {
28
+ name: "text.secondary",
29
+ description: "Secondary text on standard surfaces.",
30
+ category: "text",
31
+ query: { role: "text.secondary", usage: "text", surface: "surface", emphasis: "muted" },
32
+ },
33
+ "border.default": {
34
+ name: "border.default",
35
+ description: "Default border for surfaces and containers.",
36
+ category: "border",
37
+ query: { role: "border.default", usage: "border", surface: "surface" },
38
+ states: { hover: true },
39
+ },
40
+ "icon.default": {
41
+ name: "icon.default",
42
+ description: "Default icon color on standard surfaces.",
43
+ category: "icon",
44
+ query: { role: "icon.default", usage: "icon", surface: "surface" },
45
+ states: { hover: true },
46
+ },
47
+ "ring.default": {
48
+ name: "ring.default",
49
+ description: "Base ring color (derive focus via state operator).",
50
+ category: "ring",
51
+ query: { role: "ring.default", usage: "ring", surface: "surface", emphasis: "strong" },
52
+ states: { focus: true },
53
+ },
54
+ },
55
+ };
@@ -0,0 +1,85 @@
1
+ import type { TokenRegistry } from "../../types/index.js";
2
+
3
+ /**
4
+ * Modern preset aimed at richer UI surfaces with strong emphasis options.
5
+ */
6
+ export const modernUiTokens: TokenRegistry = {
7
+ tokens: {
8
+ "bg.app": {
9
+ name: "bg.app",
10
+ description: "Primary application background.",
11
+ category: "background",
12
+ query: { role: "bg.app", usage: "bg", surface: "app" },
13
+ },
14
+ "bg.surface": {
15
+ name: "bg.surface",
16
+ description: "Default surface background for content containers.",
17
+ category: "background",
18
+ query: { role: "bg.surface", usage: "bg", surface: "surface" },
19
+ },
20
+ "bg.subtle": {
21
+ name: "bg.subtle",
22
+ description: "Subtle surface for secondary sections.",
23
+ category: "background",
24
+ query: { role: "bg.subtle", usage: "bg", surface: "subtle" },
25
+ },
26
+ "bg.solid": {
27
+ name: "bg.solid",
28
+ description: "Solid surface for emphasized elements.",
29
+ category: "background",
30
+ query: { role: "bg.solid", usage: "bg", surface: "solid" },
31
+ states: { hover: true, active: true },
32
+ },
33
+ "text.primary": {
34
+ name: "text.primary",
35
+ description: "Primary text for default surfaces.",
36
+ category: "text",
37
+ query: { role: "text.primary", usage: "text", surface: "surface" },
38
+ },
39
+ "text.secondary": {
40
+ name: "text.secondary",
41
+ description: "Secondary text for supporting content.",
42
+ category: "text",
43
+ query: { role: "text.secondary", usage: "text", surface: "surface", emphasis: "muted" },
44
+ },
45
+ "text.inverse": {
46
+ name: "text.inverse",
47
+ description: "Inverse text for solid or accented surfaces.",
48
+ category: "text",
49
+ query: { role: "text.inverse", usage: "text", surface: "solid", emphasis: "inverted" },
50
+ },
51
+ "border.default": {
52
+ name: "border.default",
53
+ description: "Default border for containers and layout.",
54
+ category: "border",
55
+ query: { role: "border.default", usage: "border", surface: "surface" },
56
+ },
57
+ "border.strong": {
58
+ name: "border.strong",
59
+ description: "High-emphasis border for focused containers.",
60
+ category: "border",
61
+ query: { role: "border.strong", usage: "border", surface: "surface", emphasis: "strong" },
62
+ states: { hover: true },
63
+ },
64
+ "icon.default": {
65
+ name: "icon.default",
66
+ description: "Primary icon color for default surfaces.",
67
+ category: "icon",
68
+ query: { role: "icon.default", usage: "icon", surface: "surface" },
69
+ states: { hover: true },
70
+ },
71
+ "icon.muted": {
72
+ name: "icon.muted",
73
+ description: "Muted icons for less prominent actions.",
74
+ category: "icon",
75
+ query: { role: "icon.muted", usage: "icon", surface: "surface", emphasis: "muted" },
76
+ },
77
+ "ring.default": {
78
+ name: "ring.default",
79
+ description: "Base ring color (derive focus via state operator).",
80
+ category: "ring",
81
+ query: { role: "ring.default", usage: "ring", surface: "surface", emphasis: "strong" },
82
+ states: { focus: true },
83
+ },
84
+ },
85
+ };
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { createTheme } from "../../core/createTheme.js";
4
+ import type { TokenRegistry } from "../../types/index.js";
5
+ import {
6
+ minimalUiTokens,
7
+ modernUiTokens,
8
+ radixLikeUiTokens,
9
+ } from "./index.js";
10
+ import { validateTokenRegistry } from "../../core/tokenRegistry.js";
11
+ import { exportThemeCss, exportThemeJson } from "../../export/exportTheme.js";
12
+
13
+ const toThemeTokens = (registry: TokenRegistry) =>
14
+ Object.fromEntries(
15
+ Object.entries(registry.tokens).map(([name, token]) => [name, token.query]),
16
+ );
17
+
18
+ describe("token presets", () => {
19
+ const theme = createTheme({
20
+ seeds: {
21
+ light: { neutral: "#8B8D98", accent: "#3D63DD" },
22
+ dark: { neutral: "#8B8D98", accent: "#3D63DD" },
23
+ },
24
+ preset: "modern",
25
+ });
26
+
27
+ it("validates all preset registries", () => {
28
+ expect(() => validateTokenRegistry(minimalUiTokens)).not.toThrow();
29
+ expect(() => validateTokenRegistry(radixLikeUiTokens)).not.toThrow();
30
+ expect(() => validateTokenRegistry(modernUiTokens)).not.toThrow();
31
+ });
32
+
33
+ it("exports CSS/JSON for each preset", () => {
34
+ const registries = [minimalUiTokens, radixLikeUiTokens, modernUiTokens];
35
+
36
+ for (const registry of registries) {
37
+ const tokens = toThemeTokens(registry);
38
+ const css = exportThemeCss(theme, tokens, { preferSpace: "oklch" }).css;
39
+ const json = exportThemeJson(theme, tokens, { preferSpace: "oklch" }).tokens;
40
+
41
+ expect(css.length).toBeGreaterThan(0);
42
+ expect(Object.keys(json.light).length).toBeGreaterThan(0);
43
+ expect(Object.keys(json.dark).length).toBeGreaterThan(0);
44
+ }
45
+ });
46
+ });