@domandigital/craft 0.6.0 → 0.7.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 +25 -0
- package/CHARACTER.md +254 -11
- package/dist/audit-YNODYB3U.js +20 -0
- package/dist/audit.cjs +45 -17
- package/dist/audit.d.cts +14 -1
- package/dist/audit.d.ts +14 -1
- package/dist/audit.js +43 -16
- package/dist/{chunk-GWMSSRIQ.js → chunk-3AP4PJMX.js} +480 -670
- package/dist/chunk-CU5TZHOV.js +163 -0
- package/dist/chunk-GV3QOTQU.js +458 -0
- package/dist/chunk-IQTZWNEK.js +112 -0
- package/dist/chunk-J2BOAIHJ.js +175 -0
- package/dist/{cli-7ME4VIRG.js → chunk-OA7SGPN4.js} +50 -101
- package/dist/{chunk-HYGFP3LB.js → chunk-Q54JSNC5.js} +40 -3
- package/dist/chunk-YYC4EGER.js +249 -0
- package/dist/{cli-F7W4F3ER.js → cli-6Y5E32FS.js} +14 -168
- package/dist/cli-IQT6QZ6U.js +16 -0
- package/dist/cli-MW3JAGX3.js +21 -0
- package/dist/cli-PD43TWX7.js +15 -0
- package/dist/cli-TM2HWYN7.js +312 -0
- package/dist/cli.js +2 -1
- package/dist/index.cjs +572 -12
- package/dist/index.d.cts +286 -1
- package/dist/index.d.ts +286 -1
- package/dist/index.js +544 -9
- package/package.json +3 -2
- package/word-lists.json +155 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
snapshotUrl
|
|
3
|
+
} from "./chunk-OA7SGPN4.js";
|
|
4
|
+
import {
|
|
5
|
+
describeTypicality,
|
|
6
|
+
loadNull,
|
|
7
|
+
typicality
|
|
8
|
+
} from "./chunk-GV3QOTQU.js";
|
|
9
|
+
import {
|
|
10
|
+
SOURCE_FILE,
|
|
11
|
+
finish,
|
|
12
|
+
loadExceptions,
|
|
13
|
+
parseFlags,
|
|
14
|
+
readPaths
|
|
15
|
+
} from "./chunk-YYC4EGER.js";
|
|
16
|
+
import {
|
|
17
|
+
fingerprint
|
|
18
|
+
} from "./chunk-Q54JSNC5.js";
|
|
19
|
+
import {
|
|
20
|
+
SNAPSHOT_VERSION,
|
|
21
|
+
auditSnapshot,
|
|
22
|
+
scanSource
|
|
23
|
+
} from "./chunk-3AP4PJMX.js";
|
|
24
|
+
|
|
25
|
+
// src/audit/cli.ts
|
|
26
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
27
|
+
import { resolve } from "path";
|
|
28
|
+
var fmt = (o) => o ? `oklch(${o.l.toFixed(3)} ${o.c.toFixed(3)} ${o.h.toFixed(1)})` : "none";
|
|
29
|
+
function describeFingerprint(fp) {
|
|
30
|
+
return [
|
|
31
|
+
"Fingerprint",
|
|
32
|
+
` accent ${fmt(fp.accent)}`,
|
|
33
|
+
` ground ${fmt(fp.ground)}`,
|
|
34
|
+
` display ${fp.display.family} (${fp.display.class})`,
|
|
35
|
+
` body ${fp.body.family} (${fp.body.class})`,
|
|
36
|
+
` roundness ${fp.roundness === null ? "no buttons" : fp.roundness.toFixed(2)}`,
|
|
37
|
+
` motion ${Math.round(fp.motion * 100)}% of sections below the fold reveal on scroll`,
|
|
38
|
+
` effects ${fp.effects.join(", ") || "none"}`,
|
|
39
|
+
` layout ${fp.layout.join(" > ") || "none"}`
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
function merge(a, b) {
|
|
43
|
+
const byTell = { ...a.summary.byTell };
|
|
44
|
+
for (const [k, v] of Object.entries(b.summary.byTell)) byTell[k] = (byTell[k] ?? 0) + v;
|
|
45
|
+
return {
|
|
46
|
+
catalogueVersion: a.catalogueVersion,
|
|
47
|
+
findings: [...a.findings, ...b.findings],
|
|
48
|
+
rejectedExceptions: a.rejectedExceptions,
|
|
49
|
+
excepted: [...a.excepted, ...b.excepted],
|
|
50
|
+
suppressed: [...a.suppressed, ...b.suppressed],
|
|
51
|
+
summary: {
|
|
52
|
+
files: a.summary.files + b.summary.files,
|
|
53
|
+
findings: a.summary.findings + b.summary.findings,
|
|
54
|
+
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] },
|
|
55
|
+
byTell,
|
|
56
|
+
blocking: a.summary.blocking + b.summary.blocking
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function readSnapshot(path) {
|
|
61
|
+
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
62
|
+
if (data.version !== SNAPSHOT_VERSION) throw new Error(`${path} is snapshot version ${String(data.version)}; this craft reads ${SNAPSHOT_VERSION}`);
|
|
63
|
+
return data;
|
|
64
|
+
}
|
|
65
|
+
async function runAudit(command, args, io) {
|
|
66
|
+
try {
|
|
67
|
+
const flags = parseFlags(args);
|
|
68
|
+
if (typeof flags === "string") throw new Error(flags);
|
|
69
|
+
const target = flags.positional[0];
|
|
70
|
+
if (!target) throw new Error(`usage: craft ${command} <url${command === "audit" ? " | snapshot.json" : ""}>`);
|
|
71
|
+
const viewport = flags.width || flags.height ? { width: Number(flags.width ?? 1440), height: Number(flags.height ?? 900) } : void 0;
|
|
72
|
+
const local = resolve(io.cwd, target);
|
|
73
|
+
const snapshot = command === "audit" && /\.json$/i.test(target) && existsSync(local) ? readSnapshot(local) : await snapshotUrl(target, { viewport });
|
|
74
|
+
if (flags.out) writeFileSync(resolve(io.cwd, flags.out), `${JSON.stringify(snapshot, null, 2)}
|
|
75
|
+
`);
|
|
76
|
+
if (command === "snapshot") {
|
|
77
|
+
if (!flags.out) io.out(JSON.stringify(snapshot, null, 2));
|
|
78
|
+
else io.out(`craft snapshot: ${snapshot.url} saved to ${flags.out}`);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
const exceptions = loadExceptions(flags, flags.repo ? resolve(io.cwd, flags.repo) : io.cwd);
|
|
82
|
+
let report = auditSnapshot(snapshot, { exceptions });
|
|
83
|
+
if (flags.repo) {
|
|
84
|
+
const repo = resolve(io.cwd, flags.repo);
|
|
85
|
+
report = merge(scanSource(readPaths(["."], repo, SOURCE_FILE), { exceptions }), report);
|
|
86
|
+
}
|
|
87
|
+
const fp = fingerprint(snapshot);
|
|
88
|
+
const nulls = flags.null ? flags.null.split(",").map((p) => loadNull(p.trim(), io.cwd)) : [];
|
|
89
|
+
const typical = nulls.length ? typicality(fp, nulls.flatMap((m, i) => m.runs.map((r) => nulls.length > 1 ? { ...r, id: `${i + 1}/${r.id}` } : r))) : null;
|
|
90
|
+
if (flags.json) {
|
|
91
|
+
io.out(JSON.stringify({ report, fingerprint: fp, typicality: typical, snapshot: flags.out ?? null }, null, 2));
|
|
92
|
+
return report.summary.blocking > 0 || flags.strict && report.summary.findings > 0 ? 1 : 0;
|
|
93
|
+
}
|
|
94
|
+
const code = finish(report, { ...flags, json: false }, `craft audit ${snapshot.url}`, io);
|
|
95
|
+
io.out("");
|
|
96
|
+
io.out(describeFingerprint(fp));
|
|
97
|
+
if (typical) {
|
|
98
|
+
io.out("");
|
|
99
|
+
io.out(describeTypicality(typical, nulls.reduce((s, m) => s + m.runs.length, 0), nulls.length));
|
|
100
|
+
}
|
|
101
|
+
return code;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
io.err(`craft: ${error.message}`);
|
|
104
|
+
return 2;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
describeFingerprint,
|
|
110
|
+
merge,
|
|
111
|
+
runAudit
|
|
112
|
+
};
|
|
@@ -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-3AP4PJMX.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
|
+
};
|
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
fingerprint
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-Q54JSNC5.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-3AP4PJMX.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
|
};
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
+
deltaEOk,
|
|
2
3
|
fontClass,
|
|
3
4
|
normaliseFamily,
|
|
4
5
|
parseColour
|
|
5
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-3AP4PJMX.js";
|
|
6
7
|
|
|
7
8
|
// src/fingerprint/index.ts
|
|
8
9
|
var FINGERPRINT_VERSION = 1;
|
|
9
10
|
var CHROMATIC = 0.03;
|
|
11
|
+
var MIN_ACCENT_AREA = 2e4;
|
|
10
12
|
var oklch = (value) => {
|
|
11
13
|
const parsed = parseColour(value);
|
|
12
14
|
return parsed && parsed.alpha > 0.5 ? parsed.oklch : null;
|
|
@@ -19,7 +21,7 @@ function accentOf(s) {
|
|
|
19
21
|
}
|
|
20
22
|
const topButton = [...buttons].sort((a, b) => b[1] - a[1])[0];
|
|
21
23
|
if (topButton) return oklch(topButton[0]);
|
|
22
|
-
const bg = s.colours.backgrounds.map((b) => ({ o: oklch(b.value), w: b.area })).filter((x) => x.o && x.o.c >= CHROMATIC).sort((a, b) => b.w - a.w)[0];
|
|
24
|
+
const bg = s.colours.backgrounds.map((b) => ({ o: oklch(b.value), w: b.area })).filter((x) => x.o && x.o.c >= CHROMATIC && x.w >= MIN_ACCENT_AREA).sort((a, b) => b.w - a.w)[0];
|
|
23
25
|
if (bg) return bg.o;
|
|
24
26
|
const text = s.colours.text.map((t) => ({ o: oklch(t.value), w: t.chars })).filter((x) => x.o && x.o.c >= CHROMATIC).sort((a, b) => b.w - a.w)[0];
|
|
25
27
|
return text ? text.o : null;
|
|
@@ -62,7 +64,42 @@ function fingerprint(s) {
|
|
|
62
64
|
layout: s.sections.map((x) => x.kind)
|
|
63
65
|
};
|
|
64
66
|
}
|
|
67
|
+
var DISTANCE_WEIGHTS = { accent: 0.25, type: 0.25, ground: 0.1, shape: 0.1, motion: 0.1, effects: 0.1, layout: 0.1 };
|
|
68
|
+
function levenshtein(a, b) {
|
|
69
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
70
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
71
|
+
let prev = row[0];
|
|
72
|
+
row[0] = i;
|
|
73
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
74
|
+
const cur = row[j];
|
|
75
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
76
|
+
prev = cur;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return row[b.length];
|
|
80
|
+
}
|
|
81
|
+
var clamp = (n) => Math.max(0, Math.min(1, n));
|
|
82
|
+
function faceDistance(a, b) {
|
|
83
|
+
if (a.family.toLowerCase() === b.family.toLowerCase()) return 0;
|
|
84
|
+
return a.class === b.class ? 0.5 : 1;
|
|
85
|
+
}
|
|
86
|
+
function fingerprintDistance(a, b) {
|
|
87
|
+
const accent = a.accent && b.accent ? clamp(deltaEOk(a.accent, b.accent) / 0.3) : a.accent || b.accent ? 1 : 0;
|
|
88
|
+
const ground = clamp(deltaEOk(a.ground, b.ground) / 0.15);
|
|
89
|
+
const type = 0.6 * faceDistance(a.display, b.display) + 0.4 * faceDistance(a.body, b.body);
|
|
90
|
+
const shape = a.roundness === null || b.roundness === null ? a.roundness === b.roundness ? 0 : 0.5 : clamp(Math.abs(a.roundness - b.roundness) / 0.5);
|
|
91
|
+
const motion = clamp(Math.abs(a.motion - b.motion));
|
|
92
|
+
const union = /* @__PURE__ */ new Set([...a.effects, ...b.effects]);
|
|
93
|
+
const shared = a.effects.filter((e) => b.effects.includes(e)).length;
|
|
94
|
+
const effects = union.size === 0 ? 0 : 1 - shared / union.size;
|
|
95
|
+
const longest = Math.max(a.layout.length, b.layout.length);
|
|
96
|
+
const layout = longest === 0 ? 0 : levenshtein(a.layout, b.layout) / longest;
|
|
97
|
+
const parts = { accent, ground, type, shape, motion, effects, layout };
|
|
98
|
+
const total = Object.keys(DISTANCE_WEIGHTS).reduce((sum, k) => sum + DISTANCE_WEIGHTS[k] * parts[k], 0);
|
|
99
|
+
return { total: Math.round(total * 1e3) / 1e3, parts };
|
|
100
|
+
}
|
|
65
101
|
|
|
66
102
|
export {
|
|
67
|
-
fingerprint
|
|
103
|
+
fingerprint,
|
|
104
|
+
fingerprintDistance
|
|
68
105
|
};
|