@trishchuk/coolors-mcp 1.0.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 (197) hide show
  1. package/.claude/settings.local.json +39 -0
  2. package/.env +2 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +73 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +71 -0
  5. package/.github/pull_request_template.md +97 -0
  6. package/.github/workflows/ci.yml +127 -0
  7. package/.github/workflows/deploy-docs.yml +56 -0
  8. package/.github/workflows/release.yml +99 -0
  9. package/.mcp.json +12 -0
  10. package/.prettierignore +1 -0
  11. package/CLAUDE.md +201 -0
  12. package/DOCUMENTATION.md +274 -0
  13. package/GEMINI.md +54 -0
  14. package/LICENSE +21 -0
  15. package/README.md +401 -0
  16. package/demo/content_based_color.png +0 -0
  17. package/demo/music-player.html +621 -0
  18. package/demo/podcast-player.html +903 -0
  19. package/dist/bin/coolors-mcp.d.ts +1 -0
  20. package/dist/bin/coolors-mcp.js +154 -0
  21. package/dist/bin/coolors-mcp.js.map +1 -0
  22. package/dist/bin/server.d.ts +1 -0
  23. package/dist/bin/server.js +3292 -0
  24. package/dist/bin/server.js.map +1 -0
  25. package/dist/chunk-IQ7NN26V.js +114 -0
  26. package/dist/chunk-IQ7NN26V.js.map +1 -0
  27. package/dist/chunk-P3ARRKLS.js +1214 -0
  28. package/dist/chunk-P3ARRKLS.js.map +1 -0
  29. package/dist/color/index.d.ts +716 -0
  30. package/dist/color/index.js +153 -0
  31. package/dist/color/index.js.map +1 -0
  32. package/dist/coolors-mcp.d.ts +136 -0
  33. package/dist/coolors-mcp.js +7 -0
  34. package/dist/coolors-mcp.js.map +1 -0
  35. package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js +93 -0
  36. package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js.map +7 -0
  37. package/docs/.vitepress/cache/deps/_metadata.json +127 -0
  38. package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js +9 -0
  39. package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +7 -0
  40. package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js +12683 -0
  41. package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js.map +7 -0
  42. package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js +9719 -0
  43. package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js.map +7 -0
  44. package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js +4710 -0
  45. package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +7 -0
  46. package/docs/.vitepress/cache/deps/cytoscape.js +30278 -0
  47. package/docs/.vitepress/cache/deps/cytoscape.js.map +7 -0
  48. package/docs/.vitepress/cache/deps/dayjs.js +285 -0
  49. package/docs/.vitepress/cache/deps/dayjs.js.map +7 -0
  50. package/docs/.vitepress/cache/deps/debug.js +468 -0
  51. package/docs/.vitepress/cache/deps/debug.js.map +7 -0
  52. package/docs/.vitepress/cache/deps/package.json +3 -0
  53. package/docs/.vitepress/cache/deps/prismjs.js +1466 -0
  54. package/docs/.vitepress/cache/deps/prismjs.js.map +7 -0
  55. package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js +228 -0
  56. package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js.map +7 -0
  57. package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js +142 -0
  58. package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js.map +7 -0
  59. package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js +27 -0
  60. package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js.map +7 -0
  61. package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js +65 -0
  62. package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js.map +7 -0
  63. package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js +53 -0
  64. package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js.map +7 -0
  65. package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js +73 -0
  66. package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js.map +7 -0
  67. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4507 -0
  68. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
  69. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +584 -0
  70. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
  71. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +1146 -0
  72. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
  73. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +1667 -0
  74. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +7 -0
  75. package/docs/.vitepress/cache/deps/vitepress___minisearch.js +1814 -0
  76. package/docs/.vitepress/cache/deps/vitepress___minisearch.js.map +7 -0
  77. package/docs/.vitepress/cache/deps/vue.js +344 -0
  78. package/docs/.vitepress/cache/deps/vue.js.map +7 -0
  79. package/docs/.vitepress/components/ClientGrid.vue +125 -0
  80. package/docs/.vitepress/components/CodeBlock.vue +231 -0
  81. package/docs/.vitepress/components/ConfigModal.vue +477 -0
  82. package/docs/.vitepress/components/DiagramModal.vue +528 -0
  83. package/docs/.vitepress/components/TroubleshootingModal.vue +472 -0
  84. package/docs/.vitepress/config.js +162 -0
  85. package/docs/.vitepress/theme/FundingLayout.vue +251 -0
  86. package/docs/.vitepress/theme/Layout.vue +134 -0
  87. package/docs/.vitepress/theme/components/AdBanner.vue +317 -0
  88. package/docs/.vitepress/theme/components/AdPlaceholder.vue +78 -0
  89. package/docs/.vitepress/theme/components/FundingEffects.vue +322 -0
  90. package/docs/.vitepress/theme/components/FundingHero.vue +345 -0
  91. package/docs/.vitepress/theme/components/SupportSection.vue +511 -0
  92. package/docs/.vitepress/theme/custom-app.css +339 -0
  93. package/docs/.vitepress/theme/custom.css +699 -0
  94. package/docs/.vitepress/theme/index.js +25 -0
  95. package/docs/README.md +198 -0
  96. package/docs/concepts/accessibility.md +473 -0
  97. package/docs/concepts/color-spaces.md +222 -0
  98. package/docs/concepts/distance-metrics.md +384 -0
  99. package/docs/concepts/hct.md +261 -0
  100. package/docs/concepts/image-analysis.md +396 -0
  101. package/docs/concepts/material-design.md +306 -0
  102. package/docs/concepts/theme-matching.md +399 -0
  103. package/docs/examples/basic-colors.md +490 -0
  104. package/docs/examples/creating-themes.md +898 -0
  105. package/docs/examples/css-refactoring.md +824 -0
  106. package/docs/examples/image-extraction.md +882 -0
  107. package/docs/getting-started.md +366 -0
  108. package/docs/index.md +190 -0
  109. package/docs/installation.md +157 -0
  110. package/docs/tools/README.md +234 -0
  111. package/docs/tools/accessibility.md +614 -0
  112. package/docs/tools/color-operations.md +374 -0
  113. package/docs/tools/image-extraction.md +624 -0
  114. package/docs/tools/material-design.md +347 -0
  115. package/docs/tools/theme-matching.md +552 -0
  116. package/eslint.config.ts +14 -0
  117. package/examples/theme-matching.md +113 -0
  118. package/jsr.json +7 -0
  119. package/mcp-config.json +8 -0
  120. package/note.md +35 -0
  121. package/package.json +122 -0
  122. package/research_results.md +53 -0
  123. package/src/bin/coolors-mcp.ts +194 -0
  124. package/src/bin/server.ts +61 -0
  125. package/src/color/__tests__/conversions-argb.test.ts +198 -0
  126. package/src/color/__tests__/extract-colors.test.ts +360 -0
  127. package/src/color/__tests__/image-utils.test.ts +242 -0
  128. package/src/color/__tests__/reference-colors.test.ts +278 -0
  129. package/src/color/__tests__/round-trip.test.ts +197 -0
  130. package/src/color/conversions.test.ts +402 -0
  131. package/src/color/conversions.ts +393 -0
  132. package/src/color/dislike/__tests__/dislike-analyzer.test.ts +248 -0
  133. package/src/color/dislike/dislike-analyzer.ts +114 -0
  134. package/src/color/extract-colors.ts +228 -0
  135. package/src/color/hct/__tests__/hct-class.test.ts +232 -0
  136. package/src/color/hct/harmonization.ts +204 -0
  137. package/src/color/hct/hct-class.ts +109 -0
  138. package/src/color/hct/hct-solver.ts +168 -0
  139. package/src/color/hct/index.ts +39 -0
  140. package/src/color/hct/tonal-palette.ts +211 -0
  141. package/src/color/hct/types.ts +88 -0
  142. package/src/color/image-utils.ts +79 -0
  143. package/src/color/index.ts +87 -0
  144. package/src/color/material-theme.ts +157 -0
  145. package/src/color/metrics.test.ts +276 -0
  146. package/src/color/metrics.ts +281 -0
  147. package/src/color/quantize/__tests__/quantizer_celebi.test.ts +195 -0
  148. package/src/color/quantize/lab_point_provider.ts +55 -0
  149. package/src/color/quantize/point_provider.ts +27 -0
  150. package/src/color/quantize/quantizer_celebi.ts +51 -0
  151. package/src/color/quantize/quantizer_celebi_test.ts +71 -0
  152. package/src/color/quantize/quantizer_map.ts +47 -0
  153. package/src/color/quantize/quantizer_wsmeans.ts +232 -0
  154. package/src/color/quantize/quantizer_wu.ts +472 -0
  155. package/src/color/score/__tests__/score.test.ts +224 -0
  156. package/src/color/score/score.ts +175 -0
  157. package/src/color/types.ts +151 -0
  158. package/src/color/utils/color_utils.ts +292 -0
  159. package/src/color/utils/math_utils.ts +145 -0
  160. package/src/color/utils.test.ts +403 -0
  161. package/src/color/utils.ts +315 -0
  162. package/src/constants.ts +5 -0
  163. package/src/coolors-mcp.ts +37 -0
  164. package/src/examples/addition.ts +333 -0
  165. package/src/examples/color-demo.ts +125 -0
  166. package/src/examples/custom-logger.ts +201 -0
  167. package/src/examples/oauth-server.ts +113 -0
  168. package/src/examples/session-context.ts +269 -0
  169. package/src/session.ts +116 -0
  170. package/src/theme/__tests__/matcher.test.ts +180 -0
  171. package/src/theme/__tests__/parser.test.ts +148 -0
  172. package/src/theme/__tests__/refactor.test.ts +224 -0
  173. package/src/theme/index.ts +34 -0
  174. package/src/theme/matcher.ts +395 -0
  175. package/src/theme/parser.ts +392 -0
  176. package/src/theme/refactor.ts +360 -0
  177. package/src/theme/types.ts +152 -0
  178. package/src/tools/__tests__/gradient-generator.test.ts +206 -0
  179. package/src/tools/__tests__/palette-with-locks.test.ts +109 -0
  180. package/src/tools/color-conversion.tool.ts +54 -0
  181. package/src/tools/color-distance.tool.ts +41 -0
  182. package/src/tools/colors.ts +31 -0
  183. package/src/tools/contrast-checker.tool.ts +37 -0
  184. package/src/tools/dislike-analyzer.tool.ts +247 -0
  185. package/src/tools/gradient-generator.tool.ts +250 -0
  186. package/src/tools/image-extraction.tools.ts +289 -0
  187. package/src/tools/index.ts +39 -0
  188. package/src/tools/material-theme.tools.ts +250 -0
  189. package/src/tools/palette-generator.tool.ts +135 -0
  190. package/src/tools/palette-with-locks.tool.ts +221 -0
  191. package/src/tools/registry.ts +142 -0
  192. package/src/tools/simple-tools.ts +37 -0
  193. package/src/tools/theme-matching.tools.ts +334 -0
  194. package/src/types.ts +182 -0
  195. package/src/utils.ts +22 -0
  196. package/tsconfig.json +8 -0
  197. package/vitest.config.js +15 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2021 Google LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ // This file is automatically generated. Do not modify it.
19
+
20
+ /**
21
+ * Utility methods for mathematical operations.
22
+ */
23
+
24
+ /**
25
+ * Clamps an integer between two floating-point numbers.
26
+ *
27
+ * @return input when min <= input <= max, and either min or max
28
+ * otherwise.
29
+ */
30
+ export function clampDouble(min: number, max: number, input: number): number {
31
+ if (input < min) {
32
+ return min;
33
+ } else if (input > max) {
34
+ return max;
35
+ }
36
+
37
+ return input;
38
+ }
39
+
40
+ /**
41
+ * Clamps an integer between two integers.
42
+ *
43
+ * @return input when min <= input <= max, and either min or max
44
+ * otherwise.
45
+ */
46
+ export function clampInt(min: number, max: number, input: number): number {
47
+ if (input < min) {
48
+ return min;
49
+ } else if (input > max) {
50
+ return max;
51
+ }
52
+
53
+ return input;
54
+ }
55
+
56
+ /**
57
+ * Distance of two points on a circle, represented using degrees.
58
+ */
59
+ export function differenceDegrees(a: number, b: number): number {
60
+ return 180.0 - Math.abs(Math.abs(a - b) - 180.0);
61
+ }
62
+
63
+ /**
64
+ * The linear interpolation function.
65
+ *
66
+ * @return start if amount = 0 and stop if amount = 1
67
+ */
68
+ export function lerp(start: number, stop: number, amount: number): number {
69
+ return (1.0 - amount) * start + amount * stop;
70
+ }
71
+
72
+ /**
73
+ * Multiplies a 1x3 row vector with a 3x3 matrix.
74
+ */
75
+ export function matrixMultiply(row: number[], matrix: number[][]): number[] {
76
+ const a =
77
+ row[0] * matrix[0][0] + row[1] * matrix[0][1] + row[2] * matrix[0][2];
78
+ const b =
79
+ row[0] * matrix[1][0] + row[1] * matrix[1][1] + row[2] * matrix[1][2];
80
+ const c =
81
+ row[0] * matrix[2][0] + row[1] * matrix[2][1] + row[2] * matrix[2][2];
82
+ return [a, b, c];
83
+ }
84
+
85
+ /**
86
+ * Sign of direction change needed to travel from one angle to
87
+ * another.
88
+ *
89
+ * For angles that are 180 degrees apart from each other, both
90
+ * directions have the same travel distance, so either direction is
91
+ * shortest. The value 1.0 is returned in this case.
92
+ *
93
+ * @param from The angle travel starts from, in degrees.
94
+ * @param to The angle travel ends at, in degrees.
95
+ * @return -1 if decreasing from leads to the shortest travel
96
+ * distance, 1 if increasing from leads to the shortest travel
97
+ * distance.
98
+ */
99
+ export function rotationDirection(from: number, to: number): number {
100
+ const increasingDifference = sanitizeDegreesDouble(to - from);
101
+ return increasingDifference <= 180.0 ? 1.0 : -1.0;
102
+ }
103
+
104
+ /**
105
+ * Sanitizes a degree measure as a floating-point number.
106
+ *
107
+ * @return a degree measure between 0.0 (inclusive) and 360.0
108
+ * (exclusive).
109
+ */
110
+ export function sanitizeDegreesDouble(degrees: number): number {
111
+ degrees = degrees % 360.0;
112
+ if (degrees < 0) {
113
+ degrees = degrees + 360.0;
114
+ }
115
+ return degrees;
116
+ }
117
+
118
+ /**
119
+ * Sanitizes a degree measure as an integer.
120
+ *
121
+ * @return a degree measure between 0 (inclusive) and 360
122
+ * (exclusive).
123
+ */
124
+ export function sanitizeDegreesInt(degrees: number): number {
125
+ degrees = degrees % 360;
126
+ if (degrees < 0) {
127
+ degrees = degrees + 360;
128
+ }
129
+ return degrees;
130
+ }
131
+
132
+ /**
133
+ * The signum function.
134
+ *
135
+ * @return 1 if num > 0, -1 if num < 0, and 0 if num = 0
136
+ */
137
+ export function signum(num: number): number {
138
+ if (num < 0) {
139
+ return -1;
140
+ } else if (num === 0) {
141
+ return 0;
142
+ } else {
143
+ return 1;
144
+ }
145
+ }
@@ -0,0 +1,403 @@
1
+ /**
2
+ * Tests for color utility functions
3
+ */
4
+
5
+ import { describe, expect, it } from "vitest";
6
+
7
+ import type { RGB } from "./types";
8
+
9
+ import {
10
+ clampHsl,
11
+ clampHsv,
12
+ clampRgb,
13
+ darken,
14
+ desaturate,
15
+ formatColor,
16
+ formatHsl,
17
+ formatRgb,
18
+ getComplementary,
19
+ getContrastRatio,
20
+ getLuminance,
21
+ invertColor,
22
+ isDark,
23
+ isLight,
24
+ isValidHex,
25
+ isValidHsl,
26
+ isValidHsv,
27
+ isValidRgb,
28
+ lighten,
29
+ meetsContrastAA,
30
+ meetsContrastAAA,
31
+ mixColors,
32
+ randomColor,
33
+ saturate,
34
+ toGrayscale,
35
+ } from "./utils";
36
+
37
+ describe("Color Utils", () => {
38
+ describe("Validation functions", () => {
39
+ describe("isValidRgb", () => {
40
+ it("should validate RGB colors correctly", () => {
41
+ expect(isValidRgb({ b: 0, g: 0, r: 0 })).toBe(true);
42
+ expect(isValidRgb({ b: 255, g: 255, r: 255 })).toBe(true);
43
+ expect(isValidRgb({ b: 128, g: 128, r: 128 })).toBe(true);
44
+
45
+ expect(isValidRgb({ b: 0, g: 0, r: -1 })).toBe(false);
46
+ expect(isValidRgb({ b: 0, g: 0, r: 256 })).toBe(false);
47
+ expect(isValidRgb({ b: 0, g: -1, r: 0 })).toBe(false);
48
+ expect(isValidRgb({ b: 256, g: 0, r: 0 })).toBe(false);
49
+ });
50
+ });
51
+
52
+ describe("isValidHsl", () => {
53
+ it("should validate HSL colors correctly", () => {
54
+ expect(isValidHsl({ h: 0, l: 0, s: 0 })).toBe(true);
55
+ expect(isValidHsl({ h: 360, l: 100, s: 100 })).toBe(true);
56
+ expect(isValidHsl({ h: 180, l: 50, s: 50 })).toBe(true);
57
+
58
+ expect(isValidHsl({ h: -1, l: 0, s: 0 })).toBe(false);
59
+ expect(isValidHsl({ h: 361, l: 0, s: 0 })).toBe(false);
60
+ expect(isValidHsl({ h: 0, l: 0, s: -1 })).toBe(false);
61
+ expect(isValidHsl({ h: 0, l: 0, s: 101 })).toBe(false);
62
+ expect(isValidHsl({ h: 0, l: -1, s: 0 })).toBe(false);
63
+ expect(isValidHsl({ h: 0, l: 101, s: 0 })).toBe(false);
64
+ });
65
+ });
66
+
67
+ describe("isValidHsv", () => {
68
+ it("should validate HSV colors correctly", () => {
69
+ expect(isValidHsv({ h: 0, s: 0, v: 0 })).toBe(true);
70
+ expect(isValidHsv({ h: 360, s: 100, v: 100 })).toBe(true);
71
+ expect(isValidHsv({ h: 180, s: 50, v: 50 })).toBe(true);
72
+
73
+ expect(isValidHsv({ h: -1, s: 0, v: 0 })).toBe(false);
74
+ expect(isValidHsv({ h: 361, s: 0, v: 0 })).toBe(false);
75
+ expect(isValidHsv({ h: 0, s: -1, v: 0 })).toBe(false);
76
+ expect(isValidHsv({ h: 0, s: 101, v: 0 })).toBe(false);
77
+ expect(isValidHsv({ h: 0, s: 0, v: -1 })).toBe(false);
78
+ expect(isValidHsv({ h: 0, s: 0, v: 101 })).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe("isValidHex", () => {
83
+ it("should validate hex colors correctly", () => {
84
+ expect(isValidHex("#fff")).toBe(true);
85
+ expect(isValidHex("#ffffff")).toBe(true);
86
+ expect(isValidHex("#000")).toBe(true);
87
+ expect(isValidHex("#000000")).toBe(true);
88
+ expect(isValidHex("fff")).toBe(true);
89
+ expect(isValidHex("ffffff")).toBe(true);
90
+
91
+ expect(isValidHex("#ff")).toBe(false);
92
+ expect(isValidHex("#ffff")).toBe(false);
93
+ expect(isValidHex("#fffff")).toBe(false);
94
+ expect(isValidHex("#fffffff")).toBe(false);
95
+ expect(isValidHex("#ggg")).toBe(false);
96
+ expect(isValidHex("xyz")).toBe(false);
97
+ });
98
+ });
99
+ });
100
+
101
+ describe("Clamping functions", () => {
102
+ describe("clampRgb", () => {
103
+ it("should clamp RGB values to valid range", () => {
104
+ expect(clampRgb({ b: 128, g: 300, r: -10 })).toEqual({
105
+ b: 128,
106
+ g: 255,
107
+ r: 0,
108
+ });
109
+ expect(clampRgb({ b: 0, g: 0, r: 0 })).toEqual({ b: 0, g: 0, r: 0 });
110
+ expect(clampRgb({ b: 255, g: 255, r: 255 })).toEqual({
111
+ b: 255,
112
+ g: 255,
113
+ r: 255,
114
+ });
115
+ });
116
+ });
117
+
118
+ describe("clampHsl", () => {
119
+ it("should clamp HSL values to valid range", () => {
120
+ expect(clampHsl({ h: -10, l: 50, s: 150 })).toEqual({
121
+ h: 0,
122
+ l: 50,
123
+ s: 100,
124
+ });
125
+ expect(clampHsl({ h: 400, l: 150, s: -10 })).toEqual({
126
+ h: 360,
127
+ l: 100,
128
+ s: 0,
129
+ });
130
+ });
131
+ });
132
+
133
+ describe("clampHsv", () => {
134
+ it("should clamp HSV values to valid range", () => {
135
+ expect(clampHsv({ h: -10, s: 150, v: 50 })).toEqual({
136
+ h: 0,
137
+ s: 100,
138
+ v: 50,
139
+ });
140
+ expect(clampHsv({ h: 400, s: -10, v: 150 })).toEqual({
141
+ h: 360,
142
+ s: 0,
143
+ v: 100,
144
+ });
145
+ });
146
+ });
147
+ });
148
+
149
+ describe("Luminance and contrast", () => {
150
+ describe("getLuminance", () => {
151
+ it("should calculate luminance correctly", () => {
152
+ expect(getLuminance({ b: 0, g: 0, r: 0 })).toBeCloseTo(0, 3);
153
+ expect(getLuminance({ b: 255, g: 255, r: 255 })).toBeCloseTo(1, 3);
154
+
155
+ // Red has lower luminance than green
156
+ const redLum = getLuminance({ b: 0, g: 0, r: 255 });
157
+ const greenLum = getLuminance({ b: 0, g: 255, r: 0 });
158
+ expect(redLum).toBeLessThan(greenLum);
159
+ });
160
+ });
161
+
162
+ describe("getContrastRatio", () => {
163
+ it("should calculate contrast ratio correctly", () => {
164
+ const black: RGB = { b: 0, g: 0, r: 0 };
165
+ const white: RGB = { b: 255, g: 255, r: 255 };
166
+
167
+ expect(getContrastRatio(black, white)).toBeCloseTo(21, 0);
168
+ expect(getContrastRatio(white, black)).toBeCloseTo(21, 0);
169
+ expect(getContrastRatio(black, black)).toBeCloseTo(1, 0);
170
+ expect(getContrastRatio(white, white)).toBeCloseTo(1, 0);
171
+ });
172
+ });
173
+
174
+ describe("meetsContrastAA", () => {
175
+ it("should check AA contrast requirements", () => {
176
+ const black: RGB = { b: 0, g: 0, r: 0 };
177
+ const white: RGB = { b: 255, g: 255, r: 255 };
178
+ const gray: RGB = { b: 128, g: 128, r: 128 };
179
+
180
+ expect(meetsContrastAA(black, white)).toBe(true);
181
+ expect(meetsContrastAA(black, gray)).toBe(true);
182
+ expect(meetsContrastAA(white, gray)).toBe(false);
183
+
184
+ // Large text has lower requirements
185
+ expect(meetsContrastAA(white, gray, true)).toBe(true);
186
+ });
187
+ });
188
+
189
+ describe("meetsContrastAAA", () => {
190
+ it("should check AAA contrast requirements", () => {
191
+ const black: RGB = { b: 0, g: 0, r: 0 };
192
+ const white: RGB = { b: 255, g: 255, r: 255 };
193
+ const darkGray: RGB = { b: 85, g: 85, r: 85 }; // Adjusted for better contrast
194
+
195
+ expect(meetsContrastAAA(black, white)).toBe(true);
196
+ expect(meetsContrastAAA(black, darkGray)).toBe(false);
197
+
198
+ // Large text has lower requirements - use lighter gray for AAA large text
199
+ const mediumGray: RGB = { b: 118, g: 118, r: 118 };
200
+ expect(meetsContrastAAA(black, mediumGray, true)).toBe(true);
201
+ });
202
+ });
203
+ });
204
+
205
+ describe("Formatting functions", () => {
206
+ describe("formatRgb", () => {
207
+ it("should format RGB to string", () => {
208
+ expect(formatRgb({ b: 0, g: 0, r: 255 })).toBe("rgb(255, 0, 0)");
209
+ expect(formatRgb({ b: 128, g: 128, r: 128 }, 0.5)).toBe(
210
+ "rgba(128, 128, 128, 0.5)",
211
+ );
212
+ });
213
+ });
214
+
215
+ describe("formatHsl", () => {
216
+ it("should format HSL to string", () => {
217
+ expect(formatHsl({ h: 0, l: 50, s: 100 })).toBe("hsl(0, 100%, 50%)");
218
+ expect(formatHsl({ h: 180, l: 50, s: 50 }, 0.5)).toBe(
219
+ "hsla(180, 50%, 50%, 0.5)",
220
+ );
221
+ });
222
+ });
223
+
224
+ describe("formatColor", () => {
225
+ it("should format color to various formats", () => {
226
+ const red: RGB = { b: 0, g: 0, r: 255 };
227
+
228
+ expect(formatColor(red, "hex")).toBe("#ff0000");
229
+ expect(formatColor(red, "rgb")).toBe("rgb(255, 0, 0)");
230
+ expect(formatColor(red, "hsl")).toBe("hsl(0, 100%, 50%)");
231
+ expect(formatColor(red, "hsv")).toBe("hsv(0, 100%, 100%)");
232
+ expect(formatColor(red)).toBe("#ff0000"); // Default to hex
233
+ });
234
+ });
235
+ });
236
+
237
+ describe("Color manipulation", () => {
238
+ describe("mixColors", () => {
239
+ it("should mix colors correctly", () => {
240
+ const red: RGB = { b: 0, g: 0, r: 255 };
241
+ const blue: RGB = { b: 255, g: 0, r: 0 };
242
+
243
+ const purple = mixColors(red, blue, 0.5);
244
+ expect(purple.r).toBeCloseTo(128, 0);
245
+ expect(purple.g).toBe(0);
246
+ expect(purple.b).toBeCloseTo(128, 0);
247
+
248
+ // Full weight to first color
249
+ expect(mixColors(red, blue, 1)).toEqual(red);
250
+
251
+ // Full weight to second color
252
+ expect(mixColors(red, blue, 0)).toEqual(blue);
253
+ });
254
+ });
255
+
256
+ describe("lighten", () => {
257
+ it("should lighten colors", () => {
258
+ const color: RGB = { b: 0, g: 0, r: 128 };
259
+ const lighter = lighten(color, 20);
260
+
261
+ expect(lighter.r).toBeGreaterThan(color.r);
262
+ });
263
+ });
264
+
265
+ describe("darken", () => {
266
+ it("should darken colors", () => {
267
+ const color: RGB = { b: 128, g: 128, r: 255 };
268
+ const darker = darken(color, 20);
269
+
270
+ expect(darker.r).toBeLessThanOrEqual(color.r);
271
+ });
272
+ });
273
+
274
+ describe("saturate", () => {
275
+ it("should increase saturation", () => {
276
+ const color: RGB = { b: 100, g: 100, r: 200 };
277
+ const saturated = saturate(color, 20);
278
+
279
+ // More saturated colors have greater difference between channels
280
+ const originalDiff =
281
+ Math.max(color.r, color.g, color.b) -
282
+ Math.min(color.r, color.g, color.b);
283
+ const saturatedDiff =
284
+ Math.max(saturated.r, saturated.g, saturated.b) -
285
+ Math.min(saturated.r, saturated.g, saturated.b);
286
+
287
+ expect(saturatedDiff).toBeGreaterThanOrEqual(originalDiff);
288
+ });
289
+ });
290
+
291
+ describe("desaturate", () => {
292
+ it("should decrease saturation", () => {
293
+ const color: RGB = { b: 0, g: 0, r: 255 };
294
+ const desaturated = desaturate(color, 50);
295
+
296
+ // Less saturated colors have less difference between channels
297
+ const originalDiff =
298
+ Math.max(color.r, color.g, color.b) -
299
+ Math.min(color.r, color.g, color.b);
300
+ const desaturatedDiff =
301
+ Math.max(desaturated.r, desaturated.g, desaturated.b) -
302
+ Math.min(desaturated.r, desaturated.g, desaturated.b);
303
+
304
+ expect(desaturatedDiff).toBeLessThan(originalDiff);
305
+ });
306
+ });
307
+
308
+ describe("toGrayscale", () => {
309
+ it("should convert to grayscale", () => {
310
+ const red: RGB = { b: 0, g: 0, r: 255 };
311
+ const gray = toGrayscale(red);
312
+
313
+ expect(gray.r).toBe(gray.g);
314
+ expect(gray.g).toBe(gray.b);
315
+ expect(gray.r).toBeCloseTo(76, 0); // Weighted average
316
+ });
317
+ });
318
+
319
+ describe("invertColor", () => {
320
+ it("should invert colors", () => {
321
+ expect(invertColor({ b: 128, g: 0, r: 255 })).toEqual({
322
+ b: 127,
323
+ g: 255,
324
+ r: 0,
325
+ });
326
+ expect(invertColor({ b: 0, g: 0, r: 0 })).toEqual({
327
+ b: 255,
328
+ g: 255,
329
+ r: 255,
330
+ });
331
+ expect(invertColor({ b: 255, g: 255, r: 255 })).toEqual({
332
+ b: 0,
333
+ g: 0,
334
+ r: 0,
335
+ });
336
+ });
337
+ });
338
+
339
+ describe("getComplementary", () => {
340
+ it("should get complementary color", () => {
341
+ const red: RGB = { b: 0, g: 0, r: 255 };
342
+ const complementary = getComplementary(red);
343
+
344
+ // Complementary of red should be cyan
345
+ expect(complementary.r).toBe(0);
346
+ expect(complementary.g).toBe(255);
347
+ expect(complementary.b).toBe(255);
348
+ });
349
+ });
350
+ });
351
+
352
+ describe("Random and detection", () => {
353
+ describe("randomColor", () => {
354
+ it("should generate valid random colors", () => {
355
+ for (let i = 0; i < 10; i++) {
356
+ const color = randomColor();
357
+ expect(isValidRgb(color)).toBe(true);
358
+ }
359
+ });
360
+
361
+ it("should generate different colors", () => {
362
+ const colors = new Set();
363
+ for (let i = 0; i < 100; i++) {
364
+ const color = randomColor();
365
+ colors.add(`${color.r},${color.g},${color.b}`);
366
+ }
367
+ // Should generate at least some different colors
368
+ expect(colors.size).toBeGreaterThan(50);
369
+ });
370
+ });
371
+
372
+ describe("isDark", () => {
373
+ it("should identify dark colors", () => {
374
+ expect(isDark({ b: 0, g: 0, r: 0 })).toBe(true);
375
+ expect(isDark({ b: 50, g: 50, r: 50 })).toBe(true);
376
+ expect(isDark({ b: 255, g: 255, r: 255 })).toBe(false);
377
+ expect(isDark({ b: 200, g: 200, r: 200 })).toBe(false);
378
+ });
379
+
380
+ it("should use custom threshold", () => {
381
+ const gray: RGB = { b: 128, g: 128, r: 128 };
382
+ // Gray at 128 has luminance ~0.22
383
+ expect(isDark(gray, 0.2)).toBe(false);
384
+ expect(isDark(gray, 0.25)).toBe(true); // Use lower threshold
385
+ });
386
+ });
387
+
388
+ describe("isLight", () => {
389
+ it("should identify light colors", () => {
390
+ expect(isLight({ b: 255, g: 255, r: 255 })).toBe(true);
391
+ expect(isLight({ b: 200, g: 200, r: 200 })).toBe(true);
392
+ expect(isLight({ b: 0, g: 0, r: 0 })).toBe(false);
393
+ expect(isLight({ b: 50, g: 50, r: 50 })).toBe(false);
394
+ });
395
+
396
+ it("should use custom threshold", () => {
397
+ const gray: RGB = { b: 128, g: 128, r: 128 };
398
+ expect(isLight(gray, 0.2)).toBe(true); // Use lower threshold
399
+ expect(isLight(gray, 0.25)).toBe(false);
400
+ });
401
+ });
402
+ });
403
+ });