@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/dist/phone.d.cts CHANGED
@@ -1,3 +1,9 @@
1
+ interface SourceFile {
2
+ /** Repo-relative path. The extension decides how the file is read. */
3
+ path: string;
4
+ text: string;
5
+ }
6
+
1
7
  /**
2
8
  * The in-page half of the phone-first gate.
3
9
  *
@@ -18,20 +24,25 @@ interface PhoneFloors {
18
24
  fieldFontPx: number;
19
25
  textPx: number;
20
26
  }
21
- type PhoneCheck = "scroll" | "target" | "field" | "text" | "title";
27
+ /**
28
+ * What a measure can find. The first five are the 375px floors (DOM-775); the
29
+ * rest are the every-viewport and phone-optimisation checks (DOM-787).
30
+ */
31
+ type PhoneCheck = "scroll" | "target" | "field" | "text" | "title" | "overlap" | "action" | "layers" | "landscape" | "image" | "input" | "autocomplete" | "tel" | "linelength";
22
32
  interface RawPhoneFinding {
23
33
  check: PhoneCheck;
24
34
  /** `<tag> "text"`: what a person looks for on the page. */
25
35
  element: string;
26
36
  detail: string;
27
37
  }
28
- declare function measurePhoneInPage(floors: PhoneFloors): RawPhoneFinding[];
29
-
30
- interface SourceFile {
31
- /** Repo-relative path. The extension decides how the file is read. */
32
- path: string;
33
- text: string;
34
- }
38
+ /**
39
+ * `floors.only` narrows the measure to some checks, for the viewports where
40
+ * only some apply (text at every width, targets and fields at phone widths).
41
+ * Left out, all five run, as they always did.
42
+ */
43
+ declare function measurePhoneInPage(floors: PhoneFloors & {
44
+ only?: PhoneCheck[];
45
+ }): RawPhoneFinding[];
35
46
 
36
47
  /**
37
48
  * The judging half of the phone-first gate: pure text work, tested without a
@@ -68,6 +79,8 @@ interface MeasuredRoute {
68
79
  width: number;
69
80
  height: number;
70
81
  };
82
+ /** Set by the every-viewport measure: the viewports and widths the findings were gathered across, phone first. */
83
+ viewports?: string[];
71
84
  findings: RawPhoneFinding[];
72
85
  }
73
86
  interface Baseline {
@@ -117,6 +130,242 @@ declare function judgeMeasured(routes: MeasuredRoute[], baseline: Baseline): Jud
117
130
  /** The baseline that makes this run pass: what is there now, counted. */
118
131
  declare function writeBaseline(findings: StaticFinding[], routes: MeasuredRoute[], note?: string): Baseline;
119
132
 
133
+ /**
134
+ * What "every viewport" means (STANDARD.md section 10, DOM-787): the ladder
135
+ * the page is measured at, the five bands it is designed in, the width sweep
136
+ * between the rungs, and the thresholds the checks judge by. Pure data and
137
+ * text work, so it is tested without a browser.
138
+ */
139
+
140
+ type Band = "phone" | "large-phone" | "tablet" | "laptop" | "wide";
141
+ /** The five bands every page is designed in. A width belongs to exactly one. */
142
+ declare const BANDS: {
143
+ band: Band;
144
+ from: number;
145
+ to: number | null;
146
+ }[];
147
+ declare const bandOf: (width: number) => Band;
148
+ interface Viewport {
149
+ /** The label a report uses: `375`, or `844x390` for a landscape phone. */
150
+ name: string;
151
+ width: number;
152
+ height: number;
153
+ /** A phone held either way up: the touch floors (targets, fields) apply. */
154
+ phone: boolean;
155
+ landscape?: boolean;
156
+ }
157
+ /**
158
+ * The ladder, phone first in order and in every report. Heights are the
159
+ * phones' own; 375 x 812 stays the house phone (`PHONE`), so a baseline made
160
+ * at 375 means the same thing here.
161
+ */
162
+ declare const LADDER: Viewport[];
163
+ /** The short phone the first screen is judged on: an iPhone SE, the least height a visitor has. */
164
+ declare const SHORT_PHONE: Viewport;
165
+ /** Widths where body line length and wide-screen image scaling are judged. */
166
+ declare const WIDE_WIDTHS: number[];
167
+ interface Sweep {
168
+ from: number;
169
+ to: number;
170
+ /** Step below `wideFrom`, where layouts change most. */
171
+ step: number;
172
+ /** Step from `wideFrom` up. */
173
+ wideStep: number;
174
+ wideFrom: number;
175
+ /** The height every swept width is laid out at. */
176
+ height: number;
177
+ }
178
+ declare const DEFAULT_SWEEP: Sweep;
179
+ /** 320, 336 ... 1024, 1056 ... 1920. */
180
+ declare function sweepWidths(sweep?: Sweep): number[];
181
+ /** House defaults for the checks. Each can be overridden per site or portal; the defaults are what the standard says. */
182
+ interface Thresholds {
183
+ /** Fixed and sticky layers together cover at most this share of a short phone's screen. */
184
+ maxLayerShare: number;
185
+ /** In landscape, header and fixed layers leave at least this share of the height: they cover at most 1 minus it. */
186
+ landscapeMaxLayerShare: number;
187
+ /** An image whose file is more than this many times its displayed width is oversized (at DPR 2 it needs 2; the rest is slack). */
188
+ oversizeRatio: number;
189
+ /** An image whose file is narrower than this many times its displayed width is upscaled. */
190
+ upscaleRatio: number;
191
+ /** Body text lines at 1440 and 1920 are at most this many characters (`ch` of the computed font). */
192
+ maxLineCh: number;
193
+ /** Two pieces of visible text overlap when the overlap is this share of the shorter one's height and at least 3px wide. */
194
+ overlapShare: number;
195
+ /** Image files under this displayed width (CSS px) are icons and marks: not judged. */
196
+ minImagePx: number;
197
+ }
198
+ declare const DEFAULT_THRESHOLDS: Thresholds;
199
+ /** The main action, as a selector: one for the whole site, or one per route (`*` is the fallback). */
200
+ type MainAction = string | Record<string, string>;
201
+ declare const mainActionFor: (config: MainAction | undefined, route: string) => string | undefined;
202
+ /**
203
+ * Widths as a person reads them: `320-352, 400, 1440`. A run is consecutive
204
+ * tested widths that all fail. A landscape or short-phone name (`844x390`)
205
+ * stands alone.
206
+ */
207
+ declare function formatWidths(failing: string[], tested: string[]): string;
208
+ /**
209
+ * One finding per element and check across every viewport, so the baseline
210
+ * counts what is on the page and not how many widths it was seen at. The count
211
+ * is the most seen at any single viewport; the detail names every viewport it
212
+ * failed at, as ranges.
213
+ */
214
+ declare function mergeViewportFindings(perViewport: {
215
+ viewport: string;
216
+ findings: RawPhoneFinding[];
217
+ }[], tested: string[]): RawPhoneFinding[];
218
+
219
+ /**
220
+ * The in-page half of the every-viewport measure (DOM-787).
221
+ *
222
+ * `measureViewportInPage` runs inside the browser through `page.evaluate`, so
223
+ * like `measurePhoneInPage` it must be self-contained: no imports, nothing
224
+ * from outside its own body. The caller has set the viewport, waited for the
225
+ * page to settle and scrolled to the top; the function judges the page as it
226
+ * stands, for whichever groups of checks it is asked for:
227
+ *
228
+ * - `sweep`: at any width, the page does not scroll sideways, no visible text
229
+ * sits on other visible text, and no page title is cut off.
230
+ * - `optimise`: on the short phone (375 x 667), the main action is on the
231
+ * first screen or pinned, fixed and sticky layers leave the screen to the
232
+ * page, images are sized for the phone, fields use the right keyboard and
233
+ * carry autocomplete, and phone numbers in the text dial.
234
+ * - `landscape`: on a phone held sideways, the header and fixed layers leave
235
+ * at least half the height.
236
+ * - `wide`: at 1440 and above, body text keeps a readable line length and no
237
+ * image is stretched past its file.
238
+ */
239
+
240
+ type InPageGroup = "sweep" | "optimise" | "landscape" | "wide";
241
+ interface ViewportArg {
242
+ groups: InPageGroup[];
243
+ thresholds: Thresholds;
244
+ /** CSS selector for the page's main action. Absent: the action check is not run. */
245
+ mainAction?: string;
246
+ /** Also fail a page with no width=device-width viewport meta, which a phone lays out at 980px. Meaningful at phone widths. */
247
+ metaViewport?: boolean;
248
+ }
249
+ declare function measureViewportInPage(arg: ViewportArg): RawPhoneFinding[];
250
+
251
+ /** What the phone measures share: the house phone, the house floors, where results go. */
252
+
253
+ /**
254
+ * The phone every surface is designed at (STANDARD.md section 10): 375 x 812,
255
+ * touch, at 2x. Spread into Playwright's `test.use` or `browser.newContext`.
256
+ */
257
+ declare const PHONE: {
258
+ readonly viewport: {
259
+ readonly width: 375;
260
+ readonly height: 812;
261
+ };
262
+ readonly deviceScaleFactor: 2;
263
+ readonly isMobile: true;
264
+ readonly hasTouch: true;
265
+ };
266
+ /** The house floors at phone width. Not per-repo settings. */
267
+ declare const PHONE_FLOORS: PhoneFloors;
268
+ /** Where `measurePhone` writes and `craft phone-first --measured` reads, unless told otherwise. */
269
+ declare const DEFAULT_MEASURED_DIR = "phone-first/measured";
270
+ /** A route as a file name: `/portal/invoices` is `portal-invoices.json`, `/` is `home.json`. */
271
+ declare const routeFile: (route: string) => string;
272
+
273
+ /**
274
+ * The runner for the every-viewport measure (DOM-787): drives a page through
275
+ * the ladder and the width sweep and hands each stop to the in-page checks.
276
+ *
277
+ * Two entries:
278
+ *
279
+ * - `measureViewports(page, options)` resizes the page it is given, in place.
280
+ * A portal's Playwright suite owns the sign-in and the fixture data, so it
281
+ * opens the route and calls this.
282
+ * - `measureSite(browser, options)` opens each route itself, once in a phone
283
+ * context (touch, 2x, mobile viewport handling) for everything under 768 and
284
+ * once in a desktop context for the rest: what create-site's measure.mjs and
285
+ * `craft viewports` use for a page anyone can open.
286
+ *
287
+ * Neither imports Playwright: the caller drives the browser.
288
+ */
289
+
290
+ interface ViewportPage {
291
+ evaluate<R, A>(fn: (arg: A) => R, arg: A): Promise<R>;
292
+ setViewportSize(size: {
293
+ width: number;
294
+ height: number;
295
+ }): Promise<void>;
296
+ }
297
+ interface MeasureViewportsOptions {
298
+ /** The route as the baseline names it, without host or query: `/portal/invoices`. */
299
+ route: string;
300
+ /** The folder to write `<route>.json` into. Default `phone-first/measured`. `false` writes nothing. */
301
+ out?: string | false;
302
+ /**
303
+ * Which half to run on this page. `all` (default) runs everything on the one
304
+ * page; `phone` stops below 768 and adds the short phone and landscape;
305
+ * `wide` runs 768 and up. A page in a desktop browser context has no business
306
+ * judging a phone, and the other way round: `measureSite` runs each half in
307
+ * the context it belongs to.
308
+ */
309
+ scope?: "all" | "phone" | "wide";
310
+ /** Override house thresholds. */
311
+ thresholds?: Partial<Thresholds>;
312
+ /** Override the 12 / 16 / 44px floors. The defaults are the house standard; a site may set `textPx` higher. */
313
+ floors?: Partial<PhoneFloors>;
314
+ /** The main action's selector: one for all routes, or per route with `*` as the fallback. Absent: not checked. */
315
+ mainAction?: MainAction;
316
+ /** The width sweep, or `false` to measure the ladder only. */
317
+ sweep?: Partial<Sweep> | false;
318
+ /** Replace the ladder. */
319
+ ladder?: Viewport[];
320
+ /** Called at each ladder viewport once the page has settled, for captures. */
321
+ onViewport?: (page: ViewportPage, viewport: Viewport, route: string) => Promise<void>;
322
+ }
323
+ interface ViewportsResult extends MeasuredRoute {
324
+ viewports: string[];
325
+ /** What each viewport found before the merge, in the order measured. */
326
+ perViewport: {
327
+ viewport: string;
328
+ findings: RawPhoneFinding[];
329
+ }[];
330
+ }
331
+ /**
332
+ * Measure the page the caller has opened at every viewport, resizing it in
333
+ * place: the ladder, the width sweep, the short phone, landscape and the wide
334
+ * checks. Writes the merged result for `craft phone-first --measured`. Does
335
+ * not judge or throw on a finding: the baseline decides.
336
+ */
337
+ declare function measureViewports(page: ViewportPage, options: MeasureViewportsOptions): Promise<ViewportsResult>;
338
+ interface SitePage extends ViewportPage {
339
+ goto(url: string): Promise<{
340
+ ok(): boolean;
341
+ status(): number | string;
342
+ } | null>;
343
+ }
344
+ interface SiteBrowser {
345
+ newContext(options: Record<string, unknown>): Promise<{
346
+ newPage(): Promise<SitePage>;
347
+ close(): Promise<void>;
348
+ }>;
349
+ }
350
+ interface MeasureSiteOptions extends Omit<MeasureViewportsOptions, "route" | "scope"> {
351
+ /** The running site, without a trailing slash. */
352
+ base: string;
353
+ routes: string[];
354
+ }
355
+ interface SiteResult {
356
+ routes: ViewportsResult[];
357
+ /** Routes that did not answer, with why. Not measured, and not passed. */
358
+ errors: {
359
+ route: string;
360
+ error: string;
361
+ }[];
362
+ }
363
+ /**
364
+ * Open every route on a running site and measure it: the phone half in a
365
+ * phone context, the wide half in a desktop one, merged per route.
366
+ */
367
+ declare function measureSite(browser: SiteBrowser, options: MeasureSiteOptions): Promise<SiteResult>;
368
+
120
369
  /**
121
370
  * `@domandigital/craft/phone`: the phone-first gate for an app behind a
122
371
  * sign-in, such as a client portal (DOM-775).
@@ -140,26 +389,16 @@ declare function writeBaseline(findings: StaticFinding[], routes: MeasuredRoute[
140
389
  * });
141
390
  * }
142
391
  *
392
+ * `measureViewports` (DOM-787) is the same for every viewport: it resizes the
393
+ * page the suite opened through the ladder and the width sweep, and adds the
394
+ * phone-optimisation and wide-screen checks. `measureSite` opens a running
395
+ * site's routes itself, and `craft viewports` is that on the command line.
396
+ * Both write the same measured files, so `craft phone-first --measured` judges
397
+ * them against the same baseline.
398
+ *
143
399
  * Nothing here imports Playwright: the page is whatever the caller drives.
144
400
  */
145
401
 
146
- /**
147
- * The phone every surface is designed at (STANDARD.md section 10): 375 x 812,
148
- * touch, at 2x. Spread into Playwright's `test.use` or `browser.newContext`.
149
- */
150
- declare const PHONE: {
151
- readonly viewport: {
152
- readonly width: 375;
153
- readonly height: 812;
154
- };
155
- readonly deviceScaleFactor: 2;
156
- readonly isMobile: true;
157
- readonly hasTouch: true;
158
- };
159
- /** The house floors at phone width. Not per-repo settings. */
160
- declare const PHONE_FLOORS: PhoneFloors;
161
- /** Where `measurePhone` writes and `craft phone-first --measured` reads, unless told otherwise. */
162
- declare const DEFAULT_MEASURED_DIR = "phone-first/measured";
163
402
  interface MinimalPage {
164
403
  evaluate<R, A>(fn: (arg: A) => R, arg: A): Promise<R>;
165
404
  viewportSize?(): {
@@ -173,8 +412,6 @@ interface MeasurePhoneOptions {
173
412
  /** The folder to write `<route>.json` into. Default `phone-first/measured`, from the working directory. */
174
413
  out?: string;
175
414
  }
176
- /** A route as a file name: `/portal/invoices` is `portal-invoices.json`, `/` is `home.json`. */
177
- declare const routeFile: (route: string) => string;
178
415
  /**
179
416
  * Measure the page as it is now and write the result for `craft phone-first`.
180
417
  * Waits for web fonts first, so text is measured in the face it is set in.
@@ -182,4 +419,4 @@ declare const routeFile: (route: string) => string;
182
419
  */
183
420
  declare function measurePhone(page: MinimalPage, options: MeasurePhoneOptions): Promise<MeasuredRoute>;
184
421
 
185
- export { type Baseline, DEFAULT_MEASURED_DIR, EMPTY_BASELINE, type FileException, type Judgement, type MeasurePhoneOptions, type MeasuredRoute, PHONE, PHONE_FLOORS, type PhoneCheck, type PhoneFloors, type RawPhoneFinding, type StaticFinding, checkExceptions, judgeMeasured, judgeStatic, measurePhone, measurePhoneInPage, normaliseElement, reasonOf, routeFile, scanPhoneFirst, writeBaseline };
422
+ export { BANDS, type Band, type Baseline, DEFAULT_MEASURED_DIR, DEFAULT_SWEEP, DEFAULT_THRESHOLDS, EMPTY_BASELINE, type FileException, type InPageGroup, type Judgement, LADDER, type MainAction, type MeasurePhoneOptions, type MeasureSiteOptions, type MeasureViewportsOptions, type MeasuredRoute, PHONE, PHONE_FLOORS, type PhoneCheck, type PhoneFloors, type RawPhoneFinding, SHORT_PHONE, type SiteBrowser, type SitePage, type SiteResult, type StaticFinding, type Sweep, type Thresholds, type Viewport, type ViewportArg, type ViewportPage, type ViewportsResult, WIDE_WIDTHS, bandOf, checkExceptions, formatWidths, judgeMeasured, judgeStatic, mainActionFor, measurePhone, measurePhoneInPage, measureSite, measureViewportInPage, measureViewports, mergeViewportFindings, normaliseElement, reasonOf, routeFile, scanPhoneFirst, sweepWidths, writeBaseline };
package/dist/phone.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ interface SourceFile {
2
+ /** Repo-relative path. The extension decides how the file is read. */
3
+ path: string;
4
+ text: string;
5
+ }
6
+
1
7
  /**
2
8
  * The in-page half of the phone-first gate.
3
9
  *
@@ -18,20 +24,25 @@ interface PhoneFloors {
18
24
  fieldFontPx: number;
19
25
  textPx: number;
20
26
  }
21
- type PhoneCheck = "scroll" | "target" | "field" | "text" | "title";
27
+ /**
28
+ * What a measure can find. The first five are the 375px floors (DOM-775); the
29
+ * rest are the every-viewport and phone-optimisation checks (DOM-787).
30
+ */
31
+ type PhoneCheck = "scroll" | "target" | "field" | "text" | "title" | "overlap" | "action" | "layers" | "landscape" | "image" | "input" | "autocomplete" | "tel" | "linelength";
22
32
  interface RawPhoneFinding {
23
33
  check: PhoneCheck;
24
34
  /** `<tag> "text"`: what a person looks for on the page. */
25
35
  element: string;
26
36
  detail: string;
27
37
  }
28
- declare function measurePhoneInPage(floors: PhoneFloors): RawPhoneFinding[];
29
-
30
- interface SourceFile {
31
- /** Repo-relative path. The extension decides how the file is read. */
32
- path: string;
33
- text: string;
34
- }
38
+ /**
39
+ * `floors.only` narrows the measure to some checks, for the viewports where
40
+ * only some apply (text at every width, targets and fields at phone widths).
41
+ * Left out, all five run, as they always did.
42
+ */
43
+ declare function measurePhoneInPage(floors: PhoneFloors & {
44
+ only?: PhoneCheck[];
45
+ }): RawPhoneFinding[];
35
46
 
36
47
  /**
37
48
  * The judging half of the phone-first gate: pure text work, tested without a
@@ -68,6 +79,8 @@ interface MeasuredRoute {
68
79
  width: number;
69
80
  height: number;
70
81
  };
82
+ /** Set by the every-viewport measure: the viewports and widths the findings were gathered across, phone first. */
83
+ viewports?: string[];
71
84
  findings: RawPhoneFinding[];
72
85
  }
73
86
  interface Baseline {
@@ -117,6 +130,242 @@ declare function judgeMeasured(routes: MeasuredRoute[], baseline: Baseline): Jud
117
130
  /** The baseline that makes this run pass: what is there now, counted. */
118
131
  declare function writeBaseline(findings: StaticFinding[], routes: MeasuredRoute[], note?: string): Baseline;
119
132
 
133
+ /**
134
+ * What "every viewport" means (STANDARD.md section 10, DOM-787): the ladder
135
+ * the page is measured at, the five bands it is designed in, the width sweep
136
+ * between the rungs, and the thresholds the checks judge by. Pure data and
137
+ * text work, so it is tested without a browser.
138
+ */
139
+
140
+ type Band = "phone" | "large-phone" | "tablet" | "laptop" | "wide";
141
+ /** The five bands every page is designed in. A width belongs to exactly one. */
142
+ declare const BANDS: {
143
+ band: Band;
144
+ from: number;
145
+ to: number | null;
146
+ }[];
147
+ declare const bandOf: (width: number) => Band;
148
+ interface Viewport {
149
+ /** The label a report uses: `375`, or `844x390` for a landscape phone. */
150
+ name: string;
151
+ width: number;
152
+ height: number;
153
+ /** A phone held either way up: the touch floors (targets, fields) apply. */
154
+ phone: boolean;
155
+ landscape?: boolean;
156
+ }
157
+ /**
158
+ * The ladder, phone first in order and in every report. Heights are the
159
+ * phones' own; 375 x 812 stays the house phone (`PHONE`), so a baseline made
160
+ * at 375 means the same thing here.
161
+ */
162
+ declare const LADDER: Viewport[];
163
+ /** The short phone the first screen is judged on: an iPhone SE, the least height a visitor has. */
164
+ declare const SHORT_PHONE: Viewport;
165
+ /** Widths where body line length and wide-screen image scaling are judged. */
166
+ declare const WIDE_WIDTHS: number[];
167
+ interface Sweep {
168
+ from: number;
169
+ to: number;
170
+ /** Step below `wideFrom`, where layouts change most. */
171
+ step: number;
172
+ /** Step from `wideFrom` up. */
173
+ wideStep: number;
174
+ wideFrom: number;
175
+ /** The height every swept width is laid out at. */
176
+ height: number;
177
+ }
178
+ declare const DEFAULT_SWEEP: Sweep;
179
+ /** 320, 336 ... 1024, 1056 ... 1920. */
180
+ declare function sweepWidths(sweep?: Sweep): number[];
181
+ /** House defaults for the checks. Each can be overridden per site or portal; the defaults are what the standard says. */
182
+ interface Thresholds {
183
+ /** Fixed and sticky layers together cover at most this share of a short phone's screen. */
184
+ maxLayerShare: number;
185
+ /** In landscape, header and fixed layers leave at least this share of the height: they cover at most 1 minus it. */
186
+ landscapeMaxLayerShare: number;
187
+ /** An image whose file is more than this many times its displayed width is oversized (at DPR 2 it needs 2; the rest is slack). */
188
+ oversizeRatio: number;
189
+ /** An image whose file is narrower than this many times its displayed width is upscaled. */
190
+ upscaleRatio: number;
191
+ /** Body text lines at 1440 and 1920 are at most this many characters (`ch` of the computed font). */
192
+ maxLineCh: number;
193
+ /** Two pieces of visible text overlap when the overlap is this share of the shorter one's height and at least 3px wide. */
194
+ overlapShare: number;
195
+ /** Image files under this displayed width (CSS px) are icons and marks: not judged. */
196
+ minImagePx: number;
197
+ }
198
+ declare const DEFAULT_THRESHOLDS: Thresholds;
199
+ /** The main action, as a selector: one for the whole site, or one per route (`*` is the fallback). */
200
+ type MainAction = string | Record<string, string>;
201
+ declare const mainActionFor: (config: MainAction | undefined, route: string) => string | undefined;
202
+ /**
203
+ * Widths as a person reads them: `320-352, 400, 1440`. A run is consecutive
204
+ * tested widths that all fail. A landscape or short-phone name (`844x390`)
205
+ * stands alone.
206
+ */
207
+ declare function formatWidths(failing: string[], tested: string[]): string;
208
+ /**
209
+ * One finding per element and check across every viewport, so the baseline
210
+ * counts what is on the page and not how many widths it was seen at. The count
211
+ * is the most seen at any single viewport; the detail names every viewport it
212
+ * failed at, as ranges.
213
+ */
214
+ declare function mergeViewportFindings(perViewport: {
215
+ viewport: string;
216
+ findings: RawPhoneFinding[];
217
+ }[], tested: string[]): RawPhoneFinding[];
218
+
219
+ /**
220
+ * The in-page half of the every-viewport measure (DOM-787).
221
+ *
222
+ * `measureViewportInPage` runs inside the browser through `page.evaluate`, so
223
+ * like `measurePhoneInPage` it must be self-contained: no imports, nothing
224
+ * from outside its own body. The caller has set the viewport, waited for the
225
+ * page to settle and scrolled to the top; the function judges the page as it
226
+ * stands, for whichever groups of checks it is asked for:
227
+ *
228
+ * - `sweep`: at any width, the page does not scroll sideways, no visible text
229
+ * sits on other visible text, and no page title is cut off.
230
+ * - `optimise`: on the short phone (375 x 667), the main action is on the
231
+ * first screen or pinned, fixed and sticky layers leave the screen to the
232
+ * page, images are sized for the phone, fields use the right keyboard and
233
+ * carry autocomplete, and phone numbers in the text dial.
234
+ * - `landscape`: on a phone held sideways, the header and fixed layers leave
235
+ * at least half the height.
236
+ * - `wide`: at 1440 and above, body text keeps a readable line length and no
237
+ * image is stretched past its file.
238
+ */
239
+
240
+ type InPageGroup = "sweep" | "optimise" | "landscape" | "wide";
241
+ interface ViewportArg {
242
+ groups: InPageGroup[];
243
+ thresholds: Thresholds;
244
+ /** CSS selector for the page's main action. Absent: the action check is not run. */
245
+ mainAction?: string;
246
+ /** Also fail a page with no width=device-width viewport meta, which a phone lays out at 980px. Meaningful at phone widths. */
247
+ metaViewport?: boolean;
248
+ }
249
+ declare function measureViewportInPage(arg: ViewportArg): RawPhoneFinding[];
250
+
251
+ /** What the phone measures share: the house phone, the house floors, where results go. */
252
+
253
+ /**
254
+ * The phone every surface is designed at (STANDARD.md section 10): 375 x 812,
255
+ * touch, at 2x. Spread into Playwright's `test.use` or `browser.newContext`.
256
+ */
257
+ declare const PHONE: {
258
+ readonly viewport: {
259
+ readonly width: 375;
260
+ readonly height: 812;
261
+ };
262
+ readonly deviceScaleFactor: 2;
263
+ readonly isMobile: true;
264
+ readonly hasTouch: true;
265
+ };
266
+ /** The house floors at phone width. Not per-repo settings. */
267
+ declare const PHONE_FLOORS: PhoneFloors;
268
+ /** Where `measurePhone` writes and `craft phone-first --measured` reads, unless told otherwise. */
269
+ declare const DEFAULT_MEASURED_DIR = "phone-first/measured";
270
+ /** A route as a file name: `/portal/invoices` is `portal-invoices.json`, `/` is `home.json`. */
271
+ declare const routeFile: (route: string) => string;
272
+
273
+ /**
274
+ * The runner for the every-viewport measure (DOM-787): drives a page through
275
+ * the ladder and the width sweep and hands each stop to the in-page checks.
276
+ *
277
+ * Two entries:
278
+ *
279
+ * - `measureViewports(page, options)` resizes the page it is given, in place.
280
+ * A portal's Playwright suite owns the sign-in and the fixture data, so it
281
+ * opens the route and calls this.
282
+ * - `measureSite(browser, options)` opens each route itself, once in a phone
283
+ * context (touch, 2x, mobile viewport handling) for everything under 768 and
284
+ * once in a desktop context for the rest: what create-site's measure.mjs and
285
+ * `craft viewports` use for a page anyone can open.
286
+ *
287
+ * Neither imports Playwright: the caller drives the browser.
288
+ */
289
+
290
+ interface ViewportPage {
291
+ evaluate<R, A>(fn: (arg: A) => R, arg: A): Promise<R>;
292
+ setViewportSize(size: {
293
+ width: number;
294
+ height: number;
295
+ }): Promise<void>;
296
+ }
297
+ interface MeasureViewportsOptions {
298
+ /** The route as the baseline names it, without host or query: `/portal/invoices`. */
299
+ route: string;
300
+ /** The folder to write `<route>.json` into. Default `phone-first/measured`. `false` writes nothing. */
301
+ out?: string | false;
302
+ /**
303
+ * Which half to run on this page. `all` (default) runs everything on the one
304
+ * page; `phone` stops below 768 and adds the short phone and landscape;
305
+ * `wide` runs 768 and up. A page in a desktop browser context has no business
306
+ * judging a phone, and the other way round: `measureSite` runs each half in
307
+ * the context it belongs to.
308
+ */
309
+ scope?: "all" | "phone" | "wide";
310
+ /** Override house thresholds. */
311
+ thresholds?: Partial<Thresholds>;
312
+ /** Override the 12 / 16 / 44px floors. The defaults are the house standard; a site may set `textPx` higher. */
313
+ floors?: Partial<PhoneFloors>;
314
+ /** The main action's selector: one for all routes, or per route with `*` as the fallback. Absent: not checked. */
315
+ mainAction?: MainAction;
316
+ /** The width sweep, or `false` to measure the ladder only. */
317
+ sweep?: Partial<Sweep> | false;
318
+ /** Replace the ladder. */
319
+ ladder?: Viewport[];
320
+ /** Called at each ladder viewport once the page has settled, for captures. */
321
+ onViewport?: (page: ViewportPage, viewport: Viewport, route: string) => Promise<void>;
322
+ }
323
+ interface ViewportsResult extends MeasuredRoute {
324
+ viewports: string[];
325
+ /** What each viewport found before the merge, in the order measured. */
326
+ perViewport: {
327
+ viewport: string;
328
+ findings: RawPhoneFinding[];
329
+ }[];
330
+ }
331
+ /**
332
+ * Measure the page the caller has opened at every viewport, resizing it in
333
+ * place: the ladder, the width sweep, the short phone, landscape and the wide
334
+ * checks. Writes the merged result for `craft phone-first --measured`. Does
335
+ * not judge or throw on a finding: the baseline decides.
336
+ */
337
+ declare function measureViewports(page: ViewportPage, options: MeasureViewportsOptions): Promise<ViewportsResult>;
338
+ interface SitePage extends ViewportPage {
339
+ goto(url: string): Promise<{
340
+ ok(): boolean;
341
+ status(): number | string;
342
+ } | null>;
343
+ }
344
+ interface SiteBrowser {
345
+ newContext(options: Record<string, unknown>): Promise<{
346
+ newPage(): Promise<SitePage>;
347
+ close(): Promise<void>;
348
+ }>;
349
+ }
350
+ interface MeasureSiteOptions extends Omit<MeasureViewportsOptions, "route" | "scope"> {
351
+ /** The running site, without a trailing slash. */
352
+ base: string;
353
+ routes: string[];
354
+ }
355
+ interface SiteResult {
356
+ routes: ViewportsResult[];
357
+ /** Routes that did not answer, with why. Not measured, and not passed. */
358
+ errors: {
359
+ route: string;
360
+ error: string;
361
+ }[];
362
+ }
363
+ /**
364
+ * Open every route on a running site and measure it: the phone half in a
365
+ * phone context, the wide half in a desktop one, merged per route.
366
+ */
367
+ declare function measureSite(browser: SiteBrowser, options: MeasureSiteOptions): Promise<SiteResult>;
368
+
120
369
  /**
121
370
  * `@domandigital/craft/phone`: the phone-first gate for an app behind a
122
371
  * sign-in, such as a client portal (DOM-775).
@@ -140,26 +389,16 @@ declare function writeBaseline(findings: StaticFinding[], routes: MeasuredRoute[
140
389
  * });
141
390
  * }
142
391
  *
392
+ * `measureViewports` (DOM-787) is the same for every viewport: it resizes the
393
+ * page the suite opened through the ladder and the width sweep, and adds the
394
+ * phone-optimisation and wide-screen checks. `measureSite` opens a running
395
+ * site's routes itself, and `craft viewports` is that on the command line.
396
+ * Both write the same measured files, so `craft phone-first --measured` judges
397
+ * them against the same baseline.
398
+ *
143
399
  * Nothing here imports Playwright: the page is whatever the caller drives.
144
400
  */
145
401
 
146
- /**
147
- * The phone every surface is designed at (STANDARD.md section 10): 375 x 812,
148
- * touch, at 2x. Spread into Playwright's `test.use` or `browser.newContext`.
149
- */
150
- declare const PHONE: {
151
- readonly viewport: {
152
- readonly width: 375;
153
- readonly height: 812;
154
- };
155
- readonly deviceScaleFactor: 2;
156
- readonly isMobile: true;
157
- readonly hasTouch: true;
158
- };
159
- /** The house floors at phone width. Not per-repo settings. */
160
- declare const PHONE_FLOORS: PhoneFloors;
161
- /** Where `measurePhone` writes and `craft phone-first --measured` reads, unless told otherwise. */
162
- declare const DEFAULT_MEASURED_DIR = "phone-first/measured";
163
402
  interface MinimalPage {
164
403
  evaluate<R, A>(fn: (arg: A) => R, arg: A): Promise<R>;
165
404
  viewportSize?(): {
@@ -173,8 +412,6 @@ interface MeasurePhoneOptions {
173
412
  /** The folder to write `<route>.json` into. Default `phone-first/measured`, from the working directory. */
174
413
  out?: string;
175
414
  }
176
- /** A route as a file name: `/portal/invoices` is `portal-invoices.json`, `/` is `home.json`. */
177
- declare const routeFile: (route: string) => string;
178
415
  /**
179
416
  * Measure the page as it is now and write the result for `craft phone-first`.
180
417
  * Waits for web fonts first, so text is measured in the face it is set in.
@@ -182,4 +419,4 @@ declare const routeFile: (route: string) => string;
182
419
  */
183
420
  declare function measurePhone(page: MinimalPage, options: MeasurePhoneOptions): Promise<MeasuredRoute>;
184
421
 
185
- export { type Baseline, DEFAULT_MEASURED_DIR, EMPTY_BASELINE, type FileException, type Judgement, type MeasurePhoneOptions, type MeasuredRoute, PHONE, PHONE_FLOORS, type PhoneCheck, type PhoneFloors, type RawPhoneFinding, type StaticFinding, checkExceptions, judgeMeasured, judgeStatic, measurePhone, measurePhoneInPage, normaliseElement, reasonOf, routeFile, scanPhoneFirst, writeBaseline };
422
+ export { BANDS, type Band, type Baseline, DEFAULT_MEASURED_DIR, DEFAULT_SWEEP, DEFAULT_THRESHOLDS, EMPTY_BASELINE, type FileException, type InPageGroup, type Judgement, LADDER, type MainAction, type MeasurePhoneOptions, type MeasureSiteOptions, type MeasureViewportsOptions, type MeasuredRoute, PHONE, PHONE_FLOORS, type PhoneCheck, type PhoneFloors, type RawPhoneFinding, SHORT_PHONE, type SiteBrowser, type SitePage, type SiteResult, type StaticFinding, type Sweep, type Thresholds, type Viewport, type ViewportArg, type ViewportPage, type ViewportsResult, WIDE_WIDTHS, bandOf, checkExceptions, formatWidths, judgeMeasured, judgeStatic, mainActionFor, measurePhone, measurePhoneInPage, measureSite, measureViewportInPage, measureViewports, mergeViewportFindings, normaliseElement, reasonOf, routeFile, scanPhoneFirst, sweepWidths, writeBaseline };