@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @domandigital/craft
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 7622bd9: Phase K: snapshot version 2.
|
|
8
|
+
|
|
9
|
+
- Every section gains a `role`: `cta-band`, `footer-cta`, `pricing`, `testimonials`, `faq`, `process`, `features`, `team`, `contact`, or its `kind`. `kind` keeps its version 1 meaning, so the rendered tells and the calibrated null models and estate read exactly as before.
|
|
10
|
+
- Every section gains `geometry`: `centredShare`, `mirrorSymmetry`, `whitespaceRatio`, `contentWidthRatio`, `background` and `controls`. The page gains `rhythmVariance` (how much section heights vary).
|
|
11
|
+
- `visual`: colourfulness (Hasler and Süsstrunk), edge density and left-right symmetry, from a screenshot of the first 6000px. None of them passes or fails anything. `snapshotUrl(url, { visual: false })` skips the screenshot; a driver without `page.screenshot` gets no `visual`.
|
|
12
|
+
- `SNAPSHOT_VERSION` is 2. `readSnapshot()` reads version 1 files as version 2 marked `migratedFrom: 1`; every command that loads a saved snapshot uses it, so old snapshots still work.
|
|
13
|
+
- `FINGERPRINT_VERSION` is 2: fingerprints carry `sections` (role, centring, width), and `layoutDistance` compares by role and geometry when both sides have them. Against a version 1 fingerprint it compares by `kind`, exactly as before.
|
|
14
|
+
- New exports: `readSnapshot`, `READABLE_SNAPSHOT_VERSIONS`, `visualMeasures`, `EDGE_STEP`, `layoutDistance`, and the types `SectionRole`, `SectionGeometry`, `SnapshotVisual`, `FingerprintSection`, `RgbaImage`.
|
|
15
|
+
|
|
16
|
+
- 07d1cf5: From the 2026-09-11 AI-look research. A new tell, `shadcn-card-stock`: the registry Card recipe left unchanged (`warn`). Cal Sans joins the second-wave reflex fonts. The icon-tile tells now see Unicons, MUI icons and Material Symbols as well as Lucide, Heroicons, Phosphor and Tabler. `craft direction validate` warns when a display or body face is missing from the new licence register, or is capped, per-site or unverified there. The register ships as `licences.json` and as `LICENCES` and `faceLicence()`. Catalogue `2026.09.9`.
|
|
17
|
+
- c63cded: **Behaviour change:** under `craft copy --gate`, a repo can no longer lower the house blocking tier. A `warn` or `off` in `craft.config.json`, or an `art-direction.json` exception, naming a blocking tell (em dash, contrastive negation, chatbot residue and the rest) is refused and listed as "Exception not applied". Raising a tell still works, runs without `--gate` are unchanged, and a genuine one-off still takes `copy-ok` on its line. `holdHouseBlocks` and `HOUSE_LOCKED` are exported from the house policy.
|
|
18
|
+
|
|
3
19
|
## 0.11.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/CHARACTER.md
CHANGED
|
@@ -88,6 +88,13 @@ clean, premium, elegant and the rest), and a value on the tell catalogue
|
|
|
88
88
|
without an exception carrying the same reason. Given a snapshot, it warns
|
|
89
89
|
where the page does not show what the file declares.
|
|
90
90
|
|
|
91
|
+
It also reads the display and body faces against the licence register
|
|
92
|
+
(`src/direction/licences.ts`, exported as `licences.json`) and warns on a face
|
|
93
|
+
the register does not know, or knows only as capped, per-site or unverified. A
|
|
94
|
+
licence warning does not undecide the choice: the reason can be sound and the
|
|
95
|
+
face still unlicensed for this client. The register records facts, checked on
|
|
96
|
+
the licensor's own page. It is not a shortlist, and nothing proposes from it.
|
|
97
|
+
|
|
91
98
|
`craft direction init` writes what the site does today with every reason
|
|
92
99
|
empty, so the first thing it produces is the list of things nobody decided.
|
|
93
100
|
Run on RMP's live home page (2026-09-23) it records Fraunces, Manrope, a cream
|
|
@@ -534,7 +541,7 @@ Generated from the package. Run `pnpm --filter @domandigital/craft run docs`
|
|
|
534
541
|
after changing an entry; a test fails until you do.
|
|
535
542
|
|
|
536
543
|
<!-- craft:catalogue:start -->
|
|
537
|
-
Catalogue version `2026.09.
|
|
544
|
+
Catalogue version `2026.09.9`, 56 tells.
|
|
538
545
|
|
|
539
546
|
| Id | Gen | Surface | Severity | Tell | Why it is a default |
|
|
540
547
|
| --- | --- | --- | --- | --- | --- |
|
|
@@ -549,6 +556,7 @@ Catalogue version `2026.09.8`, 55 tells.
|
|
|
549
556
|
| `reveal-everywhere` | 1 | source + rendered | warn | Reveal on every section | When every block fades up on scroll, the motion stops meaning anything and the page feels slow. MMM had 70, DD 40, sen-sphere 27. |
|
|
550
557
|
| `pill-everything` | 1 | source + rendered | warn | Pills everywhere | rounded-full on every button, badge and tag is the framework's friendliest default. MMM had 162, DD 125. A shape used everywhere is not a shape language. |
|
|
551
558
|
| `shadcn-dump` | 1 | source | warn | Stock component dump | A generator exports the whole shadcn/ui kit whether the site uses it or not. Rise & Bloom ships a full set under its own book, vine and petal assets. |
|
|
559
|
+
| `shadcn-card-stock` | 1 | source | warn | Stock shadcn Card | The registry Card (bg-card, a plain border, rounded-lg or rounded-xl, shadow-sm) is the container every generated page is built from. Left as shipped, every card on the site looks like every other shadcn site's. |
|
|
552
560
|
| `cream-palette` | 2 | source + rendered | warn | Cream ground | Once white-and-violet was named, models moved to a warm off-white: parchment, linen, oat. It reads as 'tasteful' in the same way everywhere. |
|
|
553
561
|
| `italic-serif-display` | 2 | source + rendered | warn | Italic serif display | A serif heading with one italic word ('Beauty, reimagined') is the second wave's signature move. |
|
|
554
562
|
| `hero-eyebrow-chip` | 2 | source + rendered | warn | Eyebrow chip above the hero | A small bordered pill ('New · Now booking') above the headline is in almost every generated hero. |
|
package/COPY.md
CHANGED
|
@@ -256,8 +256,12 @@ For genuine exceptions only, and the reason matters more than the mechanism:
|
|
|
256
256
|
- `.copyauditignore` at the repo root: globs, matched on the repo-relative
|
|
257
257
|
path and the bare filename.
|
|
258
258
|
- `.nocopycheck` at the repo root: skips the repo.
|
|
259
|
-
- An `exceptions` entry in `art-direction.json`,
|
|
260
|
-
a sentence.
|
|
259
|
+
- An `exceptions` entry in `art-direction.json`, or a `severity` entry in
|
|
260
|
+
`craft.config.json`, with a `because` of at least a sentence. **Never for
|
|
261
|
+
the blocking tier.** Under `craft copy --gate` a repo can raise a rule but
|
|
262
|
+
not lower one that blocks: the gate refuses the entry and says so. A
|
|
263
|
+
genuine one-off in the blocking tier takes `copy-ok` on its line, where a
|
|
264
|
+
reviewer sees it.
|
|
261
265
|
|
|
262
266
|
Reaching for these more than occasionally means a rule is miscalibrated. Fix
|
|
263
267
|
it here and in craft, not in forty repos.
|
package/README.md
CHANGED
|
@@ -129,6 +129,11 @@ repo. `.claude`, `.agents` and `.cursor` are never walked, config or not.
|
|
|
129
129
|
A severity change needs a `because`, like an exception. `off` is applied as an
|
|
130
130
|
exception, so the report lists what it silenced.
|
|
131
131
|
|
|
132
|
+
Under `craft copy --gate`, a repo can raise a copy tell but never lower one in
|
|
133
|
+
the house blocking tier. A `warn` or `off` for a blocking tell, here or as an
|
|
134
|
+
exception in `art-direction.json`, is refused and listed in the report. A
|
|
135
|
+
genuine one-off takes `copy-ok` on its line.
|
|
136
|
+
|
|
132
137
|
### Copy
|
|
133
138
|
|
|
134
139
|
[COPY.md](./COPY.md) is the house copy standard: the blocking tier, the review
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
VISUAL_MAX_HEIGHT,
|
|
2
3
|
audit,
|
|
3
4
|
collectInPage,
|
|
4
5
|
loadPlaywright,
|
|
@@ -6,10 +7,12 @@ import {
|
|
|
6
7
|
snapshotPage,
|
|
7
8
|
snapshotUrl,
|
|
8
9
|
snapshotUrls
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
11
|
-
import "./chunk-
|
|
10
|
+
} from "./chunk-ULPI335S.js";
|
|
11
|
+
import "./chunk-JYNXGCYZ.js";
|
|
12
|
+
import "./chunk-PZ4MF2VV.js";
|
|
13
|
+
import "./chunk-AL6MRG6V.js";
|
|
12
14
|
export {
|
|
15
|
+
VISUAL_MAX_HEIGHT,
|
|
13
16
|
audit,
|
|
14
17
|
collectInPage,
|
|
15
18
|
loadPlaywright,
|
package/dist/audit.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/audit/index.ts
|
|
21
21
|
var audit_exports = {};
|
|
22
22
|
__export(audit_exports, {
|
|
23
|
+
VISUAL_MAX_HEIGHT: () => VISUAL_MAX_HEIGHT,
|
|
23
24
|
audit: () => audit,
|
|
24
25
|
collectInPage: () => collectInPage,
|
|
25
26
|
loadPlaywright: () => loadPlaywright,
|
|
@@ -1567,6 +1568,7 @@ var REFLEX_FONTS_2 = [
|
|
|
1567
1568
|
"Plus Jakarta Sans",
|
|
1568
1569
|
"Outfit",
|
|
1569
1570
|
"Geist",
|
|
1571
|
+
"Cal Sans",
|
|
1570
1572
|
"Sora",
|
|
1571
1573
|
"Instrument Sans",
|
|
1572
1574
|
"Instrument Serif",
|
|
@@ -1788,7 +1790,7 @@ function iconTiles(file) {
|
|
|
1788
1790
|
return sized && has(unit, /^rounded-(?:md|lg|xl|2xl|full)$/) && has(unit, /^(?:items-center|place-items-center|place-content-center)$/) && has(unit, /^bg-/);
|
|
1789
1791
|
});
|
|
1790
1792
|
}
|
|
1791
|
-
var ICON_LIBRARY = /from\s+["'](?:lucide-react|@heroicons\/react[^"']*|react-icons[^"']*|@phosphor-icons\/react|@tabler\/icons-react|@radix-ui\/react-icons)["']/;
|
|
1793
|
+
var ICON_LIBRARY = /from\s+["'](?:lucide-react|@heroicons\/react[^"']*|react-icons[^"']*|@phosphor-icons\/react|@tabler\/icons-react|@radix-ui\/react-icons|@iconscout\/react-unicons[^"']*|@mui\/icons-material[^"']*|react-material-symbols[^"']*)["']/;
|
|
1792
1794
|
function iconTileStackHits(ctx) {
|
|
1793
1795
|
const hits = [];
|
|
1794
1796
|
for (const file of ctx.files) {
|
|
@@ -1935,6 +1937,12 @@ function shadcnHits(ctx) {
|
|
|
1935
1937
|
const detail = unused.length > 0 ? `; ${unused.length} imported nowhere` : "";
|
|
1936
1938
|
return [{ path: ui[0].path, offset: 0, message: `${ui.length} stock shadcn primitives${detail}` }];
|
|
1937
1939
|
}
|
|
1940
|
+
function shadcnCardHits(ctx) {
|
|
1941
|
+
return eachUnit(
|
|
1942
|
+
ctx,
|
|
1943
|
+
(unit) => has(unit, /^bg-card$/) && has(unit, /^text-card-foreground$/) && has(unit, /^border$/) && has(unit, /^rounded-(?:lg|xl)$/) && has(unit, /^shadow(?:-sm)?$/) ? "the registry Card recipe, unchanged" : null
|
|
1944
|
+
);
|
|
1945
|
+
}
|
|
1938
1946
|
function marqueeHits(ctx) {
|
|
1939
1947
|
const hits = [];
|
|
1940
1948
|
const pattern = /@keyframes\s+[\w-]*marquee|animate-(?:marquee|scroll-x|infinite-scroll)\b|<Marquee\b|["']react-fast-marquee["']|ui\/marquee["']|animation(?:-name)?\s*:\s*[^;]*marquee/i;
|
|
@@ -2152,6 +2160,23 @@ const inter = Inter({ subsets: ["latin"] });`),
|
|
|
2152
2160
|
pass: [f3("src/components/ui/button.tsx", `export function Button() { return null; }`)]
|
|
2153
2161
|
}
|
|
2154
2162
|
},
|
|
2163
|
+
{
|
|
2164
|
+
id: "shadcn-card-stock",
|
|
2165
|
+
name: "Stock shadcn Card",
|
|
2166
|
+
generation: 1,
|
|
2167
|
+
severity: "warn",
|
|
2168
|
+
surface: "source",
|
|
2169
|
+
why: "The registry Card (bg-card, a plain border, rounded-lg or rounded-xl, shadow-sm) is the container every generated page is built from. Left as shipped, every card on the site looks like every other shadcn site's.",
|
|
2170
|
+
fix: "Give the card this site's radius and shadow from the restraint budget, or drop the shadow and let a visible border carry it.",
|
|
2171
|
+
detect: shadcnCardHits,
|
|
2172
|
+
fixtures: {
|
|
2173
|
+
flag: [
|
|
2174
|
+
f3("components/ui/card.tsx", `className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}`),
|
|
2175
|
+
f3("components/ui/card.tsx", `className={cn("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", className)}`)
|
|
2176
|
+
],
|
|
2177
|
+
pass: [f3("components/ui/card.tsx", `className={cn("rounded-sm border-2 border-ink bg-card text-card-foreground", className)}`)]
|
|
2178
|
+
}
|
|
2179
|
+
},
|
|
2155
2180
|
{
|
|
2156
2181
|
id: "cream-palette",
|
|
2157
2182
|
name: "Cream ground",
|
|
@@ -2395,7 +2420,7 @@ function excerptAt(text, offset, max = 140) {
|
|
|
2395
2420
|
}
|
|
2396
2421
|
|
|
2397
2422
|
// src/snapshot/types.ts
|
|
2398
|
-
var SNAPSHOT_VERSION =
|
|
2423
|
+
var SNAPSHOT_VERSION = 2;
|
|
2399
2424
|
|
|
2400
2425
|
// src/snapshot/fixture.ts
|
|
2401
2426
|
function makeSnapshot(patch = {}) {
|
|
@@ -2729,7 +2754,7 @@ var RENDERED_PATHS = {
|
|
|
2729
2754
|
};
|
|
2730
2755
|
|
|
2731
2756
|
// src/character/check.ts
|
|
2732
|
-
var CATALOGUE_VERSION = "2026.09.
|
|
2757
|
+
var CATALOGUE_VERSION = "2026.09.9";
|
|
2733
2758
|
var CATALOGUE = [...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map(
|
|
2734
2759
|
(t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t
|
|
2735
2760
|
);
|
|
@@ -2827,7 +2852,7 @@ function auditSnapshot(snapshot, options = {}) {
|
|
|
2827
2852
|
}
|
|
2828
2853
|
|
|
2829
2854
|
// src/fingerprint/index.ts
|
|
2830
|
-
var FINGERPRINT_VERSION =
|
|
2855
|
+
var FINGERPRINT_VERSION = 2;
|
|
2831
2856
|
var CHROMATIC = 0.03;
|
|
2832
2857
|
var MIN_ACCENT_AREA = 2e4;
|
|
2833
2858
|
var oklch = (value) => {
|
|
@@ -2882,7 +2907,8 @@ function fingerprint(s) {
|
|
|
2882
2907
|
roundness: median(s.controls.map((c) => Math.min(0.5, c.radiusPx / Math.max(1, c.heightPx)))),
|
|
2883
2908
|
motion: below ? Math.min(1, s.motion.hiddenSections / below) : 0,
|
|
2884
2909
|
effects: effects.sort(),
|
|
2885
|
-
layout: s.sections.map((x) => x.kind)
|
|
2910
|
+
layout: s.sections.map((x) => x.kind),
|
|
2911
|
+
...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 })) } : {}
|
|
2886
2912
|
};
|
|
2887
2913
|
}
|
|
2888
2914
|
|
|
@@ -2983,18 +3009,26 @@ function collectInPage() {
|
|
|
2983
3009
|
top: Math.round(box(h).top)
|
|
2984
3010
|
};
|
|
2985
3011
|
});
|
|
3012
|
+
const isButtonEl = (el) => el.tagName === "BUTTON" || el.tagName === "INPUT" && /^(submit|button)$/i.test(el.type);
|
|
3013
|
+
const isControl = (el) => {
|
|
3014
|
+
const isButton = isButtonEl(el);
|
|
3015
|
+
if (!isButton && el.tagName !== "A") return false;
|
|
3016
|
+
if (!shown(el)) return false;
|
|
3017
|
+
const cs = style(el);
|
|
3018
|
+
const b = box(el);
|
|
3019
|
+
const label = (el.textContent ?? el.value ?? "").replace(/\s+/g, " ").trim();
|
|
3020
|
+
if (!label || b.height < 24 || b.height > 90) return false;
|
|
3021
|
+
if (!isButton && !(painted(cs.backgroundColor) || px(cs.borderTopWidth) >= 1)) return false;
|
|
3022
|
+
if (!isButton && px(cs.paddingLeft) < 8) return false;
|
|
3023
|
+
return true;
|
|
3024
|
+
};
|
|
2986
3025
|
const controls = [];
|
|
2987
3026
|
for (const el of all) {
|
|
2988
|
-
|
|
2989
|
-
const isButton =
|
|
2990
|
-
if (!isButton && tag !== "A") continue;
|
|
2991
|
-
if (!shown(el)) continue;
|
|
3027
|
+
if (!isControl(el)) continue;
|
|
3028
|
+
const isButton = isButtonEl(el);
|
|
2992
3029
|
const cs = style(el);
|
|
2993
3030
|
const b = box(el);
|
|
2994
3031
|
const label = (el.textContent ?? el.value ?? "").replace(/\s+/g, " ").trim();
|
|
2995
|
-
if (!label || b.height < 24 || b.height > 90) continue;
|
|
2996
|
-
if (!isButton && !(painted(cs.backgroundColor) || px(cs.borderTopWidth) >= 1)) continue;
|
|
2997
|
-
if (!isButton && px(cs.paddingLeft) < 8) continue;
|
|
2998
3032
|
const r = cs.borderTopLeftRadius;
|
|
2999
3033
|
const radius = r.endsWith("%") ? parseFloat(r) / 100 * b.height : px(r);
|
|
3000
3034
|
controls.push({
|
|
@@ -3074,7 +3108,80 @@ function collectInPage() {
|
|
|
3074
3108
|
}
|
|
3075
3109
|
return best;
|
|
3076
3110
|
};
|
|
3077
|
-
const
|
|
3111
|
+
const backgroundOf = (el) => {
|
|
3112
|
+
for (let n = el; n && n !== document.documentElement; n = n.parentElement) {
|
|
3113
|
+
const c = style(n).backgroundColor;
|
|
3114
|
+
if (painted(c)) return c;
|
|
3115
|
+
}
|
|
3116
|
+
return ground;
|
|
3117
|
+
};
|
|
3118
|
+
const geometryOf = (el, b) => {
|
|
3119
|
+
const leaves = Array.from(el.querySelectorAll("*")).slice(0, 1500).filter((n) => (ownText(n).length > 1 || /^(IMG|SVG|VIDEO|PICTURE|BUTTON|INPUT|TEXTAREA|SELECT)$/i.test(n.tagName)) && shown(n)).slice(0, 400);
|
|
3120
|
+
const centreX = b.left + b.width / 2;
|
|
3121
|
+
let left = 0;
|
|
3122
|
+
let right = 0;
|
|
3123
|
+
let covered = 0;
|
|
3124
|
+
let minX = Infinity;
|
|
3125
|
+
let maxX = -Infinity;
|
|
3126
|
+
let textBlocks = 0;
|
|
3127
|
+
let centred = 0;
|
|
3128
|
+
for (const n of leaves) {
|
|
3129
|
+
const r = box(n);
|
|
3130
|
+
const area = r.width * r.height;
|
|
3131
|
+
covered += area;
|
|
3132
|
+
minX = Math.min(minX, r.left);
|
|
3133
|
+
maxX = Math.max(maxX, r.left + r.width);
|
|
3134
|
+
const split = Math.max(0, Math.min(r.width, centreX - r.left));
|
|
3135
|
+
left += split * r.height;
|
|
3136
|
+
right += (r.width - split) * r.height;
|
|
3137
|
+
if (ownText(n).length > 1) {
|
|
3138
|
+
textBlocks += 1;
|
|
3139
|
+
const mid = r.left + r.width / 2;
|
|
3140
|
+
if (style(n).textAlign === "center" || Math.abs(mid - centreX) < vw * 0.03 && r.width < b.width * 0.8) centred += 1;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
const round = (v) => Math.round(v * 1e3) / 1e3;
|
|
3144
|
+
return {
|
|
3145
|
+
centredShare: textBlocks ? round(centred / textBlocks) : 0,
|
|
3146
|
+
mirrorSymmetry: left + right > 0 ? round(1 - Math.abs(left - right) / (left + right)) : 1,
|
|
3147
|
+
whitespaceRatio: round(Math.max(0, 1 - Math.min(1, covered / Math.max(1, b.width * b.height)))),
|
|
3148
|
+
contentWidthRatio: leaves.length ? round(Math.min(1, (maxX - minX) / vw)) : 0,
|
|
3149
|
+
background: backgroundOf(el),
|
|
3150
|
+
controls: Array.from(el.querySelectorAll("a, button, input")).slice(0, 200).filter(isControl).length
|
|
3151
|
+
};
|
|
3152
|
+
};
|
|
3153
|
+
const CURRENCY = /[£$€]\s?\d/;
|
|
3154
|
+
const roleOf = (el, kind, geo, b, cards, iconCards, last) => {
|
|
3155
|
+
if (kind === "marquee" || kind === "hero") return kind;
|
|
3156
|
+
const heading = (el.querySelector("h1, h2, h3")?.textContent ?? "").toLowerCase();
|
|
3157
|
+
const leaves = Array.from(el.querySelectorAll("*")).slice(0, 1500);
|
|
3158
|
+
const own = leaves.map(ownText);
|
|
3159
|
+
const questions = own.filter((t) => /\?$/.test(t) && t.length > 8).length;
|
|
3160
|
+
if (el.querySelectorAll("details").length >= 3 || el.querySelectorAll("[aria-expanded]").length >= 3 || /\bfaq|frequently asked|questions\b/.test(heading) && questions >= 3) return "faq";
|
|
3161
|
+
const prices = own.filter((t) => CURRENCY.test(t) && t.length < 40).length;
|
|
3162
|
+
if (prices >= 2 && (cards >= 2 || /pric|plan|package/.test(heading))) return "pricing";
|
|
3163
|
+
const quotes = el.querySelectorAll("blockquote, q").length;
|
|
3164
|
+
if (quotes >= 2 || /testimonial|review|what (?:our )?(?:clients|customers|people) say/.test(heading)) return "testimonials";
|
|
3165
|
+
const steps = Math.max(0, ...Array.from(el.querySelectorAll("ol")).map((ol) => ol.children.length));
|
|
3166
|
+
const numbered = own.filter((t) => /^(?:step\s*)?0?[1-9][.:]?$/i.test(t)).length;
|
|
3167
|
+
if (steps >= 3 || numbered >= 3 || /how it works|our process|\bsteps?\b/.test(heading)) return "process";
|
|
3168
|
+
const form = el.querySelector("form");
|
|
3169
|
+
const fields = form ? form.querySelectorAll("input:not([type=hidden]):not([type=submit]), textarea, select").length : 0;
|
|
3170
|
+
if (fields >= 2 || /contact|get in touch/.test(heading)) return "contact";
|
|
3171
|
+
const portraits = Array.from(el.querySelectorAll("img")).filter((i) => {
|
|
3172
|
+
const r = box(i);
|
|
3173
|
+
return r.width >= 80 && r.width <= 420 && Math.abs(r.width - r.height) < r.width * 0.25;
|
|
3174
|
+
}).length;
|
|
3175
|
+
if (portraits >= 3 && /team|people|meet|who we are/.test(heading)) return "team";
|
|
3176
|
+
if (kind === "stats" || kind === "logos") return kind;
|
|
3177
|
+
const words = (el.textContent ?? "").replace(/\s+/g, " ").trim().length;
|
|
3178
|
+
const setApart = geo.background !== ground || geo.centredShare >= 0.6;
|
|
3179
|
+
if (setApart && heading && b.height <= vh * 0.9 && geo.controls >= 1 && geo.controls <= 2 && words < 400 && cards < 3) return last ? "footer-cta" : "cta-band";
|
|
3180
|
+
if (cards >= 3 && iconCards >= 2) return "features";
|
|
3181
|
+
return kind;
|
|
3182
|
+
};
|
|
3183
|
+
const kept = blocks.slice(0, 40);
|
|
3184
|
+
const sections = kept.map((el, index) => {
|
|
3078
3185
|
const b = box(el);
|
|
3079
3186
|
const heading = el.querySelector("h1, h2, h3");
|
|
3080
3187
|
const label = (heading?.textContent ?? el.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 60);
|
|
@@ -3088,6 +3195,7 @@ function collectInPage() {
|
|
|
3088
3195
|
else if (images.length >= 4 && textChars < 300) kind = "logos";
|
|
3089
3196
|
else if (numbers >= 3 && textChars < 600) kind = "stats";
|
|
3090
3197
|
else if (cards >= 3) kind = "cards";
|
|
3198
|
+
const geometry = geometryOf(el, b);
|
|
3091
3199
|
return {
|
|
3092
3200
|
top: Math.round(b.top),
|
|
3093
3201
|
height: Math.round(b.height),
|
|
@@ -3095,7 +3203,9 @@ function collectInPage() {
|
|
|
3095
3203
|
label,
|
|
3096
3204
|
cards,
|
|
3097
3205
|
iconCards,
|
|
3098
|
-
hiddenAtLoad: b.top > vh && hiddenAtLoad(el)
|
|
3206
|
+
hiddenAtLoad: b.top > vh && hiddenAtLoad(el),
|
|
3207
|
+
role: roleOf(el, kind, geometry, b, cards, iconCards, index === kept.length - 1),
|
|
3208
|
+
geometry
|
|
3099
3209
|
};
|
|
3100
3210
|
});
|
|
3101
3211
|
if (header && sections.length === 0) {
|
|
@@ -3188,6 +3298,13 @@ function collectInPage() {
|
|
|
3188
3298
|
headings: headings2,
|
|
3189
3299
|
controls,
|
|
3190
3300
|
sections,
|
|
3301
|
+
rhythmVariance: (() => {
|
|
3302
|
+
const hs = sections.map((x) => x.height);
|
|
3303
|
+
if (hs.length < 2) return null;
|
|
3304
|
+
const mean = hs.reduce((t, v) => t + v, 0) / hs.length;
|
|
3305
|
+
const sd = Math.sqrt(hs.reduce((t, v) => t + (v - mean) ** 2, 0) / hs.length);
|
|
3306
|
+
return mean > 0 ? Math.round(sd / mean * 1e3) / 1e3 : null;
|
|
3307
|
+
})(),
|
|
3191
3308
|
effects,
|
|
3192
3309
|
motion: {
|
|
3193
3310
|
hiddenSections: sections.filter((s) => s.hiddenAtLoad).length,
|
|
@@ -3221,7 +3338,139 @@ function overlayInPage() {
|
|
|
3221
3338
|
return false;
|
|
3222
3339
|
}
|
|
3223
3340
|
|
|
3341
|
+
// src/direction/png.ts
|
|
3342
|
+
var import_node_zlib = require("zlib");
|
|
3343
|
+
function decodePng(buf) {
|
|
3344
|
+
const sig = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
3345
|
+
if (!sig.every((b, i) => buf[i] === b)) throw new Error("not a PNG");
|
|
3346
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
3347
|
+
let pos = 8;
|
|
3348
|
+
let width = 0;
|
|
3349
|
+
let height = 0;
|
|
3350
|
+
let depth = 0;
|
|
3351
|
+
let colour = 0;
|
|
3352
|
+
let interlace = 0;
|
|
3353
|
+
const idat = [];
|
|
3354
|
+
while (pos < buf.length) {
|
|
3355
|
+
const len = view.getUint32(pos);
|
|
3356
|
+
const type = String.fromCharCode(...buf.subarray(pos + 4, pos + 8));
|
|
3357
|
+
const data = buf.subarray(pos + 8, pos + 8 + len);
|
|
3358
|
+
if (type === "IHDR") {
|
|
3359
|
+
width = view.getUint32(pos + 8);
|
|
3360
|
+
height = view.getUint32(pos + 12);
|
|
3361
|
+
depth = data[8];
|
|
3362
|
+
colour = data[9];
|
|
3363
|
+
interlace = data[12];
|
|
3364
|
+
} else if (type === "IDAT") idat.push(data);
|
|
3365
|
+
else if (type === "IEND") break;
|
|
3366
|
+
pos += 12 + len;
|
|
3367
|
+
}
|
|
3368
|
+
const channels = { 0: 1, 2: 3, 4: 2, 6: 4 }[colour];
|
|
3369
|
+
if (depth !== 8 || !channels || interlace !== 0) throw new Error(`unsupported PNG (depth ${depth}, colour type ${colour}${interlace ? ", interlaced" : ""}); export it as 8-bit RGB`);
|
|
3370
|
+
const raw = (0, import_node_zlib.inflateSync)(Buffer.concat(idat.map((d) => Buffer.from(d))));
|
|
3371
|
+
const stride = width * channels;
|
|
3372
|
+
const out = new Uint8Array(width * height * 4);
|
|
3373
|
+
const prev = new Uint8Array(stride);
|
|
3374
|
+
const line = new Uint8Array(stride);
|
|
3375
|
+
for (let y = 0; y < height; y += 1) {
|
|
3376
|
+
const filter = raw[y * (stride + 1)];
|
|
3377
|
+
const src = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
|
|
3378
|
+
for (let x = 0; x < stride; x += 1) {
|
|
3379
|
+
const a = x >= channels ? line[x - channels] : 0;
|
|
3380
|
+
const b = prev[x];
|
|
3381
|
+
const c = x >= channels ? prev[x - channels] : 0;
|
|
3382
|
+
let v = src[x];
|
|
3383
|
+
if (filter === 1) v += a;
|
|
3384
|
+
else if (filter === 2) v += b;
|
|
3385
|
+
else if (filter === 3) v += a + b >> 1;
|
|
3386
|
+
else if (filter === 4) {
|
|
3387
|
+
const p = a + b - c;
|
|
3388
|
+
const pa = Math.abs(p - a);
|
|
3389
|
+
const pb = Math.abs(p - b);
|
|
3390
|
+
const pc = Math.abs(p - c);
|
|
3391
|
+
v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
|
3392
|
+
}
|
|
3393
|
+
line[x] = v & 255;
|
|
3394
|
+
}
|
|
3395
|
+
for (let x = 0; x < width; x += 1) {
|
|
3396
|
+
const i = x * channels;
|
|
3397
|
+
const o = (y * width + x) * 4;
|
|
3398
|
+
if (channels >= 3) {
|
|
3399
|
+
out[o] = line[i];
|
|
3400
|
+
out[o + 1] = line[i + 1];
|
|
3401
|
+
out[o + 2] = line[i + 2];
|
|
3402
|
+
out[o + 3] = channels === 4 ? line[i + 3] : 255;
|
|
3403
|
+
} else {
|
|
3404
|
+
out[o] = out[o + 1] = out[o + 2] = line[i];
|
|
3405
|
+
out[o + 3] = channels === 2 ? line[i + 1] : 255;
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
prev.set(line);
|
|
3409
|
+
}
|
|
3410
|
+
return { width, height, rgba: out };
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
// src/snapshot/visual.ts
|
|
3414
|
+
var EDGE_STEP = 32;
|
|
3415
|
+
function visualMeasures(img, maxSamples = 1e6) {
|
|
3416
|
+
const { width: w, height: h, rgba } = img;
|
|
3417
|
+
if (w < 2 || h < 2 || rgba.length < w * h * 4) throw new Error(`visualMeasures: ${w}x${h} image with ${rgba.length} bytes`);
|
|
3418
|
+
const step = Math.max(1, Math.ceil(Math.sqrt(w * h / maxSamples)));
|
|
3419
|
+
const lum = (x, y) => {
|
|
3420
|
+
const i = (y * w + x) * 4;
|
|
3421
|
+
return 0.2126 * rgba[i] + 0.7152 * rgba[i + 1] + 0.0722 * rgba[i + 2];
|
|
3422
|
+
};
|
|
3423
|
+
let n = 0;
|
|
3424
|
+
let sumRg = 0;
|
|
3425
|
+
let sumYb = 0;
|
|
3426
|
+
let sumRg2 = 0;
|
|
3427
|
+
let sumYb2 = 0;
|
|
3428
|
+
let edges = 0;
|
|
3429
|
+
let edgeN = 0;
|
|
3430
|
+
let asym = 0;
|
|
3431
|
+
let asymN = 0;
|
|
3432
|
+
for (let y = 0; y < h; y += step) {
|
|
3433
|
+
for (let x = 0; x < w; x += step) {
|
|
3434
|
+
const i = (y * w + x) * 4;
|
|
3435
|
+
const r = rgba[i];
|
|
3436
|
+
const g = rgba[i + 1];
|
|
3437
|
+
const b = rgba[i + 2];
|
|
3438
|
+
const rg = r - g;
|
|
3439
|
+
const yb = 0.5 * (r + g) - b;
|
|
3440
|
+
sumRg += rg;
|
|
3441
|
+
sumYb += yb;
|
|
3442
|
+
sumRg2 += rg * rg;
|
|
3443
|
+
sumYb2 += yb * yb;
|
|
3444
|
+
n += 1;
|
|
3445
|
+
const l = lum(x, y);
|
|
3446
|
+
if (x + step < w && y + step < h) {
|
|
3447
|
+
if (Math.abs(lum(x + step, y) - l) + Math.abs(lum(x, y + step) - l) > EDGE_STEP) edges += 1;
|
|
3448
|
+
edgeN += 1;
|
|
3449
|
+
}
|
|
3450
|
+
if (x < w / 2) {
|
|
3451
|
+
asym += Math.abs(l - lum(w - 1 - x, y));
|
|
3452
|
+
asymN += 1;
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
const meanRg = sumRg / n;
|
|
3457
|
+
const meanYb = sumYb / n;
|
|
3458
|
+
const sdRg = Math.sqrt(Math.max(0, sumRg2 / n - meanRg * meanRg));
|
|
3459
|
+
const sdYb = Math.sqrt(Math.max(0, sumYb2 / n - meanYb * meanYb));
|
|
3460
|
+
const colourfulness = Math.sqrt(sdRg ** 2 + sdYb ** 2) + 0.3 * Math.sqrt(meanRg ** 2 + meanYb ** 2);
|
|
3461
|
+
const round = (v, d = 3) => Math.round(v * 10 ** d) / 10 ** d;
|
|
3462
|
+
return {
|
|
3463
|
+
colourfulness: round(colourfulness, 1),
|
|
3464
|
+
edgeDensity: round(edgeN ? edges / edgeN : 0),
|
|
3465
|
+
symmetry: round(asymN ? 1 - asym / asymN / 255 : 1),
|
|
3466
|
+
width: w,
|
|
3467
|
+
height: h,
|
|
3468
|
+
sampled: n
|
|
3469
|
+
};
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3224
3472
|
// src/audit/index.ts
|
|
3473
|
+
var VISUAL_MAX_HEIGHT = 6e3;
|
|
3225
3474
|
async function loadPlaywright() {
|
|
3226
3475
|
for (const name of ["playwright", "playwright-core"]) {
|
|
3227
3476
|
try {
|
|
@@ -3239,12 +3488,23 @@ async function snapshotPage(page, url, options = {}) {
|
|
|
3239
3488
|
await page.waitForTimeout(options.introWindowMs ?? 5e3);
|
|
3240
3489
|
const late = await page.evaluate(overlayInPage).catch(() => false);
|
|
3241
3490
|
const data = await page.evaluate(collectInPage);
|
|
3491
|
+
let visual;
|
|
3492
|
+
if (page.screenshot && options.visual !== false) {
|
|
3493
|
+
try {
|
|
3494
|
+
const height = Math.min(data.pageHeight, VISUAL_MAX_HEIGHT);
|
|
3495
|
+
const png = await page.screenshot({ fullPage: true, type: "png", clip: { x: 0, y: 0, width: data.viewport.width, height } });
|
|
3496
|
+
visual = visualMeasures(decodePng(png));
|
|
3497
|
+
} catch {
|
|
3498
|
+
visual = void 0;
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3242
3501
|
return {
|
|
3243
3502
|
version: SNAPSHOT_VERSION,
|
|
3244
3503
|
url,
|
|
3245
3504
|
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3246
3505
|
...data,
|
|
3247
|
-
motion: { ...data.motion, introOverlay: early && !late }
|
|
3506
|
+
motion: { ...data.motion, introOverlay: early && !late },
|
|
3507
|
+
...visual ? { visual } : {}
|
|
3248
3508
|
};
|
|
3249
3509
|
}
|
|
3250
3510
|
async function launch(pw, url, options) {
|
|
@@ -3291,6 +3551,7 @@ async function audit(url, options = {}) {
|
|
|
3291
3551
|
}
|
|
3292
3552
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3293
3553
|
0 && (module.exports = {
|
|
3554
|
+
VISUAL_MAX_HEIGHT,
|
|
3294
3555
|
audit,
|
|
3295
3556
|
collectInPage,
|
|
3296
3557
|
loadPlaywright,
|
package/dist/audit.d.cts
CHANGED
|
@@ -11,7 +11,13 @@
|
|
|
11
11
|
* to a report and judged again later without a browser. Capturing one needs a
|
|
12
12
|
* browser (see `@domandigital/craft/audit`); judging one does not.
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* 2 adds `role` and `geometry` per section, `rhythmVariance` for the page and
|
|
16
|
+
* the screenshot measures in `visual`. All four are optional: a version 1
|
|
17
|
+
* snapshot reads as version 2 without them (see `readSnapshot`), and nothing
|
|
18
|
+
* that judged a v1 snapshot judges it differently.
|
|
19
|
+
*/
|
|
20
|
+
declare const SNAPSHOT_VERSION = 2;
|
|
15
21
|
interface SnapshotColour {
|
|
16
22
|
/** `rgb(r, g, b)` or `rgba(r, g, b, a)`, as the browser computed it. */
|
|
17
23
|
value: string;
|
|
@@ -50,6 +56,27 @@ interface SnapshotControl {
|
|
|
50
56
|
text: string;
|
|
51
57
|
}
|
|
52
58
|
type SectionKind = "hero" | "logos" | "stats" | "cards" | "marquee" | "text" | "other";
|
|
59
|
+
/**
|
|
60
|
+
* What a section is for, finer than `kind`. `kind` keeps its version 1
|
|
61
|
+
* meaning because the rendered tells, the null models and the estate were
|
|
62
|
+
* calibrated on it; `role` is what component-level comparison reads.
|
|
63
|
+
*/
|
|
64
|
+
type SectionRole = SectionKind | "cta-band" | "footer-cta" | "pricing" | "testimonials" | "faq" | "process" | "features" | "team" | "contact";
|
|
65
|
+
/** How a section is laid out, measured, not judged. */
|
|
66
|
+
interface SectionGeometry {
|
|
67
|
+
/** Share of the section's text blocks that are centred, 0 to 1. */
|
|
68
|
+
centredShare: number;
|
|
69
|
+
/** Content area balance about the vertical centre line: 1 mirror-even, 0 all on one side. */
|
|
70
|
+
mirrorSymmetry: number;
|
|
71
|
+
/** Share of the section's area with no text, image or control in it, 0 to 1. */
|
|
72
|
+
whitespaceRatio: number;
|
|
73
|
+
/** Width the content spans, over the viewport width. */
|
|
74
|
+
contentWidthRatio: number;
|
|
75
|
+
/** The painted background behind the section, as computed. */
|
|
76
|
+
background: string;
|
|
77
|
+
/** Buttons and button-styled links in the section. */
|
|
78
|
+
controls: number;
|
|
79
|
+
}
|
|
53
80
|
interface SnapshotSection {
|
|
54
81
|
top: number;
|
|
55
82
|
height: number;
|
|
@@ -62,6 +89,26 @@ interface SnapshotSection {
|
|
|
62
89
|
iconCards: number;
|
|
63
90
|
/** Hidden at load, waiting for a scroll to reveal it. */
|
|
64
91
|
hiddenAtLoad: boolean;
|
|
92
|
+
/** Version 2. */
|
|
93
|
+
role?: SectionRole;
|
|
94
|
+
/** Version 2. */
|
|
95
|
+
geometry?: SectionGeometry;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Measures of the rendered pixels, from a full-page screenshot (capped at
|
|
99
|
+
* `height`). None of them passes or fails anything: they describe.
|
|
100
|
+
*/
|
|
101
|
+
interface SnapshotVisual {
|
|
102
|
+
/** Hasler and Süsstrunk (2003) colourfulness, M = σ_rgyb + 0.3 μ_rgyb, on 0-255 channels. Grey is 0. */
|
|
103
|
+
colourfulness: number;
|
|
104
|
+
/** Share of sampled pixels on a luminance edge: a complexity proxy, 0 to 1. */
|
|
105
|
+
edgeDensity: number;
|
|
106
|
+
/** 1 minus the mean luminance difference between each pixel and its left-right mirror, 0 to 1. */
|
|
107
|
+
symmetry: number;
|
|
108
|
+
width: number;
|
|
109
|
+
height: number;
|
|
110
|
+
/** Pixels actually read: large pages are sampled on a grid. */
|
|
111
|
+
sampled: number;
|
|
65
112
|
}
|
|
66
113
|
interface SnapshotGradient {
|
|
67
114
|
/** Colour stops, as computed. */
|
|
@@ -116,6 +163,12 @@ interface Snapshot {
|
|
|
116
163
|
sections: SnapshotSection[];
|
|
117
164
|
effects: SnapshotEffects;
|
|
118
165
|
motion: SnapshotMotion;
|
|
166
|
+
/** Version 2: coefficient of variation of the section heights. Null with fewer than two sections. */
|
|
167
|
+
rhythmVariance?: number | null;
|
|
168
|
+
/** Version 2: absent when the page could not be screenshotted. */
|
|
169
|
+
visual?: SnapshotVisual;
|
|
170
|
+
/** Set when a version 1 snapshot was read and upgraded in memory. */
|
|
171
|
+
migratedFrom?: 1;
|
|
119
172
|
/** The design-system measures trawl has collected since v7, kept so it can switch to this. */
|
|
120
173
|
metrics: {
|
|
121
174
|
fontSizes: number[];
|
|
@@ -266,9 +319,16 @@ type FontClass = "serif" | "sans" | "mono" | "script" | "system";
|
|
|
266
319
|
* Pure. Built from a Snapshot, so it describes what a visitor gets.
|
|
267
320
|
*/
|
|
268
321
|
|
|
269
|
-
|
|
322
|
+
/** One section as layout comparison sees it. */
|
|
323
|
+
interface FingerprintSection {
|
|
324
|
+
role: SectionRole;
|
|
325
|
+
/** Share of centred text, 0 to 1. */
|
|
326
|
+
centred: number;
|
|
327
|
+
/** Content width over viewport width, 0 to 1. */
|
|
328
|
+
width: number;
|
|
329
|
+
}
|
|
270
330
|
interface Fingerprint {
|
|
271
|
-
version:
|
|
331
|
+
version: 1 | 2;
|
|
272
332
|
/** The most prominent chromatic colour: button fills first, then painted area, then text. */
|
|
273
333
|
accent: Oklch | null;
|
|
274
334
|
ground: Oklch;
|
|
@@ -288,6 +348,8 @@ interface Fingerprint {
|
|
|
288
348
|
effects: string[];
|
|
289
349
|
/** Section kinds in running order, hero first. */
|
|
290
350
|
layout: SectionKind[];
|
|
351
|
+
/** Version 2: present when every section carries a role and geometry. */
|
|
352
|
+
sections?: FingerprintSection[];
|
|
291
353
|
}
|
|
292
354
|
|
|
293
355
|
/**
|
|
@@ -329,6 +391,8 @@ interface SnapshotOptions {
|
|
|
329
391
|
/** A Chromium to launch instead of Playwright's own. Also read from CRAFT_CHROMIUM. */
|
|
330
392
|
executablePath?: string;
|
|
331
393
|
timeoutMs?: number;
|
|
394
|
+
/** Take a screenshot and measure its pixels (`visual`). Default true. */
|
|
395
|
+
visual?: boolean;
|
|
332
396
|
}
|
|
333
397
|
interface MinimalPage {
|
|
334
398
|
close?(): Promise<void>;
|
|
@@ -338,7 +402,20 @@ interface MinimalPage {
|
|
|
338
402
|
}): Promise<unknown>;
|
|
339
403
|
evaluate<R>(fn: () => R): Promise<R>;
|
|
340
404
|
waitForTimeout(ms: number): Promise<void>;
|
|
405
|
+
/** Optional: a driver without it gets a snapshot with no `visual` measures. */
|
|
406
|
+
screenshot?(options: {
|
|
407
|
+
fullPage?: boolean;
|
|
408
|
+
type?: "png";
|
|
409
|
+
clip?: {
|
|
410
|
+
x: number;
|
|
411
|
+
y: number;
|
|
412
|
+
width: number;
|
|
413
|
+
height: number;
|
|
414
|
+
};
|
|
415
|
+
}): Promise<Uint8Array>;
|
|
341
416
|
}
|
|
417
|
+
/** The most of a page the screenshot measures read, in CSS px. A long page is judged on its opening. */
|
|
418
|
+
declare const VISUAL_MAX_HEIGHT = 6000;
|
|
342
419
|
interface MinimalBrowser {
|
|
343
420
|
newPage(options?: {
|
|
344
421
|
viewport?: {
|
|
@@ -388,4 +465,4 @@ interface AuditResult {
|
|
|
388
465
|
/** Snapshot a URL, judge it against the rendered tells, and fingerprint it. */
|
|
389
466
|
declare function audit(url: string, options?: SnapshotOptions & CheckOptions): Promise<AuditResult>;
|
|
390
467
|
|
|
391
|
-
export { type AuditResult, type SnapshotOptions, type SnapshotResult, audit, collectInPage, loadPlaywright, overlayInPage, snapshotPage, snapshotUrl, snapshotUrls };
|
|
468
|
+
export { type AuditResult, type SnapshotOptions, type SnapshotResult, VISUAL_MAX_HEIGHT, audit, collectInPage, loadPlaywright, overlayInPage, snapshotPage, snapshotUrl, snapshotUrls };
|