@domandigital/craft 0.21.0 → 0.22.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 +6 -0
- package/README.md +14 -1
- package/dist/{chunk-FNB3NP57.js → chunk-6PZG4MJS.js} +1 -1
- package/dist/{chunk-4WAIOZ4W.js → chunk-OVWVKMPS.js} +10 -10
- package/dist/{chunk-STWH5EYU.js → chunk-RBQCGDQQ.js} +2 -2
- package/dist/{chunk-BQ7H23QP.js → chunk-XSKLVEI6.js} +109 -2
- package/dist/{chunk-BYXDX66D.js → chunk-ZS7PQNJU.js} +2 -2
- package/dist/{cli-XAWJWK7I.js → cli-7DL4BKA7.js} +3 -3
- package/dist/{cli-5VHX5EJF.js → cli-AM4FLXGM.js} +2 -2
- package/dist/{cli-RHDNHL4U.js → cli-R2HBESHI.js} +1 -1
- package/dist/{cli-PBCU7Z64.js → cli-SZ4SZLUN.js} +2 -2
- package/dist/{cli-YSGPGQ7R.js → cli-T7A3BU5J.js} +4 -4
- package/dist/{cli-5QI6NIZN.js → cli-WZA5IXSR.js} +3 -3
- package/dist/{cli-EYHRJJAX.js → cli-YNIJOEJK.js} +6 -3
- package/dist/cli.js +1 -1
- package/dist/phone.cjs +57 -7
- package/dist/phone.d.cts +28 -2
- package/dist/phone.d.ts +28 -2
- package/dist/phone.js +54 -7
- package/dist/{viewports-cli-XYYX4SKV.js → viewports-cli-F7KJIVZL.js} +40 -79
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @domandigital/craft
|
|
2
2
|
|
|
3
|
+
## 0.22.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3902169: The every-viewport measure counts phone screens to the footer (DOM-1052, part of DOM-772): page height at 375 x 667 over 667, with lazy images loaded. `screens` sets a length budget per route, route pattern or prefix (`{ "/": 6.5, "/services/*": 6, "/areas/[town]": 5, "*": 9 }`, the most specific wins), on `measureViewports`, `measureRoute`, `measureSite` and `viewports.json`. A route over its budget fails a new `screens` check ("page too long for a phone"), baselined like the others; with no budget the count is only reported. `ViewportsResult.screens` and the measured file carry the count, `craft viewports` and `craft phone-first --measured` print it for every route, and `screenBudgetFor`, `checkScreenBudgets` and `formatScreens` are exported. The Bellerose home page ran 12.7 phone screens against a target of 6.5.
|
|
8
|
+
|
|
3
9
|
## 0.21.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -371,6 +371,13 @@ the one loaded page in place:
|
|
|
371
371
|
`tel:` link.
|
|
372
372
|
- **On a landscape phone (844 x 390):** header and fixed layers leave at least
|
|
373
373
|
half the height.
|
|
374
|
+
- **Page length (phone screens):** page height at 375 x 667 over 667, to the
|
|
375
|
+
footer (lazy images loaded). A phone page has a length budget (the Bellerose
|
|
376
|
+
home page ran 12.7 screens against a target of 6.5). Set one per route or
|
|
377
|
+
route pattern with `screens`; a route over its budget fails the `screens`
|
|
378
|
+
check, and with no budget the count is only reported. Keys are a route
|
|
379
|
+
(`/`), a pattern (`/areas/[town]`), a prefix (`/services/*`, the route and
|
|
380
|
+
everything under it) or `*`; the most specific wins.
|
|
374
381
|
- **At 1440 and 1920:** body lines are at most 80 characters (width over the
|
|
375
382
|
`ch` of the element's own font) and no image is stretched past its file.
|
|
376
383
|
|
|
@@ -384,10 +391,16 @@ test.use(PHONE);
|
|
|
384
391
|
test(`every viewport: ${route}`, async ({ page }) => {
|
|
385
392
|
await page.goto(route);
|
|
386
393
|
// Resizes this page in place; writes phone-first/measured/<route>.json.
|
|
387
|
-
await measureViewports(page, { route, mainAction: ".hero a" });
|
|
394
|
+
await measureViewports(page, { route, mainAction: ".hero a", screens: { "/": 6.5, "/services/*": 6 } });
|
|
388
395
|
});
|
|
389
396
|
```
|
|
390
397
|
|
|
398
|
+
`result.screens` holds the count, and it is written to the measured file;
|
|
399
|
+
`craft viewports` and `craft phone-first --measured` print it for every route
|
|
400
|
+
(`phone screens to the footer: / 5.8 of 6.5`). In `viewports.json` the budget is
|
|
401
|
+
`"screens": { "/": 6.5, "/services/*": 6 }`; in create-site's
|
|
402
|
+
`site.programme.json` it is `measure.screens`.
|
|
403
|
+
|
|
391
404
|
`measureViewports` works on the page the suite opened and signed in, so a
|
|
392
405
|
portal keeps its fixtures. `measureRoute(open, { route, mainAction })` does
|
|
393
406
|
both halves for such a page: `open("phone")` and `open("wide")` return the
|
|
@@ -633,7 +633,7 @@ async function briefFor(flags, cwd) {
|
|
|
633
633
|
if (typeof brief === "string" && brief.trim()) return brief;
|
|
634
634
|
} else if (flags.direction) throw new Error(`no such file: ${flags.direction}`);
|
|
635
635
|
if (flags.null) {
|
|
636
|
-
const { loadNull } = await import("./cli-
|
|
636
|
+
const { loadNull } = await import("./cli-SZ4SZLUN.js");
|
|
637
637
|
return loadNull(flags.null, cwd).brief;
|
|
638
638
|
}
|
|
639
639
|
return void 0;
|
|
@@ -659,37 +659,37 @@ async function compareWithCompetitor(flags, io) {
|
|
|
659
659
|
function run(argv, io) {
|
|
660
660
|
const [command, ...rest] = argv;
|
|
661
661
|
if (command === "direction") {
|
|
662
|
-
return import("./cli-
|
|
662
|
+
return import("./cli-R2HBESHI.js").then((m) => m.runDirection(rest, io));
|
|
663
663
|
}
|
|
664
664
|
if (command === "report" || command === "retrofit") {
|
|
665
|
-
return import("./cli-
|
|
665
|
+
return import("./cli-T7A3BU5J.js").then((m) => m.runReport(command, rest, io));
|
|
666
666
|
}
|
|
667
667
|
if (command === "register") {
|
|
668
668
|
return import("./cli-7QL26YPR.js").then((m) => m.runRegister(rest, io));
|
|
669
669
|
}
|
|
670
670
|
if (command === "estate") {
|
|
671
|
-
return import("./cli-
|
|
671
|
+
return import("./cli-WZA5IXSR.js").then((m) => m.runEstate(rest, io));
|
|
672
672
|
}
|
|
673
673
|
if (command === "null") {
|
|
674
|
-
return import("./cli-
|
|
674
|
+
return import("./cli-SZ4SZLUN.js").then((m) => m.runNull(rest, io));
|
|
675
675
|
}
|
|
676
676
|
if (command === "calibrate") {
|
|
677
|
-
return import("./cli-
|
|
677
|
+
return import("./cli-AM4FLXGM.js").then((m) => m.runCalibrate(rest, io));
|
|
678
678
|
}
|
|
679
679
|
if (command === "tells" && rest[0] === "harvest") {
|
|
680
|
-
return import("./cli-
|
|
680
|
+
return import("./cli-SZ4SZLUN.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
|
|
681
681
|
io.err(`craft: ${error.message}`);
|
|
682
682
|
return 2;
|
|
683
683
|
});
|
|
684
684
|
}
|
|
685
685
|
if (command === "phone-first") {
|
|
686
|
-
return import("./cli-
|
|
686
|
+
return import("./cli-YNIJOEJK.js").then((m) => m.runPhoneFirst(rest, io));
|
|
687
687
|
}
|
|
688
688
|
if (command === "viewports") {
|
|
689
|
-
return import("./viewports-cli-
|
|
689
|
+
return import("./viewports-cli-F7KJIVZL.js").then((m) => m.runViewports(rest, io));
|
|
690
690
|
}
|
|
691
691
|
if (command === "snapshot" || command === "audit") {
|
|
692
|
-
return import("./cli-
|
|
692
|
+
return import("./cli-7DL4BKA7.js").then((m) => m.runAudit(command, rest, io));
|
|
693
693
|
}
|
|
694
694
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
695
695
|
io.out(HELP);
|
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
} from "./chunk-G47ZCBD5.js";
|
|
8
8
|
import {
|
|
9
9
|
loadNull
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-6PZG4MJS.js";
|
|
11
11
|
import {
|
|
12
12
|
parseFlags
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-OVWVKMPS.js";
|
|
14
14
|
import {
|
|
15
15
|
toJson
|
|
16
16
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -147,7 +147,8 @@ var LABEL = {
|
|
|
147
147
|
input: "wrong keyboard",
|
|
148
148
|
autocomplete: "field the phone cannot fill in",
|
|
149
149
|
tel: "phone number is not a link",
|
|
150
|
-
linelength: "line too long"
|
|
150
|
+
linelength: "line too long",
|
|
151
|
+
screens: "page too long for a phone"
|
|
151
152
|
};
|
|
152
153
|
function writeBaseline(findings, routes, note, previous) {
|
|
153
154
|
const byFile = tally(findings, (f) => f.file);
|
|
@@ -245,6 +246,102 @@ function judgeRoutes(pages, measuredRoutes, excluded = []) {
|
|
|
245
246
|
return { missing: pages.filter((p) => !covered.has(p)), problems, covered: covered.size };
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
// src/phone/viewports.ts
|
|
250
|
+
var LADDER = [
|
|
251
|
+
{ name: "320", width: 320, height: 568, phone: true },
|
|
252
|
+
{ name: "375", width: 375, height: 812, phone: true },
|
|
253
|
+
{ name: "390", width: 390, height: 844, phone: true },
|
|
254
|
+
{ name: "430", width: 430, height: 932, phone: true },
|
|
255
|
+
{ name: "844x390", width: 844, height: 390, phone: true, landscape: true },
|
|
256
|
+
{ name: "768", width: 768, height: 1024, phone: false },
|
|
257
|
+
{ name: "1024", width: 1024, height: 768, phone: false },
|
|
258
|
+
{ name: "1280", width: 1280, height: 800, phone: false },
|
|
259
|
+
{ name: "1440", width: 1440, height: 900, phone: false },
|
|
260
|
+
{ name: "1920", width: 1920, height: 1080, phone: false }
|
|
261
|
+
];
|
|
262
|
+
var SHORT_PHONE = { name: "375x667", width: 375, height: 667, phone: true };
|
|
263
|
+
var DEFAULT_SWEEP = { from: 320, to: 1920, step: 16, wideStep: 32, wideFrom: 1024, height: 800 };
|
|
264
|
+
function sweepWidths(sweep = DEFAULT_SWEEP) {
|
|
265
|
+
const out = [];
|
|
266
|
+
for (let w = sweep.from; w <= sweep.to; w += w < sweep.wideFrom ? sweep.step : sweep.wideStep) out.push(w);
|
|
267
|
+
if (out.at(-1) !== sweep.to) out.push(sweep.to);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
var DEFAULT_THRESHOLDS = {
|
|
271
|
+
maxLayerShare: 0.25,
|
|
272
|
+
landscapeMaxLayerShare: 0.5,
|
|
273
|
+
oversizeRatio: 2.5,
|
|
274
|
+
upscaleRatio: 1,
|
|
275
|
+
maxLineCh: 80,
|
|
276
|
+
overlapShare: 0.4,
|
|
277
|
+
minImagePx: 32
|
|
278
|
+
};
|
|
279
|
+
var mainActionFor = (config, route) => typeof config === "string" ? config : config ? config[route] ?? config["*"] : void 0;
|
|
280
|
+
function screenBudgetFor(budgets, route) {
|
|
281
|
+
if (!budgets) return void 0;
|
|
282
|
+
const path = cleanRoute(route);
|
|
283
|
+
let best;
|
|
284
|
+
for (const key of Object.keys(budgets)) {
|
|
285
|
+
const prefix = key.endsWith("/*") ? key.slice(0, -1) : null;
|
|
286
|
+
const hit = key === "*" ? 0 : prefix ? path === key.slice(0, -2) || path.startsWith(prefix) ? 1 + prefix.length : -1 : routeMatches(key, path) ? key === path ? 1e4 : 5e3 + key.length : -1;
|
|
287
|
+
if (hit >= 0 && (!best || hit > best.score)) best = { key, score: hit };
|
|
288
|
+
}
|
|
289
|
+
return best ? budgets[best.key] : void 0;
|
|
290
|
+
}
|
|
291
|
+
function checkScreenBudgets(budgets, where = "screens") {
|
|
292
|
+
if (budgets === void 0) return void 0;
|
|
293
|
+
if (!budgets || typeof budgets !== "object" || Array.isArray(budgets)) throw new Error(`${where} should map a route to its budget in phone screens, such as { "/": 6.5, "/services/*": 6 }`);
|
|
294
|
+
for (const [route, value] of Object.entries(budgets)) {
|
|
295
|
+
if (route !== "*" && !route.startsWith("/")) throw new Error(`${where}: "${route}" should start with / (or be *)`);
|
|
296
|
+
if (typeof value !== "number" || !(value > 0)) throw new Error(`${where}: "${route}" should be a number of phone screens above 0, not ${JSON.stringify(value)}`);
|
|
297
|
+
}
|
|
298
|
+
return budgets;
|
|
299
|
+
}
|
|
300
|
+
function formatScreens(routes, budgets) {
|
|
301
|
+
return routes.filter((r) => r.screens !== void 0).map((r) => {
|
|
302
|
+
const budget = screenBudgetFor(budgets, r.route);
|
|
303
|
+
return `${r.route} ${r.screens}${budget === void 0 ? "" : ` of ${budget}`}`;
|
|
304
|
+
}).join(", ");
|
|
305
|
+
}
|
|
306
|
+
function formatWidths(failing, tested) {
|
|
307
|
+
const plain = tested.filter((n) => /^\d+$/.test(n)).sort((a, b) => Number(a) - Number(b));
|
|
308
|
+
const rank2 = new Map(plain.map((name, i) => [name, i]));
|
|
309
|
+
const names = [...new Set(failing)];
|
|
310
|
+
const runs = [];
|
|
311
|
+
for (const name of names.filter((n) => rank2.has(n)).sort((a, b) => rank2.get(a) - rank2.get(b))) {
|
|
312
|
+
const last = runs.at(-1);
|
|
313
|
+
if (last && rank2.get(name) === rank2.get(last.at(-1)) + 1) last.push(name);
|
|
314
|
+
else runs.push([name]);
|
|
315
|
+
}
|
|
316
|
+
const out = runs.map((r) => r.length > 1 ? `${r[0]}-${r.at(-1)}` : r[0]);
|
|
317
|
+
return [...out, ...names.filter((n) => !rank2.has(n))].join(", ");
|
|
318
|
+
}
|
|
319
|
+
var keyOf = (f) => `${f.check}\0${f.element.replace(/("[^]*)$/, (t) => t.replace(/\d+/g, "#"))}`;
|
|
320
|
+
function mergeViewportFindings(perViewport, tested) {
|
|
321
|
+
const merged = /* @__PURE__ */ new Map();
|
|
322
|
+
for (const { viewport, findings } of perViewport) {
|
|
323
|
+
const counts = /* @__PURE__ */ new Map();
|
|
324
|
+
for (const f of findings) {
|
|
325
|
+
const k = keyOf(f);
|
|
326
|
+
const had = counts.get(k);
|
|
327
|
+
if (had) had.n++;
|
|
328
|
+
else counts.set(k, { first: f, n: 1 });
|
|
329
|
+
}
|
|
330
|
+
for (const [k, { first, n }] of counts) {
|
|
331
|
+
const had = merged.get(k);
|
|
332
|
+
if (had) {
|
|
333
|
+
had.at.push(viewport);
|
|
334
|
+
had.most = Math.max(had.most, n);
|
|
335
|
+
} else merged.set(k, { first, at: [viewport], most: n });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const out = [];
|
|
339
|
+
for (const { first, at, most } of merged.values()) {
|
|
340
|
+
for (let i = 0; i < most; i++) out.push({ ...first, detail: `${first.detail} (at ${formatWidths(at, tested)})` });
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
|
|
248
345
|
export {
|
|
249
346
|
parseBaseline,
|
|
250
347
|
EMPTY_BASELINE,
|
|
@@ -257,5 +354,15 @@ export {
|
|
|
257
354
|
hasUnexcusedEntries,
|
|
258
355
|
routesFromPages,
|
|
259
356
|
routesFromManifest,
|
|
260
|
-
judgeRoutes
|
|
357
|
+
judgeRoutes,
|
|
358
|
+
LADDER,
|
|
359
|
+
SHORT_PHONE,
|
|
360
|
+
DEFAULT_SWEEP,
|
|
361
|
+
sweepWidths,
|
|
362
|
+
DEFAULT_THRESHOLDS,
|
|
363
|
+
mainActionFor,
|
|
364
|
+
screenBudgetFor,
|
|
365
|
+
checkScreenBudgets,
|
|
366
|
+
formatScreens,
|
|
367
|
+
mergeViewportFindings
|
|
261
368
|
};
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
describeTypicality,
|
|
7
7
|
loadNull
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-6PZG4MJS.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-
|
|
17
|
+
} from "./chunk-OVWVKMPS.js";
|
|
18
18
|
import {
|
|
19
19
|
toJson
|
|
20
20
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
merge,
|
|
5
5
|
resolvePages,
|
|
6
6
|
runAudit
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-ZS7PQNJU.js";
|
|
8
8
|
import "./chunk-6DRBFDWT.js";
|
|
9
9
|
import "./chunk-JYNXGCYZ.js";
|
|
10
|
-
import "./chunk-
|
|
10
|
+
import "./chunk-6PZG4MJS.js";
|
|
11
11
|
import "./chunk-OQXTQQAU.js";
|
|
12
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-OVWVKMPS.js";
|
|
13
13
|
import "./chunk-QIEDBL4J.js";
|
|
14
14
|
import "./chunk-WUACK2HX.js";
|
|
15
15
|
import "./chunk-DE2ZAV4G.js";
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
expandNullTargets,
|
|
6
6
|
loadNull
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-6PZG4MJS.js";
|
|
8
8
|
import "./chunk-OQXTQQAU.js";
|
|
9
9
|
import {
|
|
10
10
|
parseFlags
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-OVWVKMPS.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-
|
|
12
|
+
} from "./chunk-6PZG4MJS.js";
|
|
13
13
|
import "./chunk-OQXTQQAU.js";
|
|
14
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-OVWVKMPS.js";
|
|
15
15
|
import "./chunk-QIEDBL4J.js";
|
|
16
16
|
import "./chunk-WUACK2HX.js";
|
|
17
17
|
import "./chunk-DE2ZAV4G.js";
|
|
@@ -3,21 +3,21 @@ import {
|
|
|
3
3
|
} from "./chunk-OKVHWXYV.js";
|
|
4
4
|
import {
|
|
5
5
|
merge
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ZS7PQNJU.js";
|
|
7
7
|
import "./chunk-6DRBFDWT.js";
|
|
8
8
|
import "./chunk-JYNXGCYZ.js";
|
|
9
9
|
import {
|
|
10
10
|
SIBLING_AT,
|
|
11
11
|
compareToEstate,
|
|
12
12
|
loadEstate
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-RBQCGDQQ.js";
|
|
14
14
|
import {
|
|
15
15
|
PAGE_TYPES,
|
|
16
16
|
validateDirection
|
|
17
17
|
} from "./chunk-G47ZCBD5.js";
|
|
18
18
|
import {
|
|
19
19
|
loadNull
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-6PZG4MJS.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-
|
|
31
|
+
} from "./chunk-OVWVKMPS.js";
|
|
32
32
|
import {
|
|
33
33
|
toJson
|
|
34
34
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
formatPairs,
|
|
5
5
|
loadEstate,
|
|
6
6
|
runEstate
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-RBQCGDQQ.js";
|
|
8
8
|
import "./chunk-G47ZCBD5.js";
|
|
9
|
-
import "./chunk-
|
|
9
|
+
import "./chunk-6PZG4MJS.js";
|
|
10
10
|
import "./chunk-OQXTQQAU.js";
|
|
11
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-OVWVKMPS.js";
|
|
12
12
|
import "./chunk-QIEDBL4J.js";
|
|
13
13
|
import "./chunk-WUACK2HX.js";
|
|
14
14
|
import "./chunk-DE2ZAV4G.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EMPTY_BASELINE,
|
|
3
3
|
checkExceptions,
|
|
4
|
+
formatScreens,
|
|
4
5
|
hasUnexcusedEntries,
|
|
5
6
|
judgeGrowth,
|
|
6
7
|
judgeMeasured,
|
|
@@ -11,11 +12,11 @@ import {
|
|
|
11
12
|
routesFromPages,
|
|
12
13
|
scanPhoneFirst,
|
|
13
14
|
writeBaseline
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-XSKLVEI6.js";
|
|
15
16
|
import {
|
|
16
17
|
SOURCE_FILE,
|
|
17
18
|
readPaths
|
|
18
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-OVWVKMPS.js";
|
|
19
20
|
import {
|
|
20
21
|
toJson
|
|
21
22
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -213,7 +214,9 @@ ${lines.map((l) => ` ${l}`).join("\n")}` : "";
|
|
|
213
214
|
covered && app.routes.length ? `${app.routes.length} page route${app.routes.length === 1 ? "" : "s"} covered` : null
|
|
214
215
|
].filter(Boolean).join(", ");
|
|
215
216
|
if (!failed) {
|
|
216
|
-
|
|
217
|
+
const screens = routes ? formatScreens(routes) : "";
|
|
218
|
+
io.out(`craft phone-first: pass. ${summary}.${screens ? `
|
|
219
|
+
phone screens to the footer: ${screens}` : ""}${[baseNote, routeNote].filter(Boolean).map((n) => `
|
|
217
220
|
note: ${n}`).join("")}`);
|
|
218
221
|
return 0;
|
|
219
222
|
}
|
package/dist/cli.js
CHANGED
package/dist/phone.cjs
CHANGED
|
@@ -33,7 +33,9 @@ __export(phone_exports, {
|
|
|
33
33
|
WIDE_WIDTHS: () => WIDE_WIDTHS,
|
|
34
34
|
bandOf: () => bandOf,
|
|
35
35
|
checkExceptions: () => checkExceptions,
|
|
36
|
+
checkScreenBudgets: () => checkScreenBudgets,
|
|
36
37
|
cleanRoute: () => cleanRoute,
|
|
38
|
+
formatScreens: () => formatScreens,
|
|
37
39
|
formatWidths: () => formatWidths,
|
|
38
40
|
hasReason: () => hasReason,
|
|
39
41
|
hasUnexcusedEntries: () => hasUnexcusedEntries,
|
|
@@ -57,6 +59,7 @@ __export(phone_exports, {
|
|
|
57
59
|
routesFromManifest: () => routesFromManifest,
|
|
58
60
|
routesFromPages: () => routesFromPages,
|
|
59
61
|
scanPhoneFirst: () => scanPhoneFirst,
|
|
62
|
+
screenBudgetFor: () => screenBudgetFor,
|
|
60
63
|
sweepWidths: () => sweepWidths,
|
|
61
64
|
writeBaseline: () => writeBaseline
|
|
62
65
|
});
|
|
@@ -580,7 +583,8 @@ var LABEL = {
|
|
|
580
583
|
input: "wrong keyboard",
|
|
581
584
|
autocomplete: "field the phone cannot fill in",
|
|
582
585
|
tel: "phone number is not a link",
|
|
583
|
-
linelength: "line too long"
|
|
586
|
+
linelength: "line too long",
|
|
587
|
+
screens: "page too long for a phone"
|
|
584
588
|
};
|
|
585
589
|
function writeBaseline(findings, routes, note, previous) {
|
|
586
590
|
const byFile = tally(findings, (f) => f.file);
|
|
@@ -718,6 +722,32 @@ var DEFAULT_THRESHOLDS = {
|
|
|
718
722
|
minImagePx: 32
|
|
719
723
|
};
|
|
720
724
|
var mainActionFor = (config, route) => typeof config === "string" ? config : config ? config[route] ?? config["*"] : void 0;
|
|
725
|
+
function screenBudgetFor(budgets, route) {
|
|
726
|
+
if (!budgets) return void 0;
|
|
727
|
+
const path = cleanRoute(route);
|
|
728
|
+
let best;
|
|
729
|
+
for (const key of Object.keys(budgets)) {
|
|
730
|
+
const prefix = key.endsWith("/*") ? key.slice(0, -1) : null;
|
|
731
|
+
const hit = key === "*" ? 0 : prefix ? path === key.slice(0, -2) || path.startsWith(prefix) ? 1 + prefix.length : -1 : routeMatches(key, path) ? key === path ? 1e4 : 5e3 + key.length : -1;
|
|
732
|
+
if (hit >= 0 && (!best || hit > best.score)) best = { key, score: hit };
|
|
733
|
+
}
|
|
734
|
+
return best ? budgets[best.key] : void 0;
|
|
735
|
+
}
|
|
736
|
+
function checkScreenBudgets(budgets, where = "screens") {
|
|
737
|
+
if (budgets === void 0) return void 0;
|
|
738
|
+
if (!budgets || typeof budgets !== "object" || Array.isArray(budgets)) throw new Error(`${where} should map a route to its budget in phone screens, such as { "/": 6.5, "/services/*": 6 }`);
|
|
739
|
+
for (const [route, value] of Object.entries(budgets)) {
|
|
740
|
+
if (route !== "*" && !route.startsWith("/")) throw new Error(`${where}: "${route}" should start with / (or be *)`);
|
|
741
|
+
if (typeof value !== "number" || !(value > 0)) throw new Error(`${where}: "${route}" should be a number of phone screens above 0, not ${JSON.stringify(value)}`);
|
|
742
|
+
}
|
|
743
|
+
return budgets;
|
|
744
|
+
}
|
|
745
|
+
function formatScreens(routes, budgets) {
|
|
746
|
+
return routes.filter((r) => r.screens !== void 0).map((r) => {
|
|
747
|
+
const budget = screenBudgetFor(budgets, r.route);
|
|
748
|
+
return `${r.route} ${r.screens}${budget === void 0 ? "" : ` of ${budget}`}`;
|
|
749
|
+
}).join(", ");
|
|
750
|
+
}
|
|
721
751
|
function formatWidths(failing, tested) {
|
|
722
752
|
const plain = tested.filter((n) => /^\d+$/.test(n)).sort((a, b) => Number(a) - Number(b));
|
|
723
753
|
const rank2 = new Map(plain.map((name, i) => [name, i]));
|
|
@@ -814,6 +844,7 @@ async function collect(page, options) {
|
|
|
814
844
|
const mainAction = mainActionFor(options.mainAction, options.route);
|
|
815
845
|
const perViewport = [];
|
|
816
846
|
const tested = [];
|
|
847
|
+
let screens;
|
|
817
848
|
for (const stop of plan(options)) {
|
|
818
849
|
await page.setViewportSize({ width: stop.width, height: stop.height });
|
|
819
850
|
await settle(page, stop.ladder ? 150 : 60);
|
|
@@ -822,20 +853,36 @@ async function collect(page, options) {
|
|
|
822
853
|
const findings = [];
|
|
823
854
|
if (stop.ladder) findings.push(...await page.evaluate(measurePhoneInPage, { ...floors, only: stop.touch ? ["target", "field", "text"] : ["text"] }));
|
|
824
855
|
findings.push(...await page.evaluate(measureViewportInPage, { groups: stop.groups, thresholds, mainAction, metaViewport: stop.width < 768 }));
|
|
856
|
+
if (stop.name === SHORT_PHONE.name) {
|
|
857
|
+
const height = await page.evaluate(() => document.documentElement.scrollHeight, void 0);
|
|
858
|
+
screens = Math.round(height / stop.height * 10) / 10;
|
|
859
|
+
const budget = screenBudgetFor(options.screens, options.route);
|
|
860
|
+
if (budget !== void 0 && screens > budget) findings.push({ check: "screens", element: "<page>", detail: `${screens} phone screens to the footer at ${stop.width} x ${stop.height}, over the budget of ${budget} for ${options.route}` });
|
|
861
|
+
}
|
|
825
862
|
perViewport.push({ viewport: stop.name, findings });
|
|
826
863
|
tested.push(stop.name);
|
|
827
864
|
}
|
|
828
|
-
return { perViewport, tested };
|
|
865
|
+
return { perViewport, tested, ...screens !== void 0 ? { screens } : {} };
|
|
866
|
+
}
|
|
867
|
+
function mergeCollected(collected) {
|
|
868
|
+
const screens = collected.find((c) => c.screens !== void 0)?.screens;
|
|
869
|
+
return { perViewport: collected.flatMap((c) => c.perViewport), tested: collected.flatMap((c) => c.tested), ...screens !== void 0 ? { screens } : {} };
|
|
829
870
|
}
|
|
830
871
|
function finish(route, collected, out) {
|
|
831
|
-
const result = {
|
|
872
|
+
const result = {
|
|
873
|
+
route,
|
|
874
|
+
viewports: collected.tested,
|
|
875
|
+
...collected.screens !== void 0 ? { screens: collected.screens } : {},
|
|
876
|
+
findings: mergeViewportFindings(collected.perViewport, collected.tested),
|
|
877
|
+
perViewport: collected.perViewport
|
|
878
|
+
};
|
|
832
879
|
if (out !== false) writeMeasured(result, out ?? DEFAULT_MEASURED_DIR);
|
|
833
880
|
return result;
|
|
834
881
|
}
|
|
835
882
|
function writeMeasured(result, dir) {
|
|
836
883
|
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
837
|
-
const { route, viewport, viewports, findings } = result;
|
|
838
|
-
(0, import_node_fs.writeFileSync)((0, import_node_path.join)(dir, routeFile(route)), `${JSON.stringify({ route, ...viewport ? { viewport } : {}, ...viewports ? { viewports } : {}, findings }, null, 1)}
|
|
884
|
+
const { route, viewport, viewports, screens, findings } = result;
|
|
885
|
+
(0, import_node_fs.writeFileSync)((0, import_node_path.join)(dir, routeFile(route)), `${JSON.stringify({ route, ...viewport ? { viewport } : {}, ...viewports ? { viewports } : {}, ...screens !== void 0 ? { screens } : {}, findings }, null, 1)}
|
|
839
886
|
`);
|
|
840
887
|
}
|
|
841
888
|
async function measureViewports(page, options) {
|
|
@@ -856,7 +903,7 @@ async function measureRoute(open, options) {
|
|
|
856
903
|
await close?.();
|
|
857
904
|
}
|
|
858
905
|
}
|
|
859
|
-
return finish(options.route,
|
|
906
|
+
return finish(options.route, mergeCollected(collected), options.out);
|
|
860
907
|
}
|
|
861
908
|
async function measureSite(browser, options) {
|
|
862
909
|
const result = { routes: [], errors: [] };
|
|
@@ -885,7 +932,7 @@ async function measureSite(browser, options) {
|
|
|
885
932
|
result.errors.push({ route, error: failed });
|
|
886
933
|
continue;
|
|
887
934
|
}
|
|
888
|
-
result.routes.push(finish(route,
|
|
935
|
+
result.routes.push(finish(route, mergeCollected(collected), options.out));
|
|
889
936
|
}
|
|
890
937
|
return result;
|
|
891
938
|
}
|
|
@@ -918,7 +965,9 @@ async function measurePhone(page, options) {
|
|
|
918
965
|
WIDE_WIDTHS,
|
|
919
966
|
bandOf,
|
|
920
967
|
checkExceptions,
|
|
968
|
+
checkScreenBudgets,
|
|
921
969
|
cleanRoute,
|
|
970
|
+
formatScreens,
|
|
922
971
|
formatWidths,
|
|
923
972
|
hasReason,
|
|
924
973
|
hasUnexcusedEntries,
|
|
@@ -942,6 +991,7 @@ async function measurePhone(page, options) {
|
|
|
942
991
|
routesFromManifest,
|
|
943
992
|
routesFromPages,
|
|
944
993
|
scanPhoneFirst,
|
|
994
|
+
screenBudgetFor,
|
|
945
995
|
sweepWidths,
|
|
946
996
|
writeBaseline
|
|
947
997
|
});
|
package/dist/phone.d.cts
CHANGED
|
@@ -28,7 +28,7 @@ interface PhoneFloors {
|
|
|
28
28
|
* What a measure can find. The first five are the 375px floors (DOM-775); the
|
|
29
29
|
* rest are the every-viewport and phone-optimisation checks (DOM-787).
|
|
30
30
|
*/
|
|
31
|
-
type PhoneCheck = "scroll" | "target" | "field" | "text" | "title" | "overlap" | "action" | "layers" | "landscape" | "image" | "input" | "autocomplete" | "tel" | "linelength";
|
|
31
|
+
type PhoneCheck = "scroll" | "target" | "field" | "text" | "title" | "overlap" | "action" | "layers" | "landscape" | "image" | "input" | "autocomplete" | "tel" | "linelength" | "screens";
|
|
32
32
|
interface RawPhoneFinding {
|
|
33
33
|
check: PhoneCheck;
|
|
34
34
|
/** `<tag> "text"`: what a person looks for on the page. */
|
|
@@ -92,6 +92,8 @@ interface MeasuredRoute {
|
|
|
92
92
|
};
|
|
93
93
|
/** Set by the every-viewport measure: the viewports and widths the findings were gathered across, phone first. */
|
|
94
94
|
viewports?: string[];
|
|
95
|
+
/** Phone screens to the footer (page height at 375 x 667 over 667), from the every-viewport measure. Absent when the short phone was not measured. */
|
|
96
|
+
screens?: number;
|
|
95
97
|
findings: RawPhoneFinding[];
|
|
96
98
|
}
|
|
97
99
|
interface Baseline {
|
|
@@ -266,6 +268,23 @@ declare const DEFAULT_THRESHOLDS: Thresholds;
|
|
|
266
268
|
/** The main action, as a selector: one for the whole site, or one per route (`*` is the fallback). */
|
|
267
269
|
type MainAction = string | Record<string, string>;
|
|
268
270
|
declare const mainActionFor: (config: MainAction | undefined, route: string) => string | undefined;
|
|
271
|
+
/**
|
|
272
|
+
* A length budget per page type, in phone screens to the footer (page height at
|
|
273
|
+
* 375 x 667 over 667). Keys are a route (`/`), a route pattern (`/services/[slug]`),
|
|
274
|
+
* a prefix (`/services/*`: the route and everything under it) or `*` for the
|
|
275
|
+
* rest; the most specific match wins. No match is no budget: the count is
|
|
276
|
+
* reported and never fails.
|
|
277
|
+
*/
|
|
278
|
+
type ScreenBudgets = Record<string, number>;
|
|
279
|
+
/** The budget for a route, or undefined when none of the keys covers it. */
|
|
280
|
+
declare function screenBudgetFor(budgets: ScreenBudgets | undefined, route: string): number | undefined;
|
|
281
|
+
/** `screens` as a person types it into config: every budget a positive number. */
|
|
282
|
+
declare function checkScreenBudgets(budgets: unknown, where?: string): ScreenBudgets | undefined;
|
|
283
|
+
/** `/ 12.7, /services 4.1`: the count per route, for the line a run prints whether or not a budget is set. */
|
|
284
|
+
declare function formatScreens(routes: {
|
|
285
|
+
route: string;
|
|
286
|
+
screens?: number;
|
|
287
|
+
}[], budgets?: ScreenBudgets): string;
|
|
269
288
|
/**
|
|
270
289
|
* Widths as a person reads them: `320-352, 400, 1440`. A run is consecutive
|
|
271
290
|
* tested widths that all fail. A landscape or short-phone name (`844x390`)
|
|
@@ -385,6 +404,13 @@ interface MeasureViewportsOptions {
|
|
|
385
404
|
floors?: Partial<PhoneFloors>;
|
|
386
405
|
/** The main action's selector: one for all routes, or per route with `*` as the fallback. Absent: not checked. */
|
|
387
406
|
mainAction?: MainAction;
|
|
407
|
+
/**
|
|
408
|
+
* A length budget per route, in phone screens to the footer (page height at
|
|
409
|
+
* 375 x 667 over 667): `{ "/": 6.5, "/services/*": 6 }`. A route over its
|
|
410
|
+
* budget fails the `screens` check. Without a budget the count is only
|
|
411
|
+
* reported, in `ViewportsResult.screens`.
|
|
412
|
+
*/
|
|
413
|
+
screens?: ScreenBudgets;
|
|
388
414
|
/** The width sweep, or `false` to measure the ladder only. */
|
|
389
415
|
sweep?: Partial<Sweep> | false;
|
|
390
416
|
/** Replace the ladder. */
|
|
@@ -522,4 +548,4 @@ interface MeasurePhoneOptions {
|
|
|
522
548
|
*/
|
|
523
549
|
declare function measurePhone(page: MinimalPage, options: MeasurePhoneOptions): Promise<MeasuredRoute>;
|
|
524
550
|
|
|
525
|
-
export { BANDS, type Band, type Baseline, DEFAULT_MEASURED_DIR, DEFAULT_SWEEP, DEFAULT_THRESHOLDS, DESKTOP, EMPTY_BASELINE, type ExcludedRoute, type FileException, type InPageGroup, type Judgement, LADDER, type MainAction, type MeasurePhoneOptions, type MeasureSiteOptions, type MeasureViewportsOptions, type MeasuredRoute, type OpenedPage, PHONE, PHONE_FLOORS, type PhoneCheck, type PhoneFloors, type RawPhoneFinding, type RouteJudgement, 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, cleanRoute, formatWidths, hasReason, hasUnexcusedEntries, judgeGrowth, judgeMeasured, judgeRoutes, judgeStatic, mainActionFor, measurePhone, measurePhoneInPage, measureRoute, measureSite, measureViewportInPage, measureViewports, mergeViewportFindings, normaliseElement, parseBaseline, reasonOf, routeFile, routeMatches, routesFromManifest, routesFromPages, scanPhoneFirst, sweepWidths, writeBaseline };
|
|
551
|
+
export { BANDS, type Band, type Baseline, DEFAULT_MEASURED_DIR, DEFAULT_SWEEP, DEFAULT_THRESHOLDS, DESKTOP, EMPTY_BASELINE, type ExcludedRoute, type FileException, type InPageGroup, type Judgement, LADDER, type MainAction, type MeasurePhoneOptions, type MeasureSiteOptions, type MeasureViewportsOptions, type MeasuredRoute, type OpenedPage, PHONE, PHONE_FLOORS, type PhoneCheck, type PhoneFloors, type RawPhoneFinding, type RouteJudgement, SHORT_PHONE, type ScreenBudgets, type SiteBrowser, type SitePage, type SiteResult, type StaticFinding, type Sweep, type Thresholds, type Viewport, type ViewportArg, type ViewportPage, type ViewportsResult, WIDE_WIDTHS, bandOf, checkExceptions, checkScreenBudgets, cleanRoute, formatScreens, formatWidths, hasReason, hasUnexcusedEntries, judgeGrowth, judgeMeasured, judgeRoutes, judgeStatic, mainActionFor, measurePhone, measurePhoneInPage, measureRoute, measureSite, measureViewportInPage, measureViewports, mergeViewportFindings, normaliseElement, parseBaseline, reasonOf, routeFile, routeMatches, routesFromManifest, routesFromPages, scanPhoneFirst, screenBudgetFor, sweepWidths, writeBaseline };
|
package/dist/phone.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ interface PhoneFloors {
|
|
|
28
28
|
* What a measure can find. The first five are the 375px floors (DOM-775); the
|
|
29
29
|
* rest are the every-viewport and phone-optimisation checks (DOM-787).
|
|
30
30
|
*/
|
|
31
|
-
type PhoneCheck = "scroll" | "target" | "field" | "text" | "title" | "overlap" | "action" | "layers" | "landscape" | "image" | "input" | "autocomplete" | "tel" | "linelength";
|
|
31
|
+
type PhoneCheck = "scroll" | "target" | "field" | "text" | "title" | "overlap" | "action" | "layers" | "landscape" | "image" | "input" | "autocomplete" | "tel" | "linelength" | "screens";
|
|
32
32
|
interface RawPhoneFinding {
|
|
33
33
|
check: PhoneCheck;
|
|
34
34
|
/** `<tag> "text"`: what a person looks for on the page. */
|
|
@@ -92,6 +92,8 @@ interface MeasuredRoute {
|
|
|
92
92
|
};
|
|
93
93
|
/** Set by the every-viewport measure: the viewports and widths the findings were gathered across, phone first. */
|
|
94
94
|
viewports?: string[];
|
|
95
|
+
/** Phone screens to the footer (page height at 375 x 667 over 667), from the every-viewport measure. Absent when the short phone was not measured. */
|
|
96
|
+
screens?: number;
|
|
95
97
|
findings: RawPhoneFinding[];
|
|
96
98
|
}
|
|
97
99
|
interface Baseline {
|
|
@@ -266,6 +268,23 @@ declare const DEFAULT_THRESHOLDS: Thresholds;
|
|
|
266
268
|
/** The main action, as a selector: one for the whole site, or one per route (`*` is the fallback). */
|
|
267
269
|
type MainAction = string | Record<string, string>;
|
|
268
270
|
declare const mainActionFor: (config: MainAction | undefined, route: string) => string | undefined;
|
|
271
|
+
/**
|
|
272
|
+
* A length budget per page type, in phone screens to the footer (page height at
|
|
273
|
+
* 375 x 667 over 667). Keys are a route (`/`), a route pattern (`/services/[slug]`),
|
|
274
|
+
* a prefix (`/services/*`: the route and everything under it) or `*` for the
|
|
275
|
+
* rest; the most specific match wins. No match is no budget: the count is
|
|
276
|
+
* reported and never fails.
|
|
277
|
+
*/
|
|
278
|
+
type ScreenBudgets = Record<string, number>;
|
|
279
|
+
/** The budget for a route, or undefined when none of the keys covers it. */
|
|
280
|
+
declare function screenBudgetFor(budgets: ScreenBudgets | undefined, route: string): number | undefined;
|
|
281
|
+
/** `screens` as a person types it into config: every budget a positive number. */
|
|
282
|
+
declare function checkScreenBudgets(budgets: unknown, where?: string): ScreenBudgets | undefined;
|
|
283
|
+
/** `/ 12.7, /services 4.1`: the count per route, for the line a run prints whether or not a budget is set. */
|
|
284
|
+
declare function formatScreens(routes: {
|
|
285
|
+
route: string;
|
|
286
|
+
screens?: number;
|
|
287
|
+
}[], budgets?: ScreenBudgets): string;
|
|
269
288
|
/**
|
|
270
289
|
* Widths as a person reads them: `320-352, 400, 1440`. A run is consecutive
|
|
271
290
|
* tested widths that all fail. A landscape or short-phone name (`844x390`)
|
|
@@ -385,6 +404,13 @@ interface MeasureViewportsOptions {
|
|
|
385
404
|
floors?: Partial<PhoneFloors>;
|
|
386
405
|
/** The main action's selector: one for all routes, or per route with `*` as the fallback. Absent: not checked. */
|
|
387
406
|
mainAction?: MainAction;
|
|
407
|
+
/**
|
|
408
|
+
* A length budget per route, in phone screens to the footer (page height at
|
|
409
|
+
* 375 x 667 over 667): `{ "/": 6.5, "/services/*": 6 }`. A route over its
|
|
410
|
+
* budget fails the `screens` check. Without a budget the count is only
|
|
411
|
+
* reported, in `ViewportsResult.screens`.
|
|
412
|
+
*/
|
|
413
|
+
screens?: ScreenBudgets;
|
|
388
414
|
/** The width sweep, or `false` to measure the ladder only. */
|
|
389
415
|
sweep?: Partial<Sweep> | false;
|
|
390
416
|
/** Replace the ladder. */
|
|
@@ -522,4 +548,4 @@ interface MeasurePhoneOptions {
|
|
|
522
548
|
*/
|
|
523
549
|
declare function measurePhone(page: MinimalPage, options: MeasurePhoneOptions): Promise<MeasuredRoute>;
|
|
524
550
|
|
|
525
|
-
export { BANDS, type Band, type Baseline, DEFAULT_MEASURED_DIR, DEFAULT_SWEEP, DEFAULT_THRESHOLDS, DESKTOP, EMPTY_BASELINE, type ExcludedRoute, type FileException, type InPageGroup, type Judgement, LADDER, type MainAction, type MeasurePhoneOptions, type MeasureSiteOptions, type MeasureViewportsOptions, type MeasuredRoute, type OpenedPage, PHONE, PHONE_FLOORS, type PhoneCheck, type PhoneFloors, type RawPhoneFinding, type RouteJudgement, 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, cleanRoute, formatWidths, hasReason, hasUnexcusedEntries, judgeGrowth, judgeMeasured, judgeRoutes, judgeStatic, mainActionFor, measurePhone, measurePhoneInPage, measureRoute, measureSite, measureViewportInPage, measureViewports, mergeViewportFindings, normaliseElement, parseBaseline, reasonOf, routeFile, routeMatches, routesFromManifest, routesFromPages, scanPhoneFirst, sweepWidths, writeBaseline };
|
|
551
|
+
export { BANDS, type Band, type Baseline, DEFAULT_MEASURED_DIR, DEFAULT_SWEEP, DEFAULT_THRESHOLDS, DESKTOP, EMPTY_BASELINE, type ExcludedRoute, type FileException, type InPageGroup, type Judgement, LADDER, type MainAction, type MeasurePhoneOptions, type MeasureSiteOptions, type MeasureViewportsOptions, type MeasuredRoute, type OpenedPage, PHONE, PHONE_FLOORS, type PhoneCheck, type PhoneFloors, type RawPhoneFinding, type RouteJudgement, SHORT_PHONE, type ScreenBudgets, type SiteBrowser, type SitePage, type SiteResult, type StaticFinding, type Sweep, type Thresholds, type Viewport, type ViewportArg, type ViewportPage, type ViewportsResult, WIDE_WIDTHS, bandOf, checkExceptions, checkScreenBudgets, cleanRoute, formatScreens, formatWidths, hasReason, hasUnexcusedEntries, judgeGrowth, judgeMeasured, judgeRoutes, judgeStatic, mainActionFor, measurePhone, measurePhoneInPage, measureRoute, measureSite, measureViewportInPage, measureViewports, mergeViewportFindings, normaliseElement, parseBaseline, reasonOf, routeFile, routeMatches, routesFromManifest, routesFromPages, scanPhoneFirst, screenBudgetFor, sweepWidths, writeBaseline };
|
package/dist/phone.js
CHANGED
|
@@ -518,7 +518,8 @@ var LABEL = {
|
|
|
518
518
|
input: "wrong keyboard",
|
|
519
519
|
autocomplete: "field the phone cannot fill in",
|
|
520
520
|
tel: "phone number is not a link",
|
|
521
|
-
linelength: "line too long"
|
|
521
|
+
linelength: "line too long",
|
|
522
|
+
screens: "page too long for a phone"
|
|
522
523
|
};
|
|
523
524
|
function writeBaseline(findings, routes, note, previous) {
|
|
524
525
|
const byFile = tally(findings, (f) => f.file);
|
|
@@ -656,6 +657,32 @@ var DEFAULT_THRESHOLDS = {
|
|
|
656
657
|
minImagePx: 32
|
|
657
658
|
};
|
|
658
659
|
var mainActionFor = (config, route) => typeof config === "string" ? config : config ? config[route] ?? config["*"] : void 0;
|
|
660
|
+
function screenBudgetFor(budgets, route) {
|
|
661
|
+
if (!budgets) return void 0;
|
|
662
|
+
const path = cleanRoute(route);
|
|
663
|
+
let best;
|
|
664
|
+
for (const key of Object.keys(budgets)) {
|
|
665
|
+
const prefix = key.endsWith("/*") ? key.slice(0, -1) : null;
|
|
666
|
+
const hit = key === "*" ? 0 : prefix ? path === key.slice(0, -2) || path.startsWith(prefix) ? 1 + prefix.length : -1 : routeMatches(key, path) ? key === path ? 1e4 : 5e3 + key.length : -1;
|
|
667
|
+
if (hit >= 0 && (!best || hit > best.score)) best = { key, score: hit };
|
|
668
|
+
}
|
|
669
|
+
return best ? budgets[best.key] : void 0;
|
|
670
|
+
}
|
|
671
|
+
function checkScreenBudgets(budgets, where = "screens") {
|
|
672
|
+
if (budgets === void 0) return void 0;
|
|
673
|
+
if (!budgets || typeof budgets !== "object" || Array.isArray(budgets)) throw new Error(`${where} should map a route to its budget in phone screens, such as { "/": 6.5, "/services/*": 6 }`);
|
|
674
|
+
for (const [route, value] of Object.entries(budgets)) {
|
|
675
|
+
if (route !== "*" && !route.startsWith("/")) throw new Error(`${where}: "${route}" should start with / (or be *)`);
|
|
676
|
+
if (typeof value !== "number" || !(value > 0)) throw new Error(`${where}: "${route}" should be a number of phone screens above 0, not ${JSON.stringify(value)}`);
|
|
677
|
+
}
|
|
678
|
+
return budgets;
|
|
679
|
+
}
|
|
680
|
+
function formatScreens(routes, budgets) {
|
|
681
|
+
return routes.filter((r) => r.screens !== void 0).map((r) => {
|
|
682
|
+
const budget = screenBudgetFor(budgets, r.route);
|
|
683
|
+
return `${r.route} ${r.screens}${budget === void 0 ? "" : ` of ${budget}`}`;
|
|
684
|
+
}).join(", ");
|
|
685
|
+
}
|
|
659
686
|
function formatWidths(failing, tested) {
|
|
660
687
|
const plain = tested.filter((n) => /^\d+$/.test(n)).sort((a, b) => Number(a) - Number(b));
|
|
661
688
|
const rank2 = new Map(plain.map((name, i) => [name, i]));
|
|
@@ -752,6 +779,7 @@ async function collect(page, options) {
|
|
|
752
779
|
const mainAction = mainActionFor(options.mainAction, options.route);
|
|
753
780
|
const perViewport = [];
|
|
754
781
|
const tested = [];
|
|
782
|
+
let screens;
|
|
755
783
|
for (const stop of plan(options)) {
|
|
756
784
|
await page.setViewportSize({ width: stop.width, height: stop.height });
|
|
757
785
|
await settle(page, stop.ladder ? 150 : 60);
|
|
@@ -760,20 +788,36 @@ async function collect(page, options) {
|
|
|
760
788
|
const findings = [];
|
|
761
789
|
if (stop.ladder) findings.push(...await page.evaluate(measurePhoneInPage, { ...floors, only: stop.touch ? ["target", "field", "text"] : ["text"] }));
|
|
762
790
|
findings.push(...await page.evaluate(measureViewportInPage, { groups: stop.groups, thresholds, mainAction, metaViewport: stop.width < 768 }));
|
|
791
|
+
if (stop.name === SHORT_PHONE.name) {
|
|
792
|
+
const height = await page.evaluate(() => document.documentElement.scrollHeight, void 0);
|
|
793
|
+
screens = Math.round(height / stop.height * 10) / 10;
|
|
794
|
+
const budget = screenBudgetFor(options.screens, options.route);
|
|
795
|
+
if (budget !== void 0 && screens > budget) findings.push({ check: "screens", element: "<page>", detail: `${screens} phone screens to the footer at ${stop.width} x ${stop.height}, over the budget of ${budget} for ${options.route}` });
|
|
796
|
+
}
|
|
763
797
|
perViewport.push({ viewport: stop.name, findings });
|
|
764
798
|
tested.push(stop.name);
|
|
765
799
|
}
|
|
766
|
-
return { perViewport, tested };
|
|
800
|
+
return { perViewport, tested, ...screens !== void 0 ? { screens } : {} };
|
|
801
|
+
}
|
|
802
|
+
function mergeCollected(collected) {
|
|
803
|
+
const screens = collected.find((c) => c.screens !== void 0)?.screens;
|
|
804
|
+
return { perViewport: collected.flatMap((c) => c.perViewport), tested: collected.flatMap((c) => c.tested), ...screens !== void 0 ? { screens } : {} };
|
|
767
805
|
}
|
|
768
806
|
function finish(route, collected, out) {
|
|
769
|
-
const result = {
|
|
807
|
+
const result = {
|
|
808
|
+
route,
|
|
809
|
+
viewports: collected.tested,
|
|
810
|
+
...collected.screens !== void 0 ? { screens: collected.screens } : {},
|
|
811
|
+
findings: mergeViewportFindings(collected.perViewport, collected.tested),
|
|
812
|
+
perViewport: collected.perViewport
|
|
813
|
+
};
|
|
770
814
|
if (out !== false) writeMeasured(result, out ?? DEFAULT_MEASURED_DIR);
|
|
771
815
|
return result;
|
|
772
816
|
}
|
|
773
817
|
function writeMeasured(result, dir) {
|
|
774
818
|
mkdirSync(dir, { recursive: true });
|
|
775
|
-
const { route, viewport, viewports, findings } = result;
|
|
776
|
-
writeFileSync(join(dir, routeFile(route)), `${JSON.stringify({ route, ...viewport ? { viewport } : {}, ...viewports ? { viewports } : {}, findings }, null, 1)}
|
|
819
|
+
const { route, viewport, viewports, screens, findings } = result;
|
|
820
|
+
writeFileSync(join(dir, routeFile(route)), `${JSON.stringify({ route, ...viewport ? { viewport } : {}, ...viewports ? { viewports } : {}, ...screens !== void 0 ? { screens } : {}, findings }, null, 1)}
|
|
777
821
|
`);
|
|
778
822
|
}
|
|
779
823
|
async function measureViewports(page, options) {
|
|
@@ -794,7 +838,7 @@ async function measureRoute(open, options) {
|
|
|
794
838
|
await close?.();
|
|
795
839
|
}
|
|
796
840
|
}
|
|
797
|
-
return finish(options.route,
|
|
841
|
+
return finish(options.route, mergeCollected(collected), options.out);
|
|
798
842
|
}
|
|
799
843
|
async function measureSite(browser, options) {
|
|
800
844
|
const result = { routes: [], errors: [] };
|
|
@@ -823,7 +867,7 @@ async function measureSite(browser, options) {
|
|
|
823
867
|
result.errors.push({ route, error: failed });
|
|
824
868
|
continue;
|
|
825
869
|
}
|
|
826
|
-
result.routes.push(finish(route,
|
|
870
|
+
result.routes.push(finish(route, mergeCollected(collected), options.out));
|
|
827
871
|
}
|
|
828
872
|
return result;
|
|
829
873
|
}
|
|
@@ -855,7 +899,9 @@ export {
|
|
|
855
899
|
WIDE_WIDTHS,
|
|
856
900
|
bandOf,
|
|
857
901
|
checkExceptions,
|
|
902
|
+
checkScreenBudgets,
|
|
858
903
|
cleanRoute,
|
|
904
|
+
formatScreens,
|
|
859
905
|
formatWidths,
|
|
860
906
|
hasReason,
|
|
861
907
|
hasUnexcusedEntries,
|
|
@@ -879,6 +925,7 @@ export {
|
|
|
879
925
|
routesFromManifest,
|
|
880
926
|
routesFromPages,
|
|
881
927
|
scanPhoneFirst,
|
|
928
|
+
screenBudgetFor,
|
|
882
929
|
sweepWidths,
|
|
883
930
|
writeBaseline
|
|
884
931
|
};
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_SWEEP,
|
|
3
|
+
DEFAULT_THRESHOLDS,
|
|
2
4
|
EMPTY_BASELINE,
|
|
5
|
+
LADDER,
|
|
6
|
+
SHORT_PHONE,
|
|
7
|
+
checkScreenBudgets,
|
|
8
|
+
formatScreens,
|
|
3
9
|
judgeMeasured,
|
|
10
|
+
mainActionFor,
|
|
11
|
+
mergeViewportFindings,
|
|
4
12
|
parseBaseline,
|
|
13
|
+
screenBudgetFor,
|
|
14
|
+
sweepWidths,
|
|
5
15
|
writeBaseline
|
|
6
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-XSKLVEI6.js";
|
|
7
17
|
import {
|
|
8
18
|
loadPlaywright
|
|
9
19
|
} from "./chunk-6DRBFDWT.js";
|
|
@@ -390,76 +400,6 @@ function measureViewportInPage(arg) {
|
|
|
390
400
|
return out;
|
|
391
401
|
}
|
|
392
402
|
|
|
393
|
-
// src/phone/viewports.ts
|
|
394
|
-
var LADDER = [
|
|
395
|
-
{ name: "320", width: 320, height: 568, phone: true },
|
|
396
|
-
{ name: "375", width: 375, height: 812, phone: true },
|
|
397
|
-
{ name: "390", width: 390, height: 844, phone: true },
|
|
398
|
-
{ name: "430", width: 430, height: 932, phone: true },
|
|
399
|
-
{ name: "844x390", width: 844, height: 390, phone: true, landscape: true },
|
|
400
|
-
{ name: "768", width: 768, height: 1024, phone: false },
|
|
401
|
-
{ name: "1024", width: 1024, height: 768, phone: false },
|
|
402
|
-
{ name: "1280", width: 1280, height: 800, phone: false },
|
|
403
|
-
{ name: "1440", width: 1440, height: 900, phone: false },
|
|
404
|
-
{ name: "1920", width: 1920, height: 1080, phone: false }
|
|
405
|
-
];
|
|
406
|
-
var SHORT_PHONE = { name: "375x667", width: 375, height: 667, phone: true };
|
|
407
|
-
var DEFAULT_SWEEP = { from: 320, to: 1920, step: 16, wideStep: 32, wideFrom: 1024, height: 800 };
|
|
408
|
-
function sweepWidths(sweep = DEFAULT_SWEEP) {
|
|
409
|
-
const out = [];
|
|
410
|
-
for (let w = sweep.from; w <= sweep.to; w += w < sweep.wideFrom ? sweep.step : sweep.wideStep) out.push(w);
|
|
411
|
-
if (out.at(-1) !== sweep.to) out.push(sweep.to);
|
|
412
|
-
return out;
|
|
413
|
-
}
|
|
414
|
-
var DEFAULT_THRESHOLDS = {
|
|
415
|
-
maxLayerShare: 0.25,
|
|
416
|
-
landscapeMaxLayerShare: 0.5,
|
|
417
|
-
oversizeRatio: 2.5,
|
|
418
|
-
upscaleRatio: 1,
|
|
419
|
-
maxLineCh: 80,
|
|
420
|
-
overlapShare: 0.4,
|
|
421
|
-
minImagePx: 32
|
|
422
|
-
};
|
|
423
|
-
var mainActionFor = (config, route) => typeof config === "string" ? config : config ? config[route] ?? config["*"] : void 0;
|
|
424
|
-
function formatWidths(failing, tested) {
|
|
425
|
-
const plain = tested.filter((n) => /^\d+$/.test(n)).sort((a, b) => Number(a) - Number(b));
|
|
426
|
-
const rank = new Map(plain.map((name, i) => [name, i]));
|
|
427
|
-
const names = [...new Set(failing)];
|
|
428
|
-
const runs = [];
|
|
429
|
-
for (const name of names.filter((n) => rank.has(n)).sort((a, b) => rank.get(a) - rank.get(b))) {
|
|
430
|
-
const last = runs.at(-1);
|
|
431
|
-
if (last && rank.get(name) === rank.get(last.at(-1)) + 1) last.push(name);
|
|
432
|
-
else runs.push([name]);
|
|
433
|
-
}
|
|
434
|
-
const out = runs.map((r) => r.length > 1 ? `${r[0]}-${r.at(-1)}` : r[0]);
|
|
435
|
-
return [...out, ...names.filter((n) => !rank.has(n))].join(", ");
|
|
436
|
-
}
|
|
437
|
-
var keyOf = (f) => `${f.check}\0${f.element.replace(/("[^]*)$/, (t) => t.replace(/\d+/g, "#"))}`;
|
|
438
|
-
function mergeViewportFindings(perViewport, tested) {
|
|
439
|
-
const merged = /* @__PURE__ */ new Map();
|
|
440
|
-
for (const { viewport, findings } of perViewport) {
|
|
441
|
-
const counts = /* @__PURE__ */ new Map();
|
|
442
|
-
for (const f of findings) {
|
|
443
|
-
const k = keyOf(f);
|
|
444
|
-
const had = counts.get(k);
|
|
445
|
-
if (had) had.n++;
|
|
446
|
-
else counts.set(k, { first: f, n: 1 });
|
|
447
|
-
}
|
|
448
|
-
for (const [k, { first, n }] of counts) {
|
|
449
|
-
const had = merged.get(k);
|
|
450
|
-
if (had) {
|
|
451
|
-
had.at.push(viewport);
|
|
452
|
-
had.most = Math.max(had.most, n);
|
|
453
|
-
} else merged.set(k, { first, at: [viewport], most: n });
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
const out = [];
|
|
457
|
-
for (const { first, at, most } of merged.values()) {
|
|
458
|
-
for (let i = 0; i < most; i++) out.push({ ...first, detail: `${first.detail} (at ${formatWidths(at, tested)})` });
|
|
459
|
-
}
|
|
460
|
-
return out;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
403
|
// src/phone/run.ts
|
|
464
404
|
function plan(options) {
|
|
465
405
|
const ladder = options.ladder ?? LADDER;
|
|
@@ -515,6 +455,7 @@ async function collect(page, options) {
|
|
|
515
455
|
const mainAction = mainActionFor(options.mainAction, options.route);
|
|
516
456
|
const perViewport = [];
|
|
517
457
|
const tested = [];
|
|
458
|
+
let screens;
|
|
518
459
|
for (const stop of plan(options)) {
|
|
519
460
|
await page.setViewportSize({ width: stop.width, height: stop.height });
|
|
520
461
|
await settle(page, stop.ladder ? 150 : 60);
|
|
@@ -523,20 +464,36 @@ async function collect(page, options) {
|
|
|
523
464
|
const findings = [];
|
|
524
465
|
if (stop.ladder) findings.push(...await page.evaluate(measurePhoneInPage, { ...floors, only: stop.touch ? ["target", "field", "text"] : ["text"] }));
|
|
525
466
|
findings.push(...await page.evaluate(measureViewportInPage, { groups: stop.groups, thresholds, mainAction, metaViewport: stop.width < 768 }));
|
|
467
|
+
if (stop.name === SHORT_PHONE.name) {
|
|
468
|
+
const height = await page.evaluate(() => document.documentElement.scrollHeight, void 0);
|
|
469
|
+
screens = Math.round(height / stop.height * 10) / 10;
|
|
470
|
+
const budget = screenBudgetFor(options.screens, options.route);
|
|
471
|
+
if (budget !== void 0 && screens > budget) findings.push({ check: "screens", element: "<page>", detail: `${screens} phone screens to the footer at ${stop.width} x ${stop.height}, over the budget of ${budget} for ${options.route}` });
|
|
472
|
+
}
|
|
526
473
|
perViewport.push({ viewport: stop.name, findings });
|
|
527
474
|
tested.push(stop.name);
|
|
528
475
|
}
|
|
529
|
-
return { perViewport, tested };
|
|
476
|
+
return { perViewport, tested, ...screens !== void 0 ? { screens } : {} };
|
|
477
|
+
}
|
|
478
|
+
function mergeCollected(collected) {
|
|
479
|
+
const screens = collected.find((c) => c.screens !== void 0)?.screens;
|
|
480
|
+
return { perViewport: collected.flatMap((c) => c.perViewport), tested: collected.flatMap((c) => c.tested), ...screens !== void 0 ? { screens } : {} };
|
|
530
481
|
}
|
|
531
482
|
function finish(route, collected, out) {
|
|
532
|
-
const result = {
|
|
483
|
+
const result = {
|
|
484
|
+
route,
|
|
485
|
+
viewports: collected.tested,
|
|
486
|
+
...collected.screens !== void 0 ? { screens: collected.screens } : {},
|
|
487
|
+
findings: mergeViewportFindings(collected.perViewport, collected.tested),
|
|
488
|
+
perViewport: collected.perViewport
|
|
489
|
+
};
|
|
533
490
|
if (out !== false) writeMeasured(result, out ?? DEFAULT_MEASURED_DIR);
|
|
534
491
|
return result;
|
|
535
492
|
}
|
|
536
493
|
function writeMeasured(result, dir) {
|
|
537
494
|
mkdirSync(dir, { recursive: true });
|
|
538
|
-
const { route, viewport, viewports, findings } = result;
|
|
539
|
-
writeFileSync(join(dir, routeFile(route)), `${JSON.stringify({ route, ...viewport ? { viewport } : {}, ...viewports ? { viewports } : {}, findings }, null, 1)}
|
|
495
|
+
const { route, viewport, viewports, screens, findings } = result;
|
|
496
|
+
writeFileSync(join(dir, routeFile(route)), `${JSON.stringify({ route, ...viewport ? { viewport } : {}, ...viewports ? { viewports } : {}, ...screens !== void 0 ? { screens } : {}, findings }, null, 1)}
|
|
540
497
|
`);
|
|
541
498
|
}
|
|
542
499
|
var DESKTOP = { viewport: { width: 1024, height: 768 }, deviceScaleFactor: 1 };
|
|
@@ -567,7 +524,7 @@ async function measureSite(browser, options) {
|
|
|
567
524
|
result.errors.push({ route, error: failed });
|
|
568
525
|
continue;
|
|
569
526
|
}
|
|
570
|
-
result.routes.push(finish(route,
|
|
527
|
+
result.routes.push(finish(route, mergeCollected(collected), options.out));
|
|
571
528
|
}
|
|
572
529
|
return result;
|
|
573
530
|
}
|
|
@@ -633,6 +590,7 @@ ${USAGE}`);
|
|
|
633
590
|
...config.mainAction ? { mainAction: config.mainAction } : {},
|
|
634
591
|
...config.thresholds ? { thresholds: config.thresholds } : {},
|
|
635
592
|
...config.floors ? { floors: config.floors } : {},
|
|
593
|
+
...config.screens ? { screens: checkScreenBudgets(config.screens, "viewports.json screens") } : {},
|
|
636
594
|
sweep: flags.sweep ? config.sweep : false,
|
|
637
595
|
...captures ? {
|
|
638
596
|
onViewport: async (page, viewport, route) => {
|
|
@@ -657,7 +615,7 @@ ${USAGE}`);
|
|
|
657
615
|
const judged = judgeMeasured(site.routes, { ...baseline, measured: baseline.measured.filter((m) => measuredRoutes.has(m.route)) });
|
|
658
616
|
const failed = site.errors.length + judged.fresh.length + judged.stale.length > 0;
|
|
659
617
|
if (flags.json) {
|
|
660
|
-
io.out(toJson({ pass: !failed, routes: site.routes.map((r) => ({ route: r.route, viewports: r.viewports, findings: r.findings.length })), errors: site.errors, measured: judged }));
|
|
618
|
+
io.out(toJson({ pass: !failed, routes: site.routes.map((r) => ({ route: r.route, viewports: r.viewports, screens: r.screens ?? null, findings: r.findings.length })), errors: site.errors, measured: judged }));
|
|
661
619
|
return failed ? 1 : 0;
|
|
662
620
|
}
|
|
663
621
|
const section = (title, lines) => lines.length ? `
|
|
@@ -665,13 +623,16 @@ ${title}
|
|
|
665
623
|
${lines.map((l) => ` ${l}`).join("\n")}` : "";
|
|
666
624
|
const viewports = site.routes[0]?.viewports ?? [];
|
|
667
625
|
const summary = `${site.routes.length} route${site.routes.length === 1 ? "" : "s"} at ${viewports.length} viewports${judged.known ? `, ${judged.known} known breach${judged.known === 1 ? "" : "es"} in the baseline` : ""}`;
|
|
626
|
+
const screens = formatScreens(site.routes, config.screens);
|
|
627
|
+
const screensLine = screens ? `
|
|
628
|
+
phone screens to the footer: ${screens}` : "";
|
|
668
629
|
if (!failed) {
|
|
669
|
-
io.out(`craft viewports: pass. ${summary}
|
|
630
|
+
io.out(`craft viewports: pass. ${summary}.${screensLine}`);
|
|
670
631
|
return 0;
|
|
671
632
|
}
|
|
672
633
|
io.err(
|
|
673
634
|
[
|
|
674
|
-
`craft viewports: fail. ${summary}
|
|
635
|
+
`craft viewports: fail. ${summary}.${screensLine}`,
|
|
675
636
|
section("Did not answer (not measured, so not passed):", site.errors.map((e) => `${e.route}: ${e.error}`)),
|
|
676
637
|
section("Measured across the viewports, phone first:", judged.fresh),
|
|
677
638
|
section("The baseline claims more than is there (good: lower it, so the fix stays fixed):", judged.stale)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@domandigital/craft",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The Doman Digital house craft standard as numbers: OKLCh colour ramps that snap to existing anchors, derived semantic tokens with measured WCAG and APCA contrast, one shared motion vocabulary for CSS and JS, and a base stylesheet. Zero dependencies.",
|
|
6
6
|
"license": "Apache-2.0",
|