@domandigital/craft 0.11.0 → 0.12.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 +16 -0
- package/CHARACTER.md +9 -1
- package/COPY.md +6 -2
- package/README.md +5 -0
- package/dist/{audit-QIPXL7E4.js → audit-5QWFLZ6L.js} +6 -3
- package/dist/audit.cjs +276 -15
- package/dist/audit.d.cts +81 -4
- package/dist/audit.d.ts +81 -4
- package/dist/audit.js +275 -15
- package/dist/{chunk-3NGWXZZE.js → chunk-AL6MRG6V.js} +40 -3
- package/dist/{chunk-HOHHQB4N.js → chunk-CDAJUS5U.js} +9 -8
- package/dist/{chunk-2AJB27BJ.js → chunk-J3QUROJ5.js} +24 -10
- package/dist/chunk-JYNXGCYZ.js +75 -0
- package/dist/{chunk-3OZQ4C5R.js → chunk-LHGY43LL.js} +8 -5
- package/dist/{chunk-MSQ57PJU.js → chunk-PU5G5DUC.js} +11 -11
- package/dist/{chunk-QMYERCBB.js → chunk-PZ4MF2VV.js} +19 -7
- package/dist/{chunk-JWHC67ZS.js → chunk-S3NZAVU6.js} +121 -1
- package/dist/{chunk-JP75P2DU.js → chunk-ULPI335S.js} +178 -12
- package/dist/chunk-ZKVJ7K6X.js +19 -0
- package/dist/cli-2DY3V3SF.js +17 -0
- package/dist/{cli-4ESXOLKV.js → cli-C3JLFXMG.js} +5 -4
- package/dist/cli-QM2AQQMF.js +19 -0
- package/dist/{cli-DBZMVYYV.js → cli-ZB2VJ7CY.js} +14 -12
- package/dist/{cli-U3SZVHSK.js → cli-ZPJIWVEP.js} +12 -78
- package/dist/cli.js +2 -2
- package/dist/index.cjs +270 -9
- package/dist/index.d.cts +180 -6
- package/dist/index.d.ts +180 -6
- package/dist/index.js +260 -9
- package/licences.json +104 -0
- package/package.json +3 -2
- package/word-lists.json +1 -1
- package/dist/cli-T5RD2BTK.js +0 -17
- package/dist/cli-VREYWCT4.js +0 -16
|
@@ -3,19 +3,21 @@ import {
|
|
|
3
3
|
hueName,
|
|
4
4
|
loadNull,
|
|
5
5
|
shapeName
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-LHGY43LL.js";
|
|
7
|
+
import {
|
|
8
|
+
readSnapshot
|
|
9
|
+
} from "./chunk-ZKVJ7K6X.js";
|
|
7
10
|
import {
|
|
8
11
|
parseFlags,
|
|
9
12
|
toJson
|
|
10
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-J3QUROJ5.js";
|
|
11
14
|
import {
|
|
12
15
|
fingerprint,
|
|
13
16
|
fingerprintDistance
|
|
14
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-PZ4MF2VV.js";
|
|
15
18
|
import {
|
|
16
|
-
SNAPSHOT_VERSION,
|
|
17
19
|
auditSnapshot
|
|
18
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-AL6MRG6V.js";
|
|
19
21
|
|
|
20
22
|
// src/estate/cli.ts
|
|
21
23
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -88,12 +90,11 @@ function loadEstate(path) {
|
|
|
88
90
|
async function snapshotOf(target, cwd) {
|
|
89
91
|
const local = resolve(cwd, target);
|
|
90
92
|
if (/\.json$/i.test(target) && existsSync(local)) {
|
|
91
|
-
const snap = JSON.parse(readFileSync(local, "utf8"));
|
|
92
|
-
if (snap.version !== SNAPSHOT_VERSION) throw new Error(`${target} is not a snapshot`);
|
|
93
|
+
const snap = readSnapshot(JSON.parse(readFileSync(local, "utf8")), target);
|
|
93
94
|
return snap;
|
|
94
95
|
}
|
|
95
96
|
if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL, a snapshot file, nor a site in the register`);
|
|
96
|
-
const { snapshotUrl } = await import("./audit-
|
|
97
|
+
const { snapshotUrl } = await import("./audit-5QWFLZ6L.js");
|
|
97
98
|
return snapshotUrl(target);
|
|
98
99
|
}
|
|
99
100
|
var line = (distance, sibling, shared) => `${distance.toFixed(2)}${sibling ? " SIBLING" : ""}${shared.length ? ` shares ${shared.join(", ")}` : ""}`;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CATALOGUE,
|
|
3
3
|
CATALOGUE_VERSION,
|
|
4
|
+
HOUSE_LOCKED,
|
|
4
5
|
applyHouseGate,
|
|
5
6
|
checkCopy,
|
|
6
7
|
extractCopy,
|
|
7
8
|
extractStrings,
|
|
8
9
|
fileKind,
|
|
10
|
+
holdHouseBlocks,
|
|
9
11
|
houseRule,
|
|
10
12
|
scanSource
|
|
11
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-AL6MRG6V.js";
|
|
12
14
|
|
|
13
15
|
// src/character/config.ts
|
|
14
16
|
var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
|
|
@@ -637,25 +639,25 @@ var NOT_COPY_DIR = (name) => name.startsWith("_");
|
|
|
637
639
|
function run(argv, io) {
|
|
638
640
|
const [command, ...rest] = argv;
|
|
639
641
|
if (command === "direction") {
|
|
640
|
-
return import("./cli-
|
|
642
|
+
return import("./cli-ZPJIWVEP.js").then((m) => m.runDirection(rest, io));
|
|
641
643
|
}
|
|
642
644
|
if (command === "report" || command === "retrofit") {
|
|
643
|
-
return import("./cli-
|
|
645
|
+
return import("./cli-ZB2VJ7CY.js").then((m) => m.runReport(command, rest, io));
|
|
644
646
|
}
|
|
645
647
|
if (command === "estate") {
|
|
646
|
-
return import("./cli-
|
|
648
|
+
return import("./cli-2DY3V3SF.js").then((m) => m.runEstate(rest, io));
|
|
647
649
|
}
|
|
648
650
|
if (command === "null") {
|
|
649
|
-
return import("./cli-
|
|
651
|
+
return import("./cli-C3JLFXMG.js").then((m) => m.runNull(rest, io));
|
|
650
652
|
}
|
|
651
653
|
if (command === "tells" && rest[0] === "harvest") {
|
|
652
|
-
return import("./cli-
|
|
654
|
+
return import("./cli-C3JLFXMG.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
|
|
653
655
|
io.err(`craft: ${error.message}`);
|
|
654
656
|
return 2;
|
|
655
657
|
});
|
|
656
658
|
}
|
|
657
659
|
if (command === "snapshot" || command === "audit") {
|
|
658
|
-
return import("./cli-
|
|
660
|
+
return import("./cli-QM2AQQMF.js").then((m) => m.runAudit(command, rest, io));
|
|
659
661
|
}
|
|
660
662
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
661
663
|
io.out(HELP);
|
|
@@ -713,10 +715,22 @@ function run(argv, io) {
|
|
|
713
715
|
const paths = flags.positional.length > 0 ? flags.positional : command === "copy" ? config.copyPaths ?? [] : ["."];
|
|
714
716
|
if (command === "copy" && paths.length === 0 && !flags.staged) throw new Error("usage: craft copy <paths...> (or copyPaths in craft.config.json)");
|
|
715
717
|
const files = flags.staged ? readStaged(io.cwd, wanted, config.ignore) : readPaths(paths, io.cwd, wanted, command === "copy" ? NOT_COPY_DIR : void 0, config.ignore);
|
|
716
|
-
|
|
718
|
+
let exceptions = loadExceptions(flags, io.cwd);
|
|
719
|
+
let effective = config;
|
|
720
|
+
let refused = [];
|
|
721
|
+
if (command === "copy" && flags.gate) {
|
|
722
|
+
const held = holdHouseBlocks(exceptions, config.severity);
|
|
723
|
+
exceptions = held.exceptions;
|
|
724
|
+
effective = { ...config, severity: held.severity };
|
|
725
|
+
const warned = held.refusedSeverity.filter((tell) => config.severity[tell].level === "warn").map((tell) => ({ tell, because: `craft.config.json: ${config.severity[tell].because}` }));
|
|
726
|
+
refused = [...held.refused, ...warned];
|
|
727
|
+
}
|
|
717
728
|
let report = command === "scan" ? scanSource(files, { exceptions }) : checkCopy(files, { exceptions });
|
|
718
|
-
if (command === "copy" && flags.gate)
|
|
719
|
-
|
|
729
|
+
if (command === "copy" && flags.gate) {
|
|
730
|
+
report = applyHouseGate(report);
|
|
731
|
+
report = { ...report, rejectedExceptions: [...report.rejectedExceptions, ...refused.map((exception) => ({ exception, reason: HOUSE_LOCKED }))] };
|
|
732
|
+
}
|
|
733
|
+
const prepared = prepareReport(report, flags, io, effective);
|
|
720
734
|
if (!prepared) return 0;
|
|
721
735
|
return finish(prepared, flags, command === "scan" ? "craft scan" : "craft copy", io);
|
|
722
736
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// src/direction/png.ts
|
|
2
|
+
import { inflateSync } from "zlib";
|
|
3
|
+
function decodePng(buf) {
|
|
4
|
+
const sig = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
5
|
+
if (!sig.every((b, i) => buf[i] === b)) throw new Error("not a PNG");
|
|
6
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
7
|
+
let pos = 8;
|
|
8
|
+
let width = 0;
|
|
9
|
+
let height = 0;
|
|
10
|
+
let depth = 0;
|
|
11
|
+
let colour = 0;
|
|
12
|
+
let interlace = 0;
|
|
13
|
+
const idat = [];
|
|
14
|
+
while (pos < buf.length) {
|
|
15
|
+
const len = view.getUint32(pos);
|
|
16
|
+
const type = String.fromCharCode(...buf.subarray(pos + 4, pos + 8));
|
|
17
|
+
const data = buf.subarray(pos + 8, pos + 8 + len);
|
|
18
|
+
if (type === "IHDR") {
|
|
19
|
+
width = view.getUint32(pos + 8);
|
|
20
|
+
height = view.getUint32(pos + 12);
|
|
21
|
+
depth = data[8];
|
|
22
|
+
colour = data[9];
|
|
23
|
+
interlace = data[12];
|
|
24
|
+
} else if (type === "IDAT") idat.push(data);
|
|
25
|
+
else if (type === "IEND") break;
|
|
26
|
+
pos += 12 + len;
|
|
27
|
+
}
|
|
28
|
+
const channels = { 0: 1, 2: 3, 4: 2, 6: 4 }[colour];
|
|
29
|
+
if (depth !== 8 || !channels || interlace !== 0) throw new Error(`unsupported PNG (depth ${depth}, colour type ${colour}${interlace ? ", interlaced" : ""}); export it as 8-bit RGB`);
|
|
30
|
+
const raw = inflateSync(Buffer.concat(idat.map((d) => Buffer.from(d))));
|
|
31
|
+
const stride = width * channels;
|
|
32
|
+
const out = new Uint8Array(width * height * 4);
|
|
33
|
+
const prev = new Uint8Array(stride);
|
|
34
|
+
const line = new Uint8Array(stride);
|
|
35
|
+
for (let y = 0; y < height; y += 1) {
|
|
36
|
+
const filter = raw[y * (stride + 1)];
|
|
37
|
+
const src = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
|
|
38
|
+
for (let x = 0; x < stride; x += 1) {
|
|
39
|
+
const a = x >= channels ? line[x - channels] : 0;
|
|
40
|
+
const b = prev[x];
|
|
41
|
+
const c = x >= channels ? prev[x - channels] : 0;
|
|
42
|
+
let v = src[x];
|
|
43
|
+
if (filter === 1) v += a;
|
|
44
|
+
else if (filter === 2) v += b;
|
|
45
|
+
else if (filter === 3) v += a + b >> 1;
|
|
46
|
+
else if (filter === 4) {
|
|
47
|
+
const p = a + b - c;
|
|
48
|
+
const pa = Math.abs(p - a);
|
|
49
|
+
const pb = Math.abs(p - b);
|
|
50
|
+
const pc = Math.abs(p - c);
|
|
51
|
+
v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
|
52
|
+
}
|
|
53
|
+
line[x] = v & 255;
|
|
54
|
+
}
|
|
55
|
+
for (let x = 0; x < width; x += 1) {
|
|
56
|
+
const i = x * channels;
|
|
57
|
+
const o = (y * width + x) * 4;
|
|
58
|
+
if (channels >= 3) {
|
|
59
|
+
out[o] = line[i];
|
|
60
|
+
out[o + 1] = line[i + 1];
|
|
61
|
+
out[o + 2] = line[i + 2];
|
|
62
|
+
out[o + 3] = channels === 4 ? line[i + 3] : 255;
|
|
63
|
+
} else {
|
|
64
|
+
out[o] = out[o + 1] = out[o + 2] = line[i];
|
|
65
|
+
out[o + 3] = channels === 2 ? line[i + 1] : 255;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
prev.set(line);
|
|
69
|
+
}
|
|
70
|
+
return { width, height, rgba: out };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export {
|
|
74
|
+
decodePng
|
|
75
|
+
};
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readSnapshot
|
|
3
|
+
} from "./chunk-ZKVJ7K6X.js";
|
|
1
4
|
import {
|
|
2
5
|
parseFlags,
|
|
3
6
|
toJson
|
|
4
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-J3QUROJ5.js";
|
|
5
8
|
import {
|
|
6
9
|
fingerprint,
|
|
7
10
|
fingerprintDistance
|
|
8
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-PZ4MF2VV.js";
|
|
9
12
|
import {
|
|
10
13
|
CATALOGUE_VERSION,
|
|
11
14
|
REFLEX_FONTS_1,
|
|
@@ -19,7 +22,7 @@ import {
|
|
|
19
22
|
isCream,
|
|
20
23
|
oklchToRgb,
|
|
21
24
|
scanSource
|
|
22
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-AL6MRG6V.js";
|
|
23
26
|
|
|
24
27
|
// src/null/cli.ts
|
|
25
28
|
import { spawn } from "child_process";
|
|
@@ -353,7 +356,7 @@ async function build(flags, io) {
|
|
|
353
356
|
const toShoot = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && !existsSync(join(shots, `${id}.json`)));
|
|
354
357
|
if (toShoot.length) {
|
|
355
358
|
io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"}`);
|
|
356
|
-
const { snapshotUrls } = await import("./audit-
|
|
359
|
+
const { snapshotUrls } = await import("./audit-5QWFLZ6L.js");
|
|
357
360
|
const results = await snapshotUrls(toShoot.map((id) => pathToFileURL(join(pages, `${id}.html`)).href));
|
|
358
361
|
results.forEach((r, i) => {
|
|
359
362
|
const id = toShoot[i];
|
|
@@ -362,7 +365,7 @@ async function build(flags, io) {
|
|
|
362
365
|
else failed.push(`${id}: the page would not render: ${r.error}`);
|
|
363
366
|
});
|
|
364
367
|
}
|
|
365
|
-
const measured = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && existsSync(join(shots, `${id}.json`))).map((id) => measureRun(id, readFileSync(join(pages, `${id}.html`), "utf8"), readJson(join(shots, `${id}.json`))));
|
|
368
|
+
const measured = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && existsSync(join(shots, `${id}.json`))).map((id) => measureRun(id, readFileSync(join(pages, `${id}.html`), "utf8"), readSnapshot(readJson(join(shots, `${id}.json`)), `${id}.json`)));
|
|
366
369
|
const model = {
|
|
367
370
|
version: NULL_VERSION,
|
|
368
371
|
brief: brief.trim(),
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
snapshotUrl,
|
|
3
3
|
snapshotUrls
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-ULPI335S.js";
|
|
5
5
|
import {
|
|
6
6
|
describeTypicality,
|
|
7
7
|
loadNull,
|
|
8
8
|
typicality
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-LHGY43LL.js";
|
|
10
|
+
import {
|
|
11
|
+
readSnapshot
|
|
12
|
+
} from "./chunk-ZKVJ7K6X.js";
|
|
10
13
|
import {
|
|
11
14
|
SOURCE_FILE,
|
|
12
15
|
finish,
|
|
@@ -16,15 +19,14 @@ import {
|
|
|
16
19
|
prepareReport,
|
|
17
20
|
readPaths,
|
|
18
21
|
toJson
|
|
19
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-J3QUROJ5.js";
|
|
20
23
|
import {
|
|
21
24
|
fingerprint
|
|
22
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-PZ4MF2VV.js";
|
|
23
26
|
import {
|
|
24
|
-
SNAPSHOT_VERSION,
|
|
25
27
|
auditSnapshot,
|
|
26
28
|
scanSource
|
|
27
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-AL6MRG6V.js";
|
|
28
30
|
|
|
29
31
|
// src/audit/cli.ts
|
|
30
32
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -81,10 +83,8 @@ function merge(a, b) {
|
|
|
81
83
|
}
|
|
82
84
|
};
|
|
83
85
|
}
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
if (data.version !== SNAPSHOT_VERSION) throw new Error(`${path} is snapshot version ${String(data.version)}; this craft reads ${SNAPSHOT_VERSION}`);
|
|
87
|
-
return data;
|
|
86
|
+
function readSnapshotFile(path) {
|
|
87
|
+
return readSnapshot(JSON.parse(readFileSync(path, "utf8")), path);
|
|
88
88
|
}
|
|
89
89
|
async function readText(source, cwd) {
|
|
90
90
|
if (/^https?:\/\//i.test(source)) {
|
|
@@ -123,7 +123,7 @@ async function runAudit(command, args, io) {
|
|
|
123
123
|
const failed = [];
|
|
124
124
|
const local = target ? resolve(io.cwd, target) : "";
|
|
125
125
|
if (target && command === "audit" && /\.json$/i.test(target) && existsSync(local)) {
|
|
126
|
-
audited.push({ url: target, viewport: null, snapshot:
|
|
126
|
+
audited.push({ url: target, viewport: null, snapshot: readSnapshotFile(local) });
|
|
127
127
|
} else if (target && viewports.length === 1) {
|
|
128
128
|
const snapshot = await snapshotUrl(target, { viewport: viewports[0] });
|
|
129
129
|
audited.push({ url: target, viewport: viewports[0] ?? null, snapshot });
|
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
fontClass,
|
|
4
4
|
normaliseFamily,
|
|
5
5
|
parseColour
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-AL6MRG6V.js";
|
|
7
7
|
|
|
8
8
|
// src/fingerprint/index.ts
|
|
9
|
-
var FINGERPRINT_VERSION =
|
|
9
|
+
var FINGERPRINT_VERSION = 2;
|
|
10
10
|
var CHROMATIC = 0.03;
|
|
11
11
|
var MIN_ACCENT_AREA = 2e4;
|
|
12
12
|
var oklch = (value) => {
|
|
@@ -61,23 +61,36 @@ function fingerprint(s) {
|
|
|
61
61
|
roundness: median(s.controls.map((c) => Math.min(0.5, c.radiusPx / Math.max(1, c.heightPx)))),
|
|
62
62
|
motion: below ? Math.min(1, s.motion.hiddenSections / below) : 0,
|
|
63
63
|
effects: effects.sort(),
|
|
64
|
-
layout: s.sections.map((x) => x.kind)
|
|
64
|
+
layout: s.sections.map((x) => x.kind),
|
|
65
|
+
...s.sections.length > 0 && s.sections.every((x) => x.role && x.geometry) ? { sections: s.sections.map((x) => ({ role: x.role, centred: x.geometry.centredShare, width: x.geometry.contentWidthRatio })) } : {}
|
|
65
66
|
};
|
|
66
67
|
}
|
|
67
68
|
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
|
+
function levenshtein(a, b, cost = (x, y) => x === y ? 0 : 1) {
|
|
69
70
|
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
70
71
|
for (let i = 1; i <= a.length; i += 1) {
|
|
71
72
|
let prev = row[0];
|
|
72
73
|
row[0] = i;
|
|
73
74
|
for (let j = 1; j <= b.length; j += 1) {
|
|
74
75
|
const cur = row[j];
|
|
75
|
-
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1]
|
|
76
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + cost(a[i - 1], b[j - 1]));
|
|
76
77
|
prev = cur;
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
return row[b.length];
|
|
80
81
|
}
|
|
82
|
+
function sectionCost(x, y) {
|
|
83
|
+
if (x.role !== y.role) return 1;
|
|
84
|
+
return Math.min(0.5, (Math.abs(x.centred - y.centred) + Math.abs(x.width - y.width)) / 2);
|
|
85
|
+
}
|
|
86
|
+
function layoutDistance(a, b) {
|
|
87
|
+
if (a.sections && b.sections) {
|
|
88
|
+
const longest2 = Math.max(a.sections.length, b.sections.length);
|
|
89
|
+
return longest2 === 0 ? 0 : levenshtein(a.sections, b.sections, sectionCost) / longest2;
|
|
90
|
+
}
|
|
91
|
+
const longest = Math.max(a.layout.length, b.layout.length);
|
|
92
|
+
return longest === 0 ? 0 : levenshtein(a.layout, b.layout) / longest;
|
|
93
|
+
}
|
|
81
94
|
var clamp = (n) => Math.max(0, Math.min(1, n));
|
|
82
95
|
function faceDistance(a, b) {
|
|
83
96
|
if (a.family.toLowerCase() === b.family.toLowerCase()) return 0;
|
|
@@ -92,8 +105,7 @@ function fingerprintDistance(a, b) {
|
|
|
92
105
|
const union = /* @__PURE__ */ new Set([...a.effects, ...b.effects]);
|
|
93
106
|
const shared = a.effects.filter((e) => b.effects.includes(e)).length;
|
|
94
107
|
const effects = union.size === 0 ? 0 : 1 - shared / union.size;
|
|
95
|
-
const
|
|
96
|
-
const layout = longest === 0 ? 0 : levenshtein(a.layout, b.layout) / longest;
|
|
108
|
+
const layout = layoutDistance(a, b);
|
|
97
109
|
const parts = { accent, ground, type, shape, motion, effects, layout };
|
|
98
110
|
const total = Object.keys(DISTANCE_WEIGHTS).reduce((sum, k) => sum + DISTANCE_WEIGHTS[k] * parts[k], 0);
|
|
99
111
|
return { total: Math.round(total * 1e3) / 1e3, parts };
|
|
@@ -6,7 +6,115 @@ import {
|
|
|
6
6
|
isCream,
|
|
7
7
|
normaliseFamily,
|
|
8
8
|
parseColour
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-AL6MRG6V.js";
|
|
10
|
+
|
|
11
|
+
// src/direction/licences.ts
|
|
12
|
+
var LICENCES = [
|
|
13
|
+
{
|
|
14
|
+
name: "Basteleur",
|
|
15
|
+
subject: "face",
|
|
16
|
+
licence: "OFL-1.1",
|
|
17
|
+
multiClient: "yes",
|
|
18
|
+
checked: "2026-09-24",
|
|
19
|
+
source: "https://velvetyne.fr/fonts/basteleur/",
|
|
20
|
+
note: "Velvetyne, designed by Keussel. Keep the OFL notice with the font files."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "Gulax",
|
|
24
|
+
subject: "face",
|
|
25
|
+
licence: "OFL-1.1",
|
|
26
|
+
multiClient: "yes",
|
|
27
|
+
checked: "2026-09-24",
|
|
28
|
+
source: "https://velvetyne.fr/fonts/gulax/",
|
|
29
|
+
note: "Velvetyne. Keep the OFL notice with the font files."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "Le Murmure",
|
|
33
|
+
subject: "face",
|
|
34
|
+
licence: "OFL-1.1",
|
|
35
|
+
multiClient: "yes",
|
|
36
|
+
checked: "2026-09-24",
|
|
37
|
+
source: "https://velvetyne.fr/fonts/le-murmure/",
|
|
38
|
+
note: "Velvetyne. Keep the OFL notice with the font files."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "Blaze Type",
|
|
42
|
+
subject: "foundry",
|
|
43
|
+
licence: "commercial",
|
|
44
|
+
multiClient: "per-site",
|
|
45
|
+
cap: 10,
|
|
46
|
+
checked: "2026-09-24",
|
|
47
|
+
source: "https://blazetype.eu/eula",
|
|
48
|
+
note: "The EULA sells a professional licence for 1 to 10 websites, and lets an agency buy in a client's name at the listed price. Its /license page says one licence covers every domain; the EULA is the contract, so buy per client."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "Power Type",
|
|
52
|
+
subject: "foundry",
|
|
53
|
+
licence: "commercial",
|
|
54
|
+
multiClient: "unverified",
|
|
55
|
+
checked: "2026-09-24",
|
|
56
|
+
source: "https://power-type.com/eula",
|
|
57
|
+
note: "Research on 2026-09-11 reported a Super License capped at 10 self-hosted domains. The EULA page did not load when checked; read it before relying on the cap."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "Grilli Type",
|
|
61
|
+
subject: "foundry",
|
|
62
|
+
licence: "commercial",
|
|
63
|
+
multiClient: "per-site",
|
|
64
|
+
checked: "2026-09-24",
|
|
65
|
+
source: "https://www.grillitype.com/information",
|
|
66
|
+
note: "Web licences cover any number of domains controlled by the licensee, priced by monthly visitors, so a client site needs the client's own licence. Unlimited tiers start at $10,000 per style."
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "Phosphor",
|
|
70
|
+
subject: "icons",
|
|
71
|
+
licence: "MIT",
|
|
72
|
+
multiClient: "yes",
|
|
73
|
+
checked: "2026-09-24",
|
|
74
|
+
source: "https://github.com/phosphor-icons/core/blob/main/LICENSE",
|
|
75
|
+
note: "Six weights. The icon-tile tells treat it exactly as they treat Lucide: the library is not the tell, the tinted tile is."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "Tabler Icons",
|
|
79
|
+
subject: "icons",
|
|
80
|
+
licence: "MIT",
|
|
81
|
+
multiClient: "yes",
|
|
82
|
+
checked: "2026-09-24",
|
|
83
|
+
source: "https://github.com/tabler/tabler-icons/blob/main/LICENSE",
|
|
84
|
+
note: "One stroke grid across the set."
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "Iconoir",
|
|
88
|
+
subject: "icons",
|
|
89
|
+
licence: "MIT",
|
|
90
|
+
multiClient: "yes",
|
|
91
|
+
checked: "2026-09-24",
|
|
92
|
+
source: "https://github.com/iconoir-icons/iconoir/blob/main/LICENSE",
|
|
93
|
+
note: "Smaller set, no paid tier."
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "Unicons",
|
|
97
|
+
subject: "icons",
|
|
98
|
+
licence: "IconScout Simple License",
|
|
99
|
+
multiClient: "yes",
|
|
100
|
+
checked: "2026-09-24",
|
|
101
|
+
source: "https://github.com/Iconscout/unicons/blob/master/LICENSE",
|
|
102
|
+
note: "IconScout's free set, personal and commercial use, attribution optional."
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "IconScout premium",
|
|
106
|
+
subject: "icons",
|
|
107
|
+
licence: "commercial",
|
|
108
|
+
multiClient: "per-site",
|
|
109
|
+
checked: "2026-09-24",
|
|
110
|
+
source: "https://iconscout.com/licenses",
|
|
111
|
+
note: "Each licence covers a single end product. Never in a tool, template or shared kit (create-site, craft, a house component library), even modified. Record the assets each site uses. Assets come from different contributors, so take one site's set from one contributor."
|
|
112
|
+
}
|
|
113
|
+
];
|
|
114
|
+
function faceLicence(family) {
|
|
115
|
+
const name = family.trim().toLowerCase();
|
|
116
|
+
return LICENCES.find((l) => l.subject === "face" && l.name.toLowerCase() === name);
|
|
117
|
+
}
|
|
10
118
|
|
|
11
119
|
// src/direction/types.ts
|
|
12
120
|
var DIRECTION_VERSION = 1;
|
|
@@ -150,6 +258,18 @@ function validateDirection(input, ctx = {}) {
|
|
|
150
258
|
ok = false;
|
|
151
259
|
}
|
|
152
260
|
}
|
|
261
|
+
if ((key === "display" || key === "body") && typeof c.value === "string" && c.value.trim()) {
|
|
262
|
+
const family = normaliseFamily(c.value);
|
|
263
|
+
const entry = faceLicence(family);
|
|
264
|
+
if (!entry) {
|
|
265
|
+
warn(`${at}.value`, `${family}: licence not in craft's register. Read the licensor's own terms for use on this client's site, then add it to src/direction/licences.ts.`);
|
|
266
|
+
} else if (entry.multiClient === "unverified") {
|
|
267
|
+
warn(`${at}.value`, `${family}: licence unverified (${entry.note})`);
|
|
268
|
+
} else if (entry.multiClient === "capped" || entry.multiClient === "per-site") {
|
|
269
|
+
const scope = entry.multiClient === "capped" ? `covers up to ${entry.cap} sites` : "needs a licence for this site, in the client's name";
|
|
270
|
+
warn(`${at}.value`, `${family}: ${entry.licence} licence ${scope}. Record the purchase before launch.`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
153
273
|
const fp = ctx.fingerprint;
|
|
154
274
|
if (fp && typeof c.value === "string") {
|
|
155
275
|
if ((key === "accent" || key === "ground") && parseColour(c.value)) {
|