@mseep/dembrandt 0.19.5

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 (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +408 -0
  3. package/dist/index.d.ts +8 -0
  4. package/dist/index.js +532 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/lib/browser.d.ts +16 -0
  7. package/dist/lib/browser.js +27 -0
  8. package/dist/lib/browser.js.map +1 -0
  9. package/dist/lib/colors.d.ts +101 -0
  10. package/dist/lib/colors.js +405 -0
  11. package/dist/lib/colors.js.map +1 -0
  12. package/dist/lib/compare.d.ts +31 -0
  13. package/dist/lib/compare.js +46 -0
  14. package/dist/lib/compare.js.map +1 -0
  15. package/dist/lib/discovery.d.ts +31 -0
  16. package/dist/lib/discovery.js +243 -0
  17. package/dist/lib/discovery.js.map +1 -0
  18. package/dist/lib/drift.d.ts +64 -0
  19. package/dist/lib/drift.js +383 -0
  20. package/dist/lib/drift.js.map +1 -0
  21. package/dist/lib/dtcg/validate.d.ts +51 -0
  22. package/dist/lib/dtcg/validate.js +1403 -0
  23. package/dist/lib/dtcg/validate.js.map +1 -0
  24. package/dist/lib/exit-codes.d.ts +29 -0
  25. package/dist/lib/exit-codes.js +26 -0
  26. package/dist/lib/exit-codes.js.map +1 -0
  27. package/dist/lib/extractors/breakpoints.d.ts +5 -0
  28. package/dist/lib/extractors/breakpoints.js +450 -0
  29. package/dist/lib/extractors/breakpoints.js.map +1 -0
  30. package/dist/lib/extractors/colors.d.ts +2 -0
  31. package/dist/lib/extractors/colors.js +657 -0
  32. package/dist/lib/extractors/colors.js.map +1 -0
  33. package/dist/lib/extractors/components.d.ts +4 -0
  34. package/dist/lib/extractors/components.js +370 -0
  35. package/dist/lib/extractors/components.js.map +1 -0
  36. package/dist/lib/extractors/index.d.ts +9 -0
  37. package/dist/lib/extractors/index.js +1257 -0
  38. package/dist/lib/extractors/index.js.map +1 -0
  39. package/dist/lib/extractors/logo.d.ts +2 -0
  40. package/dist/lib/extractors/logo.js +626 -0
  41. package/dist/lib/extractors/logo.js.map +1 -0
  42. package/dist/lib/extractors/spacing.d.ts +4 -0
  43. package/dist/lib/extractors/spacing.js +163 -0
  44. package/dist/lib/extractors/spacing.js.map +1 -0
  45. package/dist/lib/extractors/teach.d.ts +1 -0
  46. package/dist/lib/extractors/teach.js +66 -0
  47. package/dist/lib/extractors/teach.js.map +1 -0
  48. package/dist/lib/extractors/typography.d.ts +1 -0
  49. package/dist/lib/extractors/typography.js +163 -0
  50. package/dist/lib/extractors/typography.js.map +1 -0
  51. package/dist/lib/findings.d.ts +34 -0
  52. package/dist/lib/findings.js +166 -0
  53. package/dist/lib/findings.js.map +1 -0
  54. package/dist/lib/formatters/dtcg.d.ts +10 -0
  55. package/dist/lib/formatters/dtcg.js +416 -0
  56. package/dist/lib/formatters/dtcg.js.map +1 -0
  57. package/dist/lib/formatters/html.d.ts +25 -0
  58. package/dist/lib/formatters/html.js +479 -0
  59. package/dist/lib/formatters/html.js.map +1 -0
  60. package/dist/lib/formatters/markdown.d.ts +5 -0
  61. package/dist/lib/formatters/markdown.js +568 -0
  62. package/dist/lib/formatters/markdown.js.map +1 -0
  63. package/dist/lib/formatters/pdf.d.ts +12 -0
  64. package/dist/lib/formatters/pdf.js +1121 -0
  65. package/dist/lib/formatters/pdf.js.map +1 -0
  66. package/dist/lib/formatters/terminal.d.ts +6 -0
  67. package/dist/lib/formatters/terminal.js +954 -0
  68. package/dist/lib/formatters/terminal.js.map +1 -0
  69. package/dist/lib/formatters/theme.d.ts +35 -0
  70. package/dist/lib/formatters/theme.js +37 -0
  71. package/dist/lib/formatters/theme.js.map +1 -0
  72. package/dist/lib/merger.d.ts +14 -0
  73. package/dist/lib/merger.js +362 -0
  74. package/dist/lib/merger.js.map +1 -0
  75. package/dist/lib/normalize.d.ts +29 -0
  76. package/dist/lib/normalize.js +59 -0
  77. package/dist/lib/normalize.js.map +1 -0
  78. package/dist/lib/robots.d.ts +12 -0
  79. package/dist/lib/robots.js +110 -0
  80. package/dist/lib/robots.js.map +1 -0
  81. package/dist/lib/run-summary.d.ts +40 -0
  82. package/dist/lib/run-summary.js +64 -0
  83. package/dist/lib/run-summary.js.map +1 -0
  84. package/dist/lib/types.d.ts +329 -0
  85. package/dist/lib/types.js +7 -0
  86. package/dist/lib/types.js.map +1 -0
  87. package/dist/lib/version.d.ts +134 -0
  88. package/dist/lib/version.js +153 -0
  89. package/dist/lib/version.js.map +1 -0
  90. package/dist/mcp-server.d.ts +11 -0
  91. package/dist/mcp-server.js +311 -0
  92. package/dist/mcp-server.js.map +1 -0
  93. package/dist/package.json +106 -0
  94. package/dist/test/_vitest-shim.d.ts +13 -0
  95. package/dist/test/_vitest-shim.js +23 -0
  96. package/dist/test/_vitest-shim.js.map +1 -0
  97. package/dist/test/cli.test.d.ts +1 -0
  98. package/dist/test/cli.test.js +24 -0
  99. package/dist/test/cli.test.js.map +1 -0
  100. package/dist/test/colors.test.d.ts +1 -0
  101. package/dist/test/colors.test.js +64 -0
  102. package/dist/test/colors.test.js.map +1 -0
  103. package/dist/test/compare.test.d.ts +1 -0
  104. package/dist/test/compare.test.js +57 -0
  105. package/dist/test/compare.test.js.map +1 -0
  106. package/dist/test/drift.test.d.ts +1 -0
  107. package/dist/test/drift.test.js +53 -0
  108. package/dist/test/drift.test.js.map +1 -0
  109. package/dist/test/dtcg-formatter.test.d.ts +1 -0
  110. package/dist/test/dtcg-formatter.test.js +48 -0
  111. package/dist/test/dtcg-formatter.test.js.map +1 -0
  112. package/dist/test/dtcg-validate.test.d.ts +1 -0
  113. package/dist/test/dtcg-validate.test.js +2129 -0
  114. package/dist/test/dtcg-validate.test.js.map +1 -0
  115. package/dist/test/exit-codes.test.d.ts +1 -0
  116. package/dist/test/exit-codes.test.js +53 -0
  117. package/dist/test/exit-codes.test.js.map +1 -0
  118. package/dist/test/findings.test.d.ts +1 -0
  119. package/dist/test/findings.test.js +77 -0
  120. package/dist/test/findings.test.js.map +1 -0
  121. package/dist/test/html.test.d.ts +1 -0
  122. package/dist/test/html.test.js +95 -0
  123. package/dist/test/html.test.js.map +1 -0
  124. package/dist/test/markdown.test.d.ts +1 -0
  125. package/dist/test/markdown.test.js +145 -0
  126. package/dist/test/markdown.test.js.map +1 -0
  127. package/dist/test/merger.test.d.ts +1 -0
  128. package/dist/test/merger.test.js +98 -0
  129. package/dist/test/merger.test.js.map +1 -0
  130. package/dist/test/normalize.test.d.ts +1 -0
  131. package/dist/test/normalize.test.js +47 -0
  132. package/dist/test/normalize.test.js.map +1 -0
  133. package/dist/test/run-summary.test.d.ts +1 -0
  134. package/dist/test/run-summary.test.js +45 -0
  135. package/dist/test/run-summary.test.js.map +1 -0
  136. package/dist/test/version.test.d.ts +1 -0
  137. package/dist/test/version.test.js +73 -0
  138. package/dist/test/version.test.js.map +1 -0
  139. package/package.json +106 -0
@@ -0,0 +1,657 @@
1
+ import { convertColor } from '../colors.js';
2
+ export async function extractColors(page) {
3
+ const result = await page.evaluate(() => {
4
+ const _canvas = document.createElement('canvas');
5
+ _canvas.width = _canvas.height = 1;
6
+ const _ctx = _canvas.getContext('2d');
7
+ const _colorMemo = new Map();
8
+ function normalizeColor(color) {
9
+ if (_colorMemo.has(color))
10
+ return _colorMemo.get(color);
11
+ let result;
12
+ const rgbaMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
13
+ if (rgbaMatch) {
14
+ const alpha = rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1;
15
+ if (alpha < 0.05) {
16
+ result = color.toLowerCase();
17
+ _colorMemo.set(color, result);
18
+ return result;
19
+ }
20
+ const r = parseInt(rgbaMatch[1]).toString(16).padStart(2, "0");
21
+ const g = parseInt(rgbaMatch[2]).toString(16).padStart(2, "0");
22
+ const b = parseInt(rgbaMatch[3]).toString(16).padStart(2, "0");
23
+ result = `#${r}${g}${b}`;
24
+ }
25
+ else {
26
+ const shortHex = color.match(/^#([0-9a-f]{3})$/i);
27
+ if (shortHex) {
28
+ const [, h] = shortHex;
29
+ result = `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`.toLowerCase();
30
+ }
31
+ else if (/^#[0-9a-f]{6}$/i.test(color)) {
32
+ result = color.toLowerCase();
33
+ }
34
+ else if (/^#[0-9a-f]{8}$/i.test(color)) {
35
+ result = color.toLowerCase().slice(0, 7);
36
+ }
37
+ else if (_ctx) {
38
+ try {
39
+ _ctx.clearRect(0, 0, 1, 1);
40
+ _ctx.fillStyle = 'rgba(0,0,0,0)';
41
+ _ctx.fillStyle = color;
42
+ _ctx.fillRect(0, 0, 1, 1);
43
+ const [r, g, b, a] = _ctx.getImageData(0, 0, 1, 1).data;
44
+ result = a === 0 ? color.toLowerCase() : `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
45
+ }
46
+ catch (e) {
47
+ result = color.toLowerCase();
48
+ }
49
+ }
50
+ else {
51
+ result = color.toLowerCase();
52
+ }
53
+ }
54
+ _colorMemo.set(color, result);
55
+ return result;
56
+ }
57
+ function colorAlpha(color) {
58
+ if (!color)
59
+ return 1;
60
+ const m = color.match(/rgba?\(\d+,\s*\d+,\s*\d+,\s*([\d.]+)\)/);
61
+ return m ? parseFloat(m[1]) : 1;
62
+ }
63
+ function isValidColorValue(value) {
64
+ if (!value)
65
+ return false;
66
+ if (value.includes("calc(") || value.includes("clamp(") || value.includes("var(")) {
67
+ return /#[0-9a-f]{3,6}|rgba?\(|hsla?\(/i.test(value);
68
+ }
69
+ if (/^(oklab|oklch|lch|lab|color)\s*\(/i.test(value))
70
+ return false;
71
+ return /^(#[0-9a-f]{3,8}|rgba?\(|hsla?\(|[a-z]+)/i.test(value);
72
+ }
73
+ const colorMap = new Map();
74
+ const semanticColors = {};
75
+ const cssVariables = {};
76
+ const styles = getComputedStyle(document.documentElement);
77
+ const domain = window.location.hostname;
78
+ for (let i = 0; i < styles.length; i++) {
79
+ const prop = styles[i];
80
+ if (!prop.startsWith("--"))
81
+ continue;
82
+ if (prop.startsWith("--wp--preset"))
83
+ continue;
84
+ if (prop.startsWith("--el-") || prop.startsWith("--p-") ||
85
+ prop.startsWith("--chakra-") || prop.startsWith("--mantine-") ||
86
+ prop.startsWith("--ant-") || prop.startsWith("--bs-") ||
87
+ prop.startsWith("--swiper-") || prop.startsWith("--rsbs-") ||
88
+ prop.startsWith("--toastify-"))
89
+ continue;
90
+ if (prop.includes("--system-") || prop.includes("--default-"))
91
+ continue;
92
+ if (prop.includes("--cc-") && !domain.includes("cookie") && !domain.includes("consent"))
93
+ continue;
94
+ const nonColorUtilities = [
95
+ '--tw-ring-offset-width', '--tw-ring-offset', '--tw-shadow', '--tw-blur',
96
+ '--tw-brightness', '--tw-contrast', '--tw-grayscale', '--tw-hue-rotate',
97
+ '--tw-invert', '--tw-saturate', '--tw-sepia', '--tw-drop-shadow',
98
+ '--tw-translate-x', '--tw-translate-y', '--tw-translate-z',
99
+ '--tw-rotate', '--tw-skew-x', '--tw-skew-y',
100
+ '--tw-scale-x', '--tw-scale-y', '--tw-scale-z',
101
+ '--tw-gradient-from-position', '--tw-gradient-via-position', '--tw-gradient-to-position',
102
+ '--tw-divide-', '--tw-space-', '--bs-gutter', '--bs-border-spacing'
103
+ ];
104
+ if (nonColorUtilities.some(pattern => prop.includes(pattern)))
105
+ continue;
106
+ // Non-brand semantic/status/noise custom properties. A marketing brand
107
+ // book is visual identity, not a status system, so errors/warnings,
108
+ // competitor colours and incidental UI noise are excluded.
109
+ const nonBrandNames = /(competitor|fuel|error|danger|destructive|invalid|warning|success|info|alert|notice|disabled|placeholder|skeleton|shimmer|scrim|overlay|backdrop|tooltip)/;
110
+ if (nonBrandNames.test(prop))
111
+ continue;
112
+ // Framework default-theme dumps: sites ship the entire Tailwind/Panda
113
+ // default palette as --colors-<hue>-<shade> custom properties (e.g.
114
+ // --colors-red-500). These are framework defaults, not brand tokens, so
115
+ // they never belong in a brand book. Brand-named tokens (--accent-color-*,
116
+ // --green-scale-*, --hero-background-*) do not match and are kept.
117
+ const frameworkPalette = /^--(?:tw-)?colors?-(?:slate|gray|grey|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|\d00|950)$/;
118
+ if (frameworkPalette.test(prop))
119
+ continue;
120
+ if (/^--(?:tw-)?colors?-(?:transparent|current|black|white|inherit)$/.test(prop))
121
+ continue;
122
+ const value = styles.getPropertyValue(prop).trim();
123
+ if (!value.match(/^(#|rgb|hsl|var\(--.*color|color\()/i))
124
+ continue;
125
+ if (value.includes("color.adjust(") || value.includes("rgba(0, 0, 0, 0)") ||
126
+ value.includes("rgba(0,0,0,0)") || value.includes("lighten(") ||
127
+ value.includes("darken(") || value.includes("saturate("))
128
+ continue;
129
+ if (isValidColorValue(value) &&
130
+ (prop.includes("color") || prop.includes("bg") || prop.includes("text") || prop.includes("brand"))) {
131
+ cssVariables[prop] = value;
132
+ }
133
+ }
134
+ // Declared :root custom properties carry brand-token provenance: the author
135
+ // named these colours deliberately. They outweigh ad-hoc computed colours in
136
+ // ranking, are never treated as structural, and are preferred as primary.
137
+ const tokenHexes = new Set();
138
+ for (const v of Object.values(cssVariables)) {
139
+ const n = normalizeColor(v);
140
+ if (typeof n === 'string' && /^#[0-9a-f]{6}$/.test(n))
141
+ tokenHexes.add(n);
142
+ }
143
+ const elements = document.querySelectorAll("*");
144
+ const totalElements = elements.length;
145
+ const ctaPrimaryMap = new Map(); // normalized hex → original color for CTA backgrounds
146
+ const contextScores = {
147
+ logo: 5, brand: 5, primary: 4, cta: 4, hero: 3, button: 3, link: 2, header: 2, nav: 1,
148
+ };
149
+ // Colours that appear only via status/feedback or warm-utility classes are
150
+ // not brand identity unless declared as a token or used as a CTA background.
151
+ // The semantic words cover Bootstrap (text-danger), MUI (Mui-error),
152
+ // Bulma (is-danger) and similar conventions; the second branch covers
153
+ // Tailwind's numbered warm utilities (text-red-600) that carry no word.
154
+ const statusContext = /\b(error|danger|destructive|invalid|warning|success|alert|notice|sale|discount|badge|toast|notification)\b|(?:text|bg|border|ring|fill|stroke|from|to|via|divide|outline|decoration|accent|caret)-(?:red|rose|orange|amber|yellow)-\d/;
155
+ elements.forEach((el) => {
156
+ const computed = getComputedStyle(el);
157
+ if (computed.display === "none" || computed.visibility === "hidden" || computed.opacity === "0")
158
+ return;
159
+ const rect = el.getBoundingClientRect();
160
+ if (rect.width === 0 || rect.height === 0)
161
+ return;
162
+ const bgColor = computed.backgroundColor;
163
+ const textColor = computed.color;
164
+ const borderColor = computed.borderColor;
165
+ const context = (el.className + " " + el.id + " " +
166
+ (el.getAttribute('data-tracking-linkid') || '') + " " +
167
+ (el.getAttribute('data-cta') || '') + " " +
168
+ (el.getAttribute('data-component') || '') + " " +
169
+ el.tagName).toLowerCase();
170
+ let score = 1;
171
+ for (const [keyword, weight] of Object.entries(contextScores)) {
172
+ if (context.includes(keyword))
173
+ score = Math.max(score, weight);
174
+ }
175
+ const isStatus = statusContext.test(context);
176
+ const isCta = (context.includes('button') || context.includes('btn') || context.includes('cta')) &&
177
+ bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent' &&
178
+ bgColor !== 'rgb(255, 255, 255)' && bgColor !== 'rgb(0, 0, 0)' && bgColor !== 'rgb(239, 239, 239)' &&
179
+ colorAlpha(bgColor) >= 0.7;
180
+ if (isCta) {
181
+ score = Math.max(score, 25);
182
+ // CTA background is a strong primary signal — count occurrences
183
+ const ctaNorm = normalizeColor(bgColor);
184
+ if (ctaNorm) {
185
+ const existing = ctaPrimaryMap.get(ctaNorm) || { original: bgColor, count: 0 };
186
+ existing.count++;
187
+ ctaPrimaryMap.set(ctaNorm, existing);
188
+ }
189
+ }
190
+ function extractColorsFromValue(colorValue) {
191
+ if (!colorValue)
192
+ return [];
193
+ const colorRegex = /(#[0-9a-f]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|[a-z]+)/gi;
194
+ const matches = colorValue.match(colorRegex) || [];
195
+ const cssColorFunctions = new Set(['oklab', 'oklch', 'lch', 'lab', 'color', 'display', 'hsl', 'rgb', 'rgba', 'hsla', 'inherit', 'initial', 'unset', 'none', 'auto', 'normal']);
196
+ return matches.filter(c => c !== 'transparent' && c !== 'rgba(0, 0, 0, 0)' && c !== 'rgba(0,0,0,0)' &&
197
+ c.length > 2 && !cssColorFunctions.has(c.toLowerCase()));
198
+ }
199
+ const allColors = [
200
+ ...extractColorsFromValue(bgColor),
201
+ ...extractColorsFromValue(textColor),
202
+ ...extractColorsFromValue(borderColor),
203
+ ];
204
+ allColors.forEach((color) => {
205
+ if (color && color !== "rgba(0, 0, 0, 0)" && color !== "transparent" && colorAlpha(color) >= 0.3) {
206
+ const normalized = normalizeColor(color);
207
+ const existing = colorMap.get(normalized) || { original: color, count: 0, bgCount: 0, score: 0, sources: new Set(), statusCount: 0, nonStatusCount: 0, isToken: tokenHexes.has(normalized) };
208
+ existing.count++;
209
+ if (isStatus)
210
+ existing.statusCount++;
211
+ else
212
+ existing.nonStatusCount++;
213
+ if (extractColorsFromValue(bgColor).includes(color))
214
+ existing.bgCount++;
215
+ existing.score += score;
216
+ if (score > 1) {
217
+ const source = context.split(" ")[0].substring(0, 30);
218
+ if (source && !source.includes("__"))
219
+ existing.sources.add(source);
220
+ }
221
+ colorMap.set(normalized, existing);
222
+ }
223
+ });
224
+ if (context.includes("primary") || el.matches('[class*="primary"]')) {
225
+ const candidate = bgColor !== "rgba(0, 0, 0, 0)" && bgColor !== "transparent" ? bgColor : textColor;
226
+ if (colorAlpha(candidate) >= 0.7)
227
+ semanticColors.primary = candidate;
228
+ }
229
+ if (context.includes("secondary"))
230
+ semanticColors.secondary = bgColor;
231
+ });
232
+ // Use most-common CTA background as primary if class-based detection missed it
233
+ // Require at least 2 CTA occurrences to avoid single "Sign up" buttons dominating
234
+ if (!semanticColors.primary && ctaPrimaryMap.size > 0) {
235
+ let bestScore = -1;
236
+ for (const [norm, entry] of ctaPrimaryMap) {
237
+ if (entry.count < 2)
238
+ continue;
239
+ const data = colorMap.get(norm);
240
+ if (data && data.score > bestScore) {
241
+ bestScore = data.score;
242
+ semanticColors.primary = entry.original;
243
+ }
244
+ }
245
+ }
246
+ const threshold = Math.max(3, Math.floor(totalElements * 0.01));
247
+ function isStructuralColor(data, totalElements) {
248
+ if (data.isToken)
249
+ return false;
250
+ const usagePercent = (data.count / totalElements) * 100;
251
+ const normalized = normalizeColor(data.original);
252
+ if (data.original === "rgba(0, 0, 0, 0)" || data.original === "transparent")
253
+ return true;
254
+ if (usagePercent > 40 && data.score < data.count * 1.2)
255
+ return true;
256
+ if (data.bgCount === 0 && data.score < data.count * 1.5) {
257
+ const hex = normalized.replace('#', '');
258
+ const r = parseInt(hex.substring(0, 2), 16);
259
+ const g = parseInt(hex.substring(2, 4), 16);
260
+ const b = parseInt(hex.substring(4, 6), 16);
261
+ const max = Math.max(r, g, b);
262
+ const min = Math.min(r, g, b);
263
+ const saturation = max === 0 ? 0 : (max - min) / max;
264
+ if (saturation > 0.3)
265
+ return true;
266
+ }
267
+ return false;
268
+ }
269
+ function deltaE(rgb1, rgb2) {
270
+ function hexToRgb(hex) {
271
+ if (!hex.startsWith("#"))
272
+ return null;
273
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
274
+ return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null;
275
+ }
276
+ function rgbToXyz(r, g, b) {
277
+ r = r / 255;
278
+ g = g / 255;
279
+ b = b / 255;
280
+ r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
281
+ g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
282
+ b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
283
+ return {
284
+ x: (r * 0.4124564 + g * 0.3575761 + b * 0.1804375) * 100,
285
+ y: (r * 0.2126729 + g * 0.7151522 + b * 0.0721750) * 100,
286
+ z: (r * 0.0193339 + g * 0.1191920 + b * 0.9503041) * 100,
287
+ };
288
+ }
289
+ function xyzToLab(x, y, z) {
290
+ x = x / 95.047;
291
+ y = y / 100.000;
292
+ z = z / 108.883;
293
+ const fx = x > 0.008856 ? Math.pow(x, 1 / 3) : (7.787 * x + 16 / 116);
294
+ const fy = y > 0.008856 ? Math.pow(y, 1 / 3) : (7.787 * y + 16 / 116);
295
+ const fz = z > 0.008856 ? Math.pow(z, 1 / 3) : (7.787 * z + 16 / 116);
296
+ return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
297
+ }
298
+ const rgb1Obj = hexToRgb(rgb1);
299
+ const rgb2Obj = hexToRgb(rgb2);
300
+ if (!rgb1Obj || !rgb2Obj)
301
+ return 999;
302
+ const xyz1 = rgbToXyz(rgb1Obj.r, rgb1Obj.g, rgb1Obj.b);
303
+ const lab1 = xyzToLab(xyz1.x, xyz1.y, xyz1.z);
304
+ const xyz2 = rgbToXyz(rgb2Obj.r, rgb2Obj.g, rgb2Obj.b);
305
+ const lab2 = xyzToLab(xyz2.x, xyz2.y, xyz2.z);
306
+ const dL = lab1.L - lab2.L, dA = lab1.a - lab2.a, dB = lab1.b - lab2.b;
307
+ return Math.sqrt(dL * dL + dA * dA + dB * dB);
308
+ }
309
+ const rawColors = Array.from(colorMap.entries())
310
+ .filter(([, data]) => data.count >= threshold)
311
+ .map(([normalized, data]) => ({ color: data.original, normalized, count: data.count }));
312
+ const palette = Array.from(colorMap.entries())
313
+ .filter(([norm, data]) => {
314
+ // Status/utility-only colours are not brand identity unless declared as
315
+ // a token or recurring as a CTA background.
316
+ const ctaEntry = ctaPrimaryMap.get(norm);
317
+ const isCtaPrimary = ctaEntry && ctaEntry.count >= 2;
318
+ if (!data.isToken && !isCtaPrimary && data.statusCount > 0 && data.nonStatusCount === 0)
319
+ return false;
320
+ // Declared brand tokens always qualify regardless of element count.
321
+ const highScore = data.isToken || data.score >= 10 || (data.count > 0 && data.score / data.count >= 3);
322
+ if (!highScore && data.count < threshold)
323
+ return false;
324
+ if (isStructuralColor(data, totalElements))
325
+ return false;
326
+ return true;
327
+ })
328
+ .map(([normalizedColor, data]) => ({
329
+ color: data.original,
330
+ normalized: normalizedColor,
331
+ count: data.count,
332
+ confidence: data.isToken
333
+ ? (data.score > 5 ? "high" : "medium")
334
+ : data.score > 20 ? "high" : data.score > 5 ? "medium" : "low",
335
+ sources: Array.from(data.sources).slice(0, 3),
336
+ }))
337
+ .sort((a, b) => b.count - a.count);
338
+ const perceptuallyDeduped = [];
339
+ const merged = new Set();
340
+ palette.forEach((color, index) => {
341
+ if (merged.has(index))
342
+ return;
343
+ const similar = [color];
344
+ for (let i = index + 1; i < palette.length; i++) {
345
+ if (merged.has(i))
346
+ continue;
347
+ if (deltaE(color.normalized, palette[i].normalized) < 15) {
348
+ similar.push(palette[i]);
349
+ merged.add(i);
350
+ }
351
+ }
352
+ perceptuallyDeduped.push(similar.sort((a, b) => b.count - a.count)[0]);
353
+ });
354
+ const paletteNormalizedColors = new Set(perceptuallyDeduped.map((c) => c.normalized));
355
+ const cssVarsByColor = new Map();
356
+ Object.entries(cssVariables).forEach(([prop, value]) => {
357
+ const normalized = normalizeColor(value);
358
+ if (paletteNormalizedColors.has(normalized))
359
+ return;
360
+ let isDuplicate = false;
361
+ for (const paletteColor of perceptuallyDeduped) {
362
+ if (deltaE(normalized, paletteColor.normalized) < 15) {
363
+ isDuplicate = true;
364
+ break;
365
+ }
366
+ }
367
+ if (isDuplicate)
368
+ return;
369
+ if (!cssVarsByColor.has(normalized))
370
+ cssVarsByColor.set(normalized, { value, vars: [] });
371
+ cssVarsByColor.get(normalized).vars.push(prop);
372
+ });
373
+ const filteredCssVariables = {};
374
+ cssVarsByColor.forEach(({ value, vars }) => { filteredCssVariables[vars[0]] = value; });
375
+ // Fallback: pick most chromatic non-gray palette color as primary
376
+ if (!semanticColors.primary && perceptuallyDeduped.length > 0) {
377
+ function chroma(hex) {
378
+ if (!hex || !hex.startsWith('#'))
379
+ return 0;
380
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
381
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
382
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
383
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
384
+ const l = (max + min) / 2;
385
+ if (max === min)
386
+ return 0;
387
+ const s = l > 0.5 ? (max - min) / (2 - max - min) : (max - min) / (max + min);
388
+ // Penalize near-black and near-white
389
+ if (l < 0.08 || l > 0.92)
390
+ return 0;
391
+ return s;
392
+ }
393
+ const best = perceptuallyDeduped
394
+ .filter(c => c.confidence !== 'low')
395
+ .map(c => ({ c, chroma: chroma(c.normalized), isToken: tokenHexes.has(c.normalized) }))
396
+ .filter(({ chroma }) => chroma > 0.15)
397
+ // Brand-token provenance is a bonus on top of prominence, not an
398
+ // override: a declared token beats incidental colours, but a dominant
399
+ // high-usage colour still wins over a barely-used token.
400
+ .sort((a, b) => ((b.c.count + (b.isToken ? 20 : 0)) - (a.c.count + (a.isToken ? 20 : 0)))
401
+ || (b.chroma - a.chroma))[0];
402
+ if (best)
403
+ semanticColors.primary = best.c.color;
404
+ }
405
+ return { semantic: semanticColors, palette: perceptuallyDeduped, cssVariables: filteredCssVariables, _raw: rawColors };
406
+ });
407
+ if (result && result.palette) {
408
+ result.palette = result.palette.map((colorItem) => {
409
+ const converted = convertColor(colorItem.normalized || colorItem.color);
410
+ if (converted)
411
+ return { ...colorItem, lch: converted.lch, oklch: converted.oklch };
412
+ return colorItem;
413
+ });
414
+ // Cluster alpha/lightness variants of the same hue under one token
415
+ // e.g. rgba(99,91,255,0.1) is a variant of #635bff, not a separate brand color
416
+ const primaryHex = result.semantic?.primary
417
+ ? hexToRgb(toOpaque(result.semantic.primary)) : null;
418
+ result.palette = result.palette.map((colorItem) => {
419
+ const hex = colorItem.normalized || colorItem.color;
420
+ const role = colorRole(hex, colorItem);
421
+ const onColor = bestOnColor(hex);
422
+ const hover = hoverVariant(hex);
423
+ // Mark alpha/lightness variants of primary
424
+ let variantOf = null;
425
+ if (primaryHex && role !== 'surface') {
426
+ const rgb = hexToRgb(hex);
427
+ if (rgb && isSameHueFamily(rgb, primaryHex)) {
428
+ variantOf = 'primary';
429
+ }
430
+ }
431
+ return { ...colorItem, role, onColor, hover, ...(variantOf ? { variantOf } : {}) };
432
+ });
433
+ }
434
+ if (result && result.cssVariables) {
435
+ const enhancedCssVariables = {};
436
+ for (const [name, value] of Object.entries(result.cssVariables)) {
437
+ const converted = convertColor(value);
438
+ if (converted) {
439
+ enhancedCssVariables[name] = { value, lch: converted.lch, oklch: converted.oklch };
440
+ }
441
+ else {
442
+ enhancedCssVariables[name] = { value };
443
+ }
444
+ }
445
+ result.cssVariables = enhancedCssVariables;
446
+ }
447
+ return result;
448
+ }
449
+ function hexToRgb(hex) {
450
+ const m = /^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
451
+ if (!m)
452
+ return null;
453
+ return { r: parseInt(m[1], 16), g: parseInt(m[2], 16), b: parseInt(m[3], 16) };
454
+ }
455
+ function relativeLuminance({ r, g, b }) {
456
+ const lin = c => { c /= 255; return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); };
457
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
458
+ }
459
+ function contrastRatio(hex1, hex2) {
460
+ const rgb1 = hexToRgb(hex1);
461
+ const rgb2 = hexToRgb(hex2);
462
+ if (!rgb1 || !rgb2)
463
+ return 1;
464
+ const l1 = relativeLuminance(rgb1);
465
+ const l2 = relativeLuminance(rgb2);
466
+ const lighter = Math.max(l1, l2);
467
+ const darker = Math.min(l1, l2);
468
+ return (lighter + 0.05) / (darker + 0.05);
469
+ }
470
+ function bestOnColor(hex) {
471
+ const onWhite = contrastRatio(hex, '#ffffff');
472
+ const onBlack = contrastRatio(hex, '#000000');
473
+ return onWhite >= onBlack ? '#ffffff' : '#000000';
474
+ }
475
+ function colorRole(hex, colorItem) {
476
+ const rgb = hexToRgb(hex);
477
+ if (!rgb)
478
+ return 'unknown';
479
+ const lum = relativeLuminance(rgb);
480
+ // Surface: near-pure white or black only
481
+ if (lum > 0.9 || lum < 0.005)
482
+ return 'surface';
483
+ // Neutral: low HSL saturation (grays, off-whites, taupes)
484
+ const max = Math.max(rgb.r, rgb.g, rgb.b) / 255;
485
+ const min = Math.min(rgb.r, rgb.g, rgb.b) / 255;
486
+ const saturation = max === 0 ? 0 : (max - min) / max;
487
+ if (saturation < 0.12)
488
+ return 'neutral';
489
+ // Accent: anything chromatic with meaningful confidence
490
+ if (colorItem.confidence === 'high' || colorItem.confidence === 'medium')
491
+ return 'accent';
492
+ return 'supporting';
493
+ }
494
+ function hoverVariant(hex) {
495
+ const rgb = hexToRgb(hex);
496
+ if (!rgb)
497
+ return null;
498
+ const lum = relativeLuminance(rgb);
499
+ // Lighten dark colors, darken light ones — always move toward better contrast
500
+ const factor = lum < 0.18 ? 1.2 : 0.85;
501
+ const clamp = v => Math.min(255, Math.max(0, Math.round(v)));
502
+ const r = clamp(rgb.r * factor);
503
+ const g = clamp(rgb.g * factor);
504
+ const b = clamp(rgb.b * factor);
505
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
506
+ }
507
+ function toOpaque(color) {
508
+ if (!color)
509
+ return null;
510
+ if (color.startsWith('#'))
511
+ return color;
512
+ const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
513
+ if (!m)
514
+ return null;
515
+ return `#${(+m[1]).toString(16).padStart(2, '0')}${(+m[2]).toString(16).padStart(2, '0')}${(+m[3]).toString(16).padStart(2, '0')}`;
516
+ }
517
+ function isSameHueFamily(rgb, primaryRgb, threshold = 30) {
518
+ // Compare hue in HSL space — same hue ±threshold degrees = variant
519
+ function toHsl({ r, g, b }) {
520
+ r /= 255;
521
+ g /= 255;
522
+ b /= 255;
523
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
524
+ const l = (max + min) / 2;
525
+ if (max === min)
526
+ return { h: 0, s: 0, l };
527
+ const d = max - min;
528
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
529
+ let h;
530
+ switch (max) {
531
+ case r:
532
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
533
+ break;
534
+ case g:
535
+ h = ((b - r) / d + 2) / 6;
536
+ break;
537
+ default: h = ((r - g) / d + 4) / 6;
538
+ }
539
+ return { h: h * 360, s, l };
540
+ }
541
+ const h1 = toHsl(rgb);
542
+ const h2 = toHsl(primaryRgb);
543
+ // Low saturation colors are not hue-family variants
544
+ if (h1.s < 0.1 || h2.s < 0.1)
545
+ return false;
546
+ const diff = Math.abs(h1.h - h2.h);
547
+ return Math.min(diff, 360 - diff) < threshold;
548
+ }
549
+ export async function extractWcagPairs(page) {
550
+ let rawPairs = [];
551
+ try {
552
+ rawPairs = await page.evaluate(() => {
553
+ const canvas = document.createElement('canvas');
554
+ canvas.width = canvas.height = 1;
555
+ const ctx = canvas.getContext('2d');
556
+ function toHex(color) {
557
+ if (!color)
558
+ return null;
559
+ try {
560
+ const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
561
+ if (m) {
562
+ if (m[4] !== undefined && parseFloat(m[4]) < 0.1)
563
+ return null;
564
+ const r = parseInt(m[1]).toString(16).padStart(2, '0');
565
+ const g = parseInt(m[2]).toString(16).padStart(2, '0');
566
+ const b = parseInt(m[3]).toString(16).padStart(2, '0');
567
+ return `#${r}${g}${b}`;
568
+ }
569
+ if (/^#[0-9a-f]{6}$/i.test(color))
570
+ return color.toLowerCase();
571
+ if (!ctx)
572
+ return null;
573
+ ctx.clearRect(0, 0, 1, 1);
574
+ ctx.fillStyle = 'rgba(0,0,0,0)';
575
+ ctx.fillStyle = color;
576
+ ctx.fillRect(0, 0, 1, 1);
577
+ const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
578
+ if (a < 25)
579
+ return null;
580
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
581
+ }
582
+ catch {
583
+ return null;
584
+ }
585
+ }
586
+ function findBg(el) {
587
+ let node = el;
588
+ while (node && node.tagName !== 'HTML') {
589
+ try {
590
+ const bg = toHex(getComputedStyle(node).backgroundColor);
591
+ if (bg)
592
+ return bg;
593
+ }
594
+ catch { /* skip stale node */ }
595
+ node = node.parentElement;
596
+ }
597
+ return null;
598
+ }
599
+ const seen = new Map();
600
+ let checked = 0;
601
+ const els = document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, a, button, label, span, li, td, th, [role="button"]');
602
+ for (const el of els) {
603
+ if (checked++ > 1500)
604
+ break;
605
+ try {
606
+ const s = getComputedStyle(el);
607
+ if (s.display === 'none' || s.visibility === 'hidden' || s.opacity === '0')
608
+ continue;
609
+ const rect = el.getBoundingClientRect();
610
+ if (rect.width === 0 || rect.height === 0)
611
+ continue;
612
+ const fg = toHex(s.color);
613
+ if (!fg)
614
+ continue;
615
+ const bg = findBg(el);
616
+ if (!bg || fg === bg)
617
+ continue;
618
+ const key = [fg, bg].sort().join('/');
619
+ const entry = seen.get(key);
620
+ if (entry) {
621
+ entry.count++;
622
+ }
623
+ else {
624
+ seen.set(key, { fg, bg, count: 1 });
625
+ }
626
+ }
627
+ catch { /* skip any element that throws */ }
628
+ }
629
+ return Array.from(seen.values()).sort((a, b) => b.count - a.count).slice(0, 50);
630
+ });
631
+ }
632
+ catch {
633
+ return [];
634
+ }
635
+ const { relativeLuminance } = await import('../colors.js');
636
+ const pairs = [];
637
+ const seen = new Set();
638
+ for (const { fg, bg, count } of rawPairs) {
639
+ try {
640
+ const key = [fg, bg].sort().join('/');
641
+ if (seen.has(key))
642
+ continue;
643
+ seen.add(key);
644
+ const l1 = relativeLuminance(fg);
645
+ const l2 = relativeLuminance(bg);
646
+ if (l1 === null || l2 === null)
647
+ continue;
648
+ const lighter = Math.max(l1, l2);
649
+ const darker = Math.min(l1, l2);
650
+ const ratio = Math.round((lighter + 0.05) / (darker + 0.05) * 100) / 100;
651
+ pairs.push({ fg, bg, ratio, aa: ratio >= 4.5, aaLarge: ratio >= 3, aaa: ratio >= 7, count });
652
+ }
653
+ catch { /* skip malformed pair */ }
654
+ }
655
+ return pairs.sort((a, b) => b.count - a.count);
656
+ }
657
+ //# sourceMappingURL=colors.js.map