@domandigital/craft 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/CHARACTER.md +280 -20
- package/COPY.md +280 -0
- package/README.md +12 -0
- package/dist/audit-W57YDZ2K.js +20 -0
- package/dist/audit.cjs +901 -103
- package/dist/audit.d.cts +16 -1
- package/dist/audit.d.ts +16 -1
- package/dist/audit.js +899 -102
- package/dist/chunk-35OX6C4H.js +112 -0
- package/dist/{chunk-GWMSSRIQ.js → chunk-CZHRNPDD.js} +1521 -934
- package/dist/chunk-K253ZXB3.js +458 -0
- package/dist/{chunk-HYGFP3LB.js → chunk-OGSYVCMU.js} +40 -3
- package/dist/chunk-OVOH7KZY.js +163 -0
- package/dist/{cli-7ME4VIRG.js → chunk-Q4MN3OXF.js} +50 -101
- package/dist/chunk-ULYKASLA.js +175 -0
- package/dist/chunk-X65RDW65.js +364 -0
- package/dist/cli-F7ONSKZS.js +21 -0
- package/dist/cli-FKJOMOOF.js +15 -0
- package/dist/{cli-F7W4F3ER.js → cli-KA3JDNDH.js} +14 -168
- package/dist/cli-LZ7R6KJM.js +312 -0
- package/dist/cli-SHLNRHK6.js +16 -0
- package/dist/cli.js +2 -1
- package/dist/index.cjs +1528 -99
- package/dist/index.d.cts +365 -4
- package/dist/index.d.ts +365 -4
- package/dist/index.js +1493 -96
- package/package.json +4 -2
- package/word-lists.json +164 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
groundName,
|
|
3
|
+
hueName,
|
|
4
|
+
loadNull,
|
|
5
|
+
shapeName
|
|
6
|
+
} from "./chunk-K253ZXB3.js";
|
|
7
|
+
import {
|
|
8
|
+
parseFlags
|
|
9
|
+
} from "./chunk-X65RDW65.js";
|
|
10
|
+
import {
|
|
11
|
+
fingerprint,
|
|
12
|
+
fingerprintDistance
|
|
13
|
+
} from "./chunk-OGSYVCMU.js";
|
|
14
|
+
import {
|
|
15
|
+
SNAPSHOT_VERSION,
|
|
16
|
+
auditSnapshot
|
|
17
|
+
} from "./chunk-CZHRNPDD.js";
|
|
18
|
+
|
|
19
|
+
// src/estate/cli.ts
|
|
20
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
21
|
+
import { resolve } from "path";
|
|
22
|
+
|
|
23
|
+
// src/estate/index.ts
|
|
24
|
+
var ESTATE_VERSION = 1;
|
|
25
|
+
var SIBLING_AT = 0.31;
|
|
26
|
+
function emptyEstate() {
|
|
27
|
+
return { version: ESTATE_VERSION, sites: [] };
|
|
28
|
+
}
|
|
29
|
+
function addSite(register, site) {
|
|
30
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(site.id)) throw new Error(`site id "${site.id}" must be short kebab-case`);
|
|
31
|
+
const sites = [...register.sites.filter((s) => s.id !== site.id), site].sort((a, b) => a.id.localeCompare(b.id));
|
|
32
|
+
return { version: ESTATE_VERSION, sites };
|
|
33
|
+
}
|
|
34
|
+
function sharedParts(a, b, d = fingerprintDistance(a, b)) {
|
|
35
|
+
const out = [];
|
|
36
|
+
const close = (k) => d.parts[k] <= 0.2;
|
|
37
|
+
if (close("accent")) out.push(a.accent || b.accent ? `${hueName(a.accent)} accent` : "no accent");
|
|
38
|
+
if (close("type")) {
|
|
39
|
+
if (a.display.family === b.display.family) out.push(`${a.display.family} headline`);
|
|
40
|
+
if (a.body.family === b.body.family) out.push(`${a.body.family} body`);
|
|
41
|
+
}
|
|
42
|
+
if (close("ground")) out.push(`${groundName(a.ground)} ground`);
|
|
43
|
+
if (close("shape")) out.push(`${shapeName(a.roundness)} buttons`);
|
|
44
|
+
if (close("motion")) out.push(a.motion > 0 || b.motion > 0 ? "scroll reveals" : "no reveals");
|
|
45
|
+
if (close("effects") && a.effects.length) out.push(a.effects.filter((e) => b.effects.includes(e)).join(", "));
|
|
46
|
+
if (close("layout")) out.push("running order");
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
function compareToEstate(target, register, options = {}) {
|
|
50
|
+
const at = options.siblingAt ?? SIBLING_AT;
|
|
51
|
+
return register.sites.filter((s) => s.id !== options.exclude).map((s) => {
|
|
52
|
+
const d = fingerprintDistance(target, s.fingerprint);
|
|
53
|
+
return { id: s.id, distance: d.total, sibling: d.total < at, shared: sharedParts(target, s.fingerprint, d) };
|
|
54
|
+
}).sort((a, b) => a.distance - b.distance);
|
|
55
|
+
}
|
|
56
|
+
function estatePairs(register, siblingAt = SIBLING_AT) {
|
|
57
|
+
const out = [];
|
|
58
|
+
const s = register.sites;
|
|
59
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
60
|
+
for (let j = i + 1; j < s.length; j += 1) {
|
|
61
|
+
const d = fingerprintDistance(s[i].fingerprint, s[j].fingerprint);
|
|
62
|
+
out.push({ a: s[i].id, b: s[j].id, distance: d.total, sibling: d.total < siblingAt, shared: sharedParts(s[i].fingerprint, s[j].fingerprint, d) });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out.sort((x, y) => x.distance - y.distance);
|
|
66
|
+
}
|
|
67
|
+
function siblingLine(briefs) {
|
|
68
|
+
const d = [];
|
|
69
|
+
for (const runs of briefs) {
|
|
70
|
+
for (let i = 0; i < runs.length; i += 1) for (let j = i + 1; j < runs.length; j += 1) d.push(fingerprintDistance(runs[i], runs[j]).total);
|
|
71
|
+
}
|
|
72
|
+
if (d.length === 0) throw new Error("no null pages to measure the sibling line from");
|
|
73
|
+
d.sort((a, b) => a - b);
|
|
74
|
+
const mid = Math.floor(d.length / 2);
|
|
75
|
+
const m = d.length % 2 ? d[mid] : (d[mid - 1] + d[mid]) / 2;
|
|
76
|
+
return Math.round(m * 1e3) / 1e3;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/estate/cli.ts
|
|
80
|
+
var USAGE = "usage: craft estate add <url | snapshot.json> --id <id> [--client <name>] | craft estate compare [<url | snapshot.json | id>] [--null <dir>] [--json] [--strict]";
|
|
81
|
+
function loadEstate(path) {
|
|
82
|
+
if (!existsSync(path)) return emptyEstate();
|
|
83
|
+
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
84
|
+
if (data.version !== ESTATE_VERSION || !Array.isArray(data.sites)) throw new Error(`${path} is not an estate register (version ${ESTATE_VERSION})`);
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
async function snapshotOf(target, cwd) {
|
|
88
|
+
const local = resolve(cwd, target);
|
|
89
|
+
if (/\.json$/i.test(target) && existsSync(local)) {
|
|
90
|
+
const snap = JSON.parse(readFileSync(local, "utf8"));
|
|
91
|
+
if (snap.version !== SNAPSHOT_VERSION) throw new Error(`${target} is not a snapshot`);
|
|
92
|
+
return snap;
|
|
93
|
+
}
|
|
94
|
+
if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL, a snapshot file, nor a site in the register`);
|
|
95
|
+
const { snapshotUrl } = await import("./audit-W57YDZ2K.js");
|
|
96
|
+
return snapshotUrl(target);
|
|
97
|
+
}
|
|
98
|
+
var line = (distance, sibling, shared) => `${distance.toFixed(2)}${sibling ? " SIBLING" : ""}${shared.length ? ` shares ${shared.join(", ")}` : ""}`;
|
|
99
|
+
function formatMatches(label, matches, at) {
|
|
100
|
+
const lines = [`${label}: nearest sites in the estate (siblings below ${at.toFixed(2)})`];
|
|
101
|
+
for (const m of matches) lines.push(` ${m.id.padEnd(20)} ${line(m.distance, m.sibling, m.shared)}`);
|
|
102
|
+
if (matches.length === 0) lines.push(" the register is empty");
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
|
105
|
+
function formatPairs(pairs, at, sites) {
|
|
106
|
+
const siblings = pairs.filter((p) => p.sibling).length;
|
|
107
|
+
const lines = [`craft estate: ${sites} sites, ${pairs.length} pair${pairs.length === 1 ? "" : "s"}, ${siblings} sibling pair${siblings === 1 ? "" : "s"} (below ${at.toFixed(2)})`];
|
|
108
|
+
for (const p of pairs) lines.push(` ${`${p.a} + ${p.b}`.padEnd(34)} ${line(p.distance, p.sibling, p.shared)}`);
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
async function runEstate(args, io) {
|
|
112
|
+
try {
|
|
113
|
+
const flags = parseFlags(args);
|
|
114
|
+
if (typeof flags === "string") throw new Error(flags);
|
|
115
|
+
const [sub, target] = flags.positional;
|
|
116
|
+
const file = resolve(io.cwd, flags.estate ?? "estate.json");
|
|
117
|
+
const register = loadEstate(file);
|
|
118
|
+
const at = flags.null ? siblingLine(flags.null.split(",").map((p) => loadNull(p.trim(), io.cwd).runs.map((r) => r.fingerprint))) : SIBLING_AT;
|
|
119
|
+
if (sub === "add") {
|
|
120
|
+
if (!target || !flags.id) throw new Error(USAGE);
|
|
121
|
+
const snap = await snapshotOf(target, io.cwd);
|
|
122
|
+
const fp = fingerprint(snap);
|
|
123
|
+
const next = addSite(register, {
|
|
124
|
+
id: flags.id,
|
|
125
|
+
client: flags.client ?? flags.id,
|
|
126
|
+
url: snap.url,
|
|
127
|
+
fingerprint: fp,
|
|
128
|
+
tells: [...new Set(auditSnapshot(snap).findings.map((f) => f.tell))].sort(),
|
|
129
|
+
addedAt: snap.capturedAt
|
|
130
|
+
});
|
|
131
|
+
writeFileSync(file, `${JSON.stringify(next, null, 2)}
|
|
132
|
+
`);
|
|
133
|
+
io.out(`craft estate: ${flags.id} ${register.sites.some((s) => s.id === flags.id) ? "updated" : "added"}, ${next.sites.length} sites in ${flags.estate ?? "estate.json"}`);
|
|
134
|
+
io.out(formatMatches(flags.id, compareToEstate(fp, next, { exclude: flags.id, siblingAt: at }).slice(0, 3), at));
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
if (sub === "compare") {
|
|
138
|
+
if (!target) {
|
|
139
|
+
const pairs = estatePairs(register, at);
|
|
140
|
+
io.out(flags.json ? JSON.stringify({ siblingAt: at, pairs }, null, 2) : formatPairs(pairs, at, register.sites.length));
|
|
141
|
+
return flags.strict && pairs.some((p) => p.sibling) ? 1 : 0;
|
|
142
|
+
}
|
|
143
|
+
const known = register.sites.find((s) => s.id === target);
|
|
144
|
+
const fp = known ? known.fingerprint : fingerprint(await snapshotOf(target, io.cwd));
|
|
145
|
+
const matches = compareToEstate(fp, register, { exclude: known?.id, siblingAt: at });
|
|
146
|
+
io.out(flags.json ? JSON.stringify({ siblingAt: at, matches }, null, 2) : formatMatches(known?.id ?? target, matches, at));
|
|
147
|
+
return flags.strict && matches.some((m) => m.sibling) ? 1 : 0;
|
|
148
|
+
}
|
|
149
|
+
throw new Error(USAGE);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
io.err(`craft: ${error.message}`);
|
|
152
|
+
return 2;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export {
|
|
157
|
+
SIBLING_AT,
|
|
158
|
+
compareToEstate,
|
|
159
|
+
loadEstate,
|
|
160
|
+
formatMatches,
|
|
161
|
+
formatPairs,
|
|
162
|
+
runEstate
|
|
163
|
+
};
|
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
fingerprint
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-OGSYVCMU.js";
|
|
4
4
|
import {
|
|
5
5
|
SNAPSHOT_VERSION,
|
|
6
|
-
|
|
7
|
-
|
|
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";
|
|
6
|
+
auditSnapshot
|
|
7
|
+
} from "./chunk-CZHRNPDD.js";
|
|
18
8
|
|
|
19
9
|
// src/audit/collect.ts
|
|
20
10
|
function collectInPage() {
|
|
@@ -167,13 +157,18 @@ function collectInPage() {
|
|
|
167
157
|
pushBlocks(root, 0);
|
|
168
158
|
const header = document.querySelector("body > header, header");
|
|
169
159
|
const h1 = document.querySelector("h1");
|
|
160
|
+
const waiting = (c) => {
|
|
161
|
+
const cs = style(c);
|
|
162
|
+
const op = parseFloat(cs.opacity);
|
|
163
|
+
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 : "");
|
|
164
|
+
};
|
|
170
165
|
const hiddenAtLoad = (el) => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
166
|
+
if ([el, ...Array.from(el.children).slice(0, 6)].some(waiting)) return true;
|
|
167
|
+
const blocks2 = Array.from(el.querySelectorAll("div, article, li, figure, p, h2, h3")).slice(0, 200).filter((d) => {
|
|
168
|
+
const b = box(d);
|
|
169
|
+
return b.width > 100 && b.height > 40 && (d.textContent ?? "").trim().length > 12 && style(d).position !== "fixed";
|
|
170
|
+
}).slice(0, 12);
|
|
171
|
+
return blocks2.filter(waiting).length >= Math.min(2, blocks2.length) && blocks2.some(waiting);
|
|
177
172
|
};
|
|
178
173
|
const cardsIn = (section) => {
|
|
179
174
|
let best = { cards: 0, iconCards: 0 };
|
|
@@ -372,99 +367,53 @@ async function snapshotPage(page, url, options = {}) {
|
|
|
372
367
|
motion: { ...data.motion, introOverlay: early && !late }
|
|
373
368
|
};
|
|
374
369
|
}
|
|
375
|
-
async function
|
|
376
|
-
const pw = await loadPlaywright();
|
|
370
|
+
async function launch(pw, url, options) {
|
|
377
371
|
const env = globalThis.process?.env ?? {};
|
|
378
372
|
const executablePath = options.executablePath ?? env.CRAFT_CHROMIUM ?? void 0;
|
|
379
373
|
const host = new URL(url).hostname;
|
|
380
374
|
const noProxy = [`localhost`, `127.0.0.1`, `::1`, ...(env.NO_PROXY ?? env.no_proxy ?? "").split(",")].map((h) => h.trim().replace(/^\*?\./, "")).filter(Boolean);
|
|
381
375
|
const direct = /^(?:localhost|127\.|\[?::1\]?$)/.test(host) || noProxy.some((h) => host === h || host.endsWith(`.${h}`));
|
|
382
376
|
const proxy = direct ? void 0 : env.HTTPS_PROXY ?? env.https_proxy;
|
|
383
|
-
|
|
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
|
-
};
|
|
377
|
+
return pw.chromium.launch({ headless: true, ...executablePath ? { executablePath } : {}, ...proxy ? { proxy: { server: proxy } } : {} });
|
|
425
378
|
}
|
|
426
|
-
function
|
|
427
|
-
const
|
|
428
|
-
if (
|
|
429
|
-
return
|
|
379
|
+
async function snapshotUrl(url, options = {}) {
|
|
380
|
+
const [result] = await snapshotUrls([url], options);
|
|
381
|
+
if (!result.snapshot) throw new Error(result.error);
|
|
382
|
+
return result.snapshot;
|
|
430
383
|
}
|
|
431
|
-
async function
|
|
384
|
+
async function snapshotUrls(urls, options = {}) {
|
|
385
|
+
if (urls.length === 0) return [];
|
|
386
|
+
const pw = await loadPlaywright();
|
|
387
|
+
const browser = await launch(pw, urls[0], options);
|
|
388
|
+
const out = [];
|
|
432
389
|
try {
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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;
|
|
390
|
+
for (const url of urls) {
|
|
391
|
+
const page = await browser.newPage({ viewport: options.viewport ?? { width: 1440, height: 900 } });
|
|
392
|
+
try {
|
|
393
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: options.timeoutMs ?? 45e3 });
|
|
394
|
+
out.push({ url, snapshot: await snapshotPage(page, url, options) });
|
|
395
|
+
} catch (error) {
|
|
396
|
+
out.push({ url, error: error.message });
|
|
397
|
+
} finally {
|
|
398
|
+
await page.close?.();
|
|
399
|
+
}
|
|
457
400
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
return code;
|
|
462
|
-
} catch (error) {
|
|
463
|
-
io.err(`craft: ${error.message}`);
|
|
464
|
-
return 2;
|
|
401
|
+
return out;
|
|
402
|
+
} finally {
|
|
403
|
+
await browser.close();
|
|
465
404
|
}
|
|
466
405
|
}
|
|
406
|
+
async function audit(url, options = {}) {
|
|
407
|
+
const snapshot = await snapshotUrl(url, options);
|
|
408
|
+
return { snapshot, report: auditSnapshot(snapshot, options), fingerprint: fingerprint(snapshot) };
|
|
409
|
+
}
|
|
410
|
+
|
|
467
411
|
export {
|
|
468
|
-
|
|
469
|
-
|
|
412
|
+
collectInPage,
|
|
413
|
+
overlayInPage,
|
|
414
|
+
loadPlaywright,
|
|
415
|
+
snapshotPage,
|
|
416
|
+
snapshotUrl,
|
|
417
|
+
snapshotUrls,
|
|
418
|
+
audit
|
|
470
419
|
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import {
|
|
2
|
+
REFLEX_FONTS_1,
|
|
3
|
+
REFLEX_FONTS_2,
|
|
4
|
+
deltaEOk,
|
|
5
|
+
isAiViolet,
|
|
6
|
+
isCream,
|
|
7
|
+
normaliseFamily,
|
|
8
|
+
parseColour
|
|
9
|
+
} from "./chunk-CZHRNPDD.js";
|
|
10
|
+
|
|
11
|
+
// src/direction/types.ts
|
|
12
|
+
var DIRECTION_VERSION = 1;
|
|
13
|
+
var SOURCE_KINDS = [
|
|
14
|
+
"livery",
|
|
15
|
+
"shopfront",
|
|
16
|
+
"signage",
|
|
17
|
+
"packaging",
|
|
18
|
+
"product",
|
|
19
|
+
"uniform",
|
|
20
|
+
"interior",
|
|
21
|
+
"place",
|
|
22
|
+
"material",
|
|
23
|
+
"trade",
|
|
24
|
+
"print",
|
|
25
|
+
"photo",
|
|
26
|
+
"founder"
|
|
27
|
+
];
|
|
28
|
+
var CHOICE_KEYS = ["accent", "ground", "display", "body", "shape", "motif", "signature"];
|
|
29
|
+
|
|
30
|
+
// src/direction/validate.ts
|
|
31
|
+
var PREFERENCE = /\b(?:client (?:likes|liked|wants|wanted|asked|chose|prefers?)|(?:we|they|i) (?:like|liked|love|loved|prefer)|on[- ]trend|trendy|popular|what (?:everyone|competitors) (?:uses?|does)|brand (?:colou?r|font|guidelines?) (?:is|are|says?))\b/i;
|
|
32
|
+
var MOOD = /\b(?:modern|clean|fresh|professional|trustworthy|premium|elegant|sleek|timeless|minimal(?:ist)?|bold|vibrant|friendly|approachable|luxur(?:y|ious)|sophisticated|contemporary|stylish|warm|inviting|high[- ]end)\b/gi;
|
|
33
|
+
var VALUE_WORDS = /* @__PURE__ */ new Set(["green", "blue", "navy", "black", "white", "cream", "grey", "gray", "gold", "yellow", "orange", "purple", "violet", "pink", "brown", "teal", "bottle", "dark", "light", "deep", "pale", "bright", "colour", "color", "font", "face", "serif", "sans", "type", "lettering"]);
|
|
34
|
+
var STOP = /* @__PURE__ */ new Set(["with", "that", "this", "from", "their", "they", "have", "been", "were", "which", "what", "when", "where", "there", "about", "into", "onto", "over", "same", "each", "every"]);
|
|
35
|
+
function words(text) {
|
|
36
|
+
return new Set((text.toLowerCase().match(/[a-z][a-z'-]{3,}/g) ?? []).map((w) => w.replace(/'s$/, "")).filter((w) => !STOP.has(w) && !VALUE_WORDS.has(w)));
|
|
37
|
+
}
|
|
38
|
+
function tellFor(key, value) {
|
|
39
|
+
if (key === "accent") {
|
|
40
|
+
const c = parseColour(value);
|
|
41
|
+
return c && isAiViolet(c.oklch) ? "ai-violet" : null;
|
|
42
|
+
}
|
|
43
|
+
if (key === "ground") {
|
|
44
|
+
const c = parseColour(value);
|
|
45
|
+
return c && isCream(c.oklch) ? "cream-palette" : null;
|
|
46
|
+
}
|
|
47
|
+
if (key === "display" || key === "body") {
|
|
48
|
+
const name = normaliseFamily(value).toLowerCase();
|
|
49
|
+
if (REFLEX_FONTS_1.some((f) => f.toLowerCase() === name)) return "reflex-font";
|
|
50
|
+
if (REFLEX_FONTS_2.some((f) => f.toLowerCase() === name)) return "reflex-font-2";
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
function validateDirection(input, ctx = {}) {
|
|
55
|
+
const problems = [];
|
|
56
|
+
const err = (at, message) => problems.push({ severity: "error", at, message });
|
|
57
|
+
const warn = (at, message) => problems.push({ severity: "warn", at, message });
|
|
58
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
59
|
+
err("", "art-direction.json must be a JSON object");
|
|
60
|
+
return { valid: false, problems, decided: 0 };
|
|
61
|
+
}
|
|
62
|
+
const d = input;
|
|
63
|
+
const legacy = d.version === void 0 && d.choices === void 0 && Array.isArray(d.exceptions);
|
|
64
|
+
if (legacy) {
|
|
65
|
+
warn("", "exceptions only: run craft direction init to record the choices and their reasons");
|
|
66
|
+
return { valid: true, problems, decided: 0 };
|
|
67
|
+
}
|
|
68
|
+
if (d.version !== DIRECTION_VERSION) err("version", `version must be ${DIRECTION_VERSION}`);
|
|
69
|
+
if (typeof d.client !== "string" || !d.client.trim()) err("client", "name the client");
|
|
70
|
+
if (typeof d.brief !== "string" || d.brief.trim().split(/\s+/).length < 8) err("brief", "the brief needs a sentence: who, where, and what they do");
|
|
71
|
+
const sources = Array.isArray(d.sources) ? d.sources : [];
|
|
72
|
+
if (!Array.isArray(d.sources)) err("sources", "sources must be a list");
|
|
73
|
+
if (sources.length === 0) err("sources", "no sources: a reason needs something in the client's world to point at");
|
|
74
|
+
const ids = /* @__PURE__ */ new Set();
|
|
75
|
+
sources.forEach((s, i) => {
|
|
76
|
+
const at = `sources[${i}]`;
|
|
77
|
+
if (!s || typeof s !== "object") return err(at, "a source must be an object");
|
|
78
|
+
if (typeof s.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(s.id)) err(`${at}.id`, "id must be short kebab-case");
|
|
79
|
+
else if (ids.has(s.id)) err(`${at}.id`, `"${s.id}" is used twice`);
|
|
80
|
+
else ids.add(s.id);
|
|
81
|
+
if (!SOURCE_KINDS.includes(s.kind)) err(`${at}.kind`, `kind must be one of: ${SOURCE_KINDS.join(", ")}`);
|
|
82
|
+
if (typeof s.note !== "string" || s.note.trim().split(/\s+/).length < 3) err(`${at}.note`, "say what it is and where");
|
|
83
|
+
if (s.path && ctx.pathExists && !/^https?:/.test(s.path) && !ctx.pathExists(s.path)) err(`${at}.path`, `${s.path} does not exist`);
|
|
84
|
+
for (const [j, c] of (s.colours ?? []).entries()) if (!parseColour(c)) err(`${at}.colours[${j}]`, `"${c}" is not a colour`);
|
|
85
|
+
});
|
|
86
|
+
const choices = d.choices && typeof d.choices === "object" ? d.choices : {};
|
|
87
|
+
if (!d.choices || typeof d.choices !== "object") err("choices", "choices must be an object");
|
|
88
|
+
const exceptions = Array.isArray(d.exceptions) ? d.exceptions : [];
|
|
89
|
+
let decided = 0;
|
|
90
|
+
for (const key of Object.keys(choices)) {
|
|
91
|
+
if (!CHOICE_KEYS.includes(key)) err(`choices.${key}`, `not a choice craft knows. Choices are: ${CHOICE_KEYS.join(", ")}. Layout and navigation stay conventional.`);
|
|
92
|
+
}
|
|
93
|
+
for (const key of CHOICE_KEYS) {
|
|
94
|
+
const c = choices[key];
|
|
95
|
+
const at = `choices.${key}`;
|
|
96
|
+
if (!c) {
|
|
97
|
+
warn(at, "not decided yet");
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
let ok = true;
|
|
101
|
+
if (typeof c.value !== "string" || !c.value.trim()) {
|
|
102
|
+
err(`${at}.value`, "no value");
|
|
103
|
+
ok = false;
|
|
104
|
+
} else if ((key === "accent" || key === "ground") && !parseColour(c.value)) {
|
|
105
|
+
err(`${at}.value`, `"${c.value}" is not a colour`);
|
|
106
|
+
ok = false;
|
|
107
|
+
}
|
|
108
|
+
const because = typeof c.because === "string" ? c.because.trim() : "";
|
|
109
|
+
const evidence = Array.isArray(c.evidence) ? c.evidence : [];
|
|
110
|
+
if (because.split(/\s+/).filter(Boolean).length < 8) {
|
|
111
|
+
err(`${at}.because`, "a reason is at least a sentence. Without one this is a default, however good it looks.");
|
|
112
|
+
ok = false;
|
|
113
|
+
} else if (because.startsWith("PROPOSED:")) {
|
|
114
|
+
err(`${at}.because`, "a proposal, not yet a decision. Check it against the source, then rewrite the reason in your own words.");
|
|
115
|
+
ok = false;
|
|
116
|
+
} else {
|
|
117
|
+
if (PREFERENCE.test(because)) {
|
|
118
|
+
err(`${at}.because`, "that is a preference, not a reason. Say what in the client's world this comes from.");
|
|
119
|
+
ok = false;
|
|
120
|
+
}
|
|
121
|
+
const moods = because.match(MOOD) ?? [];
|
|
122
|
+
if (moods.length >= 2) {
|
|
123
|
+
err(`${at}.because`, `"${moods.join(", ")}" describes a mood, not a source. Name the thing it comes from.`);
|
|
124
|
+
ok = false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (evidence.length === 0) {
|
|
128
|
+
err(`${at}.evidence`, "cite at least one source");
|
|
129
|
+
ok = false;
|
|
130
|
+
}
|
|
131
|
+
for (const id of evidence) {
|
|
132
|
+
if (!ids.has(id)) {
|
|
133
|
+
err(`${at}.evidence`, `no source called "${id}"`);
|
|
134
|
+
ok = false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (ok) {
|
|
138
|
+
const cited = sources.filter((s) => evidence.includes(s.id));
|
|
139
|
+
const reasonWords = words(because);
|
|
140
|
+
const tied = cited.some((s) => reasonWords.has(s.id) || [...words(`${s.note} ${s.lettering ?? ""}`)].some((w) => reasonWords.has(w)));
|
|
141
|
+
if (!tied) {
|
|
142
|
+
err(`${at}.because`, `the reason does not mention what its evidence shows (${evidence.join(", ")}). Say what on it this comes from.`);
|
|
143
|
+
ok = false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (typeof c.value === "string") {
|
|
147
|
+
const tell = tellFor(key, c.value);
|
|
148
|
+
if (tell && !exceptions.some((e) => e.tell === tell)) {
|
|
149
|
+
err(`${at}.value`, `${c.value} is on the tell catalogue (${tell}). Keep it only with an exception for ${tell}, carrying the same reason.`);
|
|
150
|
+
ok = false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const fp = ctx.fingerprint;
|
|
154
|
+
if (fp && typeof c.value === "string") {
|
|
155
|
+
if ((key === "accent" || key === "ground") && parseColour(c.value)) {
|
|
156
|
+
const declared = parseColour(c.value).oklch;
|
|
157
|
+
const measured = key === "accent" ? fp.accent : fp.ground;
|
|
158
|
+
if (measured && deltaEOk(declared, measured) > 0.08) {
|
|
159
|
+
warn(`${at}.value`, `the page does not show it: its ${key} measures oklch(${measured.l.toFixed(2)} ${measured.c.toFixed(3)} ${measured.h.toFixed(0)})`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (key === "display" || key === "body") {
|
|
163
|
+
const measured = key === "display" ? fp.display.family : fp.body.family;
|
|
164
|
+
if (normaliseFamily(c.value).toLowerCase() !== measured.toLowerCase()) warn(`${at}.value`, `the page sets ${measured}, not ${c.value}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (ok) decided += 1;
|
|
168
|
+
}
|
|
169
|
+
return { valid: problems.every((p) => p.severity !== "error"), problems, decided };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export {
|
|
173
|
+
DIRECTION_VERSION,
|
|
174
|
+
validateDirection
|
|
175
|
+
};
|