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 +33 -0
- package/dist/index.js +101 -36
- package/package.json +1 -1
- package/templates/COMMAND.md.tmpl +2 -2
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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
|
-
|
|
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
|
|
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 <
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
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) &&
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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),
|
|
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
|
|