@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.
- package/LICENSE +21 -0
- package/README.md +408 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +532 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/browser.d.ts +16 -0
- package/dist/lib/browser.js +27 -0
- package/dist/lib/browser.js.map +1 -0
- package/dist/lib/colors.d.ts +101 -0
- package/dist/lib/colors.js +405 -0
- package/dist/lib/colors.js.map +1 -0
- package/dist/lib/compare.d.ts +31 -0
- package/dist/lib/compare.js +46 -0
- package/dist/lib/compare.js.map +1 -0
- package/dist/lib/discovery.d.ts +31 -0
- package/dist/lib/discovery.js +243 -0
- package/dist/lib/discovery.js.map +1 -0
- package/dist/lib/drift.d.ts +64 -0
- package/dist/lib/drift.js +383 -0
- package/dist/lib/drift.js.map +1 -0
- package/dist/lib/dtcg/validate.d.ts +51 -0
- package/dist/lib/dtcg/validate.js +1403 -0
- package/dist/lib/dtcg/validate.js.map +1 -0
- package/dist/lib/exit-codes.d.ts +29 -0
- package/dist/lib/exit-codes.js +26 -0
- package/dist/lib/exit-codes.js.map +1 -0
- package/dist/lib/extractors/breakpoints.d.ts +5 -0
- package/dist/lib/extractors/breakpoints.js +450 -0
- package/dist/lib/extractors/breakpoints.js.map +1 -0
- package/dist/lib/extractors/colors.d.ts +2 -0
- package/dist/lib/extractors/colors.js +657 -0
- package/dist/lib/extractors/colors.js.map +1 -0
- package/dist/lib/extractors/components.d.ts +4 -0
- package/dist/lib/extractors/components.js +370 -0
- package/dist/lib/extractors/components.js.map +1 -0
- package/dist/lib/extractors/index.d.ts +9 -0
- package/dist/lib/extractors/index.js +1257 -0
- package/dist/lib/extractors/index.js.map +1 -0
- package/dist/lib/extractors/logo.d.ts +2 -0
- package/dist/lib/extractors/logo.js +626 -0
- package/dist/lib/extractors/logo.js.map +1 -0
- package/dist/lib/extractors/spacing.d.ts +4 -0
- package/dist/lib/extractors/spacing.js +163 -0
- package/dist/lib/extractors/spacing.js.map +1 -0
- package/dist/lib/extractors/teach.d.ts +1 -0
- package/dist/lib/extractors/teach.js +66 -0
- package/dist/lib/extractors/teach.js.map +1 -0
- package/dist/lib/extractors/typography.d.ts +1 -0
- package/dist/lib/extractors/typography.js +163 -0
- package/dist/lib/extractors/typography.js.map +1 -0
- package/dist/lib/findings.d.ts +34 -0
- package/dist/lib/findings.js +166 -0
- package/dist/lib/findings.js.map +1 -0
- package/dist/lib/formatters/dtcg.d.ts +10 -0
- package/dist/lib/formatters/dtcg.js +416 -0
- package/dist/lib/formatters/dtcg.js.map +1 -0
- package/dist/lib/formatters/html.d.ts +25 -0
- package/dist/lib/formatters/html.js +479 -0
- package/dist/lib/formatters/html.js.map +1 -0
- package/dist/lib/formatters/markdown.d.ts +5 -0
- package/dist/lib/formatters/markdown.js +568 -0
- package/dist/lib/formatters/markdown.js.map +1 -0
- package/dist/lib/formatters/pdf.d.ts +12 -0
- package/dist/lib/formatters/pdf.js +1121 -0
- package/dist/lib/formatters/pdf.js.map +1 -0
- package/dist/lib/formatters/terminal.d.ts +6 -0
- package/dist/lib/formatters/terminal.js +954 -0
- package/dist/lib/formatters/terminal.js.map +1 -0
- package/dist/lib/formatters/theme.d.ts +35 -0
- package/dist/lib/formatters/theme.js +37 -0
- package/dist/lib/formatters/theme.js.map +1 -0
- package/dist/lib/merger.d.ts +14 -0
- package/dist/lib/merger.js +362 -0
- package/dist/lib/merger.js.map +1 -0
- package/dist/lib/normalize.d.ts +29 -0
- package/dist/lib/normalize.js +59 -0
- package/dist/lib/normalize.js.map +1 -0
- package/dist/lib/robots.d.ts +12 -0
- package/dist/lib/robots.js +110 -0
- package/dist/lib/robots.js.map +1 -0
- package/dist/lib/run-summary.d.ts +40 -0
- package/dist/lib/run-summary.js +64 -0
- package/dist/lib/run-summary.js.map +1 -0
- package/dist/lib/types.d.ts +329 -0
- package/dist/lib/types.js +7 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/version.d.ts +134 -0
- package/dist/lib/version.js +153 -0
- package/dist/lib/version.js.map +1 -0
- package/dist/mcp-server.d.ts +11 -0
- package/dist/mcp-server.js +311 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/package.json +106 -0
- package/dist/test/_vitest-shim.d.ts +13 -0
- package/dist/test/_vitest-shim.js +23 -0
- package/dist/test/_vitest-shim.js.map +1 -0
- package/dist/test/cli.test.d.ts +1 -0
- package/dist/test/cli.test.js +24 -0
- package/dist/test/cli.test.js.map +1 -0
- package/dist/test/colors.test.d.ts +1 -0
- package/dist/test/colors.test.js +64 -0
- package/dist/test/colors.test.js.map +1 -0
- package/dist/test/compare.test.d.ts +1 -0
- package/dist/test/compare.test.js +57 -0
- package/dist/test/compare.test.js.map +1 -0
- package/dist/test/drift.test.d.ts +1 -0
- package/dist/test/drift.test.js +53 -0
- package/dist/test/drift.test.js.map +1 -0
- package/dist/test/dtcg-formatter.test.d.ts +1 -0
- package/dist/test/dtcg-formatter.test.js +48 -0
- package/dist/test/dtcg-formatter.test.js.map +1 -0
- package/dist/test/dtcg-validate.test.d.ts +1 -0
- package/dist/test/dtcg-validate.test.js +2129 -0
- package/dist/test/dtcg-validate.test.js.map +1 -0
- package/dist/test/exit-codes.test.d.ts +1 -0
- package/dist/test/exit-codes.test.js +53 -0
- package/dist/test/exit-codes.test.js.map +1 -0
- package/dist/test/findings.test.d.ts +1 -0
- package/dist/test/findings.test.js +77 -0
- package/dist/test/findings.test.js.map +1 -0
- package/dist/test/html.test.d.ts +1 -0
- package/dist/test/html.test.js +95 -0
- package/dist/test/html.test.js.map +1 -0
- package/dist/test/markdown.test.d.ts +1 -0
- package/dist/test/markdown.test.js +145 -0
- package/dist/test/markdown.test.js.map +1 -0
- package/dist/test/merger.test.d.ts +1 -0
- package/dist/test/merger.test.js +98 -0
- package/dist/test/merger.test.js.map +1 -0
- package/dist/test/normalize.test.d.ts +1 -0
- package/dist/test/normalize.test.js +47 -0
- package/dist/test/normalize.test.js.map +1 -0
- package/dist/test/run-summary.test.d.ts +1 -0
- package/dist/test/run-summary.test.js +45 -0
- package/dist/test/run-summary.test.js.map +1 -0
- package/dist/test/version.test.d.ts +1 -0
- package/dist/test/version.test.js +73 -0
- package/dist/test/version.test.js.map +1 -0
- 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
|