@domandigital/craft 0.19.3 → 0.20.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @domandigital/craft
2
2
 
3
+ ## 0.20.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 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".
8
+
3
9
  ## 0.19.3
4
10
 
5
11
  ### Patch Changes
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 and the floor they are
197
- measured at; what the phone layout looks like stays free. Layout *defaults*
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.
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  describeTypicality,
7
7
  loadNull
8
- } from "./chunk-D2FTTM4F.js";
8
+ } from "./chunk-H4UF2I7V.js";
9
9
  import {
10
10
  SOURCE_FILE,
11
11
  finish,
@@ -14,7 +14,7 @@ import {
14
14
  parseFlags,
15
15
  prepareReport,
16
16
  readPaths
17
- } from "./chunk-BK4AS2RU.js";
17
+ } from "./chunk-LZVTN2KI.js";
18
18
  import {
19
19
  toJson
20
20
  } from "./chunk-QIEDBL4J.js";
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-OQXTQQAU.js";
4
4
  import {
5
5
  parseFlags
6
- } from "./chunk-BK4AS2RU.js";
6
+ } from "./chunk-LZVTN2KI.js";
7
7
  import {
8
8
  toJson
9
9
  } from "./chunk-QIEDBL4J.js";
@@ -1,18 +1,3 @@
1
- import {
2
- SOURCE_FILE,
3
- readPaths
4
- } from "./chunk-BK4AS2RU.js";
5
- import {
6
- toJson
7
- } from "./chunk-QIEDBL4J.js";
8
- import "./chunk-WK2WMWTH.js";
9
- import "./chunk-CZ7KT452.js";
10
- import "./chunk-QYBAVU3P.js";
11
-
12
- // src/phone/cli.ts
13
- import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
14
- import { join, resolve } from "path";
15
-
16
1
  // src/phone/judge.ts
17
2
  var EMPTY_BASELINE = { static: [], measured: [] };
18
3
  var MEDIA = /@(?:custom-)?media\b([^{;]*)/g;
@@ -133,7 +118,16 @@ var LABEL = {
133
118
  target: "small target",
134
119
  field: "small field",
135
120
  text: "small text",
136
- title: "title cut off"
121
+ title: "title cut off",
122
+ overlap: "text overlaps text",
123
+ action: "main action out of reach",
124
+ layers: "fixed layers cover too much",
125
+ landscape: "landscape screen eaten by fixed layers",
126
+ image: "image not sized for its place",
127
+ input: "wrong keyboard",
128
+ autocomplete: "field the phone cannot fill in",
129
+ tel: "phone number is not a link",
130
+ linelength: "line too long"
137
131
  };
138
132
  function writeBaseline(findings, routes, note) {
139
133
  const byFile = tally(findings, (f) => f.file);
@@ -147,103 +141,11 @@ function writeBaseline(findings, routes, note) {
147
141
  };
148
142
  }
149
143
 
150
- // src/phone/cli.ts
151
- var USAGE = "usage: craft phone-first [--config phone-first.json] [--measured <dir>] [--update-baseline] [--json]";
152
- function parse(args) {
153
- const flags = { config: "phone-first.json", update: false, json: false };
154
- for (let i = 0; i < args.length; i++) {
155
- const a = args[i];
156
- if (a === "--update-baseline") flags.update = true;
157
- else if (a === "--json") flags.json = true;
158
- else if (a === "--config" || a === "--measured") {
159
- const v = args[++i];
160
- if (!v || v.startsWith("--")) return `${a} needs a value`;
161
- if (a === "--config") flags.config = v;
162
- else flags.measured = v;
163
- } else return `unknown argument ${a}`;
164
- }
165
- return flags;
166
- }
167
- function loadConfig(path) {
168
- if (!existsSync(path)) throw new Error(`no ${path}. Write one: { "source": ["<dirs with styles and components>"], "baseline": "phone-first.baseline.json" }`);
169
- const c = JSON.parse(readFileSync(path, "utf8"));
170
- if (!Array.isArray(c.source) || c.source.length === 0 || c.source.some((s) => typeof s !== "string")) throw new Error(`${path}: "source" should list the folders to scan`);
171
- if (c.ignore !== void 0 && (!Array.isArray(c.ignore) || c.ignore.some((s) => typeof s !== "string"))) throw new Error(`${path}: "ignore" should be a list of globs`);
172
- if (c.exceptions !== void 0 && !Array.isArray(c.exceptions)) throw new Error(`${path}: "exceptions" should be a list of { "file", "reason" }`);
173
- return { source: c.source, ignore: c.ignore ?? [], exceptions: c.exceptions ?? [], baseline: c.baseline ?? "phone-first.baseline.json" };
174
- }
175
- function loadBaseline(path) {
176
- if (!existsSync(path)) return EMPTY_BASELINE;
177
- const b = JSON.parse(readFileSync(path, "utf8"));
178
- return { ...b.note ? { note: b.note } : {}, static: b.static ?? [], measured: b.measured ?? [] };
179
- }
180
- function loadMeasured(dir) {
181
- if (!existsSync(dir)) throw new Error(`--measured ${dir}: no such folder. Run the portal's phone-first specs first; they write it.`);
182
- const routes = readdirSync(dir).filter((f) => f.endsWith(".json")).sort().map((f) => JSON.parse(readFileSync(join(dir, f), "utf8")));
183
- if (routes.length === 0) throw new Error(`--measured ${dir}: no measured routes in it. Did the phone-first specs run?`);
184
- return routes;
185
- }
186
- function runPhoneFirst(args, io) {
187
- const flags = parse(args);
188
- if (typeof flags === "string") {
189
- io.err(`craft phone-first: ${flags}
190
- ${USAGE}`);
191
- return 2;
192
- }
193
- try {
194
- const config = loadConfig(resolve(io.cwd, flags.config));
195
- const baselinePath = resolve(io.cwd, config.baseline);
196
- const baseline = loadBaseline(baselinePath);
197
- const exceptionProblems = checkExceptions(config.exceptions, (f) => existsSync(resolve(io.cwd, f)));
198
- const sources = config.source.filter((s2) => existsSync(resolve(io.cwd, s2)));
199
- const missing = config.source.filter((s2) => !sources.includes(s2));
200
- if (missing.length) throw new Error(`${flags.config}: source ${missing.join(", ")} not found`);
201
- const files = readPaths(sources, io.cwd, SOURCE_FILE, void 0, config.ignore);
202
- const { findings, excused } = scanPhoneFirst(files, config.exceptions);
203
- const routes = flags.measured ? loadMeasured(resolve(io.cwd, flags.measured)) : null;
204
- if (flags.update) {
205
- const next = writeBaseline(findings, routes ?? [], baseline.note);
206
- if (!routes) next.measured = baseline.measured;
207
- writeFileSync(baselinePath, `${JSON.stringify(next, null, 2)}
208
- `);
209
- io.out(`craft phone-first: wrote ${config.baseline} (${next.static.reduce((n, s2) => n + s2.count, 0)} static, ${next.measured.reduce((n, m2) => n + m2.count, 0)} measured)`);
210
- return exceptionProblems.length ? 1 : 0;
211
- }
212
- const s = judgeStatic(findings, baseline);
213
- const m = routes ? judgeMeasured(routes, baseline) : { fresh: [], stale: [], known: 0 };
214
- const failed = exceptionProblems.length + s.fresh.length + s.stale.length + m.fresh.length + m.stale.length > 0;
215
- if (flags.json) {
216
- io.out(toJson({ pass: !failed, files: files.length, excused, routes: routes?.map((r) => r.route) ?? null, exceptionProblems, static: s, measured: m }));
217
- return failed ? 1 : 0;
218
- }
219
- const section = (title, lines) => lines.length ? `
220
- ${title}
221
- ${lines.map((l) => ` ${l}`).join("\n")}` : "";
222
- const known = s.known + m.known;
223
- const summary = [
224
- `${files.length} files${routes ? ` and ${routes.length} route${routes.length === 1 ? "" : "s"} at 375px` : ""}`,
225
- excused ? `${excused} recorded exception${excused === 1 ? "" : "s"}` : null,
226
- known ? `${known} known breach${known === 1 ? "" : "es"} in ${config.baseline}` : null
227
- ].filter(Boolean).join(", ");
228
- if (!failed) {
229
- io.out(`craft phone-first: pass. ${summary}.`);
230
- return 0;
231
- }
232
- io.err(
233
- [
234
- `craft phone-first: fail. ${summary}.`,
235
- section("The exceptions:", exceptionProblems),
236
- section("Built desktop first (write the phone as the base styles and widen with min-width; a genuine exception takes `mobile-first-ok: <reason>` on its line):", s.fresh),
237
- section("Measured at 375px:", m.fresh),
238
- section("The baseline claims more than is there (good: lower it, so the fix stays fixed):", [...s.stale, ...m.stale])
239
- ].join("")
240
- );
241
- return 1;
242
- } catch (error) {
243
- io.err(`craft phone-first: ${error.message}`);
244
- return 2;
245
- }
246
- }
247
144
  export {
248
- runPhoneFirst
145
+ EMPTY_BASELINE,
146
+ scanPhoneFirst,
147
+ checkExceptions,
148
+ judgeStatic,
149
+ judgeMeasured,
150
+ writeBaseline
249
151
  };
@@ -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
@@ -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-TMLHMNWU.js");
634
+ const { loadNull } = await import("./cli-EDQNTWDD.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-APYEOHFV.js").then((m) => m.runDirection(rest, io));
660
+ return import("./cli-YUVATZD5.js").then((m) => m.runDirection(rest, io));
651
661
  }
652
662
  if (command === "report" || command === "retrofit") {
653
- return import("./cli-4LEAEOOV.js").then((m) => m.runReport(command, rest, io));
663
+ return import("./cli-5DONIRMR.js").then((m) => m.runReport(command, rest, io));
654
664
  }
655
665
  if (command === "register") {
656
666
  return import("./cli-VPARW6FY.js").then((m) => m.runRegister(rest, io));
657
667
  }
658
668
  if (command === "estate") {
659
- return import("./cli-QAYH2OX5.js").then((m) => m.runEstate(rest, io));
669
+ return import("./cli-3IAIKGCP.js").then((m) => m.runEstate(rest, io));
660
670
  }
661
671
  if (command === "null") {
662
- return import("./cli-TMLHMNWU.js").then((m) => m.runNull(rest, io));
672
+ return import("./cli-EDQNTWDD.js").then((m) => m.runNull(rest, io));
663
673
  }
664
674
  if (command === "calibrate") {
665
- return import("./cli-Q5X3W6I4.js").then((m) => m.runCalibrate(rest, io));
675
+ return import("./cli-73JCUXAV.js").then((m) => m.runCalibrate(rest, io));
666
676
  }
667
677
  if (command === "tells" && rest[0] === "harvest") {
668
- return import("./cli-TMLHMNWU.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
678
+ return import("./cli-EDQNTWDD.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-6LCOSHMW.js").then((m) => m.runPhoneFirst(rest, io));
684
+ return import("./cli-YHS2VPWQ.js").then((m) => m.runPhoneFirst(rest, io));
685
+ }
686
+ if (command === "viewports") {
687
+ return import("./viewports-cli-RYV4LDQK.js").then((m) => m.runViewports(rest, io));
675
688
  }
676
689
  if (command === "snapshot" || command === "audit") {
677
- return import("./cli-WN2GHYPF.js").then((m) => m.runAudit(command, rest, io));
690
+ return import("./cli-EOXIJOJN.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);
@@ -7,10 +7,10 @@ import {
7
7
  } from "./chunk-4LJFZH2I.js";
8
8
  import {
9
9
  loadNull
10
- } from "./chunk-D2FTTM4F.js";
10
+ } from "./chunk-H4UF2I7V.js";
11
11
  import {
12
12
  parseFlags
13
- } from "./chunk-BK4AS2RU.js";
13
+ } from "./chunk-LZVTN2KI.js";
14
14
  import {
15
15
  toJson
16
16
  } from "./chunk-QIEDBL4J.js";
@@ -4,11 +4,11 @@ import {
4
4
  formatPairs,
5
5
  loadEstate,
6
6
  runEstate
7
- } from "./chunk-6M2LK7QE.js";
7
+ } from "./chunk-RFU5CIST.js";
8
8
  import "./chunk-4LJFZH2I.js";
9
- import "./chunk-D2FTTM4F.js";
9
+ import "./chunk-H4UF2I7V.js";
10
10
  import "./chunk-OQXTQQAU.js";
11
- import "./chunk-BK4AS2RU.js";
11
+ import "./chunk-LZVTN2KI.js";
12
12
  import "./chunk-QIEDBL4J.js";
13
13
  import "./chunk-WUACK2HX.js";
14
14
  import "./chunk-WK2WMWTH.js";
@@ -3,21 +3,21 @@ import {
3
3
  } from "./chunk-OKVHWXYV.js";
4
4
  import {
5
5
  merge
6
- } from "./chunk-XVMKZ4SM.js";
6
+ } from "./chunk-3CD3DTEI.js";
7
7
  import "./chunk-VDUIIWG7.js";
8
8
  import "./chunk-JYNXGCYZ.js";
9
9
  import {
10
10
  SIBLING_AT,
11
11
  compareToEstate,
12
12
  loadEstate
13
- } from "./chunk-6M2LK7QE.js";
13
+ } from "./chunk-RFU5CIST.js";
14
14
  import {
15
15
  PAGE_TYPES,
16
16
  validateDirection
17
17
  } from "./chunk-4LJFZH2I.js";
18
18
  import {
19
19
  loadNull
20
- } from "./chunk-D2FTTM4F.js";
20
+ } from "./chunk-H4UF2I7V.js";
21
21
  import "./chunk-OQXTQQAU.js";
22
22
  import {
23
23
  COPY_FILE,
@@ -28,7 +28,7 @@ import {
28
28
  loadExceptions,
29
29
  parseFlags,
30
30
  readPaths
31
- } from "./chunk-BK4AS2RU.js";
31
+ } from "./chunk-LZVTN2KI.js";
32
32
  import {
33
33
  toJson
34
34
  } from "./chunk-QIEDBL4J.js";
@@ -4,11 +4,11 @@ import {
4
4
  import {
5
5
  expandNullTargets,
6
6
  loadNull
7
- } from "./chunk-D2FTTM4F.js";
7
+ } from "./chunk-H4UF2I7V.js";
8
8
  import "./chunk-OQXTQQAU.js";
9
9
  import {
10
10
  parseFlags
11
- } from "./chunk-BK4AS2RU.js";
11
+ } from "./chunk-LZVTN2KI.js";
12
12
  import {
13
13
  toJson
14
14
  } from "./chunk-QIEDBL4J.js";
@@ -9,9 +9,9 @@ import {
9
9
  runHarvest,
10
10
  runNull,
11
11
  serveDir
12
- } from "./chunk-D2FTTM4F.js";
12
+ } from "./chunk-H4UF2I7V.js";
13
13
  import "./chunk-OQXTQQAU.js";
14
- import "./chunk-BK4AS2RU.js";
14
+ import "./chunk-LZVTN2KI.js";
15
15
  import "./chunk-QIEDBL4J.js";
16
16
  import "./chunk-WUACK2HX.js";
17
17
  import "./chunk-WK2WMWTH.js";
@@ -4,12 +4,12 @@ import {
4
4
  merge,
5
5
  resolvePages,
6
6
  runAudit
7
- } from "./chunk-XVMKZ4SM.js";
7
+ } from "./chunk-3CD3DTEI.js";
8
8
  import "./chunk-VDUIIWG7.js";
9
9
  import "./chunk-JYNXGCYZ.js";
10
- import "./chunk-D2FTTM4F.js";
10
+ import "./chunk-H4UF2I7V.js";
11
11
  import "./chunk-OQXTQQAU.js";
12
- import "./chunk-BK4AS2RU.js";
12
+ import "./chunk-LZVTN2KI.js";
13
13
  import "./chunk-QIEDBL4J.js";
14
14
  import "./chunk-WUACK2HX.js";
15
15
  import "./chunk-WK2WMWTH.js";