@domandigital/craft 0.18.2 → 0.19.1

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.
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  snapshotUrl,
3
3
  snapshotUrls
4
- } from "./chunk-SL5VZJ2J.js";
4
+ } from "./chunk-VDUIIWG7.js";
5
5
  import {
6
6
  describeTypicality,
7
7
  loadNull
8
- } from "./chunk-ZUYERAFM.js";
8
+ } from "./chunk-ORFILP5L.js";
9
9
  import {
10
10
  SOURCE_FILE,
11
11
  finish,
@@ -14,7 +14,7 @@ import {
14
14
  parseFlags,
15
15
  prepareReport,
16
16
  readPaths
17
- } from "./chunk-O5B5ELJH.js";
17
+ } from "./chunk-DUUDGR56.js";
18
18
  import {
19
19
  toJson
20
20
  } from "./chunk-QIEDBL4J.js";
@@ -25,9 +25,10 @@ import {
25
25
  auditSnapshot,
26
26
  componentTypicalities,
27
27
  fingerprint,
28
+ rhythmOf,
28
29
  scanSource,
29
30
  typicality
30
- } from "./chunk-WFB6F3BN.js";
31
+ } from "./chunk-WK2WMWTH.js";
31
32
 
32
33
  // src/audit/cli.ts
33
34
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -69,9 +70,17 @@ function describeFingerprint(fp) {
69
70
  ` roundness ${fp.roundness === null ? "no buttons" : fp.roundness.toFixed(2)}`,
70
71
  ` motion ${Math.round(fp.motion * 100)}% of sections below the fold reveal on scroll`,
71
72
  ` effects ${fp.effects.join(", ") || "none"}`,
72
- ` layout ${fp.layout.join(" > ") || "none"}`
73
+ ` layout ${fp.layout.join(" > ") || "none"}`,
74
+ ` rhythm ${rhythmOf(fp)?.join(" > ") ?? "not measured: sections carry no backgrounds before snapshot version 2"}`,
75
+ ` hero ${describeHero(fp)}`
73
76
  ].join("\n");
74
77
  }
78
+ function describeHero(fp) {
79
+ if (!fp.hero) return "none";
80
+ const section = fp.sections?.find((s) => s.role === "hero");
81
+ const parts = [section ? section.centred >= 0.5 ? "centred" : "left-aligned" : null, fp.hero.headline ? `${fp.hero.headline.class} headline at ${Math.round(fp.hero.headline.sizePx)}px` : "no headline", fp.hero.image ? fp.hero.image === "none" ? "no picture" : fp.hero.image === "full" ? "full-width picture" : `picture ${fp.hero.image}` : null];
82
+ return parts.filter((p) => p !== null).join(", ");
83
+ }
75
84
  function merge(a, b) {
76
85
  const byTell = { ...a.summary.byTell };
77
86
  for (const [k, v] of Object.entries(b.summary.byTell)) byTell[k] = (byTell[k] ?? 0) + v;
@@ -126,18 +135,20 @@ async function runAudit(command, args, io) {
126
135
  if (flags.pages && target) throw new Error("give a page or --pages, not both");
127
136
  if (!target && !flags.pages) throw new Error(`usage: craft ${command} <url${command === "audit" ? " | snapshot.json> | --pages <sitemap.xml | urls.txt>" : ">"}`);
128
137
  const viewports = viewportsOf(flags);
138
+ if (flags.scheme && flags.scheme !== "light" && flags.scheme !== "dark") throw new Error("--scheme takes light or dark");
139
+ const colorScheme = flags.scheme;
129
140
  const audited = [];
130
141
  const failed = [];
131
142
  const local = target ? resolve(io.cwd, target) : "";
132
143
  if (target && command === "audit" && /\.json$/i.test(target) && existsSync(local)) {
133
144
  audited.push({ url: target, viewport: null, snapshot: readSnapshotFile(local) });
134
145
  } else if (target && viewports.length === 1) {
135
- const snapshot = await snapshotUrl(target, { viewport: viewports[0] });
146
+ const snapshot = await snapshotUrl(target, { viewport: viewports[0], colorScheme });
136
147
  audited.push({ url: target, viewport: viewports[0] ?? null, snapshot });
137
148
  } else {
138
149
  const urls = flags.pages ? await resolvePages(flags.pages, io.cwd) : [target];
139
150
  for (const viewport of viewports) {
140
- for (const r of await snapshotUrls(urls, { viewport })) {
151
+ for (const r of await snapshotUrls(urls, { viewport, colorScheme })) {
141
152
  if (r.snapshot) audited.push({ url: r.url, viewport: viewport ?? null, snapshot: r.snapshot });
142
153
  else failed.push({ url: r.url, viewport: viewport ?? null, error: r.error ?? "no snapshot" });
143
154
  }
@@ -10,10 +10,10 @@ import {
10
10
  levenshtein,
11
11
  normaliseFamily,
12
12
  parseColour
13
- } from "./chunk-WFB6F3BN.js";
13
+ } from "./chunk-WK2WMWTH.js";
14
14
  import {
15
15
  TRADE_NOUNS
16
- } from "./chunk-4QWEAL4G.js";
16
+ } from "./chunk-CZ7KT452.js";
17
17
 
18
18
  // src/direction/types.ts
19
19
  var DIRECTION_VERSION = 2;
@@ -541,6 +541,33 @@ var LICENCES = [
541
541
  checked: "2026-09-24",
542
542
  source: "https://iconscout.com/licenses",
543
543
  note: "Each licence covers a single end product. Never in a tool, template or shared kit (create-site, craft, a house component library), even modified. Record the assets each site uses. Assets come from different contributors, so take one site's set from one contributor."
544
+ },
545
+ {
546
+ name: "Saira Semi Condensed",
547
+ subject: "face",
548
+ licence: "OFL-1.1",
549
+ multiClient: "yes",
550
+ checked: "2026-10-09",
551
+ source: "https://github.com/google/fonts/blob/main/ofl/sairasemicondensed/OFL.txt",
552
+ note: "Copyright 2016 The Saira Project Authors (omnibus.type@gmail.com), reserved font name Saira. Keep the OFL notice with the font files; a modified build may not be called Saira. Chosen for Doman Digital's own site (DOM-397, 9 October 2026)."
553
+ },
554
+ {
555
+ name: "Public Sans",
556
+ subject: "face",
557
+ licence: "OFL-1.1",
558
+ multiClient: "yes",
559
+ checked: "2026-10-09",
560
+ source: "https://github.com/google/fonts/blob/main/ofl/publicsans/OFL.txt",
561
+ note: "Copyright 2015 The Public Sans Project Authors (https://github.com/uswds/public-sans). Keep the OFL notice with the font files. Chosen for Doman Digital's own site (DOM-397, 9 October 2026)."
562
+ },
563
+ {
564
+ name: "Martian Mono",
565
+ subject: "face",
566
+ licence: "OFL-1.1",
567
+ multiClient: "yes",
568
+ checked: "2026-10-09",
569
+ source: "https://github.com/google/fonts/blob/main/ofl/martianmono/OFL.txt",
570
+ note: "Copyright 2021 The Martian Mono Project Authors (https://github.com/evilmartians/mono). Keep the OFL notice with the font files. Chosen for Doman Digital's own site (DOM-397, 9 October 2026)."
544
571
  }
545
572
  ];
546
573
  function faceLicence(family) {
@@ -263,8 +263,100 @@ function markupParts(file) {
263
263
  function textNodeIsProse(body) {
264
264
  return /[a-zA-Z]{2,}/.test(body) && /\s/.test(body.trim()) || /[a-zA-Z]{4,}[.!?]/.test(body);
265
265
  }
266
+ var FRONT_MATTER_PROSE = /* @__PURE__ */ new Set(["title", "description"]);
267
+ var KEY_LINE = /^(\s*)(?:-\s+)?([A-Za-z][\w-]*)[ \t]*:[ \t]*/;
268
+ var BLOCK_SCALAR = /^(?:[|>][+-]?\d*|[|>]\d+[+-]?)$/;
269
+ function readFrontKey(line) {
270
+ const m = KEY_LINE.exec(line);
271
+ if (!m) return null;
272
+ return { indent: m[1].length, name: m[2], valueAt: m[0].length, value: line.slice(m[0].length) };
273
+ }
274
+ function indentOf(line) {
275
+ let i = 0;
276
+ while (i < line.length && (line[i] === " " || line[i] === " ")) i += 1;
277
+ return i;
278
+ }
279
+ function commentAt(value) {
280
+ if (value.startsWith("#")) return 0;
281
+ const found = /[ \t]+#/.exec(value);
282
+ return found ? found.index : -1;
283
+ }
284
+ function keepSpan(line, from, to) {
285
+ const start = Math.max(0, Math.min(from, line.length));
286
+ const end = Math.max(start, Math.min(to, line.length));
287
+ return " ".repeat(start) + line.slice(start, end) + " ".repeat(line.length - end);
288
+ }
289
+ function quoteEnd(line, from, quote) {
290
+ for (let i = from; i < line.length; i += 1) {
291
+ if (quote === '"' && line[i] === "\\") {
292
+ i += 1;
293
+ continue;
294
+ }
295
+ if (quote === "'" && line[i] === "'" && line[i + 1] === "'") {
296
+ i += 1;
297
+ continue;
298
+ }
299
+ if (line[i] === quote) return i;
300
+ }
301
+ return -1;
302
+ }
303
+ function maskFrontMatter(block) {
304
+ const lines = block.split("\n");
305
+ const out = lines.map((line) => blank(line));
306
+ for (let i = 1; i < lines.length; ) {
307
+ const line = lines[i];
308
+ if (line === "---" || line === "...") break;
309
+ const key = readFrontKey(line);
310
+ if (!key || !FRONT_MATTER_PROSE.has(key.name.toLowerCase())) {
311
+ i += 1;
312
+ continue;
313
+ }
314
+ const quote = key.value.startsWith('"') ? '"' : key.value.startsWith("'") ? "'" : "";
315
+ if (quote) {
316
+ let cursor = key.valueAt + 1;
317
+ while (i < lines.length && lines[i] !== "---" && lines[i] !== "...") {
318
+ const current = lines[i];
319
+ const end = quoteEnd(current, cursor, quote);
320
+ out[i] = keepSpan(current, cursor, end === -1 ? current.length : end);
321
+ i += 1;
322
+ cursor = 0;
323
+ if (end !== -1) break;
324
+ }
325
+ continue;
326
+ }
327
+ const hash = commentAt(key.value);
328
+ const body = (hash === -1 ? key.value : key.value.slice(0, hash)).trim();
329
+ if (BLOCK_SCALAR.test(body)) {
330
+ i += 1;
331
+ while (i < lines.length && lines[i] !== "---" && lines[i] !== "...") {
332
+ const current = lines[i];
333
+ if (current.trim() !== "" && indentOf(current) <= key.indent) break;
334
+ if (current.trim()) out[i] = current;
335
+ i += 1;
336
+ }
337
+ continue;
338
+ }
339
+ if (body === "" || body === "~" || /^null$/i.test(body) || body.startsWith("[") || body.startsWith("{")) {
340
+ i += 1;
341
+ continue;
342
+ }
343
+ out[i] = keepSpan(line, key.valueAt, hash === -1 ? line.length : key.valueAt + hash);
344
+ i += 1;
345
+ while (i < lines.length && lines[i] !== "---" && lines[i] !== "...") {
346
+ const current = lines[i];
347
+ if (current.trim() === "" || indentOf(current) <= key.indent) break;
348
+ if (!/^\s*#/.test(current)) {
349
+ const cut = commentAt(current);
350
+ out[i] = cut === -1 ? current : keepSpan(current, 0, cut);
351
+ }
352
+ i += 1;
353
+ }
354
+ }
355
+ const masked = out.join("\n");
356
+ return masked.length === block.length ? masked : blank(block);
357
+ }
266
358
  function proseBlocks(file) {
267
- const masked = file.text.replace(/^(```|~~~)[\s\S]*?^\1/gm, blank).replace(/`[^`\n]*`/g, (m) => " ".repeat(m.length)).replace(/^---\n[\s\S]*?\n---\n/, blank).replace(/<!--[\s\S]*?-->/g, blank);
359
+ const masked = file.text.replace(/^(```|~~~)[\s\S]*?^\1/gm, blank).replace(/`[^`\n]*`/g, (m) => " ".repeat(m.length)).replace(/^---\n[\s\S]*?\n---\n/, (block) => maskFrontMatter(block)).replace(/<!--[\s\S]*?-->/g, blank);
268
360
  return [{ path: file.path, text: masked, offset: 0 }];
269
361
  }
270
362
  function isData(path) {
@@ -10,7 +10,7 @@ import {
10
10
  holdHouseBlocks,
11
11
  houseRule,
12
12
  scanSource
13
- } from "./chunk-WFB6F3BN.js";
13
+ } from "./chunk-WK2WMWTH.js";
14
14
  import {
15
15
  compareCompetitor,
16
16
  compareFacts,
@@ -21,7 +21,7 @@ import {
21
21
  pageCopy,
22
22
  protectedFacts,
23
23
  visibleText
24
- } from "./chunk-4QWEAL4G.js";
24
+ } from "./chunk-CZ7KT452.js";
25
25
 
26
26
  // src/character/config.ts
27
27
  var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
@@ -482,7 +482,8 @@ var VALUE_FLAGS = {
482
482
  "--component": "component",
483
483
  "--competitor": "competitor",
484
484
  "--page": "page",
485
- "--builder": "builder"
485
+ "--builder": "builder",
486
+ "--scheme": "scheme"
486
487
  };
487
488
  function parseFlags(args) {
488
489
  const flags = { positional: [], json: false, strict: false, staged: false, gate: false, updateBaseline: false, directions: false, visual: false, fresh: false };
@@ -600,7 +601,7 @@ async function pageSnapshot(target, cwd) {
600
601
  return readSnapshot(JSON.parse(readFileSync(local, "utf8")), target);
601
602
  }
602
603
  if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL nor a snapshot file`);
603
- const { snapshotUrl } = await import("./audit-4SFSLCKY.js");
604
+ const { snapshotUrl } = await import("./audit-43PC4LMS.js");
604
605
  return snapshotUrl(target);
605
606
  }
606
607
  async function briefFor(flags, cwd) {
@@ -611,7 +612,7 @@ async function briefFor(flags, cwd) {
611
612
  if (typeof brief === "string" && brief.trim()) return brief;
612
613
  } else if (flags.direction) throw new Error(`no such file: ${flags.direction}`);
613
614
  if (flags.null) {
614
- const { loadNull } = await import("./cli-BN4IT6N2.js");
615
+ const { loadNull } = await import("./cli-L3VGKWZH.js");
615
616
  return loadNull(flags.null, cwd).brief;
616
617
  }
617
618
  return void 0;
@@ -637,31 +638,31 @@ async function compareWithCompetitor(flags, io) {
637
638
  function run(argv, io) {
638
639
  const [command, ...rest] = argv;
639
640
  if (command === "direction") {
640
- return import("./cli-Y7Y5IROK.js").then((m) => m.runDirection(rest, io));
641
+ return import("./cli-TH76ATN3.js").then((m) => m.runDirection(rest, io));
641
642
  }
642
643
  if (command === "report" || command === "retrofit") {
643
- return import("./cli-V44QEVS6.js").then((m) => m.runReport(command, rest, io));
644
+ return import("./cli-AACBHRUG.js").then((m) => m.runReport(command, rest, io));
644
645
  }
645
646
  if (command === "register") {
646
- return import("./cli-JB4X5LAG.js").then((m) => m.runRegister(rest, io));
647
+ return import("./cli-VPARW6FY.js").then((m) => m.runRegister(rest, io));
647
648
  }
648
649
  if (command === "estate") {
649
- return import("./cli-GXIW7F3E.js").then((m) => m.runEstate(rest, io));
650
+ return import("./cli-3P44X6E3.js").then((m) => m.runEstate(rest, io));
650
651
  }
651
652
  if (command === "null") {
652
- return import("./cli-BN4IT6N2.js").then((m) => m.runNull(rest, io));
653
+ return import("./cli-L3VGKWZH.js").then((m) => m.runNull(rest, io));
653
654
  }
654
655
  if (command === "calibrate") {
655
- return import("./cli-MDW7JTEF.js").then((m) => m.runCalibrate(rest, io));
656
+ return import("./cli-7OUA6ZSE.js").then((m) => m.runCalibrate(rest, io));
656
657
  }
657
658
  if (command === "tells" && rest[0] === "harvest") {
658
- return import("./cli-BN4IT6N2.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
659
+ return import("./cli-L3VGKWZH.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
659
660
  io.err(`craft: ${error.message}`);
660
661
  return 2;
661
662
  });
662
663
  }
663
664
  if (command === "snapshot" || command === "audit") {
664
- return import("./cli-JZRQBCD4.js").then((m) => m.runAudit(command, rest, io));
665
+ return import("./cli-4D7Q5BCE.js").then((m) => m.runAudit(command, rest, io));
665
666
  }
666
667
  if (!command || command === "help" || command === "--help" || command === "-h") {
667
668
  io.out(HELP);
@@ -33,7 +33,7 @@ var PART_AREA = {
33
33
  effects: { area: "effects", choice: "motif" },
34
34
  motion: { area: "motion", choice: "signature" }
35
35
  };
36
- var DEFAULTS = /^(?:ground )?(?:white|grey)(?: ground)?$|no scroll reveals|no reveals|accent (?:none|neutral)|^no buttons|shape no buttons/;
36
+ var DEFAULTS = /^(?:ground )?(?:white|grey|light)(?: ground)?$|no scroll reveals|no reveals|accent (?:none|neutral)|^no buttons|shape no buttons|^light page throughout$/;
37
37
  function designFindings(findings, copyTells) {
38
38
  return (findings?.findings ?? []).filter((f) => !copyTells.has(f.tell));
39
39
  }
@@ -140,7 +140,7 @@ function areaOfShared(part) {
140
140
  if (/accent|ground/.test(part)) return "colour";
141
141
  if (/buttons/.test(part)) return "shape";
142
142
  if (/reveal/.test(part)) return "motion";
143
- if (/running order/.test(part)) return "layout";
143
+ if (/running order|hero|page throughout/.test(part)) return "layout";
144
144
  return "effects";
145
145
  }
146
146
  function choiceOfShared(part) {
@@ -150,7 +150,7 @@ function choiceOfShared(part) {
150
150
  if (/ground/.test(part)) return "ground";
151
151
  if (/buttons/.test(part)) return "shape";
152
152
  if (/reveal/.test(part)) return "signature";
153
- return /running order/.test(part) ? void 0 : "motif";
153
+ return /running order|hero|page throughout/.test(part) ? void 0 : "motif";
154
154
  }
155
155
 
156
156
  export {
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-OQXTQQAU.js";
4
4
  import {
5
5
  parseFlags
6
- } from "./chunk-O5B5ELJH.js";
6
+ } from "./chunk-DUUDGR56.js";
7
7
  import {
8
8
  toJson
9
9
  } from "./chunk-QIEDBL4J.js";
@@ -22,10 +22,10 @@ import {
22
22
  hueSwatch,
23
23
  nullPrompt,
24
24
  scanSource
25
- } from "./chunk-WFB6F3BN.js";
25
+ } from "./chunk-WK2WMWTH.js";
26
26
  import {
27
27
  extractStrings
28
- } from "./chunk-4QWEAL4G.js";
28
+ } from "./chunk-CZ7KT452.js";
29
29
 
30
30
  // src/null/cli.ts
31
31
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
@@ -120,7 +120,7 @@ async function build(flags, io) {
120
120
  const toShoot = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && !existsSync(join(shots, `${id}.json`)));
121
121
  if (toShoot.length) {
122
122
  io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"}`);
123
- const { snapshotUrls } = await import("./audit-4SFSLCKY.js");
123
+ const { snapshotUrls } = await import("./audit-43PC4LMS.js");
124
124
  const results = await snapshotUrls(toShoot.map((id) => pathToFileURL(join(pages, `${id}.html`)).href));
125
125
  results.forEach((r, i) => {
126
126
  const id = toShoot[i];
@@ -241,7 +241,7 @@ async function importPages(flags, io) {
241
241
  const toShoot = pages.filter((p) => !existsSync(join(shots, `${p.id}.json`)));
242
242
  if (toShoot.length) {
243
243
  io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"} from ${flags.builder}`);
244
- const { snapshotUrls } = await import("./audit-4SFSLCKY.js");
244
+ const { snapshotUrls } = await import("./audit-43PC4LMS.js");
245
245
  const servers = /* @__PURE__ */ new Map();
246
246
  try {
247
247
  for (const p of toShoot) if (!servers.has(p.root)) servers.set(p.root, await serveDir(p.root));
@@ -4,13 +4,13 @@ import {
4
4
  formatDirectionMatches,
5
5
  summariseDirection,
6
6
  validateDirection
7
- } from "./chunk-6TSDYETQ.js";
7
+ } from "./chunk-4LJFZH2I.js";
8
8
  import {
9
9
  loadNull
10
- } from "./chunk-ZUYERAFM.js";
10
+ } from "./chunk-ORFILP5L.js";
11
11
  import {
12
12
  parseFlags
13
- } from "./chunk-O5B5ELJH.js";
13
+ } from "./chunk-DUUDGR56.js";
14
14
  import {
15
15
  toJson
16
16
  } from "./chunk-QIEDBL4J.js";
@@ -25,10 +25,12 @@ import {
25
25
  estateComponentPairs,
26
26
  fingerprint,
27
27
  fingerprintDistance,
28
- groundName,
28
+ groundLabel,
29
29
  hueName,
30
- shapeName
31
- } from "./chunk-WFB6F3BN.js";
30
+ rhythmOf,
31
+ shapeName,
32
+ temperatureName
33
+ } from "./chunk-WK2WMWTH.js";
32
34
 
33
35
  // src/estate/cli.ts
34
36
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -45,19 +47,45 @@ function addSite(register, site) {
45
47
  const sites = [...register.sites.filter((s) => s.id !== site.id), site].sort((a, b) => a.id.localeCompare(b.id));
46
48
  return { version: ESTATE_VERSION, sites };
47
49
  }
50
+ function sharedGround(a, b) {
51
+ const [x, y] = [groundLabel(a.ground), groundLabel(b.ground)];
52
+ if (x === y) return `${x} ground`;
53
+ const temperature = temperatureName(a.ground);
54
+ if (temperature !== "neutral" && temperature === temperatureName(b.ground)) return `${temperature} ground`;
55
+ return `${a.ground.l < 0.3 ? "dark" : "light"} ground`;
56
+ }
57
+ function sharedRhythm(a, b) {
58
+ const bands = rhythmOf(a) ?? [];
59
+ const word = (band) => band;
60
+ if (bands.length <= 1) return `${bands[0] ? word(bands[0]) : "light"} page throughout`;
61
+ const second = bands[1] === "mid" ? "mid-tone band" : `${word(bands[1])} band`;
62
+ return `${word(bands[0])} hero then ${second}`;
63
+ }
64
+ function sharedHero(a, b) {
65
+ const hero = a.sections?.find((s) => s.role === "hero");
66
+ const placement = hero && hero.centred >= 0.5 ? "centred" : "left-aligned";
67
+ const headline = a.hero?.headline && b.hero?.headline && a.hero.headline.class === b.hero.headline.class ? `${a.hero.headline.class} ` : "";
68
+ const image = a.hero?.image && a.hero.image === b.hero?.image && a.hero.image !== "none" ? `, ${a.hero.image === "full" ? "full-width picture" : `photo ${a.hero.image}`}` : "";
69
+ return `${placement} ${headline}hero${image}`;
70
+ }
48
71
  function sharedParts(a, b, d = fingerprintDistance(a, b)) {
49
72
  const out = [];
50
- const close = (k) => d.parts[k] <= SHARED_PART;
73
+ const close = (k) => {
74
+ const p = d.parts[k];
75
+ return p !== null && p <= SHARED_PART;
76
+ };
51
77
  if (close("accent")) out.push(a.accent || b.accent ? `${hueName(a.accent)} accent` : "no accent");
52
78
  if (close("type")) {
53
79
  if (a.display.family === b.display.family) out.push(`${a.display.family} headline`);
54
80
  if (a.body.family === b.body.family) out.push(`${a.body.family} body`);
55
81
  }
56
- if (close("ground")) out.push(`${groundName(a.ground)} ground`);
82
+ if (close("ground")) out.push(sharedGround(a, b));
57
83
  if (close("shape")) out.push(`${shapeName(a.roundness)} buttons`);
58
84
  if (close("motion")) out.push(a.motion > 0 || b.motion > 0 ? "scroll reveals" : "no reveals");
59
85
  if (close("effects") && a.effects.length) out.push(a.effects.filter((e) => b.effects.includes(e)).join(", "));
60
- if (close("layout")) out.push("running order");
86
+ if (close("hero")) out.push(sharedHero(a, b));
87
+ if (close("rhythm")) out.push(sharedRhythm(a, b));
88
+ if (close("layout")) out.push("same running order");
61
89
  return out;
62
90
  }
63
91
  function compareToEstate(target, register, options = {}) {
@@ -105,7 +133,7 @@ async function snapshotOf(target, cwd) {
105
133
  return snap;
106
134
  }
107
135
  if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL, a snapshot file, nor a site in the register`);
108
- const { snapshotUrl } = await import("./audit-4SFSLCKY.js");
136
+ const { snapshotUrl } = await import("./audit-43PC4LMS.js");
109
137
  return snapshotUrl(target);
110
138
  }
111
139
  var line = (distance, sibling, shared) => `${distance.toFixed(2)}${sibling ? " SIBLING" : ""}${shared.length ? ` shares ${shared.join(", ")}` : ""}`;
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  auditSnapshot,
6
6
  fingerprint
7
- } from "./chunk-WFB6F3BN.js";
7
+ } from "./chunk-WK2WMWTH.js";
8
8
  import {
9
9
  SNAPSHOT_VERSION
10
10
  } from "./chunk-QYBAVU3P.js";
@@ -505,6 +505,7 @@ function collectInPage() {
505
505
  width: Math.round(b.width),
506
506
  height: Math.round(b.height),
507
507
  top: Math.round(b.top),
508
+ left: Math.round(b.left),
508
509
  role: roleOfImage(el, src, b.width, b.height, alt),
509
510
  alt,
510
511
  decorative,
@@ -752,7 +753,7 @@ async function snapshotUrls(urls, options = {}) {
752
753
  const out = [];
753
754
  try {
754
755
  for (const url of urls) {
755
- const page = await browser.newPage({ viewport: options.viewport ?? { width: 1440, height: 900 } });
756
+ const page = await browser.newPage({ viewport: options.viewport ?? { width: 1440, height: 900 }, ...options.colorScheme ? { colorScheme: options.colorScheme } : {} });
756
757
  try {
757
758
  const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: options.timeoutMs ?? 45e3 });
758
759
  const status = typeof response?.status === "function" ? response.status() : 200;