jig-ui 0.18.6 → 0.18.8

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 CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.8 (2026-09-25)
4
+
5
+ Two runs writing one manifest no longer lose an entry.
6
+
7
+ ### Fixed
8
+
9
+ - **Two runs writing one manifest no longer lose an entry.** The merge added
10
+ for this in 0.4 re-read after writing and retried if its own keys were gone,
11
+ but a writer only checked its own keys: one that read before another's write
12
+ could rename over it afterwards, and both reported success. The
13
+ parallel-process test caught it under load in the 0.18.7 release run. The
14
+ read, merge and write now happen under a lock file; a lock whose process has
15
+ died, or older than any write takes, is taken over, so a killed run cannot
16
+ wedge an install.
17
+
18
+ ## 0.18.7 (2026-09-25)
19
+
20
+ The mockup gate reads a spec's switches however they are spelled, and refuses
21
+ icons and images in a drawing's frames.
22
+
23
+ ### Fixed
24
+
25
+ - **`switches:` reads a switch however the spec spells it.** jig-site's
26
+ Versions spec wrote `switches: [breakpoint-nav]`; read as a switch named
27
+ that, it matched nothing, no switch was checked, and the gate passed. `nav`,
28
+ `breakpoint-nav` and `--breakpoint-nav` now name the same switch, and a name
29
+ the project does not record is reported with the ones it does.
30
+ - **The gate refuses icons and images in a mockup's frames.** The procedure
31
+ says an icon is its name in brackets and an image a labelled box; jig-site's
32
+ Versions drawing carried the site's real icons and GitHub's mark, and nothing
33
+ checked. `<svg>`, `<img>`, `<picture>`, `<canvas>` and icon fonts inside a
34
+ frame now stop the session.
35
+
3
36
  ## 0.18.6 (2026-09-25)
4
37
 
5
38
  A mockup's frames must hold each size's regions, and the drawing shows either
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ function isPublishedBuild(packageRoot2) {
42
42
  }
43
43
 
44
44
  // src/commands/install.ts
45
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
45
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
46
46
  import { join as join7 } from "path";
47
47
 
48
48
  // src/adapters/types.ts
@@ -270,7 +270,7 @@ function referenceFiles(packageRoot2) {
270
270
 
271
271
  // src/install/manifest.ts
272
272
  import { createHash } from "crypto";
273
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2 } from "fs";
273
+ import { closeSync, existsSync as existsSync3, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, statSync, unlinkSync as unlinkSync2, writeSync } from "fs";
274
274
  import { dirname as dirname3, join as join4 } from "path";
275
275
 
276
276
  // src/install/atomic.ts
@@ -342,19 +342,70 @@ function readManifest(root, manifestDir = ".jig") {
342
342
  }
343
343
  return validateManifest(raw, path);
344
344
  }
345
- var MERGE_ATTEMPTS = 5;
345
+ var LOCK_STALE_MS = 1e4;
346
+ var LOCK_WAIT_MS = 15e3;
346
347
  function writeManifest(root, m, manifestDir = ".jig") {
347
348
  const path = manifestPath(root, manifestDir);
348
349
  mkdirSync2(dirname3(path), { recursive: true });
349
- for (let attempt = 0; attempt < MERGE_ATTEMPTS; attempt++) {
350
+ withLock(`${path}.lock`, () => {
350
351
  const onDisk = readManifestQuietly(path);
351
352
  const merged = { ...m, files: { ...onDisk?.files ?? {}, ...m.files } };
352
353
  writeFileAtomic(path, `${JSON.stringify(merged, null, 2)}
353
354
  `);
354
- const after = readManifestQuietly(path);
355
- if (after && Object.keys(merged.files).every((k) => k in after.files)) return;
355
+ });
356
+ }
357
+ function withLock(lock, fn) {
358
+ const deadline = Date.now() + LOCK_WAIT_MS;
359
+ for (; ; ) {
360
+ try {
361
+ const fd = openSync(lock, "wx");
362
+ try {
363
+ writeSync(fd, `${process.pid} ${Date.now()}`);
364
+ } finally {
365
+ closeSync(fd);
366
+ }
367
+ break;
368
+ } catch (err) {
369
+ if (err.code !== "EEXIST") throw err;
370
+ if (lockIsStale(lock) || Date.now() > deadline) {
371
+ try {
372
+ unlinkSync2(lock);
373
+ } catch {
374
+ }
375
+ continue;
376
+ }
377
+ sleep(10 + Math.random() * 20);
378
+ }
379
+ }
380
+ try {
381
+ fn();
382
+ } finally {
383
+ try {
384
+ unlinkSync2(lock);
385
+ } catch {
386
+ }
387
+ }
388
+ }
389
+ function lockIsStale(lock) {
390
+ try {
391
+ const [pid, at] = readFileSync2(lock, "utf8").trim().split(/\s+/).map(Number);
392
+ if (at && Date.now() - at > LOCK_STALE_MS) return true;
393
+ if (pid && pid !== process.pid) {
394
+ try {
395
+ process.kill(pid, 0);
396
+ } catch (e) {
397
+ if (e.code === "ESRCH") return true;
398
+ }
399
+ }
400
+ if (!pid || !at) return Date.now() - statSync(lock).mtimeMs > LOCK_STALE_MS;
401
+ return false;
402
+ } catch {
403
+ return false;
356
404
  }
357
405
  }
406
+ function sleep(ms) {
407
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
408
+ }
358
409
  function readManifestQuietly(path) {
359
410
  try {
360
411
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
@@ -695,7 +746,7 @@ function installStopHook(projectRoot, version2) {
695
746
  stop.push({ hooks: [{ type: "command", command }] });
696
747
  settings.hooks = { ...hooks, Stop: stop };
697
748
  mkdirSync4(join7(projectRoot, ".claude"), { recursive: true });
698
- writeFileSync3(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
749
+ writeFileSync2(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
699
750
  return true;
700
751
  }
701
752
 
@@ -764,11 +815,11 @@ function readInitManifest(projectRoot) {
764
815
  return null;
765
816
  }
766
817
  }
767
- var MERGE_ATTEMPTS2 = 5;
818
+ var MERGE_ATTEMPTS = 5;
768
819
  function writeInitManifest(projectRoot, m) {
769
820
  const path = join8(projectRoot, INIT_MANIFEST_REL);
770
821
  mkdirSync5(dirname5(path), { recursive: true });
771
- for (let attempt = 0; attempt < MERGE_ATTEMPTS2; attempt++) {
822
+ for (let attempt = 0; attempt < MERGE_ATTEMPTS; attempt++) {
772
823
  const onDisk = readInitManifest(projectRoot);
773
824
  const merged = {
774
825
  ...m,
@@ -1476,7 +1527,7 @@ import { join as join24 } from "path";
1476
1527
 
1477
1528
  // src/check/files.ts
1478
1529
  import { execFileSync } from "child_process";
1479
- import { readdirSync as readdirSync7, realpathSync, statSync } from "fs";
1530
+ import { readdirSync as readdirSync7, realpathSync, statSync as statSync2 } from "fs";
1480
1531
  import { join as join16, relative, sep } from "path";
1481
1532
  var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
1482
1533
  "node_modules",
@@ -1557,7 +1608,7 @@ function walk(root, dir, out, seen) {
1557
1608
  let isFile = entry.isFile();
1558
1609
  if (entry.isSymbolicLink()) {
1559
1610
  try {
1560
- const target = statSync(abs);
1611
+ const target = statSync2(abs);
1561
1612
  isDir = target.isDirectory();
1562
1613
  isFile = target.isFile();
1563
1614
  } catch {
@@ -4584,7 +4635,7 @@ function check(opts) {
4584
4635
  }
4585
4636
 
4586
4637
  // src/commands/init.ts
4587
- import { existsSync as existsSync16, mkdirSync as mkdirSync6, readdirSync as readdirSync10, readFileSync as readFileSync23, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
4638
+ import { existsSync as existsSync16, mkdirSync as mkdirSync6, readdirSync as readdirSync10, readFileSync as readFileSync23, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
4588
4639
  import { dirname as dirname7, isAbsolute, join as join28, resolve as resolve2, sep as sep3 } from "path";
4589
4640
 
4590
4641
  // src/init/detect.ts
@@ -5215,7 +5266,7 @@ function wireBarrel(absPath, barrelImport) {
5215
5266
  if (legacy) {
5216
5267
  const next2 = content.slice(0, legacy.index) + `${line}
5217
5268
  ` + content.slice(legacy.index + legacy[0].length);
5218
- writeFileSync5(absPath, next2, "utf8");
5269
+ writeFileSync4(absPath, next2, "utf8");
5219
5270
  return { status: "rewired", detail: "replaced the separate brand and mode imports with the barrel" };
5220
5271
  }
5221
5272
  const tailwind = /@import\s+["']tailwindcss["'];?\r?\n?/.exec(content);
@@ -5225,7 +5276,7 @@ function wireBarrel(absPath, barrelImport) {
5225
5276
  ` : `${line}
5226
5277
  `);
5227
5278
  mkdirSync6(dirname7(absPath), { recursive: true });
5228
- writeFileSync5(next === content ? absPath : absPath, next, "utf8");
5279
+ writeFileSync4(next === content ? absPath : absPath, next, "utf8");
5229
5280
  return { status: "wired" };
5230
5281
  }
5231
5282
  async function init(opts) {
@@ -5385,7 +5436,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5385
5436
  const defaultCssRaw = readFileSync23(join28(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
5386
5437
  const content = renderBrandFile(defaultCssRaw, projectSlug, opts.version, proposal);
5387
5438
  mkdirSync6(dirname7(brandAbsPath), { recursive: true });
5388
- writeFileSync5(brandAbsPath, content, "utf8");
5439
+ writeFileSync4(brandAbsPath, content, "utf8");
5389
5440
  files[brandRelPath] = checksum(content);
5390
5441
  }
5391
5442
  if (relocateTo && configAction !== "written") {
@@ -5393,7 +5444,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5393
5444
  const existing = JSON.parse(readFileSync23(configAbsPath, "utf8"));
5394
5445
  const content = `${JSON.stringify({ ...existing, brand: brandRelPath }, null, 2)}
5395
5446
  `;
5396
- writeFileSync5(configAbsPath, content, "utf8");
5447
+ writeFileSync4(configAbsPath, content, "utf8");
5397
5448
  files[configRelPath] = checksum(content);
5398
5449
  log(` Updated ${configRelPath} to point at the new location.`);
5399
5450
  } catch {
@@ -5404,7 +5455,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5404
5455
  const config = opts.yes ? { brand: brandRelPath } : { brand: brandRelPath, surfaces };
5405
5456
  const content = `${JSON.stringify(config, null, 2)}
5406
5457
  `;
5407
- writeFileSync5(configAbsPath, content, "utf8");
5458
+ writeFileSync4(configAbsPath, content, "utf8");
5408
5459
  files[configRelPath] = checksum(content);
5409
5460
  } else if (configAction === "merged") {
5410
5461
  let existing = {};
@@ -5424,7 +5475,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5424
5475
  };
5425
5476
  const content = `${JSON.stringify(merged, null, 2)}
5426
5477
  `;
5427
- writeFileSync5(configAbsPath, content, "utf8");
5478
+ writeFileSync4(configAbsPath, content, "utf8");
5428
5479
  files[configRelPath] = checksum(content);
5429
5480
  }
5430
5481
  const effectiveConfig = loadEffectiveConfig(opts.projectRoot, configAbsPath, brandRelPath, surfaces);
@@ -5445,7 +5496,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5445
5496
  const modeSourceRaw = readFileSync23(join28(opts.packageRoot, "tokens", modeFileName), "utf8");
5446
5497
  const content = vendorHeader(modeFileName, opts.version, "css", null) + modeSourceRaw;
5447
5498
  mkdirSync6(dirname7(modeAbsPath2), { recursive: true });
5448
- writeFileSync5(modeAbsPath2, content, "utf8");
5499
+ writeFileSync4(modeAbsPath2, content, "utf8");
5449
5500
  files[modeRelPath] = checksum(content);
5450
5501
  } else {
5451
5502
  log(` ${modeRelPath} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone (re-run 'jig update' to refresh it).`);
@@ -5465,7 +5516,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5465
5516
  }
5466
5517
  const content = barrelBody(barrelFor(mode), brandFileOnly, mode, opts.version);
5467
5518
  mkdirSync6(dirname7(abs), { recursive: true });
5468
- writeFileSync5(abs, content, "utf8");
5519
+ writeFileSync4(abs, content, "utf8");
5469
5520
  files[rel] = checksum(content);
5470
5521
  }
5471
5522
  if (multiMode) {
@@ -5518,7 +5569,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5518
5569
  } else {
5519
5570
  const content = utilitiesBody(aliasable, opts.version);
5520
5571
  mkdirSync6(dirname7(abs), { recursive: true });
5521
- writeFileSync5(abs, content, "utf8");
5572
+ writeFileSync4(abs, content, "utf8");
5522
5573
  files[rel] = checksum(content);
5523
5574
  const target = findWireTarget(detection);
5524
5575
  const importPath = target ? relativeImportPath(
@@ -5570,7 +5621,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5570
5621
  return !(m && !/^[a-z]+:|^\//i.test(m[1]) && resolve2(dirname7(abs), m[1]) === bare2);
5571
5622
  }).join("\n");
5572
5623
  if (after !== before) {
5573
- writeFileSync5(abs, after, "utf8");
5624
+ writeFileSync4(abs, after, "utf8");
5574
5625
  log(`
5575
5626
  Unwired ${file}: removed its import of ${relKey(...tokensRelDir, "theme.css")}.`);
5576
5627
  log(` With more than one mode, the global stylesheet imports no barrel: every route would carry that mode's tokens.`);
@@ -5598,7 +5649,7 @@ Could not edit ${file}: ${err.message}. Remove its import of ${relKey(...tokensR
5598
5649
  const oldDirName = currentBrand.split("/").slice(0, -1).join("/");
5599
5650
  const before = existsSync16(abs) ? readFileSync23(abs, "utf8") : "";
5600
5651
  const after = before.split("\n").filter((l) => !(l.includes("@import") && l.includes(oldDirName))).join("\n");
5601
- if (after !== before) writeFileSync5(abs, after, "utf8");
5652
+ if (after !== before) writeFileSync4(abs, after, "utf8");
5602
5653
  }
5603
5654
  const outcome = wireBarrel(join28(opts.projectRoot, wireTarget), barrelImport);
5604
5655
  wiring = { target: wireTarget, status: outcome.status, snippet };
@@ -5693,7 +5744,7 @@ import { existsSync as existsSync20, readdirSync as readdirSync13, readFileSync
5693
5744
  import { join as join34 } from "path";
5694
5745
 
5695
5746
  // src/probe/save.ts
5696
- import { existsSync as existsSync19, mkdirSync as mkdirSync7, readdirSync as readdirSync12, readFileSync as readFileSync26, writeFileSync as writeFileSync6 } from "fs";
5747
+ import { existsSync as existsSync19, mkdirSync as mkdirSync7, readdirSync as readdirSync12, readFileSync as readFileSync26, writeFileSync as writeFileSync5 } from "fs";
5697
5748
  import { fileURLToPath as fileURLToPath2 } from "url";
5698
5749
  import { join as join33, relative as relative3, resolve as resolve4, sep as sep5 } from "path";
5699
5750
 
@@ -6042,7 +6093,7 @@ async function evaluate(endpoint, url, width, height, timeoutMs, expression) {
6042
6093
 
6043
6094
  // src/probe/serve.ts
6044
6095
  import { createServer } from "http";
6045
- import { existsSync as existsSync18, readFileSync as readFileSync24, statSync as statSync2 } from "fs";
6096
+ import { existsSync as existsSync18, readFileSync as readFileSync24, statSync as statSync3 } from "fs";
6046
6097
  import { extname, join as join31, resolve as resolve3, sep as sep4 } from "path";
6047
6098
  var TYPES = {
6048
6099
  ".html": "text/html; charset=utf-8",
@@ -6075,7 +6126,7 @@ async function serveDirectory(dir) {
6075
6126
  res.writeHead(403).end();
6076
6127
  return;
6077
6128
  }
6078
- if (existsSync18(file) && statSync2(file).isDirectory()) file = join31(file, "index.html");
6129
+ if (existsSync18(file) && statSync3(file).isDirectory()) file = join31(file, "index.html");
6079
6130
  else if (!existsSync18(file) && existsSync18(`${file}.html`)) file = `${file}.html`;
6080
6131
  if (!existsSync18(file)) {
6081
6132
  res.writeHead(404).end();
@@ -6143,7 +6194,7 @@ function saveProbe(opts) {
6143
6194
  const dir = join33(opts.projectRoot, ".jig", "critique", opts.surface);
6144
6195
  mkdirSync7(dir, { recursive: true });
6145
6196
  const path = join33(dir, `probe-${width}.json`);
6146
- writeFileSync6(path, JSON.stringify(probe), "utf8");
6197
+ writeFileSync5(path, JSON.stringify(probe), "utf8");
6147
6198
  return { path: `.jig/critique/${opts.surface}/probe-${width}.json`, width, page: probe.pageFile };
6148
6199
  }
6149
6200
  function projectFile(projectRoot, path) {
@@ -6392,14 +6443,14 @@ function decisionHeadings(projectRoot) {
6392
6443
  }
6393
6444
 
6394
6445
  // src/check/spec-shape.ts
6395
- import { existsSync as existsSync22, readFileSync as readFileSync29, readdirSync as readdirSync14, statSync as statSync3 } from "fs";
6446
+ import { existsSync as existsSync22, readFileSync as readFileSync29, readdirSync as readdirSync14, statSync as statSync4 } from "fs";
6396
6447
  import { join as join36 } from "path";
6397
6448
  function newestSpec(projectRoot) {
6398
6449
  const dir = join36(projectRoot, ".jig", "specs");
6399
6450
  if (!existsSync22(dir)) return void 0;
6400
6451
  const files = readdirSync14(dir).filter((f) => f.endsWith(".md"));
6401
6452
  if (files.length === 0) return void 0;
6402
- const newest = files.map((f) => ({ f, at: statSync3(join36(dir, f)).mtimeMs })).sort((a, b) => b.at - a.at)[0].f;
6453
+ const newest = files.map((f) => ({ f, at: statSync4(join36(dir, f)).mtimeMs })).sort((a, b) => b.at - a.at)[0].f;
6403
6454
  return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body: readFileSync29(join36(dir, newest), "utf8") };
6404
6455
  }
6405
6456
  function specProblems(spec) {
@@ -6685,7 +6736,7 @@ function verifyVerdicts(opts) {
6685
6736
  }
6686
6737
 
6687
6738
  // src/commands/gate.ts
6688
- import { existsSync as existsSync24, mkdirSync as mkdirSync8, readdirSync as readdirSync15, readFileSync as readFileSync32, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
6739
+ import { existsSync as existsSync24, mkdirSync as mkdirSync8, readdirSync as readdirSync15, readFileSync as readFileSync32, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
6689
6740
  import { createHash as createHash2 } from "crypto";
6690
6741
  import { join as join39 } from "path";
6691
6742
 
@@ -6708,6 +6759,12 @@ function mockupDrawingProblems(root, specBody, at) {
6708
6759
  `${at} has no frame for ${missing.map(([size, px2]) => `${size} (${px2}px)`).join(", ")}. A mockup draws the page once per size, side by side, in the wireframe template's \`<section class="frame" data-size="\u2026">\` boxes, so the owner sees every size at once. A single responsive page shows only the width of the window it is opened in.`
6709
6760
  );
6710
6761
  }
6762
+ const drawn = frames.flatMap((f) => f.art.map((a) => `${f.size ?? "a"} frame: ${a}`));
6763
+ if (drawn.length) {
6764
+ problems.push(
6765
+ `${at} draws icons or images in its frames (${[...new Set(drawn)].slice(0, 4).join("; ")}). A mockup is structure only: an icon is its name in brackets, \`[search]\`, and an image is a labelled box saying what goes there.`
6766
+ );
6767
+ }
6711
6768
  const regions = specRegions(specBody);
6712
6769
  for (const [size] of FRAMES) {
6713
6770
  const listed = regions[size];
@@ -6727,7 +6784,14 @@ function mockupDrawingProblems(root, specBody, at) {
6727
6784
  }
6728
6785
  const widths = new Set(frames.map((f) => f.width).filter((w) => w !== void 0));
6729
6786
  const crossed = specSwitches(specBody);
6730
- for (const s of declaredSwitches(root).filter((sw) => !crossed || crossed.includes(sw.name))) {
6787
+ const recorded = declaredSwitches(root);
6788
+ const unknown = (crossed ?? []).filter((name) => !recorded.some((sw) => sw.name === name));
6789
+ if (unknown.length) {
6790
+ problems.push(
6791
+ `The spec's \`switches:\` names ${unknown.map((n) => `"${n}"`).join(", ")}, which the project does not record. Recorded: ${recorded.map((sw) => `${sw.name} (${sw.px}px)`).join(", ") || "none"}. Name them as \`--breakpoint-<name>\` gives them.`
6792
+ );
6793
+ }
6794
+ for (const s of recorded.filter((sw) => !crossed || crossed.includes(sw.name))) {
6731
6795
  const absent = [s.px - 1, s.px].filter((w) => !widths.has(w));
6732
6796
  if (absent.length) {
6733
6797
  problems.push(
@@ -6742,7 +6806,7 @@ function specSwitches(specBody) {
6742
6806
  const value = /^switches\s*:\s*(.+)$/im.exec(front)?.[1]?.replace(/#.*$/, "").trim();
6743
6807
  if (value === void 0) return void 0;
6744
6808
  if (/^(none|\[\s*\])$/i.test(value)) return [];
6745
- return value.replace(/^\[|\]$/g, "").split(",").map((n) => n.trim().replace(/^["']|["']$/g, "").replace(/^--breakpoint-/, "")).filter(Boolean);
6809
+ return value.replace(/^\[|\]$/g, "").split(",").map((n) => n.trim().replace(/^["']|["']$/g, "").replace(/^(--)?breakpoint-/, "")).filter(Boolean);
6746
6810
  }
6747
6811
  function readFrames(html) {
6748
6812
  const opens = [...html.matchAll(/<\w+\b[^>]*\bclass\s*=\s*["'][^"']*\bframe\b[^"']*["'][^>]*>/gi)];
@@ -6752,7 +6816,8 @@ function readFrames(html) {
6752
6816
  const size = /\bdata-size\s*=\s*["']([\w-]+)["']/i.exec(tag)?.[1]?.toLowerCase();
6753
6817
  const width = Number(/\bdata-width\s*=\s*["'](\d+)["']/i.exec(tag)?.[1]) || void 0;
6754
6818
  const labels = [...body.matchAll(/<\w+\b[^>]*\bclass\s*=\s*["'][^"']*\bname\b[^"']*["'][^>]*>([^<]*)/gi)].map((m) => normalise(m[1])).filter(Boolean);
6755
- return { size, width, labels };
6819
+ const art = [...body.matchAll(/<(svg|img|picture|canvas)\b|<i\b[^>]*\bclass\s*=\s*["'][^"']*\b(?:fa|fa-[\w-]+|material-icons|material-symbols[\w-]*|bi|bi-[\w-]+)\b/gi)].map((m) => m[1] ? `<${m[1].toLowerCase()}>` : "an icon font");
6820
+ return { size, width, labels, art };
6756
6821
  });
6757
6822
  }
6758
6823
  function specRegions(specBody) {
@@ -6882,7 +6947,7 @@ function verdictsMtime(dir) {
6882
6947
  let newest = 0;
6883
6948
  for (const name of VERDICT_FILES) {
6884
6949
  try {
6885
- newest = Math.max(newest, statSync4(join39(dir, name)).mtimeMs);
6950
+ newest = Math.max(newest, statSync5(join39(dir, name)).mtimeMs);
6886
6951
  } catch {
6887
6952
  }
6888
6953
  }
@@ -7061,7 +7126,7 @@ function verdictGuard(root, command, inPlay) {
7061
7126
  const lockPath = join39(dir, LOCK);
7062
7127
  if (command === "critique" && (!inPlay || inPlay.includes(surface))) {
7063
7128
  try {
7064
- writeFileSync7(lockPath, JSON.stringify({ checksum: now }) + "\n", "utf8");
7129
+ writeFileSync6(lockPath, JSON.stringify({ checksum: now }) + "\n", "utf8");
7065
7130
  } catch {
7066
7131
  }
7067
7132
  continue;
@@ -7083,7 +7148,7 @@ function verdictGuard(root, command, inPlay) {
7083
7148
  function save(file, state) {
7084
7149
  try {
7085
7150
  mkdirSync8(join39(file, ".."), { recursive: true });
7086
- writeFileSync7(file, JSON.stringify(state), "utf8");
7151
+ writeFileSync6(file, JSON.stringify(state), "utf8");
7087
7152
  } catch {
7088
7153
  }
7089
7154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jig-ui",
3
- "version": "0.18.6",
3
+ "version": "0.18.8",
4
4
  "description": "A design system for coding agents. 143 numbered UI rules, brand x mode design tokens, and an installer for Claude Code, Codex, Cursor and opencode.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -645,8 +645,8 @@ for is review: is the header taking a sixth of the phone? Is a tap target under
645
645
  down the spec's `regions:` and its `nav:` and find each one in that frame. A region
646
646
  the spec names and the frame lacks is drawn now, not raised in review. The gate
647
647
  checks an HTML drawing when you stop: a frame for each of the four sizes, each
648
- size's regions labelled in its frame (by name where the spec names them), and a
649
- frame either side of each recorded switch. This matters
648
+ size's regions labelled in its frame (by name where the spec names them), a
649
+ frame either side of each recorded switch, and no icon or image in any frame. This matters
650
650
  most on the phone, where a region dropped for space is easy to miss and the reviewer
651
651
  is looking at what is there, not at what is not.
652
652