@domandigital/craft 0.5.0 → 0.6.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.
@@ -0,0 +1,68 @@
1
+ import {
2
+ fontClass,
3
+ normaliseFamily,
4
+ parseColour
5
+ } from "./chunk-GWMSSRIQ.js";
6
+
7
+ // src/fingerprint/index.ts
8
+ var FINGERPRINT_VERSION = 1;
9
+ var CHROMATIC = 0.03;
10
+ var oklch = (value) => {
11
+ const parsed = parseColour(value);
12
+ return parsed && parsed.alpha > 0.5 ? parsed.oklch : null;
13
+ };
14
+ function accentOf(s) {
15
+ const buttons = /* @__PURE__ */ new Map();
16
+ for (const c of s.controls) {
17
+ const o = oklch(c.background);
18
+ if (o && o.c >= CHROMATIC) buttons.set(c.background, (buttons.get(c.background) ?? 0) + 1);
19
+ }
20
+ const topButton = [...buttons].sort((a, b) => b[1] - a[1])[0];
21
+ if (topButton) return oklch(topButton[0]);
22
+ const bg = s.colours.backgrounds.map((b) => ({ o: oklch(b.value), w: b.area })).filter((x) => x.o && x.o.c >= CHROMATIC).sort((a, b) => b.w - a.w)[0];
23
+ if (bg) return bg.o;
24
+ const text = s.colours.text.map((t) => ({ o: oklch(t.value), w: t.chars })).filter((x) => x.o && x.o.c >= CHROMATIC).sort((a, b) => b.w - a.w)[0];
25
+ return text ? text.o : null;
26
+ }
27
+ function faces(s) {
28
+ const byChars = [...s.fonts].sort((a, b) => b.chars - a.chars);
29
+ const byDisplay = [...s.fonts].sort((a, b) => b.displayChars - a.displayChars);
30
+ const bodyName = normaliseFamily(byChars[0]?.family ?? "system-ui");
31
+ const displayName = normaliseFamily((byDisplay[0]?.displayChars ? byDisplay[0].family : byChars[0]?.family) ?? "system-ui");
32
+ return {
33
+ display: { family: displayName, class: fontClass(displayName) },
34
+ body: { family: bodyName, class: fontClass(bodyName) }
35
+ };
36
+ }
37
+ function median(values) {
38
+ if (values.length === 0) return null;
39
+ const sorted = [...values].sort((a, b) => a - b);
40
+ const mid = Math.floor(sorted.length / 2);
41
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
42
+ }
43
+ function fingerprint(s) {
44
+ const effects = [];
45
+ if (s.effects.glass > 0) effects.push("glass-panel");
46
+ if (s.effects.gradientText > 0) effects.push("gradient-text");
47
+ if (s.effects.glows > 0 || s.effects.gradients.some((g) => g.radial)) effects.push("radial-spotlight-glow");
48
+ if (s.effects.gridBackgrounds > 0) effects.push("grid-background");
49
+ if (s.effects.marquees > 0) effects.push("marquee");
50
+ if (s.effects.hairlineShadowCards >= 3) effects.push("thin-border-wide-shadow");
51
+ if (s.effects.eyebrowChip) effects.push("hero-eyebrow-chip");
52
+ if (s.motion.introOverlay) effects.push("intro-cinematic");
53
+ const below = s.sections.filter((x) => x.top > s.viewport.height).length;
54
+ return {
55
+ version: FINGERPRINT_VERSION,
56
+ accent: accentOf(s),
57
+ ground: oklch(s.ground) ?? { l: 1, c: 0, h: 0 },
58
+ ...faces(s),
59
+ roundness: median(s.controls.map((c) => Math.min(0.5, c.radiusPx / Math.max(1, c.heightPx)))),
60
+ motion: below ? Math.min(1, s.motion.hiddenSections / below) : 0,
61
+ effects: effects.sort(),
62
+ layout: s.sections.map((x) => x.kind)
63
+ };
64
+ }
65
+
66
+ export {
67
+ fingerprint
68
+ };
@@ -0,0 +1,470 @@
1
+ import {
2
+ fingerprint
3
+ } from "./chunk-HYGFP3LB.js";
4
+ import {
5
+ SNAPSHOT_VERSION,
6
+ SOURCE_FILE,
7
+ auditSnapshot,
8
+ finish,
9
+ loadExceptions,
10
+ parseFlags,
11
+ readPaths,
12
+ scanSource
13
+ } from "./chunk-GWMSSRIQ.js";
14
+
15
+ // src/audit/cli.ts
16
+ import { existsSync, readFileSync, writeFileSync } from "fs";
17
+ import { resolve } from "path";
18
+
19
+ // src/audit/collect.ts
20
+ function collectInPage() {
21
+ const vw = window.innerWidth;
22
+ const vh = window.innerHeight;
23
+ const scrollY = window.scrollY;
24
+ const LIMIT = 8e3;
25
+ const all = Array.from(document.body.querySelectorAll("*")).slice(0, LIMIT);
26
+ const styles = /* @__PURE__ */ new Map();
27
+ const style = (el) => {
28
+ let cs = styles.get(el);
29
+ if (!cs) styles.set(el, cs = getComputedStyle(el));
30
+ return cs;
31
+ };
32
+ const box = (el) => {
33
+ const r = el.getBoundingClientRect();
34
+ return { top: r.top + scrollY, left: r.left, width: r.width, height: r.height };
35
+ };
36
+ const shown = (el) => {
37
+ const cs = style(el);
38
+ if (cs.display === "none" || cs.visibility === "hidden") return false;
39
+ const r = el.getBoundingClientRect();
40
+ return r.width > 0 && r.height > 0;
41
+ };
42
+ const alphaOf = (c) => {
43
+ const m = c.match(/rgba?\(([^)]+)\)/);
44
+ if (!m) return c === "transparent" ? 0 : 1;
45
+ const parts = m[1].split(/[\s,/]+/).filter(Boolean);
46
+ return parts.length >= 4 ? parseFloat(parts[3]) : 1;
47
+ };
48
+ const painted = (c) => Boolean(c) && alphaOf(c) > 0.02;
49
+ const ownText = (el) => Array.from(el.childNodes).filter((n) => n.nodeType === 3).map((n) => n.nodeValue ?? "").join("").replace(/\s+/g, " ").trim();
50
+ const firstFamily = (stack) => (stack.split(",")[0] ?? "").replace(/["']/g, "").trim();
51
+ const px = (v) => parseFloat(v) || 0;
52
+ const bg = /* @__PURE__ */ new Map();
53
+ const text = /* @__PURE__ */ new Map();
54
+ const fonts = /* @__PURE__ */ new Map();
55
+ const sizes = /* @__PURE__ */ new Set();
56
+ const weights = /* @__PURE__ */ new Set();
57
+ const radii = /* @__PURE__ */ new Set();
58
+ const shadows = /* @__PURE__ */ new Set();
59
+ const spacing = [];
60
+ const pageArea = Math.max(1, document.documentElement.scrollHeight * vw);
61
+ for (const el of all) {
62
+ if (!shown(el)) continue;
63
+ const cs = style(el);
64
+ const b = box(el);
65
+ const area = Math.min(b.width * b.height, pageArea);
66
+ if (painted(cs.backgroundColor)) {
67
+ const e = bg.get(cs.backgroundColor) ?? { area: 0, chars: 0 };
68
+ e.area += area;
69
+ bg.set(cs.backgroundColor, e);
70
+ }
71
+ const own = ownText(el);
72
+ if (own.length > 1) {
73
+ const chars = own.length;
74
+ const t = text.get(cs.color) ?? { area: 0, chars: 0 };
75
+ t.chars += chars;
76
+ text.set(cs.color, t);
77
+ const family = firstFamily(cs.fontFamily);
78
+ const f = fonts.get(family) ?? { chars: 0, displayChars: 0, italicChars: 0, weights: /* @__PURE__ */ new Set() };
79
+ const size = px(cs.fontSize);
80
+ const weight = parseInt(cs.fontWeight, 10) || 400;
81
+ f.chars += chars;
82
+ if (/^H[1-3]$/.test(el.tagName) || size >= 28) f.displayChars += chars;
83
+ if (cs.fontStyle === "italic" || cs.fontStyle.startsWith("oblique")) f.italicChars += chars;
84
+ f.weights.add(weight);
85
+ fonts.set(family, f);
86
+ sizes.add(Math.round(size));
87
+ weights.add(weight);
88
+ }
89
+ if (/^(A|BUTTON|SECTION|ARTICLE|ASIDE)$/.test(el.tagName) || /card|panel|box|tile/i.test(typeof el.className === "string" ? el.className : "")) {
90
+ if (cs.borderRadius && cs.borderRadius !== "0px") radii.add(cs.borderRadius);
91
+ if (cs.boxShadow && cs.boxShadow !== "none") shadows.add(cs.boxShadow);
92
+ }
93
+ for (const prop of ["marginTop", "marginBottom", "paddingTop", "paddingBottom"]) {
94
+ const v = Math.round(px(cs[prop]));
95
+ if (v > 0 && v <= 200) spacing.push(v);
96
+ }
97
+ }
98
+ const htmlBg = getComputedStyle(document.documentElement).backgroundColor;
99
+ const bodyBg = getComputedStyle(document.body).backgroundColor;
100
+ const ground = painted(bodyBg) ? bodyBg : painted(htmlBg) ? htmlBg : "rgb(255, 255, 255)";
101
+ const headings = Array.from(document.querySelectorAll("h1, h2, h3")).filter(shown).slice(0, 60).map((h) => {
102
+ const cs = style(h);
103
+ const italic = cs.fontStyle !== "normal";
104
+ const italicPart = !italic && Array.from(h.querySelectorAll("*")).some((c) => style(c).fontStyle !== "normal" && (c.textContent ?? "").trim().length > 0);
105
+ return {
106
+ level: Number(h.tagName[1]),
107
+ family: firstFamily(cs.fontFamily),
108
+ sizePx: Math.round(px(cs.fontSize)),
109
+ weight: parseInt(cs.fontWeight, 10) || 400,
110
+ italic,
111
+ italicPart,
112
+ text: (h.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 120),
113
+ top: Math.round(box(h).top)
114
+ };
115
+ });
116
+ const controls = [];
117
+ for (const el of all) {
118
+ const tag = el.tagName;
119
+ const isButton = tag === "BUTTON" || tag === "INPUT" && /^(submit|button)$/i.test(el.type);
120
+ if (!isButton && tag !== "A") continue;
121
+ if (!shown(el)) continue;
122
+ const cs = style(el);
123
+ const b = box(el);
124
+ const label = (el.textContent ?? el.value ?? "").replace(/\s+/g, " ").trim();
125
+ if (!label || b.height < 24 || b.height > 90) continue;
126
+ if (!isButton && !(painted(cs.backgroundColor) || px(cs.borderTopWidth) >= 1)) continue;
127
+ if (!isButton && px(cs.paddingLeft) < 8) continue;
128
+ const r = cs.borderTopLeftRadius;
129
+ const radius = r.endsWith("%") ? parseFloat(r) / 100 * b.height : px(r);
130
+ controls.push({
131
+ kind: isButton ? "button" : "link",
132
+ radiusPx: Math.round(radius),
133
+ heightPx: Math.round(b.height),
134
+ background: cs.backgroundColor,
135
+ text: label.slice(0, 60)
136
+ });
137
+ if (controls.length >= 200) break;
138
+ }
139
+ const anims = [];
140
+ const docAny = document;
141
+ for (const a of docAny.getAnimations ? docAny.getAnimations() : []) {
142
+ const effect = a.effect;
143
+ if (!effect) continue;
144
+ let translateX = false;
145
+ try {
146
+ translateX = effect.getKeyframes().some((k) => /translate(?:X|3d)?\(\s*-?[\d.]+(?:%|px)/.test(String(k.transform ?? "")) && !/translateY/.test(String(k.transform ?? "")));
147
+ } catch {
148
+ translateX = false;
149
+ }
150
+ anims.push({ target: effect.target, infinite: effect.getTiming().iterations === Infinity, translateX });
151
+ }
152
+ const marqueeTargets = anims.filter((a) => a.infinite && a.translateX && a.target).map((a) => a.target);
153
+ const root = document.querySelector("main") ?? document.body;
154
+ const blocks = [];
155
+ const pushBlocks = (parent, depth) => {
156
+ for (const child of Array.from(parent.children)) {
157
+ if (!shown(child) && !(style(child).opacity === "0")) continue;
158
+ const b = box(child);
159
+ if (b.height < 80 || b.width < vw * 0.5) continue;
160
+ const kids = Array.from(child.children).filter((k) => box(k).height >= 80 && box(k).width >= vw * 0.5);
161
+ const covered = kids.reduce((n, k) => n + box(k).height, 0);
162
+ const wrapper = depth < 6 && !/^(SECTION|ARTICLE|HEADER|FOOTER|ASIDE|NAV)$/.test(child.tagName) && (kids.length === 1 && box(kids[0]).height >= b.height * 0.8 || kids.length >= 2 && covered >= b.height * 0.6);
163
+ if (wrapper) pushBlocks(child, depth + 1);
164
+ else blocks.push(child);
165
+ }
166
+ };
167
+ pushBlocks(root, 0);
168
+ const header = document.querySelector("body > header, header");
169
+ const h1 = document.querySelector("h1");
170
+ const hiddenAtLoad = (el) => {
171
+ const check = [el, ...Array.from(el.children).slice(0, 6)];
172
+ return check.some((c) => {
173
+ const cs = style(c);
174
+ const op = parseFloat(cs.opacity);
175
+ return op < 0.1 || op < 1 && cs.transform !== "none" || c.hasAttribute("data-aos") || /\breveal\b|\bfade-?(?:in|up)\b|\banimate-on-scroll\b/i.test(typeof c.className === "string" ? c.className : "");
176
+ });
177
+ };
178
+ const cardsIn = (section) => {
179
+ let best = { cards: 0, iconCards: 0 };
180
+ const containers = [section, ...Array.from(section.querySelectorAll("div, ul, ol"))].slice(0, 300);
181
+ for (const c of containers) {
182
+ const kids = Array.from(c.children).filter(shown);
183
+ if (kids.length < 3) continue;
184
+ const boxes = kids.map(box);
185
+ const row = boxes.filter((b) => Math.abs(b.top - boxes[0].top) < 12);
186
+ if (row.length < 3) continue;
187
+ const w = row.map((b) => b.width);
188
+ if (Math.max(...w) > Math.min(...w) * 1.35 || Math.max(...w) > vw * 0.45) continue;
189
+ const withText = kids.filter((k) => (k.textContent ?? "").trim().length > 12);
190
+ if (withText.length < 3) continue;
191
+ const iconCards = withText.filter((k) => {
192
+ const first = k.querySelector("svg, img");
193
+ if (!first) return false;
194
+ const fb = box(first);
195
+ const kb = box(k);
196
+ return fb.width <= 72 && fb.height <= 72 && fb.top - kb.top < 120;
197
+ }).length;
198
+ if (withText.length > best.cards) best = { cards: withText.length, iconCards };
199
+ }
200
+ return best;
201
+ };
202
+ const sections = blocks.slice(0, 40).map((el, index) => {
203
+ const b = box(el);
204
+ const heading = el.querySelector("h1, h2, h3");
205
+ const label = (heading?.textContent ?? el.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 60);
206
+ const images = Array.from(el.querySelectorAll("img, svg")).filter((i) => shown(i) && box(i).height <= 90);
207
+ const textChars = (el.textContent ?? "").replace(/\s+/g, "").length;
208
+ const numbers = Array.from(el.querySelectorAll("*")).filter((n) => /^[£$€]?\d[\d,.]*\s?(?:[%+kKmM★]|\/5|\+)?$/.test(ownText(n)) && px(style(n).fontSize) >= 24).length;
209
+ const { cards, iconCards } = cardsIn(el);
210
+ let kind = "text";
211
+ if (marqueeTargets.some((t) => el.contains(t))) kind = "marquee";
212
+ else if (h1 && el.contains(h1) || index === 0 && b.top < vh) kind = "hero";
213
+ else if (images.length >= 4 && textChars < 300) kind = "logos";
214
+ else if (numbers >= 3 && textChars < 600) kind = "stats";
215
+ else if (cards >= 3) kind = "cards";
216
+ return {
217
+ top: Math.round(b.top),
218
+ height: Math.round(b.height),
219
+ kind,
220
+ label,
221
+ cards,
222
+ iconCards,
223
+ hiddenAtLoad: b.top > vh && hiddenAtLoad(el)
224
+ };
225
+ });
226
+ if (header && sections.length === 0) {
227
+ const b = box(header);
228
+ sections.push({ top: Math.round(b.top), height: Math.round(b.height), kind: "hero", label: "", cards: 0, iconCards: 0, hiddenAtLoad: false });
229
+ }
230
+ const colourStops = (image) => image.match(/rgba?\([^)]*\)|#[0-9a-f]{3,8}\b/gi) ?? [];
231
+ const effects = {
232
+ glass: 0,
233
+ gradientText: 0,
234
+ gradients: [],
235
+ glows: 0,
236
+ gridBackgrounds: 0,
237
+ marquees: new Set(marqueeTargets).size,
238
+ hairlineShadowCards: 0,
239
+ eyebrowChip: false
240
+ };
241
+ for (const el of all) {
242
+ if (!shown(el)) continue;
243
+ const cs = style(el);
244
+ const b = box(el);
245
+ const area = b.width * b.height;
246
+ const backdrop = cs.backdropFilter || cs.webkitBackdropFilter || "";
247
+ if (/blur\(\s*[1-9]/.test(backdrop) && alphaOf(cs.backgroundColor) < 0.95 && b.width >= 200 && b.height >= 120 && cs.position !== "fixed" && cs.position !== "sticky" && !el.closest("header, nav, [role=dialog], dialog")) {
248
+ effects.glass += 1;
249
+ }
250
+ const image = cs.backgroundImage || "";
251
+ const clip = cs.backgroundClip || cs.webkitBackgroundClip || "";
252
+ if (clip === "text" && /gradient/.test(image)) effects.gradientText += 1;
253
+ if (/gradient/.test(image)) {
254
+ const size = cs.backgroundSize || "";
255
+ const small = /(?:^|\s)(?:[1-9]|[1-5]\d|6[0-4])px/.test(size);
256
+ const lines = (image.match(/linear-gradient/g) ?? []).length;
257
+ if (small && (lines >= 2 || /radial-gradient/.test(image)) || /repeating-linear-gradient/.test(image)) {
258
+ effects.gridBackgrounds += 1;
259
+ } else if (area >= vw * vh * 0.05) {
260
+ effects.gradients.push({ stops: colourStops(image), area: Math.round(area), radial: /radial-gradient/.test(image) });
261
+ }
262
+ }
263
+ const blur = (cs.filter || "").match(/blur\(\s*([\d.]+)px/);
264
+ if ((blur && parseFloat(blur[1]) >= 40 || /radial-gradient/.test(image) && !/(?:^|\s)\d{1,2}px/.test(cs.backgroundSize || "")) && area >= 4e4 && (el.textContent ?? "").trim().length === 0) {
265
+ effects.glows += 1;
266
+ }
267
+ const shadow = cs.boxShadow || "";
268
+ const shadowBlur = shadow.match(/(?:-?[\d.]+px\s+){2}([\d.]+)px/);
269
+ if (shadowBlur && parseFloat(shadowBlur[1]) >= 24 && px(cs.borderTopWidth) >= 1 && alphaOf(cs.borderTopColor) <= 0.2 && area >= 2e4) {
270
+ effects.hairlineShadowCards += 1;
271
+ }
272
+ }
273
+ if (h1) {
274
+ let prev = h1.previousElementSibling;
275
+ if (!prev && h1.parentElement) prev = h1.parentElement.previousElementSibling;
276
+ if (prev && shown(prev)) {
277
+ const cs = style(prev);
278
+ const b = box(prev);
279
+ const pill = b.height <= 44 && px(cs.borderTopLeftRadius) >= b.height / 2 - 1;
280
+ const drawn = painted(cs.backgroundColor) || px(cs.borderTopWidth) >= 1;
281
+ const chip = pill && drawn ? prev : Array.from(prev.querySelectorAll("*")).find((c) => {
282
+ const ccs = style(c);
283
+ const cb = box(c);
284
+ return cb.height <= 44 && px(ccs.borderTopLeftRadius) >= cb.height / 2 - 1 && (painted(ccs.backgroundColor) || px(ccs.borderTopWidth) >= 1);
285
+ });
286
+ effects.eyebrowChip = Boolean(chip && (chip.textContent ?? "").trim().length > 0 && (chip.textContent ?? "").trim().length <= 48);
287
+ }
288
+ }
289
+ const onScale = spacing.filter((v) => v % 4 === 0).length;
290
+ let bodyLineLengthCh = null;
291
+ let bodyFontSizePx = null;
292
+ const para = Array.from(document.querySelectorAll("p")).find((p) => shown(p) && (p.innerText || "").trim().split(/\s+/).length > 20);
293
+ if (para) {
294
+ const cs = style(para);
295
+ bodyFontSizePx = Math.round(px(cs.fontSize));
296
+ const probe = document.createElement("span");
297
+ probe.style.cssText = `font:${cs.font};visibility:hidden;position:absolute;white-space:pre`;
298
+ probe.textContent = "abcdefghijklmnopqrstuvwxyz";
299
+ document.body.appendChild(probe);
300
+ const avg = probe.getBoundingClientRect().width / 26;
301
+ probe.remove();
302
+ const width = para.getBoundingClientRect().width - px(cs.paddingLeft) - px(cs.paddingRight);
303
+ if (avg > 0 && width > 0) bodyLineLengthCh = Math.round(width / avg);
304
+ }
305
+ const toList = (m) => Array.from(m, ([value, v]) => ({ value, area: Math.round(v.area), chars: v.chars })).sort((a, b) => b.area - a.area || b.chars - a.chars);
306
+ const nums = (s) => Array.from(s).sort((a, b) => a - b);
307
+ return {
308
+ viewport: { width: vw, height: vh },
309
+ pageHeight: document.documentElement.scrollHeight,
310
+ ground,
311
+ colours: { backgrounds: toList(bg).slice(0, 60), text: toList(text).sort((a, b) => b.chars - a.chars).slice(0, 40) },
312
+ fonts: Array.from(fonts, ([family, f]) => ({ family, chars: f.chars, displayChars: f.displayChars, italicChars: f.italicChars, weights: nums(f.weights) })).sort((a, b) => b.chars - a.chars),
313
+ headings,
314
+ controls,
315
+ sections,
316
+ effects,
317
+ motion: {
318
+ hiddenSections: sections.filter((s) => s.hiddenAtLoad).length,
319
+ sections: sections.length,
320
+ animations: anims.length
321
+ },
322
+ metrics: {
323
+ fontSizes: nums(sizes),
324
+ fontWeights: nums(weights),
325
+ radii: Array.from(radii).sort(),
326
+ shadows: Array.from(shadows).sort().slice(0, 20),
327
+ spacingOnScalePct: spacing.length ? Math.round(onScale / spacing.length * 1e3) / 10 : null,
328
+ bodyLineLengthCh,
329
+ bodyFontSizePx
330
+ }
331
+ };
332
+ }
333
+ function overlayInPage() {
334
+ const vw = window.innerWidth;
335
+ const vh = window.innerHeight;
336
+ for (const el of Array.from(document.body.querySelectorAll("*")).slice(0, 4e3)) {
337
+ const cs = getComputedStyle(el);
338
+ if (cs.position !== "fixed" && cs.position !== "absolute") continue;
339
+ if (cs.display === "none" || cs.visibility === "hidden" || parseFloat(cs.opacity) < 0.5) continue;
340
+ const r = el.getBoundingClientRect();
341
+ if (r.width < vw * 0.9 || r.height < vh * 0.9 || r.top > 10) continue;
342
+ const bg = cs.backgroundColor;
343
+ const alpha = /rgba?\(([^)]+)\)/.test(bg) ? parseFloat((bg.match(/[\d.]+/g) ?? [])[3] ?? "1") : 0;
344
+ if (alpha > 0.8 || /gradient|url\(/.test(cs.backgroundImage)) return true;
345
+ }
346
+ return false;
347
+ }
348
+
349
+ // src/audit/index.ts
350
+ async function loadPlaywright() {
351
+ for (const name of ["playwright", "playwright-core"]) {
352
+ try {
353
+ return await import(
354
+ /* @vite-ignore */
355
+ name
356
+ );
357
+ } catch {
358
+ }
359
+ }
360
+ throw new Error("craft audit needs a browser. Install one: npm i -D playwright && npx playwright install chromium");
361
+ }
362
+ async function snapshotPage(page, url, options = {}) {
363
+ const early = await page.evaluate(overlayInPage).catch(() => false);
364
+ await page.waitForTimeout(options.introWindowMs ?? 5e3);
365
+ const late = await page.evaluate(overlayInPage).catch(() => false);
366
+ const data = await page.evaluate(collectInPage);
367
+ return {
368
+ version: SNAPSHOT_VERSION,
369
+ url,
370
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
371
+ ...data,
372
+ motion: { ...data.motion, introOverlay: early && !late }
373
+ };
374
+ }
375
+ async function snapshotUrl(url, options = {}) {
376
+ const pw = await loadPlaywright();
377
+ const env = globalThis.process?.env ?? {};
378
+ const executablePath = options.executablePath ?? env.CRAFT_CHROMIUM ?? void 0;
379
+ const host = new URL(url).hostname;
380
+ const noProxy = [`localhost`, `127.0.0.1`, `::1`, ...(env.NO_PROXY ?? env.no_proxy ?? "").split(",")].map((h) => h.trim().replace(/^\*?\./, "")).filter(Boolean);
381
+ const direct = /^(?:localhost|127\.|\[?::1\]?$)/.test(host) || noProxy.some((h) => host === h || host.endsWith(`.${h}`));
382
+ const proxy = direct ? void 0 : env.HTTPS_PROXY ?? env.https_proxy;
383
+ const browser = await pw.chromium.launch({ headless: true, ...executablePath ? { executablePath } : {}, ...proxy ? { proxy: { server: proxy } } : {} });
384
+ try {
385
+ const page = await browser.newPage({ viewport: options.viewport ?? { width: 1440, height: 900 } });
386
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: options.timeoutMs ?? 45e3 });
387
+ return await snapshotPage(page, url, options);
388
+ } finally {
389
+ await browser.close();
390
+ }
391
+ }
392
+
393
+ // src/audit/cli.ts
394
+ var fmt = (o) => o ? `oklch(${o.l.toFixed(3)} ${o.c.toFixed(3)} ${o.h.toFixed(1)})` : "none";
395
+ function describeFingerprint(fp) {
396
+ return [
397
+ "Fingerprint",
398
+ ` accent ${fmt(fp.accent)}`,
399
+ ` ground ${fmt(fp.ground)}`,
400
+ ` display ${fp.display.family} (${fp.display.class})`,
401
+ ` body ${fp.body.family} (${fp.body.class})`,
402
+ ` roundness ${fp.roundness === null ? "no buttons" : fp.roundness.toFixed(2)}`,
403
+ ` motion ${Math.round(fp.motion * 100)}% of sections below the fold reveal on scroll`,
404
+ ` effects ${fp.effects.join(", ") || "none"}`,
405
+ ` layout ${fp.layout.join(" > ") || "none"}`
406
+ ].join("\n");
407
+ }
408
+ function merge(a, b) {
409
+ const byTell = { ...a.summary.byTell };
410
+ for (const [k, v] of Object.entries(b.summary.byTell)) byTell[k] = (byTell[k] ?? 0) + v;
411
+ return {
412
+ catalogueVersion: a.catalogueVersion,
413
+ findings: [...a.findings, ...b.findings],
414
+ rejectedExceptions: a.rejectedExceptions,
415
+ excepted: [...a.excepted, ...b.excepted],
416
+ suppressed: [...a.suppressed, ...b.suppressed],
417
+ summary: {
418
+ files: a.summary.files + b.summary.files,
419
+ findings: a.summary.findings + b.summary.findings,
420
+ byGeneration: { 1: a.summary.byGeneration[1] + b.summary.byGeneration[1], 2: a.summary.byGeneration[2] + b.summary.byGeneration[2], 3: a.summary.byGeneration[3] + b.summary.byGeneration[3] },
421
+ byTell,
422
+ blocking: a.summary.blocking + b.summary.blocking
423
+ }
424
+ };
425
+ }
426
+ function readSnapshot(path) {
427
+ const data = JSON.parse(readFileSync(path, "utf8"));
428
+ if (data.version !== SNAPSHOT_VERSION) throw new Error(`${path} is snapshot version ${String(data.version)}; this craft reads ${SNAPSHOT_VERSION}`);
429
+ return data;
430
+ }
431
+ async function runAudit(command, args, io) {
432
+ try {
433
+ const flags = parseFlags(args);
434
+ if (typeof flags === "string") throw new Error(flags);
435
+ const target = flags.positional[0];
436
+ if (!target) throw new Error(`usage: craft ${command} <url${command === "audit" ? " | snapshot.json" : ""}>`);
437
+ const viewport = flags.width || flags.height ? { width: Number(flags.width ?? 1440), height: Number(flags.height ?? 900) } : void 0;
438
+ const local = resolve(io.cwd, target);
439
+ const snapshot = command === "audit" && /\.json$/i.test(target) && existsSync(local) ? readSnapshot(local) : await snapshotUrl(target, { viewport });
440
+ if (flags.out) writeFileSync(resolve(io.cwd, flags.out), `${JSON.stringify(snapshot, null, 2)}
441
+ `);
442
+ if (command === "snapshot") {
443
+ if (!flags.out) io.out(JSON.stringify(snapshot, null, 2));
444
+ else io.out(`craft snapshot: ${snapshot.url} saved to ${flags.out}`);
445
+ return 0;
446
+ }
447
+ const exceptions = loadExceptions(flags, flags.repo ? resolve(io.cwd, flags.repo) : io.cwd);
448
+ let report = auditSnapshot(snapshot, { exceptions });
449
+ if (flags.repo) {
450
+ const repo = resolve(io.cwd, flags.repo);
451
+ report = merge(scanSource(readPaths(["."], repo, SOURCE_FILE), { exceptions }), report);
452
+ }
453
+ const fp = fingerprint(snapshot);
454
+ if (flags.json) {
455
+ io.out(JSON.stringify({ report, fingerprint: fp, snapshot: flags.out ?? null }, null, 2));
456
+ return report.summary.blocking > 0 || flags.strict && report.summary.findings > 0 ? 1 : 0;
457
+ }
458
+ const code = finish(report, { ...flags, json: false }, `craft audit ${snapshot.url}`, io);
459
+ io.out("");
460
+ io.out(describeFingerprint(fp));
461
+ return code;
462
+ } catch (error) {
463
+ io.err(`craft: ${error.message}`);
464
+ return 2;
465
+ }
466
+ }
467
+ export {
468
+ describeFingerprint,
469
+ runAudit
470
+ };