@domandigital/craft 0.18.2 → 0.19.1
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 +13 -0
- package/CHARACTER.md +77 -6
- package/README.md +1 -1
- package/STANDARD.md +48 -18
- package/dist/{audit-4SFSLCKY.js → audit-43PC4LMS.js} +3 -3
- package/dist/audit.cjs +24 -3
- package/dist/audit.d.cts +30 -1
- package/dist/audit.d.ts +30 -1
- package/dist/audit.js +24 -3
- package/dist/{chunk-DARKHE6A.js → chunk-37ARRJAD.js} +18 -7
- package/dist/{chunk-6TSDYETQ.js → chunk-4LJFZH2I.js} +29 -2
- package/dist/{chunk-4QWEAL4G.js → chunk-CZ7KT452.js} +93 -1
- package/dist/{chunk-O5B5ELJH.js → chunk-DUUDGR56.js} +14 -13
- package/dist/{chunk-NTIPJ5U7.js → chunk-OKVHWXYV.js} +3 -3
- package/dist/{chunk-ZUYERAFM.js → chunk-ORFILP5L.js} +5 -5
- package/dist/{chunk-RJVG5KA6.js → chunk-P37LLKL4.js} +38 -10
- package/dist/{chunk-SL5VZJ2J.js → chunk-VDUIIWG7.js} +3 -2
- package/dist/{chunk-WFB6F3BN.js → chunk-WK2WMWTH.js} +107 -9
- package/dist/{cli-GXIW7F3E.js → cli-3P44X6E3.js} +6 -6
- package/dist/{cli-JZRQBCD4.js → cli-4D7Q5BCE.js} +6 -6
- package/dist/{cli-MDW7JTEF.js → cli-7OUA6ZSE.js} +6 -6
- package/dist/{cli-V44QEVS6.js → cli-AACBHRUG.js} +10 -10
- package/dist/{cli-BN4IT6N2.js → cli-L3VGKWZH.js} +4 -4
- package/dist/{cli-Y7Y5IROK.js → cli-TH76ATN3.js} +5 -5
- package/dist/{cli-JB4X5LAG.js → cli-VPARW6FY.js} +1 -1
- package/dist/cli.js +3 -3
- package/dist/index.cjs +279 -14
- package/dist/index.d.cts +111 -10
- package/dist/index.d.ts +111 -10
- package/dist/index.js +267 -14
- package/licences.json +27 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @domandigital/craft
|
|
2
2
|
|
|
3
|
+
## 0.19.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5056c6a: STANDARD.md: a new section 10, "The phone is the base layout" (DOM-774): base styles are the phone and wider layouts use `min-width` only, 375px is the floor every surface is designed and measured at, touch targets are at least 44 x 44 CSS px, form fields at least 16px, reviews start from the phone capture, and performance is judged on the mobile Lighthouse profile. What is not a rule is now section 11 and says layout stays free at every width; the numbers are section 12. The older sections lose their em dashes to the house copy check, with the wording unchanged. Docs only.
|
|
8
|
+
- 00d79a0: `craft copy` reads `title` and `description` in Markdown front matter. A dash or a stock phrase in an agent description or a page title is reported on that line, and `copy-ok` on the line or the line above still suppresses it. Other front matter keys stay unread, and fenced code stays unread.
|
|
9
|
+
|
|
10
|
+
## 0.19.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- 09b49ac: Fingerprint version 3: the estate comparison now measures ground temperature and section structure (DOM-637). A warm cream and a cool porcelain page used to score as the same "grey ground" because hue barely counts at a ground's chroma; `groundDistance` adds a temperature gap (warm, neutral, cool from hue, scaled by chroma) on top of ΔE, and shared grounds are named with it ("cream ground", "cool grey ground", "warm dark ground"). Two new distance parts, `rhythm` (the page's bands of light and dark in running order, the opening counted twice) and `hero` (text placement and width, headline class and size, which side the picture sits), give structure a quarter of the weight; `DISTANCE_WEIGHTS` is now accent 0.2, type 0.2, ground 0.1, shape 0.1, motion 0.05, effects 0.1, layout 0.1, rhythm 0.1, hero 0.05, and a part only one side measures is left out rather than counted as different, so version 1 and 2 fingerprints in a register or null model compare without being re-snapshotted. Shared traits name the structure too: "light hero then dark band", "left-aligned serif hero, photo right", "same running order". `craft snapshot` and `craft audit` take `--scheme light|dark` to ask the page for one colour scheme, and a snapshot's images record `left`. New exports: `groundDistance`, `groundTemperature`, `temperatureName`, `groundLabel`, `rhythmOf`, `rhythmDistance`, `heroDistance`, `bandOf`, `TEMPERATURE_FULL`, `TEMPERATURE_NAMED`, `TEMPERATURE_WEIGHT`, `OPENING_BANDS` and the `FingerprintHero`, `HeroImage`, `LightnessBand` and `Temperature` types. Calibrated on RMP Electrical against Bellerose Plumbing's stone and porcelain light modes (`calibration/estate/2026-10-09-temperature/`); the existing register's pair ordering keeps a Kendall tau of 0.83 and no pair crosses the sibling line.
|
|
15
|
+
|
|
3
16
|
## 0.18.2
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/CHARACTER.md
CHANGED
|
@@ -327,6 +327,7 @@ craft copy app content # the prose a visitor reads
|
|
|
327
327
|
craft tells list # the catalogue this build judges against
|
|
328
328
|
craft audit https://example.co.uk --repo . # the rendered page and its source, one report
|
|
329
329
|
craft snapshot https://example.co.uk --out home.json
|
|
330
|
+
craft snapshot https://example.co.uk --scheme dark --out home-dark.json # a site's dark mode
|
|
330
331
|
```
|
|
331
332
|
|
|
332
333
|
`--json` gives the full report. Exit code is 0 on warnings, 1 on a block (or
|
|
@@ -369,11 +370,59 @@ anywhere with `craft audit home.json`. `HTTPS_PROXY` is honoured, and a
|
|
|
369
370
|
`localhost` or `NO_PROXY` host is reached directly.
|
|
370
371
|
|
|
371
372
|
Every audit also prints a **fingerprint**: accent and ground in OKLCh, the
|
|
372
|
-
display and body faces, button roundness, reveal density, the effects
|
|
373
|
-
running order
|
|
374
|
-
|
|
373
|
+
display and body faces, button roundness, reveal density, the effects, the
|
|
374
|
+
running order, the rhythm of light and dark bands, and the hero's
|
|
375
|
+
composition. `fingerprintDistance` compares two of them, 0 to 1, as a
|
|
376
|
+
weighted mean of nine parts. Signals 2 and 3 are both this distance, measured
|
|
375
377
|
against different sets.
|
|
376
378
|
|
|
379
|
+
| Part | Weight | What it measures |
|
|
380
|
+
|---|---|---|
|
|
381
|
+
| accent | 0.20 | ΔE_OK between the accents over 0.3; 1 when only one site has one |
|
|
382
|
+
| type | 0.20 | display face (0.6) and body face (0.4): 0 same family, 0.5 same class, 1 |
|
|
383
|
+
| ground | 0.10 | ΔE_OK over 0.15, plus 0.6 of the temperature gap (below) |
|
|
384
|
+
| shape | 0.10 | button roundness, 0 square to 0.5 pill |
|
|
385
|
+
| motion | 0.05 | share of below-the-fold sections that reveal on scroll |
|
|
386
|
+
| effects | 0.10 | 1 minus the Jaccard overlap of the second-look effects |
|
|
387
|
+
| layout | 0.10 | edit distance over section roles and geometry (kinds before snapshot v2) |
|
|
388
|
+
| rhythm | 0.10 | edit distance over the bands of light, mid and dark, opening counted twice |
|
|
389
|
+
| hero | 0.05 | text centred or left, its width, headline class and size, picture side |
|
|
390
|
+
|
|
391
|
+
Accent and type carry the most because that is where a model's defaults show
|
|
392
|
+
first and where a real business has something of its own. Structure (layout,
|
|
393
|
+
rhythm and hero) carries a quarter between its three parts, because two sites
|
|
394
|
+
that open the same way read as the same site before any colour is compared;
|
|
395
|
+
before 0.19 structure was `layout` alone at a tenth and barely moved a score.
|
|
396
|
+
A part only one side measures (rhythm and hero on a version 1 fingerprint,
|
|
397
|
+
the picture side on a snapshot without image positions) is left out and the
|
|
398
|
+
rest are weighed over what remains, so older registers and null models
|
|
399
|
+
compare without being re-snapshotted.
|
|
400
|
+
|
|
401
|
+
**Ground temperature.** ΔE alone files every light neutral page as one
|
|
402
|
+
paper: RMP's cream (`oklch(0.96 0.010 82)`) and Bellerose's porcelain
|
|
403
|
+
(`#F1F3F4`, `oklch(0.96 0.003 229)`) are ΔE 0.013 apart, a tenth of the
|
|
404
|
+
ground scale, and nobody takes them for the same page. A page ground is the
|
|
405
|
+
largest area on the page and the eye adapts to it, so a tint far below what
|
|
406
|
+
separates two buttons still reads as warm or cool paper. `groundTemperature`
|
|
407
|
+
is the chroma over 0.006 (where a tint is unmistakable: cream sits at 0.010,
|
|
408
|
+
stone at 0.007), signed by hue: red through yellow is warm, teal through
|
|
409
|
+
violet is cool, the greens and magentas between are neither. Warm against
|
|
410
|
+
cool adds 0.6 to the ground part, warm against a neutral white 0.3, two warm
|
|
411
|
+
papers nothing. Shared grounds are named with it: "cream ground", "cool grey
|
|
412
|
+
ground", "warm dark ground", or "warm ground" when one is cream and the other
|
|
413
|
+
a warm grey.
|
|
414
|
+
|
|
415
|
+
**Rhythm** is the page's sequence of light (L 0.75 and over), mid and dark
|
|
416
|
+
(under 0.4) bands, each run of same-lightness sections merged: a cream hero,
|
|
417
|
+
a white logo strip and a near-black band are light then dark, which is what
|
|
418
|
+
a visitor sees. The first three bands are scored once on their own and once
|
|
419
|
+
as part of the whole, so the opening counts double. **Hero** compares the
|
|
420
|
+
hero section's centring and width, the h1's class and size (an octave, 48px
|
|
421
|
+
against 96px, is the whole size distance) and which side of the centre line
|
|
422
|
+
its largest picture sits, when the snapshot records image positions (craft
|
|
423
|
+
0.19 and later). Shared, they are named "light hero then dark band" and
|
|
424
|
+
"left-aligned serif hero, photo right".
|
|
425
|
+
|
|
377
426
|
Known limit: a computed font family is the family the stylesheet asked for.
|
|
378
427
|
A face that failed to load is still reported under its name.
|
|
379
428
|
|
|
@@ -539,9 +588,15 @@ the same brief usually are: the median of that distance over the null models,
|
|
|
539
588
|
0.31 on 2026-09-23 (`--null` measures it again from any null models given).
|
|
540
589
|
If a barber and an electrician look more alike than two drafts of one barber,
|
|
541
590
|
neither look was chosen. The report says what the pair shares in plain words
|
|
542
|
-
("Fraunces headline", "no accent", "pill buttons"
|
|
543
|
-
|
|
544
|
-
|
|
591
|
+
("Fraunces headline", "no accent", "pill buttons", "cream ground", "light hero
|
|
592
|
+
then dark band", "left-aligned serif hero, photo right"), which is where to
|
|
593
|
+
start pulling them apart. A sibling is a warning; `--strict` exits 1 on one,
|
|
594
|
+
for a pipeline that has decided to hold the line.
|
|
595
|
+
|
|
596
|
+
A site with a light and a dark mode is two pages to a visitor. `craft
|
|
597
|
+
snapshot <url> --scheme light|dark` asks the browser for one scheme
|
|
598
|
+
(`prefers-color-scheme`); a site that stores a choice in `localStorage` or on
|
|
599
|
+
`<html data-theme>` needs that set instead. Compare each.
|
|
545
600
|
|
|
546
601
|
`craft direction propose --estate estate.json` reads the register too, so a
|
|
547
602
|
proposed accent steers away from one a sibling already uses.
|
|
@@ -928,6 +983,22 @@ Rules for a direction board, recorded so the next one starts here:
|
|
|
928
983
|
|
|
929
984
|
The board in question is the DD redesign's Stage 05 artefact (DOM-397, CRO-21); v1.1 applied all five.
|
|
930
985
|
|
|
986
|
+
### Ground temperature and structure, 2026-10-09
|
|
987
|
+
|
|
988
|
+
Bellerose Plumbing's light mode looked, to the founder, too much like RMP Electrical, and craft gave the pair 0.59 sharing only "grey ground". Bellerose then moved its light page from warm stone (`#EEEDE8`) to cool porcelain (`#F1F3F4`, bellerose-plumbing#32), and craft scored the two Bellerose pages 0.01 apart. Two things were missing: hue barely counts at a ground's chroma, and structure moved the score by a tenth at most. Fingerprint version 3 (DOM-637) adds ground temperature to the ground part, a rhythm part and a hero part, with the weights in *The rendered page* above. Measured on RMP live (2026-10-09) and Bellerose built locally at `c62590c` (stone, before #32) and `47f9d79` (porcelain), each snapshotted with `--scheme light` and `--scheme dark`; snapshots, a copy of the redesign workstream's register and the full tables are in `calibration/estate/2026-10-09-temperature/`. "Before" is craft 0.18.1's distance on the same snapshots.
|
|
989
|
+
|
|
990
|
+
| Pair | Before | After | Shares after |
|
|
991
|
+
|---|---|---|---|
|
|
992
|
+
| RMP + Bellerose light on stone | 0.59 (cream ground) | **0.54** | warm ground, left-aligned serif hero, photo right, light hero then dark band |
|
|
993
|
+
| RMP + Bellerose light on porcelain | 0.59 (cream ground) | 0.58 | left-aligned serif hero, photo right, light hero then dark band |
|
|
994
|
+
| RMP + Bellerose dark (either) | 0.77 | 0.72 | left-aligned serif hero, photo right |
|
|
995
|
+
| Bellerose stone light + porcelain light | 0.01 | 0.06 | everything but the ground |
|
|
996
|
+
| RMP + RMP | 0.00 | 0.00 | |
|
|
997
|
+
|
|
998
|
+
Against the register as it stands (stored version 2 fingerprints, not re-added), the stone page's nearest site is RMP (0.54) and the porcelain page's is Harrison James (0.52) with RMP second (0.58): the move away from RMP that the founder made by eye now shows in the numbers, and both Bellerose light pages still share the opening with RMP, which is the next thing to pull apart. Neither pair is a sibling; the faces, the accent and the button shape are their own.
|
|
999
|
+
|
|
1000
|
+
The seven register pairs reshuffle little: Kendall tau 0.83 between the before and after orderings over 21 pairs, the closest pair is still DD + RMP (0.48 to 0.52, now "warm ground" rather than "grey ground"), and no pair crosses the sibling line. Pairs that shared "white ground" and little else move apart by 0.02 to 0.06 as the temperature counts (MMM + Sensphere keep "cool white ground"); Chair and Blade + HJ Beauty move closer (0.74 to 0.66) because both open dark then light. The sibling line measured again on the seven v2 null models is 0.337 with the old distance and 0.339 with the new, so the weights do not move it; `SIBLING_AT` stays at the 0.31 measured on the v1 models in September, and moving it to the v2 line is a separate decision.
|
|
1001
|
+
|
|
931
1002
|
## The catalogue
|
|
932
1003
|
|
|
933
1004
|
Generated from the package. Run `pnpm --filter @domandigital/craft run docs`
|
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ module.exports = { presets: [tailwindV3Preset()] };
|
|
|
67
67
|
|
|
68
68
|
## The standard itself
|
|
69
69
|
|
|
70
|
-
[STANDARD.md](./STANDARD.md) is the
|
|
70
|
+
[STANDARD.md](./STANDARD.md) is the twelve-section house standard, every numeric
|
|
71
71
|
rule cited, including a section on what is deliberately *not* a rule. Its
|
|
72
72
|
canonical numbers are compared against the code on every test run, so the
|
|
73
73
|
document cannot drift from what ships.
|
package/STANDARD.md
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
# The Doman Digital craft standard
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Twelve sections. Every numeric rule is cited, and section 11 says what is
|
|
4
4
|
deliberately *not* a rule.
|
|
5
5
|
|
|
6
6
|
This document and the package are bound together by a test: the machine-readable
|
|
7
|
-
block in section
|
|
7
|
+
block in section 12 is compared against `HOUSE_BUDGET`, `EASE` and
|
|
8
8
|
`DURATION_MS` on every run, so the standard cannot drift from what ships.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
<!-- copy-ok: the standard names the practice each rule replaces -->
|
|
12
13
|
## 1. Colour is derived, not picked
|
|
13
14
|
|
|
14
|
-
Anchors
|
|
15
|
+
Anchors, the colours a surface already ships, are sacred and come back byte-identical.
|
|
15
16
|
Everything else is generated in OKLCh from those anchors.
|
|
16
17
|
|
|
17
18
|
- Ramps have **11 steps** (50–950).
|
|
@@ -19,7 +20,7 @@ Everything else is generated in OKLCh from those anchors.
|
|
|
19
20
|
(CSS Color 4 §13.2). RGB clipping is rejected: it walks a saturated violet
|
|
20
21
|
toward magenta.
|
|
21
22
|
- Derived values standing in for a hand-picked one must be within
|
|
22
|
-
**ΔE_OK ≤ 0.02**
|
|
23
|
+
**ΔE_OK ≤ 0.02** (the just-noticeable difference) or be declared an
|
|
23
24
|
intentional change.
|
|
24
25
|
|
|
25
26
|
*Source: Ottosson, "A perceptual color space for image processing" (2020); CSS Color 4 §13.2.*
|
|
@@ -40,9 +41,9 @@ Everything else is generated in OKLCh from those anchors.
|
|
|
40
41
|
## 3. Type is fluid, and always has a rem term
|
|
41
42
|
|
|
42
43
|
- Steps **−2 to 5**, `clamp()` interpolated between a small and a large viewport.
|
|
43
|
-
- The interpolation term is **`rem + vw`, never bare `vw
|
|
44
|
+
- The interpolation term is **`rem + vw`, never bare `vw`**. A bare-`vw` size
|
|
44
45
|
ignores the browser text-size setting and fails WCAG 1.4.4 at 200% zoom.
|
|
45
|
-
- Existing sizes are **pinned**, not regenerated, so adopting the scale moves
|
|
46
|
+
- Existing sizes are **pinned**, not regenerated, so adopting the scale moves <!-- copy-ok -->
|
|
46
47
|
nothing.
|
|
47
48
|
|
|
48
49
|
*Source: Utopia (utopia.fyi); WCAG 2.2 SC 1.4.4.*
|
|
@@ -58,7 +59,7 @@ Everything else is generated in OKLCh from those anchors.
|
|
|
58
59
|
- Clipped display text gets a **descender mask** (`padding-bottom: .16em;
|
|
59
60
|
margin-bottom: -.16em`), or it loses the tails of g, j, p, q, y.
|
|
60
61
|
|
|
61
|
-
*Source: house habit
|
|
62
|
+
*Source: house habit. The −0.01em display tracking and the ch measure were arrived at independently in two client repos.*
|
|
62
63
|
|
|
63
64
|
## 5. Space is fluid and proportional to type
|
|
64
65
|
|
|
@@ -66,7 +67,7 @@ Everything else is generated in OKLCh from those anchors.
|
|
|
66
67
|
- Section rhythm has **three** sizes, not five. Five shipped with zero consumers,
|
|
67
68
|
which is the signature of a scale invented ahead of a need.
|
|
68
69
|
|
|
69
|
-
*Source: Utopia; measured
|
|
70
|
+
*Source: Utopia; measured: `--space-section-xs..xl` had no consumers in the monorepo.*
|
|
70
71
|
|
|
71
72
|
## 6. Density is a property of the surface
|
|
72
73
|
|
|
@@ -89,7 +90,7 @@ needs. Set once on a shell via `[data-density]`, inherited below.
|
|
|
89
90
|
meaning it covers more ground per unit time arriving than it averaged.
|
|
90
91
|
- **UI durations ≤ 300ms.** Drawers and one gated reveal may exceed it.
|
|
91
92
|
- **Don't animate what fires 100×/day**, and never animate a keyboard-triggered
|
|
92
|
-
interaction
|
|
93
|
+
interaction: the user is moving faster than the animation.
|
|
93
94
|
- **One signature moment per page**, gated, never the LCP element.
|
|
94
95
|
|
|
95
96
|
*Source: Emil Kowalski, "Animations on the Web"; Core Web Vitals (LCP).*
|
|
@@ -101,12 +102,12 @@ needs. Set once on a shell via `[data-density]`, inherited below.
|
|
|
101
102
|
|
|
102
103
|
**Never `animation: none` inside the block.** That drops the `forwards` fill along with the
|
|
103
104
|
animation, so a forwards-filled entrance never applies its end state and the
|
|
104
|
-
element keeps `opacity: 0` from its base rule
|
|
105
|
+
element keeps `opacity: 0` from its base rule, permanently invisible, for
|
|
105
106
|
exactly the users who asked for less motion. This shipped.
|
|
106
107
|
|
|
107
108
|
Images: AVIF and WebP, in that order.
|
|
108
109
|
|
|
109
|
-
*Source: WCAG 2.2 SC 2.3.3; measured
|
|
110
|
+
*Source: WCAG 2.2 SC 2.3.3; measured: a live site was stranded at opacity 0 by this exact defect.*
|
|
110
111
|
|
|
111
112
|
## 9. Restraint is the budget
|
|
112
113
|
|
|
@@ -118,11 +119,11 @@ step: `code { font-size: 0.875em }` states one relationship and applies it
|
|
|
118
119
|
wherever code appears, rather than adding a rung to the ladder. Counting it
|
|
119
120
|
against the scale is the same category error as counting the framework's theme.
|
|
120
121
|
It is still counted, because eight competing ratios is as unreadable as eight
|
|
121
|
-
competing steps
|
|
122
|
+
competing steps: split, not dropped. A value that stops being reported is a
|
|
122
123
|
value that grows.
|
|
123
124
|
|
|
124
125
|
Differentiation guards pass anything that differs from its siblings, however
|
|
125
|
-
ugly. What they cannot see is accumulation
|
|
126
|
+
ugly. What they cannot see is accumulation: eighteen font sizes, six shadows,
|
|
126
127
|
four competing accents, each added reasonably, together reading as noise.
|
|
127
128
|
|
|
128
129
|
The budget counts the vocabulary a surface **chose**, and the whole difficulty
|
|
@@ -141,7 +142,7 @@ second reports a pass.
|
|
|
141
142
|
|
|
142
143
|
The line is **used versus unused**, which is not a property of the layer:
|
|
143
144
|
|
|
144
|
-
- References are **resolved**, not skipped
|
|
145
|
+
- References are **resolved**, not skipped: through the token chain, to the <!-- copy-ok -->
|
|
145
146
|
value that renders.
|
|
146
147
|
- A definition is only ever reached from a declaration in a kept layer, so a
|
|
147
148
|
framework default nothing uses still counts for nothing.
|
|
@@ -154,7 +155,34 @@ The line is **used versus unused**, which is not a property of the layer:
|
|
|
154
155
|
|
|
155
156
|
*Source: measured against the estate. The budget is the observed ceiling of the surfaces that read as designed; the resolution rule is what it took to measure them at all.*
|
|
156
157
|
|
|
157
|
-
## 10.
|
|
158
|
+
## 10. The phone is the base layout
|
|
159
|
+
|
|
160
|
+
Mobile first is a rule about **order and floor**, not about a layout. The phone
|
|
161
|
+
is designed, built and measured first, and every wider layout is derived from
|
|
162
|
+
it. A desktop layout squeezed onto a phone keeps the desktop's priorities and
|
|
163
|
+
drops whatever did not fit; a phone layout widened keeps what matters and adds.
|
|
164
|
+
|
|
165
|
+
- **The phone is the base.** Base styles are the phone layout. Wider layouts
|
|
166
|
+
are added with `min-width` queries only (Tailwind: unprefixed is the phone,
|
|
167
|
+
`sm:` `md:` `lg:` add). A `max-width` query, a `width <` range or a Tailwind
|
|
168
|
+
`max-*:` variant needs a recorded exception with its reason.
|
|
169
|
+
- **375px is the floor.** Every surface is designed and measured at **375 CSS
|
|
170
|
+
px** first, the narrowest iPhone width in common use (SE, mini, X to 11 Pro).
|
|
171
|
+
Nothing scrolls sideways at 375. 390 is still measured: it is the width the
|
|
172
|
+
design is accepted at.
|
|
173
|
+
- **Touch targets at least 44 × 44 CSS px.** A link inside a sentence is exempt,
|
|
174
|
+
as in WCAG's own inline exception. 24px (SC 2.5.8, AA) is what an audit asks
|
|
175
|
+
for; 44 is the house bar.
|
|
176
|
+
- **Form inputs at least 16px.** Safari on iOS zooms the page when a field set
|
|
177
|
+
smaller gets focus, and the visitor has to pinch back out. Fix the size, never
|
|
178
|
+
the viewport: `maximum-scale=1` takes zoom away from people who need it.
|
|
179
|
+
- **Reviews look at the phone capture first**, then the desktop one.
|
|
180
|
+
- **Performance is judged on the mobile Lighthouse profile.** A desktop score
|
|
181
|
+
is not evidence: the phone is the slower device and the one Google indexes.
|
|
182
|
+
|
|
183
|
+
*Source: WCAG 2.2 SC 2.5.5 (Target Size, Enhanced), SC 2.5.8 and SC 1.4.4; Apple Human Interface Guidelines, "Accessibility" (44 × 44 pt hit targets); WebKit's focus zoom on fields under 16px; Lighthouse's default mobile emulation; Google Search Central, "Mobile-first indexing"; Luke Wroblewski, "Mobile First" (2011).*
|
|
184
|
+
|
|
185
|
+
## 11. What is deliberately not a rule
|
|
158
186
|
|
|
159
187
|
The package emits **plain CSS custom properties** precisely so these stay free:
|
|
160
188
|
|
|
@@ -162,9 +190,11 @@ The package emits **plain CSS custom properties** precisely so these stay free:
|
|
|
162
190
|
two; the third needs nothing.
|
|
163
191
|
- **The motion library.** `EASE_TUPLE` carries the same control points as `EASE`,
|
|
164
192
|
so Framer Motion, GSAP and CSS agree without the package choosing one.
|
|
165
|
-
- **Where tokens live.** A `:root` block, a generated file, a Tailwind theme
|
|
193
|
+
- **Where tokens live.** A `:root` block, a generated file, a Tailwind theme:
|
|
166
194
|
the package does not care.
|
|
167
|
-
- **Layout, grid and component structure.** Not a rule.
|
|
195
|
+
- **Layout, grid and component structure.** Not a rule, at any width. Section
|
|
196
|
+
10 fixes the order the widths are designed in and the floor they are
|
|
197
|
+
measured at; what the phone layout looks like stays free. Layout *defaults*
|
|
168
198
|
(hero straight into a trust strip, three icon cards, a reveal on every
|
|
169
199
|
section) are measured as character in [CHARACTER.md](./CHARACTER.md), as
|
|
170
200
|
warnings with a declared-exception path, never as rules here.
|
|
@@ -172,7 +202,7 @@ The package emits **plain CSS custom properties** precisely so these stay free:
|
|
|
172
202
|
|
|
173
203
|
A rule here has to be one that is wrong to break. Everything else is a default.
|
|
174
204
|
|
|
175
|
-
##
|
|
205
|
+
## 12. The numbers
|
|
176
206
|
|
|
177
207
|
<!-- craft:canonical -->
|
|
178
208
|
```json
|
|
@@ -9,10 +9,10 @@ import {
|
|
|
9
9
|
snapshotPage,
|
|
10
10
|
snapshotUrl,
|
|
11
11
|
snapshotUrls
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-VDUIIWG7.js";
|
|
13
13
|
import "./chunk-JYNXGCYZ.js";
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-WK2WMWTH.js";
|
|
15
|
+
import "./chunk-CZ7KT452.js";
|
|
16
16
|
import "./chunk-QYBAVU3P.js";
|
|
17
17
|
export {
|
|
18
18
|
PROVENANCE_BUDGET_MS,
|
package/dist/audit.cjs
CHANGED
|
@@ -3292,7 +3292,7 @@ var RENDERED_PATHS = {
|
|
|
3292
3292
|
};
|
|
3293
3293
|
|
|
3294
3294
|
// src/fingerprint/index.ts
|
|
3295
|
-
var FINGERPRINT_VERSION =
|
|
3295
|
+
var FINGERPRINT_VERSION = 3;
|
|
3296
3296
|
var CHROMATIC = 0.03;
|
|
3297
3297
|
var MIN_ACCENT_AREA = 2e4;
|
|
3298
3298
|
var oklch = (value) => {
|
|
@@ -3328,6 +3328,24 @@ function median(values) {
|
|
|
3328
3328
|
const mid = Math.floor(sorted.length / 2);
|
|
3329
3329
|
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
3330
3330
|
}
|
|
3331
|
+
var FULL_WIDTH_IMAGE = 0.8;
|
|
3332
|
+
function heroOf(s) {
|
|
3333
|
+
const h1 = s.headings.find((h) => h.level === 1);
|
|
3334
|
+
const inside = (top, x) => top >= x.top && top < x.top + x.height;
|
|
3335
|
+
const index = (() => {
|
|
3336
|
+
const withH1 = h1 ? s.sections.findIndex((x) => x.kind === "hero" && inside(h1.top, x)) : -1;
|
|
3337
|
+
return withH1 >= 0 ? withH1 : s.sections.findIndex((x) => x.kind === "hero");
|
|
3338
|
+
})();
|
|
3339
|
+
if (index < 0) return void 0;
|
|
3340
|
+
const section = s.sections[index];
|
|
3341
|
+
const headline = h1 && inside(h1.top, section) ? { sizePx: h1.sizePx, weight: h1.weight, class: fontClass(normaliseFamily(h1.family)), italic: h1.italic || h1.italicPart } : null;
|
|
3342
|
+
if (!s.images) return { headline };
|
|
3343
|
+
const picture2 = s.images.filter((i) => (i.role === "photo" || i.role === "illustration") && (i.section === index || i.section === null && inside(i.top, section))).sort((a, b) => b.width * b.height - a.width * a.height)[0];
|
|
3344
|
+
if (!picture2) return { headline, image: "none" };
|
|
3345
|
+
if (picture2.left === void 0) return { headline };
|
|
3346
|
+
const image = picture2.width >= FULL_WIDTH_IMAGE * s.viewport.width ? "full" : picture2.left + picture2.width / 2 < s.viewport.width / 2 ? "left" : "right";
|
|
3347
|
+
return { headline, image };
|
|
3348
|
+
}
|
|
3331
3349
|
function fingerprint(s) {
|
|
3332
3350
|
const effects = [];
|
|
3333
3351
|
if (s.effects.glass > 0) effects.push("glass-panel");
|
|
@@ -3339,6 +3357,7 @@ function fingerprint(s) {
|
|
|
3339
3357
|
if (s.effects.eyebrowChip) effects.push("hero-eyebrow-chip");
|
|
3340
3358
|
if (s.motion.introOverlay) effects.push("intro-cinematic");
|
|
3341
3359
|
const below = s.sections.filter((x) => x.top > s.viewport.height).length;
|
|
3360
|
+
const hero = heroOf(s);
|
|
3342
3361
|
return {
|
|
3343
3362
|
version: FINGERPRINT_VERSION,
|
|
3344
3363
|
accent: accentOf(s),
|
|
@@ -3362,7 +3381,8 @@ function fingerprint(s) {
|
|
|
3362
3381
|
carousel: x.carousel ?? false,
|
|
3363
3382
|
figures: x.figures ?? 0
|
|
3364
3383
|
}))
|
|
3365
|
-
} : {}
|
|
3384
|
+
} : {},
|
|
3385
|
+
...hero ? { hero } : {}
|
|
3366
3386
|
};
|
|
3367
3387
|
}
|
|
3368
3388
|
var ACCENT_SCALE = 0.3;
|
|
@@ -4314,6 +4334,7 @@ function collectInPage() {
|
|
|
4314
4334
|
width: Math.round(b.width),
|
|
4315
4335
|
height: Math.round(b.height),
|
|
4316
4336
|
top: Math.round(b.top),
|
|
4337
|
+
left: Math.round(b.left),
|
|
4317
4338
|
role: roleOfImage(el, src, b.width, b.height, alt),
|
|
4318
4339
|
alt,
|
|
4319
4340
|
decorative,
|
|
@@ -4633,7 +4654,7 @@ async function snapshotUrls(urls, options = {}) {
|
|
|
4633
4654
|
const out = [];
|
|
4634
4655
|
try {
|
|
4635
4656
|
for (const url of urls) {
|
|
4636
|
-
const page = await browser.newPage({ viewport: options.viewport ?? { width: 1440, height: 900 } });
|
|
4657
|
+
const page = await browser.newPage({ viewport: options.viewport ?? { width: 1440, height: 900 }, ...options.colorScheme ? { colorScheme: options.colorScheme } : {} });
|
|
4637
4658
|
try {
|
|
4638
4659
|
const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: options.timeoutMs ?? 45e3 });
|
|
4639
4660
|
const status = typeof response?.status === "function" ? response.status() : 200;
|
package/dist/audit.d.cts
CHANGED
|
@@ -134,6 +134,8 @@ interface SnapshotImage {
|
|
|
134
134
|
width: number;
|
|
135
135
|
height: number;
|
|
136
136
|
top: number;
|
|
137
|
+
/** Version 2, craft 0.19 and later: distance from the viewport's left edge, so a hero can say which side its picture sits. */
|
|
138
|
+
left?: number;
|
|
137
139
|
role: ImageRole;
|
|
138
140
|
alt: string | null;
|
|
139
141
|
/** Empty alt, `aria-hidden` or `role="presentation"`. */
|
|
@@ -412,8 +414,26 @@ interface FingerprintSection {
|
|
|
412
414
|
/** Large standalone figures. */
|
|
413
415
|
figures?: number;
|
|
414
416
|
}
|
|
417
|
+
/** Where the hero's largest picture sits, relative to the headline. */
|
|
418
|
+
type HeroImage = "left" | "right" | "full" | "none";
|
|
419
|
+
/** The hero's composition: the first thing two sites are compared on by eye. */
|
|
420
|
+
interface FingerprintHero {
|
|
421
|
+
/** The headline (the first h1 inside the hero), or null when the hero has none. */
|
|
422
|
+
headline: {
|
|
423
|
+
sizePx: number;
|
|
424
|
+
weight: number;
|
|
425
|
+
class: FontClass;
|
|
426
|
+
italic: boolean;
|
|
427
|
+
} | null;
|
|
428
|
+
/**
|
|
429
|
+
* The largest photo or illustration in the hero: beside the text (left or
|
|
430
|
+
* right of the viewport's centre line), `full` when it spans the width,
|
|
431
|
+
* `none` without one. Absent when the snapshot predates image positions.
|
|
432
|
+
*/
|
|
433
|
+
image?: HeroImage;
|
|
434
|
+
}
|
|
415
435
|
interface Fingerprint {
|
|
416
|
-
version: 1 | 2;
|
|
436
|
+
version: 1 | 2 | 3;
|
|
417
437
|
/** The most prominent chromatic colour: button fills first, then painted area, then text. */
|
|
418
438
|
accent: Oklch | null;
|
|
419
439
|
ground: Oklch;
|
|
@@ -435,6 +455,8 @@ interface Fingerprint {
|
|
|
435
455
|
layout: SectionKind[];
|
|
436
456
|
/** Version 2: present when every section carries a role and geometry. */
|
|
437
457
|
sections?: FingerprintSection[];
|
|
458
|
+
/** Version 3: present when the page has a hero section. */
|
|
459
|
+
hero?: FingerprintHero;
|
|
438
460
|
}
|
|
439
461
|
|
|
440
462
|
/**
|
|
@@ -480,6 +502,12 @@ interface SnapshotOptions {
|
|
|
480
502
|
visual?: boolean;
|
|
481
503
|
/** Read the first bytes of the page's larger images for AI provenance markers. Default true. */
|
|
482
504
|
provenance?: boolean;
|
|
505
|
+
/**
|
|
506
|
+
* The colour scheme the page is asked for (`prefers-color-scheme`). A site
|
|
507
|
+
* with a light and a dark mode is two pages to the visitor; snapshot each.
|
|
508
|
+
* Default: the browser's own, which is light in headless Chromium.
|
|
509
|
+
*/
|
|
510
|
+
colorScheme?: "light" | "dark";
|
|
483
511
|
}
|
|
484
512
|
interface MinimalPage {
|
|
485
513
|
close?(): Promise<void>;
|
|
@@ -523,6 +551,7 @@ interface MinimalBrowser {
|
|
|
523
551
|
width: number;
|
|
524
552
|
height: number;
|
|
525
553
|
};
|
|
554
|
+
colorScheme?: "light" | "dark";
|
|
526
555
|
}): Promise<MinimalPage>;
|
|
527
556
|
close(): Promise<void>;
|
|
528
557
|
}
|
package/dist/audit.d.ts
CHANGED
|
@@ -134,6 +134,8 @@ interface SnapshotImage {
|
|
|
134
134
|
width: number;
|
|
135
135
|
height: number;
|
|
136
136
|
top: number;
|
|
137
|
+
/** Version 2, craft 0.19 and later: distance from the viewport's left edge, so a hero can say which side its picture sits. */
|
|
138
|
+
left?: number;
|
|
137
139
|
role: ImageRole;
|
|
138
140
|
alt: string | null;
|
|
139
141
|
/** Empty alt, `aria-hidden` or `role="presentation"`. */
|
|
@@ -412,8 +414,26 @@ interface FingerprintSection {
|
|
|
412
414
|
/** Large standalone figures. */
|
|
413
415
|
figures?: number;
|
|
414
416
|
}
|
|
417
|
+
/** Where the hero's largest picture sits, relative to the headline. */
|
|
418
|
+
type HeroImage = "left" | "right" | "full" | "none";
|
|
419
|
+
/** The hero's composition: the first thing two sites are compared on by eye. */
|
|
420
|
+
interface FingerprintHero {
|
|
421
|
+
/** The headline (the first h1 inside the hero), or null when the hero has none. */
|
|
422
|
+
headline: {
|
|
423
|
+
sizePx: number;
|
|
424
|
+
weight: number;
|
|
425
|
+
class: FontClass;
|
|
426
|
+
italic: boolean;
|
|
427
|
+
} | null;
|
|
428
|
+
/**
|
|
429
|
+
* The largest photo or illustration in the hero: beside the text (left or
|
|
430
|
+
* right of the viewport's centre line), `full` when it spans the width,
|
|
431
|
+
* `none` without one. Absent when the snapshot predates image positions.
|
|
432
|
+
*/
|
|
433
|
+
image?: HeroImage;
|
|
434
|
+
}
|
|
415
435
|
interface Fingerprint {
|
|
416
|
-
version: 1 | 2;
|
|
436
|
+
version: 1 | 2 | 3;
|
|
417
437
|
/** The most prominent chromatic colour: button fills first, then painted area, then text. */
|
|
418
438
|
accent: Oklch | null;
|
|
419
439
|
ground: Oklch;
|
|
@@ -435,6 +455,8 @@ interface Fingerprint {
|
|
|
435
455
|
layout: SectionKind[];
|
|
436
456
|
/** Version 2: present when every section carries a role and geometry. */
|
|
437
457
|
sections?: FingerprintSection[];
|
|
458
|
+
/** Version 3: present when the page has a hero section. */
|
|
459
|
+
hero?: FingerprintHero;
|
|
438
460
|
}
|
|
439
461
|
|
|
440
462
|
/**
|
|
@@ -480,6 +502,12 @@ interface SnapshotOptions {
|
|
|
480
502
|
visual?: boolean;
|
|
481
503
|
/** Read the first bytes of the page's larger images for AI provenance markers. Default true. */
|
|
482
504
|
provenance?: boolean;
|
|
505
|
+
/**
|
|
506
|
+
* The colour scheme the page is asked for (`prefers-color-scheme`). A site
|
|
507
|
+
* with a light and a dark mode is two pages to the visitor; snapshot each.
|
|
508
|
+
* Default: the browser's own, which is light in headless Chromium.
|
|
509
|
+
*/
|
|
510
|
+
colorScheme?: "light" | "dark";
|
|
483
511
|
}
|
|
484
512
|
interface MinimalPage {
|
|
485
513
|
close?(): Promise<void>;
|
|
@@ -523,6 +551,7 @@ interface MinimalBrowser {
|
|
|
523
551
|
width: number;
|
|
524
552
|
height: number;
|
|
525
553
|
};
|
|
554
|
+
colorScheme?: "light" | "dark";
|
|
526
555
|
}): Promise<MinimalPage>;
|
|
527
556
|
close(): Promise<void>;
|
|
528
557
|
}
|
package/dist/audit.js
CHANGED
|
@@ -3257,7 +3257,7 @@ var RENDERED_PATHS = {
|
|
|
3257
3257
|
};
|
|
3258
3258
|
|
|
3259
3259
|
// src/fingerprint/index.ts
|
|
3260
|
-
var FINGERPRINT_VERSION =
|
|
3260
|
+
var FINGERPRINT_VERSION = 3;
|
|
3261
3261
|
var CHROMATIC = 0.03;
|
|
3262
3262
|
var MIN_ACCENT_AREA = 2e4;
|
|
3263
3263
|
var oklch = (value) => {
|
|
@@ -3293,6 +3293,24 @@ function median(values) {
|
|
|
3293
3293
|
const mid = Math.floor(sorted.length / 2);
|
|
3294
3294
|
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
3295
3295
|
}
|
|
3296
|
+
var FULL_WIDTH_IMAGE = 0.8;
|
|
3297
|
+
function heroOf(s) {
|
|
3298
|
+
const h1 = s.headings.find((h) => h.level === 1);
|
|
3299
|
+
const inside = (top, x) => top >= x.top && top < x.top + x.height;
|
|
3300
|
+
const index = (() => {
|
|
3301
|
+
const withH1 = h1 ? s.sections.findIndex((x) => x.kind === "hero" && inside(h1.top, x)) : -1;
|
|
3302
|
+
return withH1 >= 0 ? withH1 : s.sections.findIndex((x) => x.kind === "hero");
|
|
3303
|
+
})();
|
|
3304
|
+
if (index < 0) return void 0;
|
|
3305
|
+
const section = s.sections[index];
|
|
3306
|
+
const headline = h1 && inside(h1.top, section) ? { sizePx: h1.sizePx, weight: h1.weight, class: fontClass(normaliseFamily(h1.family)), italic: h1.italic || h1.italicPart } : null;
|
|
3307
|
+
if (!s.images) return { headline };
|
|
3308
|
+
const picture2 = s.images.filter((i) => (i.role === "photo" || i.role === "illustration") && (i.section === index || i.section === null && inside(i.top, section))).sort((a, b) => b.width * b.height - a.width * a.height)[0];
|
|
3309
|
+
if (!picture2) return { headline, image: "none" };
|
|
3310
|
+
if (picture2.left === void 0) return { headline };
|
|
3311
|
+
const image = picture2.width >= FULL_WIDTH_IMAGE * s.viewport.width ? "full" : picture2.left + picture2.width / 2 < s.viewport.width / 2 ? "left" : "right";
|
|
3312
|
+
return { headline, image };
|
|
3313
|
+
}
|
|
3296
3314
|
function fingerprint(s) {
|
|
3297
3315
|
const effects = [];
|
|
3298
3316
|
if (s.effects.glass > 0) effects.push("glass-panel");
|
|
@@ -3304,6 +3322,7 @@ function fingerprint(s) {
|
|
|
3304
3322
|
if (s.effects.eyebrowChip) effects.push("hero-eyebrow-chip");
|
|
3305
3323
|
if (s.motion.introOverlay) effects.push("intro-cinematic");
|
|
3306
3324
|
const below = s.sections.filter((x) => x.top > s.viewport.height).length;
|
|
3325
|
+
const hero = heroOf(s);
|
|
3307
3326
|
return {
|
|
3308
3327
|
version: FINGERPRINT_VERSION,
|
|
3309
3328
|
accent: accentOf(s),
|
|
@@ -3327,7 +3346,8 @@ function fingerprint(s) {
|
|
|
3327
3346
|
carousel: x.carousel ?? false,
|
|
3328
3347
|
figures: x.figures ?? 0
|
|
3329
3348
|
}))
|
|
3330
|
-
} : {}
|
|
3349
|
+
} : {},
|
|
3350
|
+
...hero ? { hero } : {}
|
|
3331
3351
|
};
|
|
3332
3352
|
}
|
|
3333
3353
|
var ACCENT_SCALE = 0.3;
|
|
@@ -4279,6 +4299,7 @@ function collectInPage() {
|
|
|
4279
4299
|
width: Math.round(b.width),
|
|
4280
4300
|
height: Math.round(b.height),
|
|
4281
4301
|
top: Math.round(b.top),
|
|
4302
|
+
left: Math.round(b.left),
|
|
4282
4303
|
role: roleOfImage(el, src, b.width, b.height, alt),
|
|
4283
4304
|
alt,
|
|
4284
4305
|
decorative,
|
|
@@ -4598,7 +4619,7 @@ async function snapshotUrls(urls, options = {}) {
|
|
|
4598
4619
|
const out = [];
|
|
4599
4620
|
try {
|
|
4600
4621
|
for (const url of urls) {
|
|
4601
|
-
const page = await browser.newPage({ viewport: options.viewport ?? { width: 1440, height: 900 } });
|
|
4622
|
+
const page = await browser.newPage({ viewport: options.viewport ?? { width: 1440, height: 900 }, ...options.colorScheme ? { colorScheme: options.colorScheme } : {} });
|
|
4602
4623
|
try {
|
|
4603
4624
|
const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: options.timeoutMs ?? 45e3 });
|
|
4604
4625
|
const status = typeof response?.status === "function" ? response.status() : 200;
|