@domandigital/craft 0.19.3 → 0.20.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 +4 -2
- package/COPY.md +3 -1
- package/README.md +51 -0
- package/STANDARD.md +86 -3
- package/dist/{audit-43PC4LMS.js → audit-BN2GSFKT.js} +3 -3
- package/dist/audit.cjs +80 -5
- package/dist/audit.js +80 -5
- package/dist/{chunk-4LJFZH2I.js → chunk-2OBDCIVW.js} +2 -2
- package/dist/{chunk-BK4AS2RU.js → chunk-6NXPDCUO.js} +26 -13
- package/dist/{chunk-CZ7KT452.js → chunk-7DQDF3VL.js} +76 -4
- package/dist/{chunk-6M2LK7QE.js → chunk-FQYKKKHI.js} +5 -5
- package/dist/{chunk-XVMKZ4SM.js → chunk-GOPHZI6O.js} +4 -4
- package/dist/{cli-6LCOSHMW.js → chunk-IJUZ7RV4.js} +16 -114
- package/dist/{chunk-D2FTTM4F.js → chunk-IZ6GTU54.js} +5 -5
- package/dist/{chunk-WK2WMWTH.js → chunk-JGWOA4LI.js} +5 -2
- package/dist/{chunk-VDUIIWG7.js → chunk-YOZLPNJS.js} +1 -1
- package/dist/{cli-Q5X3W6I4.js → cli-DVYLDCEA.js} +5 -5
- package/dist/{cli-QAYH2OX5.js → cli-HUN5ZTH5.js} +6 -6
- package/dist/{cli-WN2GHYPF.js → cli-IREKGVSM.js} +6 -6
- package/dist/cli-LSJI7QRD.js +122 -0
- package/dist/{cli-APYEOHFV.js → cli-MKZJMZUJ.js} +5 -5
- package/dist/{cli-TMLHMNWU.js → cli-NTNUZUFP.js} +4 -4
- package/dist/{cli-4LEAEOOV.js → cli-VFKKFRB4.js} +9 -9
- package/dist/{cli-VPARW6FY.js → cli-ZXDGVDYU.js} +1 -1
- package/dist/cli.js +3 -3
- package/dist/index.cjs +80 -5
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +80 -5
- package/dist/phone.cjs +546 -19
- package/dist/phone.d.cts +265 -28
- package/dist/phone.d.ts +265 -28
- package/dist/phone.js +532 -19
- package/dist/viewports-cli-C4VXMPKK.js +683 -0
- package/package.json +1 -1
- package/word-lists.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @domandigital/craft
|
|
2
2
|
|
|
3
|
+
## 0.20.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e8ec697: `review-phrase` reports "clear" and "clearly" (catalogue 2026.10.1). "A clear next step" and "clear pricing" promise clarity instead of giving it, so every use is now printed for a person to read. It never blocks: the house decision of 14 October 2026 is review first, and a move to the blocking tier waits until the existing uses across the estate are read. Review-tier phrases that end in a letter now match whole words only, so "clearance" and "clearer" pass, and "great questions" is no longer reported where "great question" is.
|
|
8
|
+
- a1f2b6b: Two new review-tier copy tells (catalogue 2026.10.2). `sales-tagline` reports advert lines such as "so your budget goes where it counts", "real results" and "pages that convert". `outcome-promise` reports a hoped-for result written as if delivered: "we'll turn that into enquiries", "once the first areas are working", "competitors you can outdo", "exactly what convinces". Ordinary statements of the work ("we'll rewrite your rewiring page", "once the fuse board is fitted") pass. Both print for review and never block, following the house rule that a new tell starts in review.
|
|
9
|
+
|
|
10
|
+
## 0.20.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- 63dfae5: Every viewport, and a phone optimised beyond fit (DOM-787, part of DOM-772). `@domandigital/craft/phone` gains `measureViewports(page, options)`, `measureSite(browser, options)` and a `craft viewports` command beside `craft phone-first`. They take a page through the ladder (320, 375, 390, 430, landscape 844 x 390, 768, 1024, 1280, 1440, 1920, phone first in order and in every report) and a width sweep from 320 to 1920 in 16 to 32px steps, resizing the one loaded page in place. At any width: sideways scroll, visible text over other visible text and a cut-off page title, with the exact widths that fail. On a short phone (375 x 667): the main action (`mainAction`, a selector, global or per route) on the first screen or pinned; fixed and sticky layers over a quarter of the screen; images more than 2.5 times their displayed width or narrower than it; phone, email and number fields with the wrong `type` or `inputmode`; personal-data fields with no `autocomplete`; UK phone numbers in the text that are not `tel:` links. On a landscape phone: header and fixed layers over half the height. At 1440 and 1920: body lines over 80 characters and stretched images. Every threshold has a house default (`DEFAULT_THRESHOLDS`) and can be overridden. The new checks are baselined like the 375px ones: they use the same `phone-first.baseline.json` and `craft phone-first --measured`, and `measurePhone` and the existing baseline format work as before. STANDARD.md section 10 gains "Optimised for the phone" and "Every viewport is intended".
|
|
15
|
+
|
|
3
16
|
## 0.19.3
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/CHARACTER.md
CHANGED
|
@@ -1005,7 +1005,7 @@ Generated from the package. Run `pnpm --filter @domandigital/craft run docs`
|
|
|
1005
1005
|
after changing an entry; a test fails until you do.
|
|
1006
1006
|
|
|
1007
1007
|
<!-- craft:catalogue:start -->
|
|
1008
|
-
Catalogue version `2026.
|
|
1008
|
+
Catalogue version `2026.10.2`, 70 tells.
|
|
1009
1009
|
|
|
1010
1010
|
| Id | Gen | Surface | Severity | Tell | Why it is a default |
|
|
1011
1011
|
| --- | --- | --- | --- | --- | --- |
|
|
@@ -1039,7 +1039,9 @@ Catalogue version `2026.09.12`, 68 tells.
|
|
|
1039
1039
|
| `buzzword` | 1 | copy | warn | Buzzword | Cutting-edge, world-class, reach out, turnkey: corporate filler older than any model, and still the first thing a model writes about a business it knows nothing about. |
|
|
1040
1040
|
| `negative-reassurance` | 1 | copy | warn | Negative reassurance | 'No hidden fees', 'no surprises', 'never locked in' reassure by naming the fear, and plant it in a reader who did not have it. |
|
|
1041
1041
|
| `vague-word` | 1 | copy | warn | Vague word | Innovative, scalable, end-to-end, solutions: each can be true, and each is used where the writer had nothing specific to say. |
|
|
1042
|
-
| `review-phrase` | 1 | copy | warn | Review-tier phrase | Truly, genuinely, ultimately, 'the single most', 'bar none': normal English once, and a tell when they stack. The house rules keep them for review, never blocking. |
|
|
1042
|
+
| `review-phrase` | 1 | copy | warn | Review-tier phrase | Truly, genuinely, clear, ultimately, 'the single most', 'bar none': normal English once, and a tell when they stack. The house rules keep them for review, never blocking. |
|
|
1043
|
+
| `sales-tagline` | 1 | copy | warn | Sales tagline | 'So your budget goes where it counts', 'Real results', 'pages that convert': advert lines that sound like a pitch and say nothing a reader can check. |
|
|
1044
|
+
| `outcome-promise` | 1 | copy | warn | Outcome promise | 'We'll turn that into enquiries', 'once the first areas are working', 'competitors you can outdo': a hoped-for result written as if it were delivered. Rankings and enquiries depend on things nobody controls. |
|
|
1043
1045
|
| `hollow-imperative` | 1 | copy | warn | Hollow imperative | 'Discover', 'Experience', 'Transform your' as the opening verb of a heading or button is a call to action with nothing in it. |
|
|
1044
1046
|
| `rhetorical-opener` | 1 | copy | warn | Rhetorical question opener | 'Looking for...?', 'Tired of...?', 'Ready to...?' opens with the model guessing at the reader instead of telling them something. |
|
|
1045
1047
|
| `em-dash` | 2 | copy | warn | Em dash | Generated copy leans on the em dash to join clauses. House copy does not use it. |
|
package/COPY.md
CHANGED
|
@@ -86,7 +86,9 @@ person reading decides which instance earns its place.
|
|
|
86
86
|
|
|
87
87
|
| Tell | What it catches |
|
|
88
88
|
| --- | --- |
|
|
89
|
-
| `review-phrase` | truly, genuinely, deeply, ultimately, great question, absolutely, one-size-fits-all, and superlative over-claiming: "the single most", "by far the most", "hands down the", "arguably the best", "bar none" |
|
|
89
|
+
| `review-phrase` | truly, genuinely, deeply, ultimately, clear and clearly ("a clear next step", "clear pricing": say what the reader actually gets), great question, absolutely, one-size-fits-all, and superlative over-claiming: "the single most", "by far the most", "hands down the", "arguably the best", "bar none" |
|
|
90
|
+
| `sales-tagline` | advert lines that say nothing a reader can check: "so your budget goes where it counts", "real results", "results that matter", "work harder for you", "pages that convert" |
|
|
91
|
+
| `outcome-promise` | a hoped-for result written as delivered: "we'll turn that into enquiries", "once the first areas are working", "competitors you can outdo", "exactly what convinces", "guaranteed rankings", "you'll get more". Promise the work; measure and report the results |
|
|
90
92
|
| `ai-vocabulary` | tapestry, testament, realm, elevate, nestled, meticulous, unparalleled, moreover; and underscore, showcasing, pivotal, measured 10 to 14 times over-used in post-2022 text |
|
|
91
93
|
| `stock-phrase` | to the next level, one-stop shop, we pride ourselves, passionate about, peace of mind, "so here's", a journey used as a metaphor |
|
|
92
94
|
| `hollow-imperative` | Discover, Experience, Transform, Elevate as the opening verb of a heading or button |
|
package/README.md
CHANGED
|
@@ -266,6 +266,57 @@ baseline that claims more than is there, so it only shrinks: fix a breach and
|
|
|
266
266
|
lower its count in the same PR. `--update-baseline` writes the baseline that
|
|
267
267
|
makes the current run pass; run it once when the gate goes in.
|
|
268
268
|
|
|
269
|
+
## Every viewport
|
|
270
|
+
|
|
271
|
+
Fitting at 375 is the floor (STANDARD.md section 10, DOM-787). The
|
|
272
|
+
every-viewport measure takes a page through the ladder (320, 375, 390, 430,
|
|
273
|
+
landscape 844 x 390, 768, 1024, 1280, 1440, 1920, phone first in order and in
|
|
274
|
+
every report) and a width sweep from 320 to 1920 in 16 to 32px steps, resizing
|
|
275
|
+
the one loaded page in place:
|
|
276
|
+
|
|
277
|
+
- **At any width:** sideways scroll, visible text over other visible text, a
|
|
278
|
+
cut-off page title (`h1`), and text under 12px. A failure names the exact
|
|
279
|
+
widths: `(at 320-352, 400)`.
|
|
280
|
+
- **On a short phone (375 x 667):** the main action (`mainAction`, a selector,
|
|
281
|
+
global or per route) is on the first screen or pinned; fixed and sticky
|
|
282
|
+
layers together cover at most 25% of the screen; an image is not more than
|
|
283
|
+
2.5 times wider than shown (`oversized`) nor narrower than shown
|
|
284
|
+
(`upscaled`); a field that looks like a phone, email or number uses
|
|
285
|
+
`type="tel"`, `type="email"` or an `inputmode`; a name, email, phone or
|
|
286
|
+
address field carries `autocomplete`; a UK phone number in the text is a
|
|
287
|
+
`tel:` link.
|
|
288
|
+
- **On a landscape phone (844 x 390):** header and fixed layers leave at least
|
|
289
|
+
half the height.
|
|
290
|
+
- **At 1440 and 1920:** body lines are at most 80 characters (width over the
|
|
291
|
+
`ch` of the element's own font) and no image is stretched past its file.
|
|
292
|
+
|
|
293
|
+
Each threshold has a house default (`DEFAULT_THRESHOLDS`) and can be
|
|
294
|
+
overridden per site or portal.
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
import { PHONE, measureViewports } from "@domandigital/craft/phone";
|
|
298
|
+
|
|
299
|
+
test.use(PHONE);
|
|
300
|
+
test(`every viewport: ${route}`, async ({ page }) => {
|
|
301
|
+
await page.goto(route);
|
|
302
|
+
// Resizes this page in place; writes phone-first/measured/<route>.json.
|
|
303
|
+
await measureViewports(page, { route, mainAction: ".hero a" });
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`measureViewports` works on the page the suite opened and signed in, so a
|
|
308
|
+
portal keeps its fixtures. `measureSite(browser, { base, routes })` opens each
|
|
309
|
+
route itself, the phone half in a phone context and the wide half in a desktop
|
|
310
|
+
one, for a page anyone can open: create-site's `measure.mjs` uses it.
|
|
311
|
+
`craft viewports --url <base> --route /` is the same for the command line: it
|
|
312
|
+
measures, writes the measured routes, and judges them against the same
|
|
313
|
+
`phone-first.baseline.json`. The new checks are baselined like the 375px ones:
|
|
314
|
+
existing failures are listed (one line per element and check, with the widths
|
|
315
|
+
left out so a failure at one more width is not a new failure), a new failure
|
|
316
|
+
fails, and a baseline that claims more than is there fails too.
|
|
317
|
+
`craft phone-first --measured` judges the same files, and keeps working on
|
|
318
|
+
routes measured only at 375 with `measurePhone`.
|
|
319
|
+
|
|
269
320
|
## API
|
|
270
321
|
|
|
271
322
|
| Area | Exports |
|
package/STANDARD.md
CHANGED
|
@@ -161,6 +161,8 @@ Mobile first is a rule about **order and floor**, not about a layout. The phone
|
|
|
161
161
|
is designed, built and measured first, and every wider layout is derived from
|
|
162
162
|
it. A desktop layout squeezed onto a phone keeps the desktop's priorities and
|
|
163
163
|
drops whatever did not fit; a phone layout widened keeps what matters and adds.
|
|
164
|
+
Fitting on a phone is the floor. The bar is a page optimised for the phone and
|
|
165
|
+
intended at every width, and the two sub-sections below set it.
|
|
164
166
|
|
|
165
167
|
- **The phone is the base.** Base styles are the phone layout. Wider layouts
|
|
166
168
|
are added with `min-width` queries only (Tailwind: unprefixed is the phone,
|
|
@@ -169,7 +171,7 @@ drops whatever did not fit; a phone layout widened keeps what matters and adds.
|
|
|
169
171
|
- **375px is the floor.** Every surface is designed and measured at **375 CSS
|
|
170
172
|
px** first, the narrowest iPhone width in common use (SE, mini, X to 11 Pro).
|
|
171
173
|
Nothing scrolls sideways at 375. 390 is still measured: it is the width the
|
|
172
|
-
design is accepted at.
|
|
174
|
+
design is accepted at. 320 still works (see "Every viewport is intended").
|
|
173
175
|
- **Touch targets at least 44 × 44 CSS px.** A link inside a sentence is exempt,
|
|
174
176
|
as in WCAG's own inline exception. 24px (SC 2.5.8, AA) is what an audit asks
|
|
175
177
|
for; 44 is the house bar.
|
|
@@ -182,6 +184,86 @@ drops whatever did not fit; a phone layout widened keeps what matters and adds.
|
|
|
182
184
|
|
|
183
185
|
*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
186
|
|
|
187
|
+
### Optimised for the phone
|
|
188
|
+
|
|
189
|
+
A visitor on a phone has a small screen, one thumb, a slow connection and a
|
|
190
|
+
task. The page is built for that and measured on a **short phone, 375 × 667**
|
|
191
|
+
(an iPhone SE), the least height a visitor has.
|
|
192
|
+
|
|
193
|
+
- **The first screen says what this is and offers the main action.** At
|
|
194
|
+
375 × 667, with no scroll, the visitor can read what the page is and who it
|
|
195
|
+
is for, and can reach the main action: call, book, buy or enquire. On a long
|
|
196
|
+
page the main action stays in thumb reach, pinned to the bottom edge or
|
|
197
|
+
repeated, because the lower half is the half a thumb reaches.
|
|
198
|
+
- **Thumb reach.** Most people tap with one thumb. Frequent actions sit in the
|
|
199
|
+
lower two thirds of the screen; a control that only the top corner holds is
|
|
200
|
+
for rare use.
|
|
201
|
+
- **Use what a phone can do.** A telephone number is a `tel:` link, a text
|
|
202
|
+
message address an `sms:` link and a street address a map link, so one tap
|
|
203
|
+
calls, messages or navigates. Each field asks for the keyboard that fits:
|
|
204
|
+
`type="tel"`, `type="email"`, `inputmode="numeric"` or `"decimal"`. A field
|
|
205
|
+
that asks for personal data carries `autocomplete` (`name`, `email`, `tel`,
|
|
206
|
+
`street-address`, `postal-code`), so the phone fills it. Nothing needs hover:
|
|
207
|
+
what hover reveals is also there on tap and on focus. Anything pinned to an
|
|
208
|
+
edge keeps clear of the safe area (`env(safe-area-inset-*)`, with
|
|
209
|
+
`viewport-fit=cover`), so the notch, the home indicator and rounded corners
|
|
210
|
+
never cover it.
|
|
211
|
+
- **Overlays take at most a quarter of a short screen.** The header, a cookie
|
|
212
|
+
bar, a chat button and every other fixed or sticky layer together cover at
|
|
213
|
+
most 25% of 375 × 667. A dialog the visitor opens is theirs and is not
|
|
214
|
+
counted; a layer that is there on arrival is.
|
|
215
|
+
- **Images are sized for the phone.** `srcset` and `sizes` give a 2x screen a
|
|
216
|
+
file about twice its displayed width. A file more than about 2.5 times its
|
|
217
|
+
displayed width is oversized: the phone downloads pixels it throws away. A
|
|
218
|
+
file narrower than its displayed width is upscaled and soft.
|
|
219
|
+
- **It is fast on a throttled phone.** On the mobile Lighthouse profile
|
|
220
|
+
(simulated slow 4G, 4x CPU slowdown): Largest Contentful Paint 2.5 s or
|
|
221
|
+
less, Interaction to Next Paint 200 ms or less, Cumulative Layout Shift 0.1
|
|
222
|
+
or less.
|
|
223
|
+
- **Landscape is usable.** A phone is held either way up. At 844 × 390 nothing
|
|
224
|
+
needed is hidden behind a fixed layer, the header and fixed layers leave at
|
|
225
|
+
least half the height, and no content is cut off.
|
|
226
|
+
|
|
227
|
+
*Source: Steven Hoober, "How Do Users Really Hold Mobile Devices?" (UXmatters, 2013) and "Design for Fingers, Touch, and People" (UXmatters, 2017); Josh Clark, "Designing for Touch" (A Book Apart, 2015); RFC 3966 (`tel:`), RFC 5724 (`sms:`) and RFC 5870 (`geo:`); WHATWG HTML Standard, `inputmode` and the autofill field names; WCAG 2.2 SC 1.3.5 (Identify Input Purpose) and SC 1.3.4 (Orientation); W3C Media Queries Level 4, the `hover` and `pointer` features; CSS Environment Variables Module Level 1 (`env(safe-area-inset-*)`) and WebKit, "Designing Websites for iPhone X" (2017); Google Search Central, "Helping users easily access content on mobile" (intrusive interstitials); web.dev, "Web Vitals" (the good thresholds for LCP, INP and CLS) and "Serve responsive images".*
|
|
228
|
+
|
|
229
|
+
### Every viewport is intended
|
|
230
|
+
|
|
231
|
+
A page is seen at every width from a 320px phone to a 1920px screen, and each
|
|
232
|
+
width gets a layout someone meant. Five bands each look designed:
|
|
233
|
+
|
|
234
|
+
| Band | Width in CSS px |
|
|
235
|
+
| --- | --- |
|
|
236
|
+
| Phone | 320 to 479 |
|
|
237
|
+
| Large phone | 480 to 767 |
|
|
238
|
+
| Tablet | 768 to 1023 |
|
|
239
|
+
| Laptop | 1024 to 1439 |
|
|
240
|
+
| Wide | 1440 and up |
|
|
241
|
+
|
|
242
|
+
- **A tablet is a layout of its own.** At 768 to 1023 the page is neither the
|
|
243
|
+
phone stretched across the width nor the laptop squeezed to fit. The tablet
|
|
244
|
+
band is designed and captured like the others.
|
|
245
|
+
- **320 works.** Content reflows at 320 CSS px with no sideways scroll and no
|
|
246
|
+
loss of information or function (WCAG SC 1.4.10, the same as 400% zoom on a
|
|
247
|
+
1280px screen).
|
|
248
|
+
- **Lines are 45 to 80 characters.** Body text keeps a readable measure at
|
|
249
|
+
every width. On a wide screen it is capped (`max-width` in `ch`), and the
|
|
250
|
+
layout holds to a sensible width instead of stretching with the window.
|
|
251
|
+
Images are never shown larger than their file.
|
|
252
|
+
- **The sweep catches what the breakpoints miss.** The ladder (320, 375, 390,
|
|
253
|
+
430, 844 × 390 landscape, 768, 1024, 1280, 1440, 1920) is captured, and one
|
|
254
|
+
page load is resized from 320 to 1920 in steps of 16 to 32px. At any width
|
|
255
|
+
nothing scrolls sideways, no visible text sits on other visible text and no
|
|
256
|
+
page title is cut off. The report names the exact widths that fail, phone
|
|
257
|
+
first.
|
|
258
|
+
|
|
259
|
+
The measure is `@domandigital/craft/phone` (`measureViewports`,
|
|
260
|
+
`measureSite`, `craft viewports`): one measure that create-site's
|
|
261
|
+
`measure.mjs` and every portal gate use. Each check can be baselined: a
|
|
262
|
+
repo's existing failures are listed in the open, a new failure fails, and the
|
|
263
|
+
baseline only shrinks.
|
|
264
|
+
|
|
265
|
+
*Source: WCAG 2.2 SC 1.4.10 (Reflow) and SC 1.4.8 (Visual Presentation: no more than 80 characters a line); Robert Bringhurst, "The Elements of Typographic Style", chapter 2, "Rhythm and Proportion" (45 to 75 characters a line); Ethan Marcotte, "Responsive Web Design" (A List Apart, 2010); web.dev, "Responsive web design basics".*
|
|
266
|
+
|
|
185
267
|
## 11. What is deliberately not a rule
|
|
186
268
|
|
|
187
269
|
The package emits **plain CSS custom properties** precisely so these stay free:
|
|
@@ -193,8 +275,9 @@ The package emits **plain CSS custom properties** precisely so these stay free:
|
|
|
193
275
|
- **Where tokens live.** A `:root` block, a generated file, a Tailwind theme:
|
|
194
276
|
the package does not care.
|
|
195
277
|
- **Layout, grid and component structure.** Not a rule, at any width. Section
|
|
196
|
-
10 fixes the order the widths are designed in
|
|
197
|
-
|
|
278
|
+
10 fixes the order the widths are designed in, the floor they are measured
|
|
279
|
+
at and the five bands each is designed for; what each layout looks like
|
|
280
|
+
stays free. Layout *defaults*
|
|
198
281
|
(hero straight into a trust strip, three icon cards, a reveal on every
|
|
199
282
|
section) are measured as character in [CHARACTER.md](./CHARACTER.md), as
|
|
200
283
|
warnings with a declared-exception path, never as rules here.
|
|
@@ -9,10 +9,10 @@ import {
|
|
|
9
9
|
snapshotPage,
|
|
10
10
|
snapshotUrl,
|
|
11
11
|
snapshotUrls
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-YOZLPNJS.js";
|
|
13
13
|
import "./chunk-JYNXGCYZ.js";
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-JGWOA4LI.js";
|
|
15
|
+
import "./chunk-7DQDF3VL.js";
|
|
16
16
|
import "./chunk-QYBAVU3P.js";
|
|
17
17
|
export {
|
|
18
18
|
PROVENANCE_BUDGET_MS,
|
package/dist/audit.cjs
CHANGED
|
@@ -218,6 +218,8 @@ var REVIEW_PHRASES = [
|
|
|
218
218
|
"truly",
|
|
219
219
|
"genuinely",
|
|
220
220
|
"deeply",
|
|
221
|
+
"clear",
|
|
222
|
+
"clearly",
|
|
221
223
|
"ultimately",
|
|
222
224
|
"one-size-fits-all",
|
|
223
225
|
"one size fits all",
|
|
@@ -228,6 +230,33 @@ var REVIEW_PHRASES = [
|
|
|
228
230
|
"arguably the best",
|
|
229
231
|
"bar none"
|
|
230
232
|
];
|
|
233
|
+
var SALES_TAGLINES = [
|
|
234
|
+
/\bwhere it (?:really |truly )?(?:counts|matters)(?: most)?\b/i,
|
|
235
|
+
/\breal,? (?:measurable |tangible )?results\b/i,
|
|
236
|
+
/\bresults that (?:matter|count|speak|last)\b/i,
|
|
237
|
+
/\bresults you can (?:see|trust|measure)\b/i,
|
|
238
|
+
/\bwork(?:s|ing)? harder for you\b/i,
|
|
239
|
+
/\bmake (?:every|your) (?:penny|pound) count\b/i,
|
|
240
|
+
/\bevery (?:penny|pound) (?:counts|works)\b/i,
|
|
241
|
+
/\bbang for your buck\b/i,
|
|
242
|
+
/\bstand out from the (?:crowd|competition)\b/i,
|
|
243
|
+
/\b(?:pages?|websites?|sites?|copy|content|ads?) that (?:actually |really )?(?:converts?|sells?)\b/i
|
|
244
|
+
];
|
|
245
|
+
var SALES_RESULTS = "(?:enquiries|leads|customers|clients|sales|jobs|bookings|revenue|calls|traffic|profit)";
|
|
246
|
+
var OUTCOME_PROMISES = [
|
|
247
|
+
// "We'll turn more of that into rewiring enquiries"
|
|
248
|
+
new RegExp(`\\bturn(?:s|ing)?\\b[^.!?\\n]{0,50}?\\binto\\s+(?:more\\s+|real\\s+|paying\\s+)?(?:[\\w-]+\\s+)?${SALES_RESULTS}\\b`, "i"),
|
|
249
|
+
// "once the first areas are working"
|
|
250
|
+
/\bonce\b[^.!?\n]{0,40}?\b(?:is|are|starts?|begins?)\s+(?:working|paying off|performing|ranking|delivering|bringing in)\b/i,
|
|
251
|
+
// "competitors you can outdo"
|
|
252
|
+
/\b(?:competitors?|competition|rivals?)\b[^.!?\n]{0,25}?\b(?:you|we)\s*(?:can|will|could|['’]ll)\s+(?:easily\s+)?(?:outdo|beat|outrank|overtake|outperform|outshine)\b/i,
|
|
253
|
+
/\b(?:outdo|outrank|overtake|outperform|outshine|beat)\s+(?:your|the|local|bigger)\s+(?:[\w-]+\s+)?(?:competitors|competition|rivals)\b/i,
|
|
254
|
+
// "exactly what convinces someone spending £8k"
|
|
255
|
+
/\bexactly what\b[^.!?\n]{0,15}?\b(?:convinces|persuades|wins|sells)\b/i,
|
|
256
|
+
new RegExp(`\\bguarantee[sd]?\\b[^.!?\\n]{0,25}?\\b(?:results|rankings?|first page|page one|top spot|${SALES_RESULTS.slice(3, -1)})\\b`, "i"),
|
|
257
|
+
/\byou(?:['’]ll| will)\s+(?:get|see|win|receive|start getting)\s+more\b/i,
|
|
258
|
+
new RegExp(`\\b(?:will|we['\u2019]ll)\\s+(?:(?:bring|drive|generate|get)\\s+you|bring|drive|generate)\\s+more\\s+(?:[\\w-]+\\s+)?${SALES_RESULTS}\\b`, "i")
|
|
259
|
+
];
|
|
231
260
|
var NEGATION = [
|
|
232
261
|
// "It's not just a haircut, it's an experience."
|
|
233
262
|
/\b(?:it['’]?s|it is|this is|this isn['’]t|we['’]re|we are|they['’]re)\s+(?:not|more than)\s+(?:just|only|merely|simply)?\s*[^.!?\n]{1,60}?[,;.—-]\s*(?:it['’]?s|it is|this is|we['’]re|we are|they['’]re)\b/i,
|
|
@@ -413,12 +442,55 @@ var COPY_TELLS = [
|
|
|
413
442
|
generation: 1,
|
|
414
443
|
severity: "warn",
|
|
415
444
|
surface: "copy",
|
|
416
|
-
why: "Truly, genuinely, ultimately, 'the single most', 'bar none': normal English once, and a tell when they stack. The house rules keep them for review, never blocking.",
|
|
445
|
+
why: "Truly, genuinely, clear, ultimately, 'the single most', 'bar none': normal English once, and a tell when they stack. The house rules keep them for review, never blocking.",
|
|
417
446
|
fix: "Keep the one that lands hardest and cut the rest. 'What is genuinely excellent' reads better as 'what is excellent'.",
|
|
418
|
-
detect: phrases([new RegExp(`(?:^|\\b)(?:${[...REVIEW_PHRASES].sort((a, b) => b.length - a.length).map((p) => escape(p).replace(/^(\w)/, "\\b$1")).join("|")})`, "i")], quoted),
|
|
447
|
+
detect: phrases([new RegExp(`(?:^|\\b)(?:${[...REVIEW_PHRASES].sort((a, b) => b.length - a.length).map((p) => escape(p).replace(/^(\w)/, "\\b$1").replace(/(\w)$/, "$1\\b")).join("|")})`, "i")], quoted),
|
|
448
|
+
fixtures: {
|
|
449
|
+
flag: [f("content/home.md", "We are truly proud of it."), f("content/home.md", "You leave with a clear next step.")],
|
|
450
|
+
pass: [f("content/home.md", "We have fitted 400 boilers since 2009."), f("content/home.md", "Keep a safe clearance around the meter.")]
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
id: "sales-tagline",
|
|
455
|
+
name: "Sales tagline",
|
|
456
|
+
generation: 1,
|
|
457
|
+
severity: "warn",
|
|
458
|
+
surface: "copy",
|
|
459
|
+
why: "'So your budget goes where it counts', 'Real results', 'pages that convert': advert lines that sound like a pitch and say nothing a reader can check.",
|
|
460
|
+
fix: "Say what you will actually do and why. 'So your budget goes where it counts' becomes 'Oxford first, because that is where you want the work'.",
|
|
461
|
+
detect: phrases(SALES_TAGLINES, quoted),
|
|
462
|
+
fixtures: {
|
|
463
|
+
flag: [
|
|
464
|
+
f("content/home.md", "Strongest opportunities first, so your budget goes where it counts."),
|
|
465
|
+
f("content/home.md", "A clearer online presence. More enquiries. Real results.")
|
|
466
|
+
],
|
|
467
|
+
pass: [
|
|
468
|
+
f("content/home.md", "We start with Oxford, then expand based on search demand and results."),
|
|
469
|
+
f("content/home.md", "We report the results every month.")
|
|
470
|
+
]
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
id: "outcome-promise",
|
|
475
|
+
name: "Outcome promise",
|
|
476
|
+
generation: 1,
|
|
477
|
+
severity: "warn",
|
|
478
|
+
surface: "copy",
|
|
479
|
+
why: "'We'll turn that into enquiries', 'once the first areas are working', 'competitors you can outdo': a hoped-for result written as if it were delivered. Rankings and enquiries depend on things nobody controls.",
|
|
480
|
+
fix: "Promise the work, not the outcome. 'We'll turn that into enquiries' becomes 'We'll rewrite the page around the questions homeowners ask'. Results are measured and reported, never promised.",
|
|
481
|
+
detect: phrases(OUTCOME_PROMISES, quoted),
|
|
419
482
|
fixtures: {
|
|
420
|
-
flag: [
|
|
421
|
-
|
|
483
|
+
flag: [
|
|
484
|
+
f("content/home.md", "We'll turn more of that into rewiring enquiries."),
|
|
485
|
+
f("content/home.md", "We add new towns once the first areas are working."),
|
|
486
|
+
f("content/home.md", "We target the competitors you can outdo."),
|
|
487
|
+
f("content/home.md", "We write exactly what convinces someone spending \xA38k.")
|
|
488
|
+
],
|
|
489
|
+
pass: [
|
|
490
|
+
f("content/home.md", "We'll rewrite your rewiring page around homeowners' questions."),
|
|
491
|
+
f("content/home.md", "We record every rewiring enquiry, quotation and won job."),
|
|
492
|
+
f("content/home.md", "Once the fuse board is fitted, we test every circuit.")
|
|
493
|
+
]
|
|
422
494
|
}
|
|
423
495
|
},
|
|
424
496
|
{
|
|
@@ -2996,6 +3068,9 @@ var HOUSE = {
|
|
|
2996
3068
|
// on 2026-09-24 after its estate hits were read (ROADMAP.md): 15 of 15 real.
|
|
2997
3069
|
"chatbot-residue": { tier: "block", label: "chatbot residue. A chat reply was pasted in: remove it, and source or cut the claim it backed" },
|
|
2998
3070
|
"review-phrase": { tier: "review", label: "review tier" },
|
|
3071
|
+
// New 2026-10-14 (founder): review first, like every new tell, until estate hits are read.
|
|
3072
|
+
"sales-tagline": { tier: "review", label: "sales tagline, say what you will actually do" },
|
|
3073
|
+
"outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
|
|
2999
3074
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
3000
3075
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
3001
3076
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -3737,7 +3812,7 @@ var IMAGERY_TELLS = [
|
|
|
3737
3812
|
];
|
|
3738
3813
|
|
|
3739
3814
|
// src/character/check.ts
|
|
3740
|
-
var CATALOGUE_VERSION = "2026.
|
|
3815
|
+
var CATALOGUE_VERSION = "2026.10.2";
|
|
3741
3816
|
var CATALOGUE = [
|
|
3742
3817
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
3743
3818
|
...SPECIFICITY_COPY_TELLS,
|
package/dist/audit.js
CHANGED
|
@@ -183,6 +183,8 @@ var REVIEW_PHRASES = [
|
|
|
183
183
|
"truly",
|
|
184
184
|
"genuinely",
|
|
185
185
|
"deeply",
|
|
186
|
+
"clear",
|
|
187
|
+
"clearly",
|
|
186
188
|
"ultimately",
|
|
187
189
|
"one-size-fits-all",
|
|
188
190
|
"one size fits all",
|
|
@@ -193,6 +195,33 @@ var REVIEW_PHRASES = [
|
|
|
193
195
|
"arguably the best",
|
|
194
196
|
"bar none"
|
|
195
197
|
];
|
|
198
|
+
var SALES_TAGLINES = [
|
|
199
|
+
/\bwhere it (?:really |truly )?(?:counts|matters)(?: most)?\b/i,
|
|
200
|
+
/\breal,? (?:measurable |tangible )?results\b/i,
|
|
201
|
+
/\bresults that (?:matter|count|speak|last)\b/i,
|
|
202
|
+
/\bresults you can (?:see|trust|measure)\b/i,
|
|
203
|
+
/\bwork(?:s|ing)? harder for you\b/i,
|
|
204
|
+
/\bmake (?:every|your) (?:penny|pound) count\b/i,
|
|
205
|
+
/\bevery (?:penny|pound) (?:counts|works)\b/i,
|
|
206
|
+
/\bbang for your buck\b/i,
|
|
207
|
+
/\bstand out from the (?:crowd|competition)\b/i,
|
|
208
|
+
/\b(?:pages?|websites?|sites?|copy|content|ads?) that (?:actually |really )?(?:converts?|sells?)\b/i
|
|
209
|
+
];
|
|
210
|
+
var SALES_RESULTS = "(?:enquiries|leads|customers|clients|sales|jobs|bookings|revenue|calls|traffic|profit)";
|
|
211
|
+
var OUTCOME_PROMISES = [
|
|
212
|
+
// "We'll turn more of that into rewiring enquiries"
|
|
213
|
+
new RegExp(`\\bturn(?:s|ing)?\\b[^.!?\\n]{0,50}?\\binto\\s+(?:more\\s+|real\\s+|paying\\s+)?(?:[\\w-]+\\s+)?${SALES_RESULTS}\\b`, "i"),
|
|
214
|
+
// "once the first areas are working"
|
|
215
|
+
/\bonce\b[^.!?\n]{0,40}?\b(?:is|are|starts?|begins?)\s+(?:working|paying off|performing|ranking|delivering|bringing in)\b/i,
|
|
216
|
+
// "competitors you can outdo"
|
|
217
|
+
/\b(?:competitors?|competition|rivals?)\b[^.!?\n]{0,25}?\b(?:you|we)\s*(?:can|will|could|['’]ll)\s+(?:easily\s+)?(?:outdo|beat|outrank|overtake|outperform|outshine)\b/i,
|
|
218
|
+
/\b(?:outdo|outrank|overtake|outperform|outshine|beat)\s+(?:your|the|local|bigger)\s+(?:[\w-]+\s+)?(?:competitors|competition|rivals)\b/i,
|
|
219
|
+
// "exactly what convinces someone spending £8k"
|
|
220
|
+
/\bexactly what\b[^.!?\n]{0,15}?\b(?:convinces|persuades|wins|sells)\b/i,
|
|
221
|
+
new RegExp(`\\bguarantee[sd]?\\b[^.!?\\n]{0,25}?\\b(?:results|rankings?|first page|page one|top spot|${SALES_RESULTS.slice(3, -1)})\\b`, "i"),
|
|
222
|
+
/\byou(?:['’]ll| will)\s+(?:get|see|win|receive|start getting)\s+more\b/i,
|
|
223
|
+
new RegExp(`\\b(?:will|we['\u2019]ll)\\s+(?:(?:bring|drive|generate|get)\\s+you|bring|drive|generate)\\s+more\\s+(?:[\\w-]+\\s+)?${SALES_RESULTS}\\b`, "i")
|
|
224
|
+
];
|
|
196
225
|
var NEGATION = [
|
|
197
226
|
// "It's not just a haircut, it's an experience."
|
|
198
227
|
/\b(?:it['’]?s|it is|this is|this isn['’]t|we['’]re|we are|they['’]re)\s+(?:not|more than)\s+(?:just|only|merely|simply)?\s*[^.!?\n]{1,60}?[,;.—-]\s*(?:it['’]?s|it is|this is|we['’]re|we are|they['’]re)\b/i,
|
|
@@ -378,12 +407,55 @@ var COPY_TELLS = [
|
|
|
378
407
|
generation: 1,
|
|
379
408
|
severity: "warn",
|
|
380
409
|
surface: "copy",
|
|
381
|
-
why: "Truly, genuinely, ultimately, 'the single most', 'bar none': normal English once, and a tell when they stack. The house rules keep them for review, never blocking.",
|
|
410
|
+
why: "Truly, genuinely, clear, ultimately, 'the single most', 'bar none': normal English once, and a tell when they stack. The house rules keep them for review, never blocking.",
|
|
382
411
|
fix: "Keep the one that lands hardest and cut the rest. 'What is genuinely excellent' reads better as 'what is excellent'.",
|
|
383
|
-
detect: phrases([new RegExp(`(?:^|\\b)(?:${[...REVIEW_PHRASES].sort((a, b) => b.length - a.length).map((p) => escape(p).replace(/^(\w)/, "\\b$1")).join("|")})`, "i")], quoted),
|
|
412
|
+
detect: phrases([new RegExp(`(?:^|\\b)(?:${[...REVIEW_PHRASES].sort((a, b) => b.length - a.length).map((p) => escape(p).replace(/^(\w)/, "\\b$1").replace(/(\w)$/, "$1\\b")).join("|")})`, "i")], quoted),
|
|
413
|
+
fixtures: {
|
|
414
|
+
flag: [f("content/home.md", "We are truly proud of it."), f("content/home.md", "You leave with a clear next step.")],
|
|
415
|
+
pass: [f("content/home.md", "We have fitted 400 boilers since 2009."), f("content/home.md", "Keep a safe clearance around the meter.")]
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
id: "sales-tagline",
|
|
420
|
+
name: "Sales tagline",
|
|
421
|
+
generation: 1,
|
|
422
|
+
severity: "warn",
|
|
423
|
+
surface: "copy",
|
|
424
|
+
why: "'So your budget goes where it counts', 'Real results', 'pages that convert': advert lines that sound like a pitch and say nothing a reader can check.",
|
|
425
|
+
fix: "Say what you will actually do and why. 'So your budget goes where it counts' becomes 'Oxford first, because that is where you want the work'.",
|
|
426
|
+
detect: phrases(SALES_TAGLINES, quoted),
|
|
427
|
+
fixtures: {
|
|
428
|
+
flag: [
|
|
429
|
+
f("content/home.md", "Strongest opportunities first, so your budget goes where it counts."),
|
|
430
|
+
f("content/home.md", "A clearer online presence. More enquiries. Real results.")
|
|
431
|
+
],
|
|
432
|
+
pass: [
|
|
433
|
+
f("content/home.md", "We start with Oxford, then expand based on search demand and results."),
|
|
434
|
+
f("content/home.md", "We report the results every month.")
|
|
435
|
+
]
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
id: "outcome-promise",
|
|
440
|
+
name: "Outcome promise",
|
|
441
|
+
generation: 1,
|
|
442
|
+
severity: "warn",
|
|
443
|
+
surface: "copy",
|
|
444
|
+
why: "'We'll turn that into enquiries', 'once the first areas are working', 'competitors you can outdo': a hoped-for result written as if it were delivered. Rankings and enquiries depend on things nobody controls.",
|
|
445
|
+
fix: "Promise the work, not the outcome. 'We'll turn that into enquiries' becomes 'We'll rewrite the page around the questions homeowners ask'. Results are measured and reported, never promised.",
|
|
446
|
+
detect: phrases(OUTCOME_PROMISES, quoted),
|
|
384
447
|
fixtures: {
|
|
385
|
-
flag: [
|
|
386
|
-
|
|
448
|
+
flag: [
|
|
449
|
+
f("content/home.md", "We'll turn more of that into rewiring enquiries."),
|
|
450
|
+
f("content/home.md", "We add new towns once the first areas are working."),
|
|
451
|
+
f("content/home.md", "We target the competitors you can outdo."),
|
|
452
|
+
f("content/home.md", "We write exactly what convinces someone spending \xA38k.")
|
|
453
|
+
],
|
|
454
|
+
pass: [
|
|
455
|
+
f("content/home.md", "We'll rewrite your rewiring page around homeowners' questions."),
|
|
456
|
+
f("content/home.md", "We record every rewiring enquiry, quotation and won job."),
|
|
457
|
+
f("content/home.md", "Once the fuse board is fitted, we test every circuit.")
|
|
458
|
+
]
|
|
387
459
|
}
|
|
388
460
|
},
|
|
389
461
|
{
|
|
@@ -2961,6 +3033,9 @@ var HOUSE = {
|
|
|
2961
3033
|
// on 2026-09-24 after its estate hits were read (ROADMAP.md): 15 of 15 real.
|
|
2962
3034
|
"chatbot-residue": { tier: "block", label: "chatbot residue. A chat reply was pasted in: remove it, and source or cut the claim it backed" },
|
|
2963
3035
|
"review-phrase": { tier: "review", label: "review tier" },
|
|
3036
|
+
// New 2026-10-14 (founder): review first, like every new tell, until estate hits are read.
|
|
3037
|
+
"sales-tagline": { tier: "review", label: "sales tagline, say what you will actually do" },
|
|
3038
|
+
"outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
|
|
2964
3039
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
2965
3040
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
2966
3041
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -3702,7 +3777,7 @@ var IMAGERY_TELLS = [
|
|
|
3702
3777
|
];
|
|
3703
3778
|
|
|
3704
3779
|
// src/character/check.ts
|
|
3705
|
-
var CATALOGUE_VERSION = "2026.
|
|
3780
|
+
var CATALOGUE_VERSION = "2026.10.2";
|
|
3706
3781
|
var CATALOGUE = [
|
|
3707
3782
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
3708
3783
|
...SPECIFICITY_COPY_TELLS,
|
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
levenshtein,
|
|
11
11
|
normaliseFamily,
|
|
12
12
|
parseColour
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-JGWOA4LI.js";
|
|
14
14
|
import {
|
|
15
15
|
TRADE_NOUNS
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-7DQDF3VL.js";
|
|
17
17
|
|
|
18
18
|
// src/direction/types.ts
|
|
19
19
|
var DIRECTION_VERSION = 2;
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
holdHouseBlocks,
|
|
11
11
|
houseRule,
|
|
12
12
|
scanSource
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-JGWOA4LI.js";
|
|
14
14
|
import {
|
|
15
15
|
compareCompetitor,
|
|
16
16
|
compareFacts,
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
pageCopy,
|
|
22
22
|
protectedFacts,
|
|
23
23
|
visibleText
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-7DQDF3VL.js";
|
|
25
25
|
|
|
26
26
|
// src/character/config.ts
|
|
27
27
|
var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
|
|
@@ -392,6 +392,7 @@ Usage
|
|
|
392
392
|
craft register brief <plain|persuasive|warm|literary> [--json]
|
|
393
393
|
craft register check <file> --as <register> [--json]
|
|
394
394
|
craft phone-first [--config phone-first.json] [--measured <dir>] [--update-baseline] [--json]
|
|
395
|
+
craft viewports [--url <base>] [--route <path>]... [--config viewports.json] [--out <dir>] [--captures <dir>] [--baseline <file>] [--update-baseline] [--no-sweep] [--json]
|
|
395
396
|
|
|
396
397
|
scan Markup, component code and stylesheets (source and compiled CSS).
|
|
397
398
|
--staged reads the git index, for a pre-commit hook.
|
|
@@ -454,6 +455,15 @@ phone-first The phone-first gate for a portal or app (STANDARD.md section
|
|
|
454
455
|
cut-off title), unless phone-first.baseline.json already lists it.
|
|
455
456
|
A baseline that claims more than is there fails too: it only
|
|
456
457
|
shrinks. Exit 1 on a breach, 2 on bad usage.
|
|
458
|
+
viewports Every viewport, for a running site (STANDARD.md section 10). Opens
|
|
459
|
+
each route at the ladder (320, 375, 390, 430, 844 x 390 landscape,
|
|
460
|
+
768, 1024, 1280, 1440, 1920) and sweeps 320 to 1920: sideways
|
|
461
|
+
scroll, text over text and a cut-off title at any width; on a
|
|
462
|
+
375 x 667 phone the main action, fixed layers, image sizes, field
|
|
463
|
+
keyboards, autocomplete and tap-to-call; landscape; line length and
|
|
464
|
+
stretched images at 1440 and 1920. Writes the measured routes for
|
|
465
|
+
craft phone-first --measured and judges them against the same
|
|
466
|
+
baseline. Needs playwright. Exit 1 on a breach, 2 on bad usage.
|
|
457
467
|
|
|
458
468
|
ci options (scan, copy, audit)
|
|
459
469
|
--baseline <file> Report only findings the baseline does not already
|
|
@@ -610,7 +620,7 @@ async function pageSnapshot(target, cwd) {
|
|
|
610
620
|
return readSnapshot(JSON.parse(readFileSync(local, "utf8")), target);
|
|
611
621
|
}
|
|
612
622
|
if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL nor a snapshot file`);
|
|
613
|
-
const { snapshotUrl } = await import("./audit-
|
|
623
|
+
const { snapshotUrl } = await import("./audit-BN2GSFKT.js");
|
|
614
624
|
return snapshotUrl(target);
|
|
615
625
|
}
|
|
616
626
|
async function briefFor(flags, cwd) {
|
|
@@ -621,7 +631,7 @@ async function briefFor(flags, cwd) {
|
|
|
621
631
|
if (typeof brief === "string" && brief.trim()) return brief;
|
|
622
632
|
} else if (flags.direction) throw new Error(`no such file: ${flags.direction}`);
|
|
623
633
|
if (flags.null) {
|
|
624
|
-
const { loadNull } = await import("./cli-
|
|
634
|
+
const { loadNull } = await import("./cli-NTNUZUFP.js");
|
|
625
635
|
return loadNull(flags.null, cwd).brief;
|
|
626
636
|
}
|
|
627
637
|
return void 0;
|
|
@@ -647,34 +657,37 @@ async function compareWithCompetitor(flags, io) {
|
|
|
647
657
|
function run(argv, io) {
|
|
648
658
|
const [command, ...rest] = argv;
|
|
649
659
|
if (command === "direction") {
|
|
650
|
-
return import("./cli-
|
|
660
|
+
return import("./cli-MKZJMZUJ.js").then((m) => m.runDirection(rest, io));
|
|
651
661
|
}
|
|
652
662
|
if (command === "report" || command === "retrofit") {
|
|
653
|
-
return import("./cli-
|
|
663
|
+
return import("./cli-VFKKFRB4.js").then((m) => m.runReport(command, rest, io));
|
|
654
664
|
}
|
|
655
665
|
if (command === "register") {
|
|
656
|
-
return import("./cli-
|
|
666
|
+
return import("./cli-ZXDGVDYU.js").then((m) => m.runRegister(rest, io));
|
|
657
667
|
}
|
|
658
668
|
if (command === "estate") {
|
|
659
|
-
return import("./cli-
|
|
669
|
+
return import("./cli-HUN5ZTH5.js").then((m) => m.runEstate(rest, io));
|
|
660
670
|
}
|
|
661
671
|
if (command === "null") {
|
|
662
|
-
return import("./cli-
|
|
672
|
+
return import("./cli-NTNUZUFP.js").then((m) => m.runNull(rest, io));
|
|
663
673
|
}
|
|
664
674
|
if (command === "calibrate") {
|
|
665
|
-
return import("./cli-
|
|
675
|
+
return import("./cli-DVYLDCEA.js").then((m) => m.runCalibrate(rest, io));
|
|
666
676
|
}
|
|
667
677
|
if (command === "tells" && rest[0] === "harvest") {
|
|
668
|
-
return import("./cli-
|
|
678
|
+
return import("./cli-NTNUZUFP.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
|
|
669
679
|
io.err(`craft: ${error.message}`);
|
|
670
680
|
return 2;
|
|
671
681
|
});
|
|
672
682
|
}
|
|
673
683
|
if (command === "phone-first") {
|
|
674
|
-
return import("./cli-
|
|
684
|
+
return import("./cli-LSJI7QRD.js").then((m) => m.runPhoneFirst(rest, io));
|
|
685
|
+
}
|
|
686
|
+
if (command === "viewports") {
|
|
687
|
+
return import("./viewports-cli-C4VXMPKK.js").then((m) => m.runViewports(rest, io));
|
|
675
688
|
}
|
|
676
689
|
if (command === "snapshot" || command === "audit") {
|
|
677
|
-
return import("./cli-
|
|
690
|
+
return import("./cli-IREKGVSM.js").then((m) => m.runAudit(command, rest, io));
|
|
678
691
|
}
|
|
679
692
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
680
693
|
io.out(HELP);
|