designlang 7.2.0 → 8.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.
- package/CHANGELOG.md +35 -0
- package/README.md +17 -0
- package/bin/design-extract.js +5 -1
- package/package.json +1 -1
- package/src/config.js +2 -0
- package/src/crawler.js +35 -6
- package/src/extractors/accessibility.js +44 -1
- package/src/extractors/colors.js +50 -12
- package/src/extractors/scoring.js +49 -30
- package/.github/FUNDING.yml +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -62
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -28
- package/.github/og-preview.png +0 -0
- package/.github/workflows/manavarya-bot.yml +0 -17
- package/chrome-extension/README.md +0 -41
- package/chrome-extension/icons/favicon.svg +0 -7
- package/chrome-extension/icons/icon-128.png +0 -0
- package/chrome-extension/icons/icon-16.png +0 -0
- package/chrome-extension/icons/icon-32.png +0 -0
- package/chrome-extension/icons/icon-48.png +0 -0
- package/chrome-extension/manifest.json +0 -26
- package/chrome-extension/popup.html +0 -167
- package/chrome-extension/popup.js +0 -59
- package/docs/superpowers/plans/2026-04-18-designlang-v7.md +0 -1121
- package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +0 -150
- package/docs/superpowers/specs/2026-04-18-website-redesign-design.md +0 -120
- package/docs/superpowers/specs/2026-04-19-designlang-v7-1-design.md +0 -111
- package/tests/cli.test.js +0 -84
- package/tests/cookies.test.js +0 -98
- package/tests/extractors.test.js +0 -792
- package/tests/formatters.test.js +0 -709
- package/tests/interaction-states.test.js +0 -62
- package/tests/mcp.test.js +0 -68
- package/tests/modern-css.test.js +0 -104
- package/tests/routes-reconciliation.test.js +0 -120
- package/tests/utils.test.js +0 -413
- package/tests/wide-gamut.test.js +0 -90
- package/website/.claude/launch.json +0 -11
- package/website/AGENTS.md +0 -5
- package/website/CLAUDE.md +0 -1
- package/website/README.md +0 -36
- package/website/app/api/extract/route.js +0 -245
- package/website/app/components/A11ySlider.js +0 -369
- package/website/app/components/Comparison.js +0 -286
- package/website/app/components/CssHealth.js +0 -243
- package/website/app/components/Extractor.js +0 -184
- package/website/app/components/HeroExtractor.js +0 -455
- package/website/app/components/Marginalia.js +0 -3
- package/website/app/components/McpSection.js +0 -223
- package/website/app/components/PlatformTabs.js +0 -250
- package/website/app/components/RegionsComponents.js +0 -429
- package/website/app/components/Rule.js +0 -13
- package/website/app/components/Specimens.js +0 -237
- package/website/app/components/StructuredData.js +0 -144
- package/website/app/components/TokenBrowser.js +0 -344
- package/website/app/components/token-browser-sample.js +0 -65
- package/website/app/globals.css +0 -505
- package/website/app/icon.svg +0 -7
- package/website/app/layout.js +0 -126
- package/website/app/opengraph-image.js +0 -170
- package/website/app/page.js +0 -399
- package/website/app/robots.js +0 -15
- package/website/app/seo-config.js +0 -82
- package/website/app/sitemap.js +0 -18
- package/website/jsconfig.json +0 -7
- package/website/lib/cache.js +0 -73
- package/website/lib/rate-limit.js +0 -30
- package/website/lib/rate-limit.test.js +0 -55
- package/website/lib/specimens.json +0 -86
- package/website/lib/token-helpers.js +0 -70
- package/website/lib/url-safety.js +0 -103
- package/website/lib/url-safety.test.js +0 -116
- package/website/lib/zip-files.js +0 -15
- package/website/next.config.mjs +0 -15
- package/website/package-lock.json +0 -1353
- package/website/package.json +0 -19
- package/website/public/favicon.svg +0 -7
- package/website/public/logo-specimen.svg +0 -76
- package/website/public/mark.svg +0 -12
- package/website/public/site.webmanifest +0 -13
package/tests/utils.test.js
DELETED
|
@@ -1,413 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import {
|
|
4
|
-
parseColor,
|
|
5
|
-
rgbToHex,
|
|
6
|
-
rgbToHsl,
|
|
7
|
-
colorDistance,
|
|
8
|
-
clusterColors,
|
|
9
|
-
clusterValues,
|
|
10
|
-
parseCSSValue,
|
|
11
|
-
detectScale,
|
|
12
|
-
nameFromUrl,
|
|
13
|
-
isSaturated,
|
|
14
|
-
safeName,
|
|
15
|
-
remToPx,
|
|
16
|
-
pxToRem,
|
|
17
|
-
} from '../src/utils.js';
|
|
18
|
-
|
|
19
|
-
// ── parseColor ──────────────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
describe('parseColor', () => {
|
|
22
|
-
it('returns null for null/undefined/empty/none/inherit', () => {
|
|
23
|
-
assert.equal(parseColor(null), null);
|
|
24
|
-
assert.equal(parseColor(undefined), null);
|
|
25
|
-
assert.equal(parseColor(''), null);
|
|
26
|
-
assert.equal(parseColor('none'), null);
|
|
27
|
-
assert.equal(parseColor('inherit'), null);
|
|
28
|
-
assert.equal(parseColor('initial'), null);
|
|
29
|
-
assert.equal(parseColor('currentcolor'), null);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('parses 3-digit hex (#RGB)', () => {
|
|
33
|
-
const c = parseColor('#f00');
|
|
34
|
-
assert.deepEqual(c, { r: 255, g: 0, b: 0, a: 1 });
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('parses 6-digit hex (#RRGGBB)', () => {
|
|
38
|
-
const c = parseColor('#0066cc');
|
|
39
|
-
assert.deepEqual(c, { r: 0, g: 102, b: 204, a: 1 });
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('parses 8-digit hex (#RRGGBBAA)', () => {
|
|
43
|
-
const c = parseColor('#0066cc80');
|
|
44
|
-
assert.equal(c.r, 0);
|
|
45
|
-
assert.equal(c.g, 102);
|
|
46
|
-
assert.equal(c.b, 204);
|
|
47
|
-
assert.ok(Math.abs(c.a - 128 / 255) < 0.01);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('parses 4-digit hex (#RGBA)', () => {
|
|
51
|
-
const c = parseColor('#f008');
|
|
52
|
-
assert.equal(c.r, 255);
|
|
53
|
-
assert.equal(c.g, 0);
|
|
54
|
-
assert.equal(c.b, 0);
|
|
55
|
-
assert.ok(Math.abs(c.a - 0x88 / 255) < 0.01);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('parses rgb(r, g, b)', () => {
|
|
59
|
-
const c = parseColor('rgb(100, 200, 50)');
|
|
60
|
-
assert.deepEqual(c, { r: 100, g: 200, b: 50, a: 1 });
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('parses rgba(r, g, b, a)', () => {
|
|
64
|
-
const c = parseColor('rgba(100, 200, 50, 0.5)');
|
|
65
|
-
assert.deepEqual(c, { r: 100, g: 200, b: 50, a: 0.5 });
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('parses hsl(h, s%, l%)', () => {
|
|
69
|
-
const c = parseColor('hsl(0, 100%, 50%)');
|
|
70
|
-
assert.equal(c.r, 255);
|
|
71
|
-
assert.equal(c.g, 0);
|
|
72
|
-
assert.equal(c.b, 0);
|
|
73
|
-
assert.equal(c.a, 1);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('parses hsla(h, s%, l%, a)', () => {
|
|
77
|
-
const c = parseColor('hsla(0, 100%, 50%, 0.5)');
|
|
78
|
-
assert.equal(c.r, 255);
|
|
79
|
-
assert.equal(c.a, 0.5);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('parses modern hsl syntax: hsl(210 50% 40%)', () => {
|
|
83
|
-
const c = parseColor('hsl(210 50% 40%)');
|
|
84
|
-
assert.ok(c);
|
|
85
|
-
assert.equal(c.a, 1);
|
|
86
|
-
// hsl(210, 50%, 40%) -> some shade of blue
|
|
87
|
-
assert.ok(c.b > c.r);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('parses modern hsl syntax with alpha: hsl(210 50% 40% / 0.5)', () => {
|
|
91
|
-
const c = parseColor('hsl(210 50% 40% / 0.5)');
|
|
92
|
-
assert.ok(c);
|
|
93
|
-
assert.equal(c.a, 0.5);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('parses oklch(L C H)', () => {
|
|
97
|
-
const c = parseColor('oklch(0.5 0.2 270)');
|
|
98
|
-
assert.ok(c);
|
|
99
|
-
assert.equal(c.a, 1);
|
|
100
|
-
assert.ok(c.r >= 0 && c.r <= 255);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('parses oklch with alpha', () => {
|
|
104
|
-
const c = parseColor('oklch(0.5 0.2 270 / 0.8)');
|
|
105
|
-
assert.ok(c);
|
|
106
|
-
assert.equal(c.a, 0.8);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('parses oklab(L a b)', () => {
|
|
110
|
-
const c = parseColor('oklab(0.5 0.1 -0.1)');
|
|
111
|
-
assert.ok(c);
|
|
112
|
-
assert.equal(c.a, 1);
|
|
113
|
-
assert.ok(c.r >= 0 && c.r <= 255);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('parses color-mix(in srgb, ...)', () => {
|
|
117
|
-
const c = parseColor('color-mix(in srgb, #ff0000 50%, #0000ff)');
|
|
118
|
-
assert.ok(c);
|
|
119
|
-
// Should be roughly halfway between red and blue
|
|
120
|
-
assert.ok(c.r > 100);
|
|
121
|
-
assert.ok(c.b > 100);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('parses named colors', () => {
|
|
125
|
-
const c = parseColor('red');
|
|
126
|
-
assert.deepEqual(c, { r: 255, g: 0, b: 0, a: 1 });
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('parses named color white', () => {
|
|
130
|
-
const c = parseColor('white');
|
|
131
|
-
assert.deepEqual(c, { r: 255, g: 255, b: 255, a: 1 });
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('parses named colors case-insensitively', () => {
|
|
135
|
-
const c = parseColor('RED');
|
|
136
|
-
assert.deepEqual(c, { r: 255, g: 0, b: 0, a: 1 });
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('returns null for unrecognized strings', () => {
|
|
140
|
-
assert.equal(parseColor('not-a-color'), null);
|
|
141
|
-
assert.equal(parseColor('blahblah'), null);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// ── rgbToHex ────────────────────────────────────────────────────
|
|
146
|
-
|
|
147
|
-
describe('rgbToHex', () => {
|
|
148
|
-
it('converts black', () => {
|
|
149
|
-
assert.equal(rgbToHex({ r: 0, g: 0, b: 0 }), '#000000');
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('converts white', () => {
|
|
153
|
-
assert.equal(rgbToHex({ r: 255, g: 255, b: 255 }), '#ffffff');
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('converts a mid color', () => {
|
|
157
|
-
assert.equal(rgbToHex({ r: 0, g: 102, b: 204 }), '#0066cc');
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// ── rgbToHsl ────────────────────────────────────────────────────
|
|
162
|
-
|
|
163
|
-
describe('rgbToHsl', () => {
|
|
164
|
-
it('converts pure red', () => {
|
|
165
|
-
const hsl = rgbToHsl({ r: 255, g: 0, b: 0 });
|
|
166
|
-
assert.equal(hsl.h, 0);
|
|
167
|
-
assert.equal(hsl.s, 100);
|
|
168
|
-
assert.equal(hsl.l, 50);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('converts pure green (CSS green = 0,128,0)', () => {
|
|
172
|
-
const hsl = rgbToHsl({ r: 0, g: 128, b: 0 });
|
|
173
|
-
assert.equal(hsl.h, 120);
|
|
174
|
-
assert.equal(hsl.l, 25);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('converts gray (achromatic)', () => {
|
|
178
|
-
const hsl = rgbToHsl({ r: 128, g: 128, b: 128 });
|
|
179
|
-
assert.equal(hsl.h, 0);
|
|
180
|
-
assert.equal(hsl.s, 0);
|
|
181
|
-
assert.equal(hsl.l, 50);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('converts white', () => {
|
|
185
|
-
const hsl = rgbToHsl({ r: 255, g: 255, b: 255 });
|
|
186
|
-
assert.equal(hsl.l, 100);
|
|
187
|
-
assert.equal(hsl.s, 0);
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
// ── colorDistance ────────────────────────────────────────────────
|
|
192
|
-
|
|
193
|
-
describe('colorDistance', () => {
|
|
194
|
-
it('returns 0 for identical colors', () => {
|
|
195
|
-
assert.equal(colorDistance({ r: 100, g: 100, b: 100 }, { r: 100, g: 100, b: 100 }), 0);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('returns correct Euclidean distance', () => {
|
|
199
|
-
const d = colorDistance({ r: 0, g: 0, b: 0 }, { r: 255, g: 255, b: 255 });
|
|
200
|
-
assert.ok(Math.abs(d - Math.sqrt(3 * 255 * 255)) < 0.01);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('returns small distance for similar colors', () => {
|
|
204
|
-
const d = colorDistance({ r: 100, g: 100, b: 100 }, { r: 105, g: 100, b: 100 });
|
|
205
|
-
assert.ok(d < 10);
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// ── clusterColors ───────────────────────────────────────────────
|
|
210
|
-
|
|
211
|
-
describe('clusterColors', () => {
|
|
212
|
-
it('groups similar colors together', () => {
|
|
213
|
-
const colors = [
|
|
214
|
-
{ hex: '#ff0000', parsed: { r: 255, g: 0, b: 0 }, count: 10 },
|
|
215
|
-
{ hex: '#ff0505', parsed: { r: 255, g: 5, b: 5 }, count: 5 },
|
|
216
|
-
{ hex: '#0000ff', parsed: { r: 0, g: 0, b: 255 }, count: 3 },
|
|
217
|
-
];
|
|
218
|
-
const clusters = clusterColors(colors, 15);
|
|
219
|
-
// Red shades should be grouped, blue separate
|
|
220
|
-
assert.equal(clusters.length, 2);
|
|
221
|
-
assert.equal(clusters[0].count, 15); // red cluster total
|
|
222
|
-
assert.equal(clusters[1].count, 3); // blue cluster
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('returns each color as its own cluster when threshold is 0', () => {
|
|
226
|
-
const colors = [
|
|
227
|
-
{ hex: '#ff0000', parsed: { r: 255, g: 0, b: 0 }, count: 10 },
|
|
228
|
-
{ hex: '#ff0505', parsed: { r: 255, g: 5, b: 5 }, count: 5 },
|
|
229
|
-
];
|
|
230
|
-
const clusters = clusterColors(colors, 0);
|
|
231
|
-
assert.equal(clusters.length, 2);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it('sorts clusters by count descending', () => {
|
|
235
|
-
const colors = [
|
|
236
|
-
{ hex: '#0000ff', parsed: { r: 0, g: 0, b: 255 }, count: 3 },
|
|
237
|
-
{ hex: '#ff0000', parsed: { r: 255, g: 0, b: 0 }, count: 10 },
|
|
238
|
-
];
|
|
239
|
-
const clusters = clusterColors(colors, 15);
|
|
240
|
-
assert.ok(clusters[0].count >= clusters[clusters.length - 1].count);
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// ── clusterValues ───────────────────────────────────────────────
|
|
245
|
-
|
|
246
|
-
describe('clusterValues', () => {
|
|
247
|
-
it('groups nearby numbers', () => {
|
|
248
|
-
const result = clusterValues([4, 5, 8, 16, 17, 32], 2);
|
|
249
|
-
// 4 and 5 cluster -> 4; 8 standalone; 16 and 17 cluster -> 16; 32 standalone
|
|
250
|
-
assert.ok(result.includes(4));
|
|
251
|
-
assert.ok(result.includes(8));
|
|
252
|
-
assert.ok(result.includes(16));
|
|
253
|
-
assert.ok(result.includes(32));
|
|
254
|
-
assert.ok(!result.includes(5));
|
|
255
|
-
assert.ok(!result.includes(17));
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('returns all values when threshold is 0', () => {
|
|
259
|
-
const result = clusterValues([1, 2, 3], 0);
|
|
260
|
-
assert.deepEqual(result, [1, 2, 3]);
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
// ── parseCSSValue ───────────────────────────────────────────────
|
|
265
|
-
|
|
266
|
-
describe('parseCSSValue', () => {
|
|
267
|
-
it('parses px values', () => {
|
|
268
|
-
const r = parseCSSValue('16px');
|
|
269
|
-
assert.deepEqual(r, { value: 16, unit: 'px' });
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it('parses rem values', () => {
|
|
273
|
-
const r = parseCSSValue('1.5rem');
|
|
274
|
-
assert.deepEqual(r, { value: 1.5, unit: 'rem' });
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it('parses em values', () => {
|
|
278
|
-
const r = parseCSSValue('2em');
|
|
279
|
-
assert.deepEqual(r, { value: 2, unit: 'em' });
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('parses percentage values', () => {
|
|
283
|
-
const r = parseCSSValue('100%');
|
|
284
|
-
assert.deepEqual(r, { value: 100, unit: '%' });
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('parses unitless numbers', () => {
|
|
288
|
-
const r = parseCSSValue('1.5');
|
|
289
|
-
assert.deepEqual(r, { value: 1.5, unit: '' });
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it('returns null for normal/auto/none', () => {
|
|
293
|
-
assert.equal(parseCSSValue('normal'), null);
|
|
294
|
-
assert.equal(parseCSSValue('auto'), null);
|
|
295
|
-
assert.equal(parseCSSValue('none'), null);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it('returns null for null/undefined', () => {
|
|
299
|
-
assert.equal(parseCSSValue(null), null);
|
|
300
|
-
assert.equal(parseCSSValue(undefined), null);
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
// ── detectScale ─────────────────────────────────────────────────
|
|
305
|
-
|
|
306
|
-
describe('detectScale', () => {
|
|
307
|
-
it('detects a base unit for multiples-of-4 scale', () => {
|
|
308
|
-
// All values are divisible by 2 and 4; the algorithm picks the first
|
|
309
|
-
// candidate (2) that reaches the 60% threshold, so base is 2.
|
|
310
|
-
const result = detectScale([4, 8, 12, 16, 24, 32, 48, 64]);
|
|
311
|
-
assert.ok(result.base !== null);
|
|
312
|
-
assert.ok([2, 4].includes(result.base));
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it('detects a base unit for multiples-of-8 scale', () => {
|
|
316
|
-
const result = detectScale([8, 16, 24, 32, 40, 48, 56, 64]);
|
|
317
|
-
assert.ok(result.base !== null);
|
|
318
|
-
assert.ok([2, 4, 8].includes(result.base));
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it('returns null base for fewer than 3 values', () => {
|
|
322
|
-
const result = detectScale([10, 20]);
|
|
323
|
-
assert.equal(result.base, null);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it('returns null base for arbitrary values', () => {
|
|
327
|
-
const result = detectScale([3, 7, 11, 19, 37, 53]);
|
|
328
|
-
assert.equal(result.base, null);
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
// ── nameFromUrl ─────────────────────────────────────────────────
|
|
333
|
-
|
|
334
|
-
describe('nameFromUrl', () => {
|
|
335
|
-
it('extracts hostname and makes it safe', () => {
|
|
336
|
-
// safeName replaces dots with hyphens
|
|
337
|
-
assert.equal(nameFromUrl('https://www.example.com/page'), 'example-com');
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
it('strips www prefix', () => {
|
|
341
|
-
const name = nameFromUrl('https://www.google.com');
|
|
342
|
-
assert.ok(!name.startsWith('www'));
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it('returns unknown-site for invalid URLs', () => {
|
|
346
|
-
assert.equal(nameFromUrl('not a url'), 'unknown-site');
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it('handles complex domains', () => {
|
|
350
|
-
const name = nameFromUrl('https://my-app.vercel.app/dashboard');
|
|
351
|
-
assert.equal(name, 'my-app-vercel-app');
|
|
352
|
-
});
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
// ── isSaturated ─────────────────────────────────────────────────
|
|
356
|
-
|
|
357
|
-
describe('isSaturated', () => {
|
|
358
|
-
it('returns true for a saturated color', () => {
|
|
359
|
-
assert.equal(isSaturated({ r: 255, g: 0, b: 0 }), true);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
it('returns false for gray', () => {
|
|
363
|
-
assert.equal(isSaturated({ r: 128, g: 128, b: 128 }), false);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it('returns false for white', () => {
|
|
367
|
-
assert.equal(isSaturated({ r: 255, g: 255, b: 255 }), false);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it('returns false for black', () => {
|
|
371
|
-
assert.equal(isSaturated({ r: 0, g: 0, b: 0 }), false);
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
// ── safeName ────────────────────────────────────────────────────
|
|
376
|
-
|
|
377
|
-
describe('safeName', () => {
|
|
378
|
-
it('replaces special characters with hyphens', () => {
|
|
379
|
-
assert.equal(safeName('Hello World!'), 'hello-world');
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
it('collapses multiple hyphens', () => {
|
|
383
|
-
assert.equal(safeName('a--b---c'), 'a-b-c');
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
it('trims leading/trailing hyphens', () => {
|
|
387
|
-
assert.equal(safeName('--test--'), 'test');
|
|
388
|
-
});
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
// ── remToPx / pxToRem ──────────────────────────────────────────
|
|
392
|
-
|
|
393
|
-
describe('remToPx', () => {
|
|
394
|
-
it('converts with default base 16', () => {
|
|
395
|
-
assert.equal(remToPx(1), 16);
|
|
396
|
-
assert.equal(remToPx(2), 32);
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
it('converts with custom base', () => {
|
|
400
|
-
assert.equal(remToPx(1, 10), 10);
|
|
401
|
-
});
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
describe('pxToRem', () => {
|
|
405
|
-
it('converts with default base 16', () => {
|
|
406
|
-
assert.equal(pxToRem(16), 1);
|
|
407
|
-
assert.equal(pxToRem(32), 2);
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
it('converts with custom base', () => {
|
|
411
|
-
assert.equal(pxToRem(10, 10), 1);
|
|
412
|
-
});
|
|
413
|
-
});
|
package/tests/wide-gamut.test.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { oklchToSrgb, oklabToSrgb, oklchLikeToHex, parseOklchOrOklab, rgbToHex } from '../src/utils/color-gamut.js';
|
|
4
|
-
import { extractWideGamut } from '../src/extractors/wide-gamut.js';
|
|
5
|
-
import { extractTokenSources } from '../src/extractors/token-sources.js';
|
|
6
|
-
|
|
7
|
-
describe('color-gamut', () => {
|
|
8
|
-
it('converts oklch(1 0 0) to white', () => {
|
|
9
|
-
const hex = oklchLikeToHex('oklch(1 0 0)');
|
|
10
|
-
assert.equal(hex, '#ffffff');
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('converts oklch(0 0 0) to black', () => {
|
|
14
|
-
const hex = oklchLikeToHex('oklch(0 0 0)');
|
|
15
|
-
assert.equal(hex, '#000000');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('converts a mid-chroma oklch to approximate sRGB', () => {
|
|
19
|
-
// oklch(62.8% 0.258 29.23) ≈ red (#ff0000) region
|
|
20
|
-
const hex = oklchLikeToHex('oklch(62.8% 0.258 29.23)');
|
|
21
|
-
assert.ok(/^#[0-9a-f]{6}$/.test(hex));
|
|
22
|
-
// Red channel should dominate
|
|
23
|
-
const r = parseInt(hex.slice(1, 3), 16);
|
|
24
|
-
const g = parseInt(hex.slice(3, 5), 16);
|
|
25
|
-
const b = parseInt(hex.slice(5, 7), 16);
|
|
26
|
-
assert.ok(r > g, `expected r>g, got ${hex}`);
|
|
27
|
-
assert.ok(r > b, `expected r>b, got ${hex}`);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('parses oklab values with percentages', () => {
|
|
31
|
-
const p = parseOklchOrOklab('oklab(62.8% 0.1 -0.1)');
|
|
32
|
-
assert.equal(p.type, 'oklab');
|
|
33
|
-
assert.ok(Math.abs(p.L - 0.628) < 1e-6);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('rgbToHex clamps and formats correctly', () => {
|
|
37
|
-
assert.equal(rgbToHex(1, 0, 0), '#ff0000');
|
|
38
|
-
assert.equal(rgbToHex(0, 1, 0), '#00ff00');
|
|
39
|
-
assert.equal(rgbToHex(1.5, -0.5, 0.5), '#ff0080');
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe('extractWideGamut', () => {
|
|
44
|
-
it('returns zeros for empty input', () => {
|
|
45
|
-
const r = extractWideGamut([]);
|
|
46
|
-
assert.equal(r.totalCount, 0);
|
|
47
|
-
assert.equal(r.oklch.count, 0);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('buckets colors by type and emits hex for oklch samples', () => {
|
|
51
|
-
const r = extractWideGamut([
|
|
52
|
-
{ raw: 'oklch(62.8% 0.258 29.23)', type: 'oklch', property: 'color', selector: '.btn' },
|
|
53
|
-
{ raw: 'color-mix(in oklab, red, blue)', type: 'color-mix', property: 'background', selector: '.x' },
|
|
54
|
-
{ raw: 'light-dark(white, black)', type: 'light-dark', property: 'color', selector: '.y' },
|
|
55
|
-
{ raw: 'color(display-p3 1 0 0)', type: 'display-p3', property: 'background', selector: '.p3' },
|
|
56
|
-
]);
|
|
57
|
-
assert.equal(r.oklch.count, 1);
|
|
58
|
-
assert.ok(r.oklch.samples[0].value?.startsWith('#'));
|
|
59
|
-
assert.equal(r.colorMix.count, 1);
|
|
60
|
-
assert.equal(r.lightDark.count, 1);
|
|
61
|
-
assert.equal(r.displayP3.count, 1);
|
|
62
|
-
assert.equal(r.totalCount, 4);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe('extractTokenSources', () => {
|
|
67
|
-
it('returns an array of token→sourceUrl entries based on first matching element', () => {
|
|
68
|
-
const design = {
|
|
69
|
-
colors: { primary: { hex: '#0072f5' }, text: ['#111111'] },
|
|
70
|
-
typography: { families: [{ name: 'Geist' }] },
|
|
71
|
-
spacing: { base: 8 },
|
|
72
|
-
borders: { radii: [{ value: '8px' }] },
|
|
73
|
-
};
|
|
74
|
-
const styles = [
|
|
75
|
-
{ color: 'rgb(0, 114, 245)', fontFamily: '"Geist", sans-serif', paddingTop: '8px', borderRadius: '8px', sources: [{ url: 'https://cdn.example/app.css', mediaText: '' }] },
|
|
76
|
-
{ color: 'rgb(17, 17, 17)', sources: [{ url: 'https://cdn.example/text.css', mediaText: '' }] },
|
|
77
|
-
];
|
|
78
|
-
const out = extractTokenSources(design, styles);
|
|
79
|
-
const primary = out.find(t => t.token === 'color.primary');
|
|
80
|
-
assert.equal(primary.sourceUrl, 'https://cdn.example/app.css');
|
|
81
|
-
const text = out.find(t => t.token === 'color.text');
|
|
82
|
-
assert.equal(text.sourceUrl, 'https://cdn.example/text.css');
|
|
83
|
-
const font = out.find(t => t.token === 'font.body');
|
|
84
|
-
assert.equal(font.sourceUrl, 'https://cdn.example/app.css');
|
|
85
|
-
const spacing = out.find(t => t.token === 'spacing.base');
|
|
86
|
-
assert.equal(spacing.sourceUrl, 'https://cdn.example/app.css');
|
|
87
|
-
const radius = out.find(t => t.token === 'radius.base');
|
|
88
|
-
assert.equal(radius.sourceUrl, 'https://cdn.example/app.css');
|
|
89
|
-
});
|
|
90
|
-
});
|
package/website/AGENTS.md
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
<!-- BEGIN:nextjs-agent-rules -->
|
|
2
|
-
# This is NOT the Next.js you know
|
|
3
|
-
|
|
4
|
-
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
|
5
|
-
<!-- END:nextjs-agent-rules -->
|
package/website/CLAUDE.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@AGENTS.md
|
package/website/README.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
|
2
|
-
|
|
3
|
-
## Getting Started
|
|
4
|
-
|
|
5
|
-
First, run the development server:
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm run dev
|
|
9
|
-
# or
|
|
10
|
-
yarn dev
|
|
11
|
-
# or
|
|
12
|
-
pnpm dev
|
|
13
|
-
# or
|
|
14
|
-
bun dev
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
|
18
|
-
|
|
19
|
-
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
|
|
20
|
-
|
|
21
|
-
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
|
22
|
-
|
|
23
|
-
## Learn More
|
|
24
|
-
|
|
25
|
-
To learn more about Next.js, take a look at the following resources:
|
|
26
|
-
|
|
27
|
-
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
28
|
-
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
29
|
-
|
|
30
|
-
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
|
31
|
-
|
|
32
|
-
## Deploy on Vercel
|
|
33
|
-
|
|
34
|
-
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
35
|
-
|
|
36
|
-
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|