designlang 7.2.0 → 9.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 (90) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +154 -13
  3. package/bin/design-extract.js +94 -1
  4. package/package.json +9 -3
  5. package/src/config.js +2 -0
  6. package/src/crawler.js +55 -6
  7. package/src/drift.js +137 -0
  8. package/src/extractors/accessibility.js +44 -1
  9. package/src/extractors/colors.js +50 -12
  10. package/src/extractors/component-anatomy.js +123 -0
  11. package/src/extractors/motion.js +184 -0
  12. package/src/extractors/scoring.js +49 -30
  13. package/src/extractors/voice.js +96 -0
  14. package/src/formatters/markdown.js +88 -0
  15. package/src/formatters/motion-tokens.js +22 -0
  16. package/src/index.js +14 -0
  17. package/src/lint.js +198 -0
  18. package/src/visual-diff.js +116 -0
  19. package/.github/FUNDING.yml +0 -1
  20. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -62
  21. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  22. package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -28
  23. package/.github/og-preview.png +0 -0
  24. package/.github/workflows/manavarya-bot.yml +0 -17
  25. package/chrome-extension/README.md +0 -41
  26. package/chrome-extension/icons/favicon.svg +0 -7
  27. package/chrome-extension/icons/icon-128.png +0 -0
  28. package/chrome-extension/icons/icon-16.png +0 -0
  29. package/chrome-extension/icons/icon-32.png +0 -0
  30. package/chrome-extension/icons/icon-48.png +0 -0
  31. package/chrome-extension/manifest.json +0 -26
  32. package/chrome-extension/popup.html +0 -167
  33. package/chrome-extension/popup.js +0 -59
  34. package/docs/superpowers/plans/2026-04-18-designlang-v7.md +0 -1121
  35. package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +0 -150
  36. package/docs/superpowers/specs/2026-04-18-website-redesign-design.md +0 -120
  37. package/docs/superpowers/specs/2026-04-19-designlang-v7-1-design.md +0 -111
  38. package/tests/cli.test.js +0 -84
  39. package/tests/cookies.test.js +0 -98
  40. package/tests/extractors.test.js +0 -792
  41. package/tests/formatters.test.js +0 -709
  42. package/tests/interaction-states.test.js +0 -62
  43. package/tests/mcp.test.js +0 -68
  44. package/tests/modern-css.test.js +0 -104
  45. package/tests/routes-reconciliation.test.js +0 -120
  46. package/tests/utils.test.js +0 -413
  47. package/tests/wide-gamut.test.js +0 -90
  48. package/website/.claude/launch.json +0 -11
  49. package/website/AGENTS.md +0 -5
  50. package/website/CLAUDE.md +0 -1
  51. package/website/README.md +0 -36
  52. package/website/app/api/extract/route.js +0 -245
  53. package/website/app/components/A11ySlider.js +0 -369
  54. package/website/app/components/Comparison.js +0 -286
  55. package/website/app/components/CssHealth.js +0 -243
  56. package/website/app/components/Extractor.js +0 -184
  57. package/website/app/components/HeroExtractor.js +0 -455
  58. package/website/app/components/Marginalia.js +0 -3
  59. package/website/app/components/McpSection.js +0 -223
  60. package/website/app/components/PlatformTabs.js +0 -250
  61. package/website/app/components/RegionsComponents.js +0 -429
  62. package/website/app/components/Rule.js +0 -13
  63. package/website/app/components/Specimens.js +0 -237
  64. package/website/app/components/StructuredData.js +0 -144
  65. package/website/app/components/TokenBrowser.js +0 -344
  66. package/website/app/components/token-browser-sample.js +0 -65
  67. package/website/app/globals.css +0 -505
  68. package/website/app/icon.svg +0 -7
  69. package/website/app/layout.js +0 -126
  70. package/website/app/opengraph-image.js +0 -170
  71. package/website/app/page.js +0 -399
  72. package/website/app/robots.js +0 -15
  73. package/website/app/seo-config.js +0 -82
  74. package/website/app/sitemap.js +0 -18
  75. package/website/jsconfig.json +0 -7
  76. package/website/lib/cache.js +0 -73
  77. package/website/lib/rate-limit.js +0 -30
  78. package/website/lib/rate-limit.test.js +0 -55
  79. package/website/lib/specimens.json +0 -86
  80. package/website/lib/token-helpers.js +0 -70
  81. package/website/lib/url-safety.js +0 -103
  82. package/website/lib/url-safety.test.js +0 -116
  83. package/website/lib/zip-files.js +0 -15
  84. package/website/next.config.mjs +0 -15
  85. package/website/package-lock.json +0 -1353
  86. package/website/package.json +0 -19
  87. package/website/public/favicon.svg +0 -7
  88. package/website/public/logo-specimen.svg +0 -76
  89. package/website/public/mark.svg +0 -12
  90. package/website/public/site.webmanifest +0 -13
@@ -1,792 +0,0 @@
1
- import { describe, it } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { extractColors } from '../src/extractors/colors.js';
4
- import { extractTypography } from '../src/extractors/typography.js';
5
- import { extractSpacing } from '../src/extractors/spacing.js';
6
- import { extractShadows } from '../src/extractors/shadows.js';
7
- import { extractBorders } from '../src/extractors/borders.js';
8
- import { extractComponents } from '../src/extractors/components.js';
9
- import { extractAccessibility } from '../src/extractors/accessibility.js';
10
- import { extractLayout } from '../src/extractors/layout.js';
11
- import { extractGradients } from '../src/extractors/gradients.js';
12
- import { extractZIndex } from '../src/extractors/zindex.js';
13
- import { scoreDesignSystem } from '../src/extractors/scoring.js';
14
- import { extractStackFingerprint } from '../src/extractors/stack-fingerprint.js';
15
- import { extractCssHealth } from '../src/extractors/css-health.js';
16
- import { remediateFailingPairs } from '../src/extractors/a11y-remediation.js';
17
- import { extractSemanticRegions } from '../src/extractors/semantic-regions.js';
18
- import { clusterComponents } from '../src/extractors/component-clusters.js';
19
-
20
- // ── Shared fixture defaults ─────────────────────────────────────
21
-
22
- function makeEl(overrides = {}) {
23
- return {
24
- tag: 'div',
25
- classList: '',
26
- role: '',
27
- area: 10000,
28
- color: 'rgb(0, 0, 0)',
29
- backgroundColor: 'rgb(255, 255, 255)',
30
- backgroundImage: 'none',
31
- borderColor: 'rgb(200, 200, 200)',
32
- fontFamily: '"Inter", sans-serif',
33
- fontSize: '16px',
34
- fontWeight: '400',
35
- lineHeight: '1.5',
36
- letterSpacing: 'normal',
37
- paddingTop: '0px',
38
- paddingRight: '0px',
39
- paddingBottom: '0px',
40
- paddingLeft: '0px',
41
- marginTop: '0px',
42
- marginRight: '0px',
43
- marginBottom: '0px',
44
- marginLeft: '0px',
45
- gap: '0px',
46
- borderRadius: '0px',
47
- boxShadow: 'none',
48
- zIndex: 'auto',
49
- transition: 'none',
50
- animation: 'none',
51
- display: 'block',
52
- position: 'static',
53
- flexDirection: 'row',
54
- flexWrap: 'nowrap',
55
- justifyContent: 'normal',
56
- alignItems: 'normal',
57
- gridTemplateColumns: 'none',
58
- gridTemplateRows: 'none',
59
- maxWidth: 'none',
60
- borderWidth: '0px',
61
- borderStyle: 'none',
62
- ...overrides,
63
- };
64
- }
65
-
66
- // ── extractColors ───────────────────────────────────────────────
67
-
68
- describe('extractColors', () => {
69
- const mockStyles = [
70
- makeEl({ tag: 'body', area: 200000, color: 'rgb(51, 51, 51)', backgroundColor: 'rgb(255, 255, 255)' }),
71
- makeEl({ tag: 'h1', color: 'rgb(0, 0, 0)', backgroundColor: 'rgb(255, 255, 255)' }),
72
- makeEl({ tag: 'a', classList: 'btn', role: 'button', area: 5000, color: 'rgb(255, 255, 255)', backgroundColor: 'rgb(0, 102, 204)' }),
73
- makeEl({ tag: 'a', classList: 'btn', role: 'button', area: 5000, color: 'rgb(255, 255, 255)', backgroundColor: 'rgb(0, 102, 204)' }),
74
- makeEl({ tag: 'a', classList: 'btn', role: 'button', area: 5000, color: 'rgb(255, 255, 255)', backgroundColor: 'rgb(0, 102, 204)' }),
75
- makeEl({ tag: 'span', color: 'rgb(200, 50, 50)', backgroundColor: 'rgba(0, 0, 0, 0)' }),
76
- makeEl({ tag: 'span', color: 'rgb(200, 50, 50)', backgroundColor: 'rgba(0, 0, 0, 0)' }),
77
- makeEl({ tag: 'p', color: 'rgb(102, 102, 102)', backgroundColor: 'rgb(255, 255, 255)' }),
78
- makeEl({ tag: 'div', area: 100000, backgroundColor: 'rgb(245, 245, 245)', color: 'rgb(0, 0, 0)' }),
79
- makeEl({ tag: 'footer', area: 80000, backgroundColor: 'rgb(34, 34, 34)', color: 'rgb(200, 200, 200)' }),
80
- ];
81
-
82
- it('returns an object with expected keys', () => {
83
- const colors = extractColors(mockStyles);
84
- assert.ok('primary' in colors);
85
- assert.ok('secondary' in colors);
86
- assert.ok('accent' in colors);
87
- assert.ok('neutrals' in colors);
88
- assert.ok('backgrounds' in colors);
89
- assert.ok('text' in colors);
90
- assert.ok('gradients' in colors);
91
- assert.ok('all' in colors);
92
- });
93
-
94
- it('detects a primary chromatic color', () => {
95
- const colors = extractColors(mockStyles);
96
- assert.ok(colors.primary);
97
- assert.ok(colors.primary.hex);
98
- assert.ok(colors.primary.rgb);
99
- assert.ok(colors.primary.hsl);
100
- });
101
-
102
- it('identifies background colors from large-area elements', () => {
103
- const colors = extractColors(mockStyles);
104
- assert.ok(colors.backgrounds.length > 0);
105
- assert.ok(colors.backgrounds.includes('#ffffff'));
106
- });
107
-
108
- it('identifies text colors', () => {
109
- const colors = extractColors(mockStyles);
110
- assert.ok(colors.text.length > 0);
111
- });
112
-
113
- it('detects neutrals (unsaturated colors)', () => {
114
- const colors = extractColors(mockStyles);
115
- assert.ok(colors.neutrals.length > 0);
116
- });
117
-
118
- it('all colors have hex, rgb, hsl, count, contexts', () => {
119
- const colors = extractColors(mockStyles);
120
- for (const c of colors.all) {
121
- assert.ok(c.hex);
122
- assert.ok(c.rgb);
123
- assert.ok(c.hsl);
124
- assert.ok(typeof c.count === 'number');
125
- assert.ok(Array.isArray(c.contexts));
126
- }
127
- });
128
-
129
- it('collects gradients from backgroundImage', () => {
130
- const styles = [
131
- makeEl({ backgroundImage: 'linear-gradient(to right, #ff0000, #0000ff)' }),
132
- makeEl(),
133
- ];
134
- const colors = extractColors(styles);
135
- assert.equal(colors.gradients.length, 1);
136
- });
137
- });
138
-
139
- // ── extractTypography ───────────────────────────────────────────
140
-
141
- describe('extractTypography', () => {
142
- const mockStyles = [
143
- makeEl({ tag: 'h1', fontFamily: '"Playfair Display", serif', fontSize: '48px', fontWeight: '700', lineHeight: '1.2', letterSpacing: '-0.02em' }),
144
- makeEl({ tag: 'h2', fontFamily: '"Playfair Display", serif', fontSize: '36px', fontWeight: '700', lineHeight: '1.3', letterSpacing: 'normal' }),
145
- makeEl({ tag: 'h3', fontFamily: '"Inter", sans-serif', fontSize: '24px', fontWeight: '600', lineHeight: '1.4', letterSpacing: 'normal' }),
146
- makeEl({ tag: 'p', fontFamily: '"Inter", sans-serif', fontSize: '16px', fontWeight: '400', lineHeight: '1.5', letterSpacing: 'normal' }),
147
- makeEl({ tag: 'p', fontFamily: '"Inter", sans-serif', fontSize: '16px', fontWeight: '400', lineHeight: '1.5', letterSpacing: 'normal' }),
148
- makeEl({ tag: 'p', fontFamily: '"Inter", sans-serif', fontSize: '16px', fontWeight: '400', lineHeight: '1.5', letterSpacing: 'normal' }),
149
- makeEl({ tag: 'span', fontFamily: '"Inter", sans-serif', fontSize: '14px', fontWeight: '400', lineHeight: '1.5', letterSpacing: 'normal' }),
150
- makeEl({ tag: 'li', fontFamily: '"Inter", sans-serif', fontSize: '16px', fontWeight: '400', lineHeight: '1.5', letterSpacing: 'normal' }),
151
- makeEl({ tag: 'strong', fontFamily: '"Inter", sans-serif', fontSize: '16px', fontWeight: '700', lineHeight: '1.5', letterSpacing: 'normal' }),
152
- ];
153
-
154
- it('returns expected keys', () => {
155
- const typo = extractTypography(mockStyles);
156
- assert.ok('families' in typo);
157
- assert.ok('scale' in typo);
158
- assert.ok('headings' in typo);
159
- assert.ok('body' in typo);
160
- assert.ok('weights' in typo);
161
- });
162
-
163
- it('detects font families sorted by usage', () => {
164
- const typo = extractTypography(mockStyles);
165
- assert.ok(typo.families.length >= 2);
166
- // Inter is most used
167
- assert.equal(typo.families[0].name, 'Inter');
168
- });
169
-
170
- it('builds a type scale from unique sizes', () => {
171
- const typo = extractTypography(mockStyles);
172
- assert.ok(typo.scale.length >= 3);
173
- // Scale should be sorted descending by size
174
- for (let i = 1; i < typo.scale.length; i++) {
175
- assert.ok(typo.scale[i - 1].size >= typo.scale[i].size);
176
- }
177
- });
178
-
179
- it('identifies heading sizes from h1-h6 tags', () => {
180
- const typo = extractTypography(mockStyles);
181
- assert.ok(typo.headings.length >= 2);
182
- assert.ok(typo.headings.some(h => h.tags.includes('h1')));
183
- });
184
-
185
- it('identifies body text', () => {
186
- const typo = extractTypography(mockStyles);
187
- assert.ok(typo.body);
188
- assert.equal(typo.body.size, 16);
189
- });
190
-
191
- it('counts font weights', () => {
192
- const typo = extractTypography(mockStyles);
193
- assert.ok(typo.weights.length >= 2);
194
- assert.ok(typo.weights.some(w => w.weight === '400'));
195
- assert.ok(typo.weights.some(w => w.weight === '700'));
196
- });
197
- });
198
-
199
- // ── extractSpacing ──────────────────────────────────────────────
200
-
201
- describe('extractSpacing', () => {
202
- const mockStyles = [
203
- makeEl({ paddingTop: '4px', paddingRight: '8px', paddingBottom: '4px', paddingLeft: '8px' }),
204
- makeEl({ paddingTop: '16px', paddingRight: '16px', marginBottom: '24px' }),
205
- makeEl({ paddingTop: '8px', paddingRight: '12px', gap: '16px' }),
206
- makeEl({ marginTop: '32px', marginBottom: '32px' }),
207
- makeEl({ paddingTop: '48px', paddingBottom: '48px' }),
208
- makeEl({ paddingTop: '64px' }),
209
- ];
210
-
211
- it('returns expected keys', () => {
212
- const spacing = extractSpacing(mockStyles);
213
- assert.ok('base' in spacing);
214
- assert.ok('scale' in spacing);
215
- assert.ok('tokens' in spacing);
216
- assert.ok('raw' in spacing);
217
- });
218
-
219
- it('detects a base unit', () => {
220
- const spacing = extractSpacing(mockStyles);
221
- // The algorithm picks the best-scoring candidate from [2,4,6,8];
222
- // since all values are divisible by 2, base may be 2, 4, or another factor.
223
- assert.ok(spacing.base !== null);
224
- assert.ok([2, 4, 8].includes(spacing.base));
225
- });
226
-
227
- it('raw values are sorted ascending', () => {
228
- const spacing = extractSpacing(mockStyles);
229
- for (let i = 1; i < spacing.raw.length; i++) {
230
- assert.ok(spacing.raw[i] >= spacing.raw[i - 1]);
231
- }
232
- });
233
-
234
- it('generates tokens object', () => {
235
- const spacing = extractSpacing(mockStyles);
236
- assert.ok(Object.keys(spacing.tokens).length > 0);
237
- });
238
- });
239
-
240
- // ── extractShadows ──────────────────────────────────────────────
241
-
242
- describe('extractShadows', () => {
243
- const mockStyles = [
244
- makeEl({ boxShadow: '0 1px 3px rgba(0,0,0,0.1)' }),
245
- makeEl({ boxShadow: '0 4px 12px rgba(0,0,0,0.15)' }),
246
- makeEl({ boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.06)' }),
247
- makeEl({ boxShadow: '0 20px 40px rgba(0,0,0,0.2)' }),
248
- makeEl({ boxShadow: 'none' }),
249
- makeEl(),
250
- ];
251
-
252
- it('returns an object with values array', () => {
253
- const shadows = extractShadows(mockStyles);
254
- assert.ok(Array.isArray(shadows.values));
255
- });
256
-
257
- it('extracts unique shadows', () => {
258
- const shadows = extractShadows(mockStyles);
259
- assert.equal(shadows.values.length, 4);
260
- });
261
-
262
- it('classifies shadow sizes', () => {
263
- const shadows = extractShadows(mockStyles);
264
- const labels = shadows.values.map(s => s.label);
265
- assert.ok(labels.includes('sm'));
266
- assert.ok(labels.includes('lg') || labels.includes('xl'));
267
- });
268
-
269
- it('detects inset shadows', () => {
270
- const shadows = extractShadows(mockStyles);
271
- assert.ok(shadows.values.some(s => s.inset));
272
- });
273
-
274
- it('sorts by blur ascending', () => {
275
- const shadows = extractShadows(mockStyles);
276
- for (let i = 1; i < shadows.values.length; i++) {
277
- assert.ok(shadows.values[i].blur >= shadows.values[i - 1].blur);
278
- }
279
- });
280
- });
281
-
282
- // ── extractBorders ──────────────────────────────────────────────
283
-
284
- describe('extractBorders', () => {
285
- const mockStyles = [
286
- makeEl({ borderRadius: '4px', borderWidth: '1px', borderStyle: 'solid' }),
287
- makeEl({ borderRadius: '4px', borderWidth: '1px', borderStyle: 'solid' }),
288
- makeEl({ borderRadius: '4px', borderWidth: '1px', borderStyle: 'solid' }),
289
- makeEl({ borderRadius: '8px', borderWidth: '2px', borderStyle: 'solid' }),
290
- makeEl({ borderRadius: '8px', borderWidth: '2px', borderStyle: 'solid' }),
291
- makeEl({ borderRadius: '16px', borderStyle: 'dashed', borderWidth: '1px' }),
292
- makeEl({ borderRadius: '9999px' }),
293
- makeEl({ borderRadius: '0px' }),
294
- ];
295
-
296
- it('returns expected keys', () => {
297
- const borders = extractBorders(mockStyles);
298
- assert.ok('radii' in borders);
299
- assert.ok('widths' in borders);
300
- assert.ok('styles' in borders);
301
- });
302
-
303
- it('extracts unique border radii', () => {
304
- const borders = extractBorders(mockStyles);
305
- assert.ok(borders.radii.length >= 3);
306
- });
307
-
308
- it('labels border radii', () => {
309
- const borders = extractBorders(mockStyles);
310
- for (const r of borders.radii) {
311
- assert.ok(r.label);
312
- assert.ok(r.value > 0);
313
- assert.ok(typeof r.count === 'number');
314
- }
315
- });
316
-
317
- it('extracts border widths', () => {
318
- const borders = extractBorders(mockStyles);
319
- assert.ok(borders.widths.length >= 1);
320
- assert.ok(borders.widths.includes(1));
321
- });
322
-
323
- it('extracts border styles', () => {
324
- const borders = extractBorders(mockStyles);
325
- assert.ok(borders.styles.includes('solid'));
326
- assert.ok(borders.styles.includes('dashed'));
327
- });
328
- });
329
-
330
- // ── extractComponents ───────────────────────────────────────────
331
-
332
- describe('extractComponents', () => {
333
- const mockStyles = [
334
- makeEl({ tag: 'button', role: 'button', area: 3000, backgroundColor: 'rgb(0, 102, 204)', color: 'rgb(255, 255, 255)', fontSize: '14px', fontWeight: '600', borderRadius: '8px', paddingTop: '10px', paddingRight: '20px' }),
335
- makeEl({ tag: 'button', role: 'button', area: 3000, backgroundColor: 'rgb(0, 102, 204)', color: 'rgb(255, 255, 255)', fontSize: '14px', fontWeight: '600', borderRadius: '8px', paddingTop: '10px', paddingRight: '20px' }),
336
- makeEl({ tag: 'a', classList: 'btn-secondary', role: 'button', area: 2500, backgroundColor: 'rgb(240, 240, 240)', color: 'rgb(0, 0, 0)', fontSize: '14px', fontWeight: '500', borderRadius: '8px', paddingTop: '10px', paddingRight: '20px' }),
337
- makeEl({ tag: 'input', area: 2000, backgroundColor: 'rgb(255, 255, 255)', color: 'rgb(0, 0, 0)', borderColor: 'rgb(200, 200, 200)', borderRadius: '4px', fontSize: '14px', paddingTop: '8px', paddingRight: '12px' }),
338
- makeEl({ tag: 'input', area: 2000, backgroundColor: 'rgb(255, 255, 255)', color: 'rgb(0, 0, 0)', borderColor: 'rgb(200, 200, 200)', borderRadius: '4px', fontSize: '14px', paddingTop: '8px', paddingRight: '12px' }),
339
- makeEl({ tag: 'a', area: 500, color: 'rgb(0, 102, 204)', fontSize: '16px', fontWeight: '400' }),
340
- makeEl({ tag: 'a', area: 500, color: 'rgb(0, 102, 204)', fontSize: '16px', fontWeight: '400' }),
341
- makeEl({ tag: 'nav', role: 'navigation', area: 50000, backgroundColor: 'rgb(255, 255, 255)', paddingTop: '12px', paddingBottom: '12px', paddingLeft: '24px', paddingRight: '24px', position: 'sticky', boxShadow: '0 1px 3px rgba(0,0,0,0.1)' }),
342
- makeEl({ tag: 'div', classList: 'card', area: 20000, backgroundColor: 'rgb(255, 255, 255)', borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)', paddingTop: '24px', paddingRight: '24px' }),
343
- ];
344
-
345
- it('detects buttons', () => {
346
- const components = extractComponents(mockStyles);
347
- assert.ok(components.buttons);
348
- assert.ok(components.buttons.count >= 2);
349
- assert.ok(components.buttons.baseStyle);
350
- });
351
-
352
- it('detects inputs', () => {
353
- const components = extractComponents(mockStyles);
354
- assert.ok(components.inputs);
355
- assert.equal(components.inputs.count, 2);
356
- });
357
-
358
- it('detects links', () => {
359
- const components = extractComponents(mockStyles);
360
- assert.ok(components.links);
361
- });
362
-
363
- it('detects navigation', () => {
364
- const components = extractComponents(mockStyles);
365
- assert.ok(components.navigation);
366
- });
367
-
368
- it('detects cards', () => {
369
- const components = extractComponents(mockStyles);
370
- assert.ok(components.cards);
371
- });
372
-
373
- it('generates CSS for detected components', () => {
374
- const components = extractComponents(mockStyles);
375
- // At least buttons should have CSS
376
- assert.ok(components.buttons.css);
377
- assert.ok(components.buttons.css.includes('{'));
378
- });
379
- });
380
-
381
- // ── extractAccessibility ────────────────────────────────────────
382
-
383
- describe('extractAccessibility', () => {
384
- const mockStyles = [
385
- // Good contrast: black on white
386
- makeEl({ tag: 'p', color: 'rgb(0, 0, 0)', backgroundColor: 'rgb(255, 255, 255)', fontSize: '16px' }),
387
- makeEl({ tag: 'p', color: 'rgb(0, 0, 0)', backgroundColor: 'rgb(255, 255, 255)', fontSize: '16px' }),
388
- makeEl({ tag: 'p', color: 'rgb(0, 0, 0)', backgroundColor: 'rgb(255, 255, 255)', fontSize: '16px' }),
389
- // Good contrast: white on dark blue
390
- makeEl({ tag: 'a', color: 'rgb(255, 255, 255)', backgroundColor: 'rgb(0, 51, 153)', fontSize: '14px' }),
391
- makeEl({ tag: 'a', color: 'rgb(255, 255, 255)', backgroundColor: 'rgb(0, 51, 153)', fontSize: '14px' }),
392
- // Bad contrast: light gray on white
393
- makeEl({ tag: 'span', color: 'rgb(200, 200, 200)', backgroundColor: 'rgb(255, 255, 255)', fontSize: '12px' }),
394
- makeEl({ tag: 'span', color: 'rgb(200, 200, 200)', backgroundColor: 'rgb(255, 255, 255)', fontSize: '12px' }),
395
- // Transparent bg should be skipped
396
- makeEl({ tag: 'div', color: 'rgb(0, 0, 0)', backgroundColor: 'rgba(0, 0, 0, 0)', fontSize: '16px' }),
397
- ];
398
-
399
- it('returns expected keys', () => {
400
- const a11y = extractAccessibility(mockStyles);
401
- assert.ok('score' in a11y);
402
- assert.ok('passCount' in a11y);
403
- assert.ok('failCount' in a11y);
404
- assert.ok('totalPairs' in a11y);
405
- assert.ok('pairs' in a11y);
406
- });
407
-
408
- it('detects passing pairs', () => {
409
- const a11y = extractAccessibility(mockStyles);
410
- assert.ok(a11y.passCount > 0);
411
- });
412
-
413
- it('detects failing pairs', () => {
414
- const a11y = extractAccessibility(mockStyles);
415
- assert.ok(a11y.failCount > 0);
416
- });
417
-
418
- it('score is between 0 and 100', () => {
419
- const a11y = extractAccessibility(mockStyles);
420
- assert.ok(a11y.score >= 0 && a11y.score <= 100);
421
- });
422
-
423
- it('sorts failures first', () => {
424
- const a11y = extractAccessibility(mockStyles);
425
- if (a11y.pairs.length >= 2) {
426
- const firstFail = a11y.pairs.findIndex(p => p.level === 'FAIL');
427
- const firstPass = a11y.pairs.findIndex(p => p.level !== 'FAIL');
428
- if (firstFail >= 0 && firstPass >= 0) {
429
- assert.ok(firstFail < firstPass);
430
- }
431
- }
432
- });
433
-
434
- it('each pair has ratio and level', () => {
435
- const a11y = extractAccessibility(mockStyles);
436
- for (const p of a11y.pairs) {
437
- assert.ok(typeof p.ratio === 'number');
438
- assert.ok(['AA', 'AAA', 'FAIL'].includes(p.level));
439
- }
440
- });
441
- });
442
-
443
- // ── extractLayout ───────────────────────────────────────────────
444
-
445
- describe('extractLayout', () => {
446
- const mockStyles = [
447
- makeEl({ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gridTemplateRows: 'auto', gap: '24px', area: 80000 }),
448
- makeEl({ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', area: 40000 }),
449
- makeEl({ display: 'flex', flexDirection: 'row', flexWrap: 'nowrap', justifyContent: 'space-between', alignItems: 'center', gap: '16px', area: 50000 }),
450
- makeEl({ display: 'flex', flexDirection: 'column', flexWrap: 'nowrap', justifyContent: 'flex-start', alignItems: 'stretch', area: 30000 }),
451
- makeEl({ display: 'flex', flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'flex-start', alignItems: 'flex-start', gap: '12px', area: 60000 }),
452
- makeEl({ tag: 'main', area: 200000, maxWidth: '1200px', paddingLeft: '24px', paddingRight: '24px' }),
453
- ];
454
-
455
- it('returns expected keys', () => {
456
- const layout = extractLayout(mockStyles);
457
- assert.ok('gridCount' in layout);
458
- assert.ok('flexCount' in layout);
459
- assert.ok('gridColumns' in layout);
460
- assert.ok('flexDirections' in layout);
461
- assert.ok('containerWidths' in layout);
462
- assert.ok('gaps' in layout);
463
- assert.ok('topGrids' in layout);
464
- assert.ok('topFlex' in layout);
465
- });
466
-
467
- it('counts grid and flex containers', () => {
468
- const layout = extractLayout(mockStyles);
469
- assert.equal(layout.gridCount, 2);
470
- assert.equal(layout.flexCount, 3);
471
- });
472
-
473
- it('detects grid column patterns', () => {
474
- const layout = extractLayout(mockStyles);
475
- assert.ok(layout.gridColumns.length >= 1);
476
- assert.ok(layout.gridColumns.some(g => g.columns === 3));
477
- });
478
-
479
- it('summarizes flex directions', () => {
480
- const layout = extractLayout(mockStyles);
481
- assert.ok(layout.flexDirections['row/nowrap'] >= 1);
482
- assert.ok(layout.flexDirections['column/nowrap'] >= 1);
483
- });
484
-
485
- it('collects gap values', () => {
486
- const layout = extractLayout(mockStyles);
487
- assert.ok(layout.gaps.length >= 1);
488
- });
489
-
490
- it('detects container widths', () => {
491
- const layout = extractLayout(mockStyles);
492
- assert.ok(layout.containerWidths.length >= 1);
493
- assert.ok(layout.containerWidths.some(c => c.maxWidth === '1200px'));
494
- });
495
- });
496
-
497
- // ── extractGradients ────────────────────────────────────────────
498
-
499
- describe('extractGradients', () => {
500
- const mockStyles = [
501
- makeEl({ backgroundImage: 'linear-gradient(to right, #ff0000, #0000ff)' }),
502
- makeEl({ backgroundImage: 'radial-gradient(circle, #ff0000, #00ff00, #0000ff)' }),
503
- makeEl({ backgroundImage: 'linear-gradient(135deg, #ff0000 0%, #ff7700 50%, #ffff00 100%)' }),
504
- makeEl({ backgroundImage: 'none' }),
505
- makeEl({ backgroundImage: '' }),
506
- // Duplicate should not be counted twice
507
- makeEl({ backgroundImage: 'linear-gradient(to right, #ff0000, #0000ff)' }),
508
- ];
509
-
510
- it('returns expected keys', () => {
511
- const result = extractGradients(mockStyles);
512
- assert.ok('gradients' in result);
513
- assert.ok('count' in result);
514
- });
515
-
516
- it('counts unique gradients', () => {
517
- const result = extractGradients(mockStyles);
518
- assert.equal(result.count, 3);
519
- });
520
-
521
- it('parses gradient type', () => {
522
- const result = extractGradients(mockStyles);
523
- assert.ok(result.gradients.some(g => g.type === 'linear'));
524
- assert.ok(result.gradients.some(g => g.type === 'radial'));
525
- });
526
-
527
- it('parses gradient stops', () => {
528
- const result = extractGradients(mockStyles);
529
- for (const g of result.gradients) {
530
- assert.ok(Array.isArray(g.stops));
531
- assert.ok(g.stops.length >= 2);
532
- }
533
- });
534
-
535
- it('classifies gradients', () => {
536
- const result = extractGradients(mockStyles);
537
- for (const g of result.gradients) {
538
- assert.ok(['subtle', 'brand', 'bold', 'complex'].includes(g.classification));
539
- }
540
- });
541
- });
542
-
543
- // ── extractZIndex ───────────────────────────────────────────────
544
-
545
- describe('extractZIndex', () => {
546
- const mockStyles = [
547
- makeEl({ tag: 'header', classList: 'navbar', zIndex: '100', position: 'sticky' }),
548
- makeEl({ tag: 'div', classList: 'modal', zIndex: '1000', position: 'fixed' }),
549
- makeEl({ tag: 'div', classList: 'dropdown', zIndex: '200', position: 'absolute' }),
550
- makeEl({ tag: 'div', classList: 'tooltip', zIndex: '500', position: 'absolute' }),
551
- makeEl({ zIndex: 'auto' }),
552
- makeEl({ tag: 'div', zIndex: '1' }),
553
- ];
554
-
555
- it('returns expected keys', () => {
556
- const result = extractZIndex(mockStyles);
557
- assert.ok('layers' in result);
558
- assert.ok('allValues' in result);
559
- assert.ok('issues' in result);
560
- assert.ok('scale' in result);
561
- });
562
-
563
- it('collects sorted unique z-index values', () => {
564
- const result = extractZIndex(mockStyles);
565
- assert.ok(result.allValues.length >= 4);
566
- for (let i = 1; i < result.allValues.length; i++) {
567
- assert.ok(result.allValues[i] >= result.allValues[i - 1]);
568
- }
569
- });
570
-
571
- it('groups values into layers', () => {
572
- const result = extractZIndex(mockStyles);
573
- assert.ok(result.layers.length >= 1);
574
- assert.ok(result.layers.some(l => l.name === 'modal'));
575
- assert.ok(result.layers.some(l => l.name === 'dropdown'));
576
- });
577
-
578
- it('skips auto z-index values', () => {
579
- const result = extractZIndex(mockStyles);
580
- assert.ok(!result.allValues.includes(NaN));
581
- });
582
- });
583
-
584
- // ── scoreDesignSystem ───────────────────────────────────────────
585
-
586
- describe('scoreDesignSystem', () => {
587
- const mockDesign = {
588
- colors: {
589
- primary: { hex: '#0066cc', rgb: { r: 0, g: 102, b: 204 }, hsl: { h: 210, s: 100, l: 40 }, count: 50 },
590
- secondary: null,
591
- accent: null,
592
- neutrals: [{ hex: '#333333', rgb: { r: 51, g: 51, b: 51 }, hsl: { h: 0, s: 0, l: 20 }, count: 30 }],
593
- backgrounds: ['#ffffff'],
594
- text: ['#333333'],
595
- all: [
596
- { hex: '#0066cc', count: 50 },
597
- { hex: '#333333', count: 30 },
598
- { hex: '#ffffff', count: 80 },
599
- { hex: '#666666', count: 20 },
600
- { hex: '#f5f5f5', count: 15 },
601
- ],
602
- },
603
- typography: {
604
- families: [{ name: 'Inter', count: 80, usage: 'all' }],
605
- scale: [
606
- { size: 48, weight: '700', lineHeight: '1.2', tags: ['h1'], count: 5 },
607
- { size: 16, weight: '400', lineHeight: '1.5', tags: ['p'], count: 60 },
608
- ],
609
- weights: [{ weight: '400', count: 60 }, { weight: '700', count: 20 }],
610
- },
611
- spacing: { base: 4, scale: [4, 8, 12, 16, 24, 32, 48, 64] },
612
- shadows: { values: [{ raw: '0 1px 3px rgba(0,0,0,0.1)', blur: 3, inset: false, label: 'sm' }] },
613
- borders: { radii: [{ value: 4, label: 'sm', count: 20 }, { value: 8, label: 'md', count: 15 }] },
614
- variables: { colors: { '--color-primary': '#0066cc' }, spacing: {}, typography: {} },
615
- accessibility: { score: 90, passCount: 45, failCount: 5, totalPairs: 50, pairs: [] },
616
- };
617
-
618
- it('returns expected keys', () => {
619
- const result = scoreDesignSystem(mockDesign);
620
- assert.ok('overall' in result);
621
- assert.ok('grade' in result);
622
- assert.ok('scores' in result);
623
- assert.ok('issues' in result);
624
- assert.ok('strengths' in result);
625
- });
626
-
627
- it('overall score is between 0 and 100', () => {
628
- const result = scoreDesignSystem(mockDesign);
629
- assert.ok(result.overall >= 0 && result.overall <= 100);
630
- });
631
-
632
- it('assigns a letter grade', () => {
633
- const result = scoreDesignSystem(mockDesign);
634
- assert.ok(['A', 'B', 'C', 'D', 'F'].includes(result.grade));
635
- });
636
-
637
- it('scores each category', () => {
638
- const result = scoreDesignSystem(mockDesign);
639
- assert.ok('colorDiscipline' in result.scores);
640
- assert.ok('typographyConsistency' in result.scores);
641
- assert.ok('spacingSystem' in result.scores);
642
- assert.ok('shadowConsistency' in result.scores);
643
- assert.ok('radiusConsistency' in result.scores);
644
- assert.ok('accessibility' in result.scores);
645
- assert.ok('tokenization' in result.scores);
646
- });
647
-
648
- it('gives good scores for a clean design', () => {
649
- const result = scoreDesignSystem(mockDesign);
650
- // This mock has tight palette, 1 font, base-4 spacing
651
- assert.ok(result.scores.colorDiscipline >= 80);
652
- assert.ok(result.scores.typographyConsistency >= 80);
653
- assert.ok(result.scores.spacingSystem >= 80);
654
- });
655
-
656
- it('identifies strengths', () => {
657
- const result = scoreDesignSystem(mockDesign);
658
- assert.ok(Array.isArray(result.strengths));
659
- });
660
-
661
- it('penalizes missing primary color', () => {
662
- const noPrimary = { ...mockDesign, colors: { ...mockDesign.colors, primary: null } };
663
- const result = scoreDesignSystem(noPrimary);
664
- assert.ok(result.issues.some(i => i.includes('primary')));
665
- });
666
- });
667
-
668
- // ── extractStackFingerprint ─────────────────────────────────────
669
-
670
- describe('extractStackFingerprint', () => {
671
- it('detects Next.js from __NEXT_DATA__', () => {
672
- const out = extractStackFingerprint({ windowGlobals: ['__NEXT_DATA__'], scripts: [], metas: [], classNameSample: [] });
673
- assert.equal(out.framework, 'next');
674
- });
675
-
676
- it('detects Tailwind from utility-heavy classNames', () => {
677
- const out = extractStackFingerprint({
678
- windowGlobals: [],
679
- scripts: [],
680
- metas: [],
681
- classNameSample: [
682
- 'flex items-center gap-4 text-sm text-gray-600',
683
- 'px-4 py-2 rounded-md bg-blue-500',
684
- 'grid grid-cols-3 md:grid-cols-4',
685
- 'flex justify-center',
686
- 'p-4 shadow-md',
687
- 'mt-4 text-lg',
688
- ],
689
- });
690
- assert.equal(out.css.layer, 'tailwind');
691
- assert.ok(out.css.tailwind.utilities.length > 0);
692
- });
693
-
694
- it('returns unknown when nothing matches', () => {
695
- const out = extractStackFingerprint({ windowGlobals: [], scripts: [], metas: [], classNameSample: ['foo', 'bar'] });
696
- assert.equal(out.framework, 'unknown');
697
- assert.equal(out.css.layer, 'unknown');
698
- });
699
- });
700
-
701
- // ── extractCssHealth ────────────────────────────────────────────
702
-
703
- describe('extractCssHealth', () => {
704
- const payload = [{
705
- url: 'https://x.com/a.css',
706
- text: '.a{color:red}.a{color:red}.b{color:blue !important}.c-webkit-foo{color:x}@keyframes fade{0%{opacity:0}100%{opacity:1}\n}',
707
- totalBytes: 1000,
708
- ranges: [{ start: 0, end: 400 }], // 60% unused
709
- }];
710
-
711
- it('counts !important', () => {
712
- const r = extractCssHealth(payload);
713
- assert.equal(r.importantCount, 1);
714
- });
715
-
716
- it('counts duplicate declarations', () => {
717
- const r = extractCssHealth(payload);
718
- assert.ok(r.duplicates >= 1);
719
- });
720
-
721
- it('reports unused bytes', () => {
722
- const r = extractCssHealth(payload);
723
- assert.equal(r.unusedBytes, 600);
724
- assert.equal(r.usedBytes, 400);
725
- });
726
-
727
- it('catalogs keyframes', () => {
728
- const r = extractCssHealth(payload);
729
- assert.ok(r.keyframes.some(k => k.name === 'fade'));
730
- });
731
- });
732
-
733
- // ── remediateFailingPairs ───────────────────────────────────────
734
-
735
- describe('remediateFailingPairs', () => {
736
- it('suggests a palette color that passes AA', () => {
737
- const failing = [{ fg: '#777777', bg: '#ffffff', ratio: 3.5, rule: 'AA-normal' }];
738
- const palette = ['#000000', '#222222', '#555555', '#cccccc'];
739
- const out = remediateFailingPairs(failing, palette);
740
- assert.equal(out.length, 1);
741
- assert.ok(out[0].suggestion);
742
- assert.ok(out[0].suggestion.newRatio >= 4.5);
743
- });
744
-
745
- it('returns null suggestion when no palette color passes', () => {
746
- const failing = [{ fg: '#eee', bg: '#fff', ratio: 1.1, rule: 'AA-normal' }];
747
- const palette = ['#dedede'];
748
- const out = remediateFailingPairs(failing, palette);
749
- assert.equal(out[0].suggestion, null);
750
- });
751
- });
752
-
753
- // ── extractSemanticRegions ──────────────────────────────────────
754
-
755
- describe('extractSemanticRegions', () => {
756
- it('labels header as nav', () => {
757
- const out = extractSemanticRegions([{ tag: 'header', role: '', className: '', id: '', text: 'Home About', headings: [], buttonCount: 3, cardCount: 0, bounds: { x: 0, y: 0, w: 1280, h: 80 } }]);
758
- assert.equal(out[0].role, 'nav');
759
- });
760
-
761
- it('labels section with CTA + heading as hero', () => {
762
- const out = extractSemanticRegions([{ tag: 'section', role: '', className: 'hero', id: '', text: 'Welcome', headings: ['Build better'], buttonCount: 2, cardCount: 0, bounds: { x: 0, y: 80, w: 1280, h: 600 } }]);
763
- assert.equal(out[0].role, 'hero');
764
- });
765
-
766
- it('labels pricing based on cards + keyword', () => {
767
- const out = extractSemanticRegions([{ tag: 'section', role: '', className: '', id: '', text: 'Basic $9/mo Pro $29/mo Team $99/mo', headings: ['Pricing'], buttonCount: 3, cardCount: 3, bounds: { x: 0, y: 0, w: 1280, h: 400 } }]);
768
- assert.equal(out[0].role, 'pricing');
769
- });
770
- });
771
-
772
- // ── clusterComponents ───────────────────────────────────────────
773
-
774
- describe('clusterComponents', () => {
775
- it('collapses identical instances into one entry', () => {
776
- const els = Array.from({ length: 5 }, () => ({ kind: 'button', structuralHash: 'button>span', styleVector: [16, 8, 0, 0], css: { bg: '#f00' } }));
777
- const out = clusterComponents(els);
778
- assert.equal(out.length, 1);
779
- assert.equal(out[0].instanceCount, 5);
780
- });
781
-
782
- it('separates variants with different style vectors', () => {
783
- const els = [
784
- { kind: 'button', structuralHash: 'button>span', styleVector: [16, 8, 0, 0], css: { bg: '#f00' } },
785
- { kind: 'button', structuralHash: 'button>span', styleVector: [16, 8, 0, 0], css: { bg: '#f00' } },
786
- { kind: 'button', structuralHash: 'button>span', styleVector: [0, 0, 16, 8], css: { bg: '#0f0' } },
787
- ];
788
- const out = clusterComponents(els);
789
- assert.equal(out.length, 1);
790
- assert.equal(out[0].variants.length, 2);
791
- });
792
- });