@domandigital/craft 0.5.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 +110 -0
- package/CHARACTER.md +578 -0
- package/PRINCIPLES.md +14 -0
- package/README.md +36 -1
- package/STANDARD.md +4 -1
- package/art-direction.schema.json +59 -0
- package/dist/audit-YNODYB3U.js +20 -0
- package/dist/audit.cjs +2472 -0
- package/dist/audit.d.cts +389 -0
- package/dist/audit.d.ts +389 -0
- package/dist/audit.js +2439 -0
- package/dist/chunk-3AP4PJMX.js +2352 -0
- 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/chunk-OA7SGPN4.js +419 -0
- package/dist/chunk-Q54JSNC5.js +105 -0
- package/dist/chunk-YYC4EGER.js +249 -0
- package/dist/cli-6Y5E32FS.js +334 -0
- 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 +22 -0
- package/dist/index.cjs +3385 -27
- package/dist/index.d.cts +1011 -1
- package/dist/index.d.ts +1011 -1
- package/dist/index.js +3311 -24
- package/package.json +28 -4
- package/word-lists.json +155 -0
package/dist/audit.d.cts
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A snapshot: what a page looks like once it has rendered, as plain data.
|
|
3
|
+
*
|
|
4
|
+
* Source tells read what a site writes. A snapshot reads what a visitor gets:
|
|
5
|
+
* the font that actually loaded, the colour that actually covers the page,
|
|
6
|
+
* the sections that actually fade in. The two disagree often enough to need
|
|
7
|
+
* both. A Tailwind class list can name indigo and a CMS can override it; a
|
|
8
|
+
* source can name Fraunces and the font can fail to load.
|
|
9
|
+
*
|
|
10
|
+
* Everything here is plain JSON so a snapshot can be saved, diffed, attached
|
|
11
|
+
* to a report and judged again later without a browser. Capturing one needs a
|
|
12
|
+
* browser (see `@domandigital/craft/audit`); judging one does not.
|
|
13
|
+
*/
|
|
14
|
+
declare const SNAPSHOT_VERSION = 1;
|
|
15
|
+
interface SnapshotColour {
|
|
16
|
+
/** `rgb(r, g, b)` or `rgba(r, g, b, a)`, as the browser computed it. */
|
|
17
|
+
value: string;
|
|
18
|
+
/** Painted area in CSS px², summed over every element using it. */
|
|
19
|
+
area: number;
|
|
20
|
+
/** Characters of text drawn in it, for text colours. */
|
|
21
|
+
chars: number;
|
|
22
|
+
}
|
|
23
|
+
interface SnapshotFont {
|
|
24
|
+
/** The first family in the computed stack, unquoted. */
|
|
25
|
+
family: string;
|
|
26
|
+
/** Characters of visible text set in it. */
|
|
27
|
+
chars: number;
|
|
28
|
+
/** Characters of it in headings (h1 to h3 and elements at 28px or more). */
|
|
29
|
+
displayChars: number;
|
|
30
|
+
italicChars: number;
|
|
31
|
+
weights: number[];
|
|
32
|
+
}
|
|
33
|
+
interface SnapshotHeading {
|
|
34
|
+
level: number;
|
|
35
|
+
family: string;
|
|
36
|
+
sizePx: number;
|
|
37
|
+
weight: number;
|
|
38
|
+
italic: boolean;
|
|
39
|
+
/** An `<em>`, `<i>` or italic span inside a non-italic heading: "Beauty, *reimagined*". */
|
|
40
|
+
italicPart: boolean;
|
|
41
|
+
text: string;
|
|
42
|
+
top: number;
|
|
43
|
+
}
|
|
44
|
+
interface SnapshotControl {
|
|
45
|
+
/** `button` for a <button> or submit, `link` for an anchor styled as a button. */
|
|
46
|
+
kind: "button" | "link";
|
|
47
|
+
radiusPx: number;
|
|
48
|
+
heightPx: number;
|
|
49
|
+
background: string;
|
|
50
|
+
text: string;
|
|
51
|
+
}
|
|
52
|
+
type SectionKind = "hero" | "logos" | "stats" | "cards" | "marquee" | "text" | "other";
|
|
53
|
+
interface SnapshotSection {
|
|
54
|
+
top: number;
|
|
55
|
+
height: number;
|
|
56
|
+
kind: SectionKind;
|
|
57
|
+
/** The first heading's text, or the first words, so a report can name it. */
|
|
58
|
+
label: string;
|
|
59
|
+
/** Cards laid out side by side: siblings of similar size in one row. */
|
|
60
|
+
cards: number;
|
|
61
|
+
/** Of those cards, how many open with an icon (an <svg> or small image). */
|
|
62
|
+
iconCards: number;
|
|
63
|
+
/** Hidden at load, waiting for a scroll to reveal it. */
|
|
64
|
+
hiddenAtLoad: boolean;
|
|
65
|
+
}
|
|
66
|
+
interface SnapshotGradient {
|
|
67
|
+
/** Colour stops, as computed. */
|
|
68
|
+
stops: string[];
|
|
69
|
+
area: number;
|
|
70
|
+
radial: boolean;
|
|
71
|
+
}
|
|
72
|
+
interface SnapshotEffects {
|
|
73
|
+
/** Elements with backdrop-filter blur and a translucent background. */
|
|
74
|
+
glass: number;
|
|
75
|
+
/** Text painted with a gradient (background-clip: text). */
|
|
76
|
+
gradientText: number;
|
|
77
|
+
gradients: SnapshotGradient[];
|
|
78
|
+
/** Large blurred shapes or radial gradients behind content with no content of their own. */
|
|
79
|
+
glows: number;
|
|
80
|
+
/** Repeating 1px line or dot backgrounds. */
|
|
81
|
+
gridBackgrounds: number;
|
|
82
|
+
/** Infinite horizontal translate animations. */
|
|
83
|
+
marquees: number;
|
|
84
|
+
/** Cards with a near-invisible border and a wide soft shadow. */
|
|
85
|
+
hairlineShadowCards: number;
|
|
86
|
+
/** A small bordered pill directly above the first h1. */
|
|
87
|
+
eyebrowChip: boolean;
|
|
88
|
+
}
|
|
89
|
+
interface SnapshotMotion {
|
|
90
|
+
/** Sections hidden at load (opacity near 0 or translated) that appear on scroll. */
|
|
91
|
+
hiddenSections: number;
|
|
92
|
+
sections: number;
|
|
93
|
+
/** Running CSS or Web Animations at capture time. */
|
|
94
|
+
animations: number;
|
|
95
|
+
/** A full-screen layer over the page at load that is gone a few seconds later. */
|
|
96
|
+
introOverlay: boolean;
|
|
97
|
+
}
|
|
98
|
+
interface Snapshot {
|
|
99
|
+
version: typeof SNAPSHOT_VERSION;
|
|
100
|
+
url: string;
|
|
101
|
+
capturedAt: string;
|
|
102
|
+
viewport: {
|
|
103
|
+
width: number;
|
|
104
|
+
height: number;
|
|
105
|
+
};
|
|
106
|
+
pageHeight: number;
|
|
107
|
+
/** The page ground: the body's computed background, or the html's. */
|
|
108
|
+
ground: string;
|
|
109
|
+
colours: {
|
|
110
|
+
backgrounds: SnapshotColour[];
|
|
111
|
+
text: SnapshotColour[];
|
|
112
|
+
};
|
|
113
|
+
fonts: SnapshotFont[];
|
|
114
|
+
headings: SnapshotHeading[];
|
|
115
|
+
controls: SnapshotControl[];
|
|
116
|
+
sections: SnapshotSection[];
|
|
117
|
+
effects: SnapshotEffects;
|
|
118
|
+
motion: SnapshotMotion;
|
|
119
|
+
/** The design-system measures trawl has collected since v7, kept so it can switch to this. */
|
|
120
|
+
metrics: {
|
|
121
|
+
fontSizes: number[];
|
|
122
|
+
fontWeights: number[];
|
|
123
|
+
radii: string[];
|
|
124
|
+
shadows: string[];
|
|
125
|
+
spacingOnScalePct: number | null;
|
|
126
|
+
bodyLineLengthCh: number | null;
|
|
127
|
+
bodyFontSizePx: number | null;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The shapes shared by the tell catalogue, the source scanner and the copy
|
|
133
|
+
* engine.
|
|
134
|
+
*
|
|
135
|
+
* A tell is a choice an AI model makes when nobody told it otherwise. It is not
|
|
136
|
+
* a mistake. Every tell here would be fine as a decision; it is flagged because
|
|
137
|
+
* it is the answer the model reaches for first, so a site full of them reads as
|
|
138
|
+
* nobody having decided anything.
|
|
139
|
+
*/
|
|
140
|
+
/**
|
|
141
|
+
* Which wave of model defaults a tell belongs to.
|
|
142
|
+
*
|
|
143
|
+
* 1: the first wave (indigo, blue-to-purple gradients, Inter, glass cards).
|
|
144
|
+
* 2: what models moved to once the first wave was named and banned (cream and
|
|
145
|
+
* italic serif, eyebrow chips, bento grids, the marquee).
|
|
146
|
+
* 3: reserved for what the harvester finds next.
|
|
147
|
+
*
|
|
148
|
+
* The tag exists because any fixed list goes stale. Knowing which wave a tell
|
|
149
|
+
* came from is what lets a report say "this site was built in 2024" rather
|
|
150
|
+
* than only "this site has tells".
|
|
151
|
+
*/
|
|
152
|
+
type Generation = 1 | 2 | 3;
|
|
153
|
+
/**
|
|
154
|
+
* `warn` is reported and never fails a run unless `--strict` is passed.
|
|
155
|
+
* `block` fails a run.
|
|
156
|
+
*
|
|
157
|
+
* Every entry ships as `warn`. A rule blocks only after its hits across the
|
|
158
|
+
* estate have been read by a person and the rule tuned against them. A rule
|
|
159
|
+
* that blocks on day one gets switched off by day three.
|
|
160
|
+
*/
|
|
161
|
+
type Severity = "warn" | "block";
|
|
162
|
+
interface Finding {
|
|
163
|
+
tell: string;
|
|
164
|
+
name: string;
|
|
165
|
+
generation: Generation;
|
|
166
|
+
severity: Severity;
|
|
167
|
+
path: string;
|
|
168
|
+
/** 1-indexed. 0 for a finding on a rendered page, where `path` is the URL. */
|
|
169
|
+
line: number;
|
|
170
|
+
excerpt: string;
|
|
171
|
+
message: string;
|
|
172
|
+
fix: string;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* A declared reason to keep a tell. Lives in the site's `art-direction.json`.
|
|
176
|
+
*
|
|
177
|
+
* DD's own brand is violet. A hue rule with no way to say so flags the agency's
|
|
178
|
+
* own site on every commit, and gets switched off. An exception without a
|
|
179
|
+
* `because` is not applied: the reason is the point.
|
|
180
|
+
*/
|
|
181
|
+
interface TellException {
|
|
182
|
+
tell: string;
|
|
183
|
+
because: string;
|
|
184
|
+
/** Where the reason comes from: the shopfront, the van, the trade's own look. */
|
|
185
|
+
evidence?: string;
|
|
186
|
+
/** Limit the exception to paths containing this string. */
|
|
187
|
+
path?: string;
|
|
188
|
+
}
|
|
189
|
+
interface CheckOptions {
|
|
190
|
+
exceptions?: TellException[];
|
|
191
|
+
/** Run only these tell ids. */
|
|
192
|
+
only?: string[];
|
|
193
|
+
}
|
|
194
|
+
interface CheckReport {
|
|
195
|
+
catalogueVersion: string;
|
|
196
|
+
findings: Finding[];
|
|
197
|
+
/** Exceptions that were declared but not applied, and why. */
|
|
198
|
+
rejectedExceptions: {
|
|
199
|
+
exception: TellException;
|
|
200
|
+
reason: string;
|
|
201
|
+
}[];
|
|
202
|
+
/** Findings each applied exception silenced, so an exception stays visible. */
|
|
203
|
+
excepted: {
|
|
204
|
+
tell: string;
|
|
205
|
+
because: string;
|
|
206
|
+
count: number;
|
|
207
|
+
}[];
|
|
208
|
+
/**
|
|
209
|
+
* Findings silenced by a `copy-ok` or `craft-ok` marker on the line or the
|
|
210
|
+
* line above. Listed, never hidden: a marker nobody can see is a hole.
|
|
211
|
+
*/
|
|
212
|
+
suppressed: {
|
|
213
|
+
tell: string;
|
|
214
|
+
path: string;
|
|
215
|
+
line: number;
|
|
216
|
+
}[];
|
|
217
|
+
summary: {
|
|
218
|
+
files: number;
|
|
219
|
+
findings: number;
|
|
220
|
+
byGeneration: Record<Generation, number>;
|
|
221
|
+
byTell: Record<string, number>;
|
|
222
|
+
blocking: number;
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* OKLab / OKLCh conversions, after Björn Ottosson's published M1/M2 matrices.
|
|
228
|
+
* https://bottosson.github.io/posts/oklab/
|
|
229
|
+
*
|
|
230
|
+
* Zero dependencies is a hard constraint for every dd-packages package, so the
|
|
231
|
+
* colour maths is implemented here rather than pulled from culori/colorjs.
|
|
232
|
+
*/
|
|
233
|
+
interface Oklch {
|
|
234
|
+
/** Perceptual lightness, 0..1. */
|
|
235
|
+
l: number;
|
|
236
|
+
/** Chroma, 0..~0.4 in practice. */
|
|
237
|
+
c: number;
|
|
238
|
+
/** Hue angle in degrees, 0..360. */
|
|
239
|
+
h: number;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Font family names as a browser reports them, turned back into the names a
|
|
244
|
+
* person would use.
|
|
245
|
+
*
|
|
246
|
+
* `next/font` renames every family it loads: Inter arrives as
|
|
247
|
+
* `__Inter_d65c78` and its metric-matched fallback as `__Inter_Fallback_d65c78`.
|
|
248
|
+
* A rendered check that compared raw names would never see a single reflex
|
|
249
|
+
* font on a Next.js site, which is most of the estate.
|
|
250
|
+
*/
|
|
251
|
+
type FontClass = "serif" | "sans" | "mono" | "script" | "system";
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* A fingerprint: the handful of choices that make a site look like itself,
|
|
255
|
+
* reduced to something two sites can be compared on.
|
|
256
|
+
*
|
|
257
|
+
* Signals 2 and 3 of CHARACTER.md both need a distance. "Would a model have
|
|
258
|
+
* built this anyway" is a distance from what the model builds; "does this
|
|
259
|
+
* look like the agency's other sites" is a distance from the estate. Neither
|
|
260
|
+
* cares about the exact pixels. They care whether the accent, the type, the
|
|
261
|
+
* ground, the shape language, the motion and the running order were chosen
|
|
262
|
+
* or defaulted, so that is what a fingerprint records.
|
|
263
|
+
*
|
|
264
|
+
* Pure. Built from a Snapshot, so it describes what a visitor gets.
|
|
265
|
+
*/
|
|
266
|
+
|
|
267
|
+
declare const FINGERPRINT_VERSION = 1;
|
|
268
|
+
interface Fingerprint {
|
|
269
|
+
version: typeof FINGERPRINT_VERSION;
|
|
270
|
+
/** The most prominent chromatic colour: button fills first, then painted area, then text. */
|
|
271
|
+
accent: Oklch | null;
|
|
272
|
+
ground: Oklch;
|
|
273
|
+
display: {
|
|
274
|
+
family: string;
|
|
275
|
+
class: FontClass;
|
|
276
|
+
};
|
|
277
|
+
body: {
|
|
278
|
+
family: string;
|
|
279
|
+
class: FontClass;
|
|
280
|
+
};
|
|
281
|
+
/** Median button corner radius over its height, 0 (square) to 0.5 (pill). */
|
|
282
|
+
roundness: number | null;
|
|
283
|
+
/** Share of below-the-fold sections that wait for a scroll to appear. */
|
|
284
|
+
motion: number;
|
|
285
|
+
/** Which second-look effects are present, as tell ids. */
|
|
286
|
+
effects: string[];
|
|
287
|
+
/** Section kinds in running order, hero first. */
|
|
288
|
+
layout: SectionKind[];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The in-page half of a snapshot.
|
|
293
|
+
*
|
|
294
|
+
* `collectInPage` runs inside the browser through `page.evaluate`, so it must
|
|
295
|
+
* be self-contained: no imports, no references to anything outside its own
|
|
296
|
+
* body. Anything that drives a browser can call it: craft's own `snapshotUrl`,
|
|
297
|
+
* trawl, a Playwright test in a client repo.
|
|
298
|
+
*
|
|
299
|
+
* It measures, it does not judge. Every threshold that decides whether
|
|
300
|
+
* something is a tell lives in the pure detectors, where it can be tested
|
|
301
|
+
* against a fixture without a browser.
|
|
302
|
+
*/
|
|
303
|
+
|
|
304
|
+
type InPageSnapshot = Omit<Snapshot, "version" | "url" | "capturedAt" | "motion"> & {
|
|
305
|
+
motion: Omit<Snapshot["motion"], "introOverlay">;
|
|
306
|
+
};
|
|
307
|
+
declare function collectInPage(): InPageSnapshot;
|
|
308
|
+
/** True while something covers the viewport: the first frame of an intro cinematic. */
|
|
309
|
+
declare function overlayInPage(): boolean;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* `@domandigital/craft/audit`: capture a snapshot of a live page and judge it.
|
|
313
|
+
*
|
|
314
|
+
* The only part of craft that needs a browser, so the only part that imports
|
|
315
|
+
* one, and only when called. `playwright` (or `playwright-core`) is an
|
|
316
|
+
* optional peer dependency: the core import stays pure, and a machine without
|
|
317
|
+
* a browser can still judge a snapshot someone else captured.
|
|
318
|
+
*/
|
|
319
|
+
|
|
320
|
+
interface SnapshotOptions {
|
|
321
|
+
viewport?: {
|
|
322
|
+
width: number;
|
|
323
|
+
height: number;
|
|
324
|
+
};
|
|
325
|
+
/** How long an intro may run before it counts as one. Default 5000 ms. */
|
|
326
|
+
introWindowMs?: number;
|
|
327
|
+
/** A Chromium to launch instead of Playwright's own. Also read from CRAFT_CHROMIUM. */
|
|
328
|
+
executablePath?: string;
|
|
329
|
+
timeoutMs?: number;
|
|
330
|
+
}
|
|
331
|
+
interface MinimalPage {
|
|
332
|
+
close?(): Promise<void>;
|
|
333
|
+
goto(url: string, options?: {
|
|
334
|
+
waitUntil?: string;
|
|
335
|
+
timeout?: number;
|
|
336
|
+
}): Promise<unknown>;
|
|
337
|
+
evaluate<R>(fn: () => R): Promise<R>;
|
|
338
|
+
waitForTimeout(ms: number): Promise<void>;
|
|
339
|
+
}
|
|
340
|
+
interface MinimalBrowser {
|
|
341
|
+
newPage(options?: {
|
|
342
|
+
viewport?: {
|
|
343
|
+
width: number;
|
|
344
|
+
height: number;
|
|
345
|
+
};
|
|
346
|
+
}): Promise<MinimalPage>;
|
|
347
|
+
close(): Promise<void>;
|
|
348
|
+
}
|
|
349
|
+
type Launcher = {
|
|
350
|
+
chromium: {
|
|
351
|
+
launch(options?: {
|
|
352
|
+
executablePath?: string;
|
|
353
|
+
headless?: boolean;
|
|
354
|
+
proxy?: {
|
|
355
|
+
server: string;
|
|
356
|
+
};
|
|
357
|
+
}): Promise<MinimalBrowser>;
|
|
358
|
+
};
|
|
359
|
+
};
|
|
360
|
+
/** Playwright, whichever flavour is installed, or a clear error saying how to get it. */
|
|
361
|
+
declare function loadPlaywright(): Promise<Launcher>;
|
|
362
|
+
/**
|
|
363
|
+
* Capture a snapshot from a page someone else is already driving. The caller
|
|
364
|
+
* owns the browser; craft only measures.
|
|
365
|
+
*/
|
|
366
|
+
declare function snapshotPage(page: MinimalPage, url: string, options?: SnapshotOptions): Promise<Snapshot>;
|
|
367
|
+
/** Launch a browser, open `url`, and capture a snapshot. */
|
|
368
|
+
declare function snapshotUrl(url: string, options?: SnapshotOptions): Promise<Snapshot>;
|
|
369
|
+
interface SnapshotResult {
|
|
370
|
+
url: string;
|
|
371
|
+
snapshot?: Snapshot;
|
|
372
|
+
/** Why this page has no snapshot. The others are still taken. */
|
|
373
|
+
error?: string;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Several pages through one browser, in order. A null model is twenty pages;
|
|
377
|
+
* launching Chromium twenty times is most of the wait. One page that will not
|
|
378
|
+
* load costs that page, not the batch.
|
|
379
|
+
*/
|
|
380
|
+
declare function snapshotUrls(urls: string[], options?: SnapshotOptions): Promise<SnapshotResult[]>;
|
|
381
|
+
interface AuditResult {
|
|
382
|
+
snapshot: Snapshot;
|
|
383
|
+
report: CheckReport;
|
|
384
|
+
fingerprint: Fingerprint;
|
|
385
|
+
}
|
|
386
|
+
/** Snapshot a URL, judge it against the rendered tells, and fingerprint it. */
|
|
387
|
+
declare function audit(url: string, options?: SnapshotOptions & CheckOptions): Promise<AuditResult>;
|
|
388
|
+
|
|
389
|
+
export { type AuditResult, type SnapshotOptions, type SnapshotResult, audit, collectInPage, loadPlaywright, overlayInPage, snapshotPage, snapshotUrl, snapshotUrls };
|