@domandigital/craft 0.20.4 → 0.21.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 +23 -0
- package/CHARACTER.md +3 -1
- package/COPY.md +9 -3
- package/README.md +73 -3
- package/dist/{audit-BN2GSFKT.js → audit-LQ2TDISV.js} +3 -3
- package/dist/audit.cjs +104 -3
- package/dist/audit.js +104 -3
- package/dist/{chunk-NQAYRK2K.js → chunk-4WAIOZ4W.js} +14 -14
- package/dist/{chunk-YOZLPNJS.js → chunk-6DRBFDWT.js} +1 -1
- package/dist/chunk-BQ7H23QP.js +261 -0
- package/dist/{chunk-2276FJVN.js → chunk-BYXDX66D.js} +4 -4
- package/dist/{chunk-JGWOA4LI.js → chunk-DE2ZAV4G.js} +6 -2
- package/dist/{chunk-KQF44RI3.js → chunk-FNB3NP57.js} +5 -5
- package/dist/{chunk-S3L7TEJF.js → chunk-G47ZCBD5.js} +2 -2
- package/dist/{chunk-7DQDF3VL.js → chunk-ONA73THJ.js} +99 -2
- package/dist/{chunk-GBDPNNC2.js → chunk-STWH5EYU.js} +5 -5
- package/dist/{cli-QGJB6OUB.js → cli-5QI6NIZN.js} +6 -6
- package/dist/{cli-AK4EQHUF.js → cli-5VHX5EJF.js} +5 -5
- package/dist/{cli-ZXDGVDYU.js → cli-7QL26YPR.js} +1 -1
- package/dist/cli-EYHRJJAX.js +241 -0
- package/dist/{cli-AFKXDGUH.js → cli-PBCU7Z64.js} +4 -4
- package/dist/{cli-6AZVLNWL.js → cli-RHDNHL4U.js} +5 -5
- package/dist/{cli-OYVZOHHV.js → cli-XAWJWK7I.js} +6 -6
- package/dist/{cli-7HKVVL3P.js → cli-YSGPGQ7R.js} +9 -9
- package/dist/cli.js +3 -3
- package/dist/index.cjs +104 -3
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +104 -3
- package/dist/phone.cjs +137 -11
- package/dist/phone.d.cts +86 -12
- package/dist/phone.d.ts +86 -12
- package/dist/phone.js +128 -11
- package/dist/{viewports-cli-WWWWEBKI.js → viewports-cli-XYYX4SKV.js} +12 -8
- package/package.json +1 -1
- package/word-lists.json +1 -1
- package/dist/chunk-IJUZ7RV4.js +0 -151
- package/dist/cli-6INEFJYN.js +0 -122
package/dist/index.js
CHANGED
|
@@ -1740,6 +1740,33 @@ var OUTCOME_PROMISES = [
|
|
|
1740
1740
|
/\byou(?:['’]ll| will)\s+(?:get|see|win|receive|start getting)\s+more\b/i,
|
|
1741
1741
|
new RegExp(`\\b(?:will|we['\u2019]ll)\\s+(?:(?:bring|drive|generate|get)\\s+you|bring|drive|generate)\\s+more\\s+(?:[\\w-]+\\s+)?${SALES_RESULTS}\\b`, "i")
|
|
1742
1742
|
];
|
|
1743
|
+
var SENTENCE_START = String.raw`(?<=^|[\n>"“]|[.!?]\s+)`;
|
|
1744
|
+
var MATEY = [
|
|
1745
|
+
// "Alright Carlo,", "Oi Dave", "Alright, mate"
|
|
1746
|
+
/\b(?:Alright|Oi)(?:,\s*|\s+)(?:mate\b|[A-Z][a-z]+\b)/,
|
|
1747
|
+
// "mate" as a form of address, not "a mate who knows"
|
|
1748
|
+
/,\s*mate\b|\b(?:thanks|ta|cheers|alright|oi|fussed|sorry|worries|bother|hiya|hey)\s+mate\b/i,
|
|
1749
|
+
/\b(?:innit|chuffed|lovely jubbly)\b|\bbish,?\s+bash,?\s+bosh\b|\bbob['’]?s\s+your\s+uncle\b/i,
|
|
1750
|
+
/\bno\s+(?:worries|bother)\b|\bnot\s+fussed\b/i,
|
|
1751
|
+
// "Right then," opening a sentence, not "the right then left turn"
|
|
1752
|
+
new RegExp(`${SENTENCE_START}Right,?\\s+then\\b[,!.]`),
|
|
1753
|
+
// "we'll get you sorted", "all sorted", "Sorted!", not "sorted by date"
|
|
1754
|
+
/\b(?:all|get(?:ting)?\s+(?:it|that|this|you|them|everything)|got\s+(?:it|that|you)|you['’]re)\s+sorted\b(?!\s+(?:by|in|into|alphabetically)\b)/i,
|
|
1755
|
+
new RegExp(`${SENTENCE_START}Sorted[.!]`),
|
|
1756
|
+
// "proper" as an intensifier before an adjective, not "proper planning"
|
|
1757
|
+
/\bproper\s+(?:good|nice|quick|tidy|lovely|decent|chuffed|busy|mint|smart|happy|gutted|brilliant|buzzing)\b/i,
|
|
1758
|
+
/\bgive\s+(?:me|us)\s+a\s+(?:shout|bell|buzz|tinkle)\b/i,
|
|
1759
|
+
/\bwhen\s+you\s+(?:get|have|['’]ve\s+got)\s+a\s+(?:sec|min|mo)\b/i,
|
|
1760
|
+
/\bhave\s+a\s+(?:quick\s+)?(?:gander|squiz|nosey|nose(?:\s+(?:round|around))?)\b(?!\s+for\b)/i,
|
|
1761
|
+
/\bcheck\s+out\s+(?:our|my|this|these)\b/i
|
|
1762
|
+
];
|
|
1763
|
+
var CLIPPED = [
|
|
1764
|
+
/\b(?:take|takes|took|taking|have|has|had|having)\s+(?:quick\s+)?look\s+(?:at|on|over|through|in)\b/i,
|
|
1765
|
+
/\bgive\s+(?:it|this|that|them)\s+(?:go|try|look|read|whirl|bash)\b(?![-\w])/i,
|
|
1766
|
+
new RegExp(`${SENTENCE_START}Quick\\s+one\\b(?=\\s*[:,.!?\u2014\u2013-]|\\s+(?:for you|before|on|about)\\b)`),
|
|
1767
|
+
// "Alright Carlo, built you a proper site": the "I" left out
|
|
1768
|
+
/(?<=^|[\n>"“]|[.!?—–-]\s*|,\s+)(?:[Bb]uilt|[Mm]ade|[Pp]ut together|[Kk]nocked up)\s+(?:you|your\s+\w+|the\s+\w+)\s+an?\b/
|
|
1769
|
+
];
|
|
1743
1770
|
var NEGATION = [
|
|
1744
1771
|
// "It's not just a haircut, it's an experience."
|
|
1745
1772
|
/\b(?:it['’]?s|it is|this is|this isn['’]t|we['’]re|we are|they['’]re)\s+(?:not|more than)\s+(?:just|only|merely|simply)?\s*[^.!?\n]{1,60}?[,;.—-]\s*(?:it['’]?s|it is|this is|we['’]re|we are|they['’]re)\b/i,
|
|
@@ -1976,6 +2003,40 @@ var COPY_TELLS = [
|
|
|
1976
2003
|
]
|
|
1977
2004
|
}
|
|
1978
2005
|
},
|
|
2006
|
+
{
|
|
2007
|
+
id: "matey-register",
|
|
2008
|
+
name: "Matey register",
|
|
2009
|
+
generation: 2,
|
|
2010
|
+
severity: "warn",
|
|
2011
|
+
surface: "copy",
|
|
2012
|
+
why: "'Alright Dave,', 'no worries, we'll get you sorted', 'Quick one:', 'have look at': a put-on, over-familiar British voice, with the small words clipped out. A model asked to sound British writes it; the UK is broad, and a client reads it as cringe and a little rude.",
|
|
2013
|
+
fix: "Write plain, complete British English. 'Alright Dave, built you a site' becomes 'Hi Dave, I have built you a site'; 'No worries, we'll get you sorted' becomes 'We can fit you in on Thursday'.",
|
|
2014
|
+
detect: phrases([...MATEY, ...CLIPPED], quoted),
|
|
2015
|
+
fixtures: {
|
|
2016
|
+
flag: [
|
|
2017
|
+
// From the portal's seeded prospect follow-ups, 2026-10-19.
|
|
2018
|
+
f("emails/followup.md", "Alright Dave, it's Dmitri. No pressure, have a look when you get a min."),
|
|
2019
|
+
f("emails/followup.md", "All good if you're not fussed mate, I'll leave it there."),
|
|
2020
|
+
f("emails/followup.md", "Carlo, built the shop a proper site."),
|
|
2021
|
+
f("content/contact.md", "No worries, we'll get you sorted."),
|
|
2022
|
+
f("emails/quote.md", "Take look on these photos and give it go."),
|
|
2023
|
+
f("emails/quote.md", "Quick one: is Thursday still good?"),
|
|
2024
|
+
f("emails/quote.md", "Right then, here is the plan."),
|
|
2025
|
+
f("content/reviews.md", "The fitters did a proper good job and we were chuffed.")
|
|
2026
|
+
],
|
|
2027
|
+
pass: [
|
|
2028
|
+
f("content/home.md", "Proper planning saves a second visit."),
|
|
2029
|
+
f("content/certificates.md", "Download a sorted list of certificates, sorted by date."),
|
|
2030
|
+
f("content/directions.md", "At the junction, take the right then left turn."),
|
|
2031
|
+
// Chair and Blade's own approved line, and "mate" as a noun.
|
|
2032
|
+
f("content/home.md", "Every cut starts with a proper chat about what you actually want."),
|
|
2033
|
+
f("content/home.md", "It feels like getting a cut from a mate who knows what he is doing."),
|
|
2034
|
+
f("emails/quote.md", "Have a look at the quote and ring me if Thursday does not work."),
|
|
2035
|
+
f("content/contact.md", "Give us a ring on 01280 700123. We take a look at the fault before quoting."),
|
|
2036
|
+
f("content/home.md", "Built to last, and made for older houses.")
|
|
2037
|
+
]
|
|
2038
|
+
}
|
|
2039
|
+
},
|
|
1979
2040
|
{
|
|
1980
2041
|
id: "hollow-imperative",
|
|
1981
2042
|
name: "Hollow imperative",
|
|
@@ -2176,7 +2237,12 @@ var FALSE_RANGE = [
|
|
|
2176
2237
|
];
|
|
2177
2238
|
var RESIDUE = [
|
|
2178
2239
|
/\bas an AI(?:\s+language)?\s+(?:model|assistant)\b/i,
|
|
2179
|
-
|
|
2240
|
+
// At the start of a line, or after a sentence that ends in a full stop or
|
|
2241
|
+
// an exclamation mark: "I hope this email finds you well! Certainly, here
|
|
2242
|
+
// is the handover request." slipped through when only a line start counted
|
|
2243
|
+
// (found live, 2026-10-19). Not after a question mark: an FAQ answer may
|
|
2244
|
+
// honestly open "Do you work weekends? Absolutely, here are our hours."
|
|
2245
|
+
/(?<=^|[\n>]|[.!]\s+)\s*(?:Certainly|Sure|Absolutely|Of course|Great question)[!,.]\s+(?:Here(?:['’]s| is| are)|Below)\b/i,
|
|
2180
2246
|
/\bhere(?:['’]s| is)\s+(?:a|an|the|your)\s+(?:revised|rewritten|updated|polished|refined|improved|draft|more concise)\s+(?:version|draft|copy)\b/i,
|
|
2181
2247
|
/\blet me know if you(?:['’]d| would) like me to\b/i,
|
|
2182
2248
|
/\b(?:my|the) (?:knowledge|training) cut-?off\b|\bas of my last (?:update|training)\b/i,
|
|
@@ -2192,6 +2258,13 @@ var RESIDUE = [
|
|
|
2192
2258
|
/[\uE200-\uE2FF]*entity[\uE200-\uE2FF]*\["[a-z_]+"\s*,\s*"[^"\n]{1,80}"(?:\s*,\s*"[^"\n]{0,160}")*\][\uE200-\uE2FF]*/,
|
|
2193
2259
|
/【\d+(?:[:†][^】]{0,40})?】/
|
|
2194
2260
|
];
|
|
2261
|
+
var STOCK_EMAIL = [
|
|
2262
|
+
/\bI (?:hope|trust) (?:this|my|the) (?:e-?mail|message|note|letter) finds you(?: all)? (?:well|in good health)\b/i,
|
|
2263
|
+
/\bI hope you(?:['’]re| are)(?: all)? (?:doing |keeping )?well\b/i,
|
|
2264
|
+
/\b(?:I )?hope you(?:['’]re| are) having an? (?:great|good|lovely|wonderful|fantastic) (?:day|week|weekend)\b/i,
|
|
2265
|
+
/\blet (?:me|us) know if you have any (?:further |other |more )?questions\b/i,
|
|
2266
|
+
/\b(?:do not|don['’]t) hesitate to (?:contact|reach out|get in touch|ask|call)\b/i
|
|
2267
|
+
];
|
|
2195
2268
|
var AI_UTM = /utm_source=(?:chatgpt\.com|chat\.openai\.com|openai|perplexity(?:\.ai)?|claude\.ai|copilot(?:\.microsoft\.com)?|gemini(?:\.google\.com)?)\b/gi;
|
|
2196
2269
|
function residueHits(ctx) {
|
|
2197
2270
|
const hits = phrases(RESIDUE, quoted, allText)(ctx);
|
|
@@ -2273,15 +2346,39 @@ var RESEARCH_TELLS = [
|
|
|
2273
2346
|
f("content/home.md", "Prices start at \xA340 :contentReference[oaicite:2]{index=2} for a full set."),
|
|
2274
2347
|
f("content/home.md", "Prices start at \xA340\u30104\u2020source\u3011 for a full set."),
|
|
2275
2348
|
f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=chatgpt.com">Read the guide</a>;`),
|
|
2276
|
-
f("content/home.md", "Here's a more concise version for the hero.")
|
|
2349
|
+
f("content/home.md", "Here's a more concise version for the hero."),
|
|
2350
|
+
// The client handover email that a clean copy-check let through, 2026-10-19.
|
|
2351
|
+
f("emails/handover.md", "Hi,\n\nI hope this email finds you well! Certainly, here is the handover request. Let me know if you have any questions.")
|
|
2277
2352
|
],
|
|
2278
2353
|
pass: [
|
|
2279
2354
|
f("content/home.md", "Certainly the busiest week of the year: book early."),
|
|
2355
|
+
f("content/faq.md", "Do you work weekends? Absolutely, here are our hours."),
|
|
2280
2356
|
f("content/home.md", "Here's the price list for 2026."),
|
|
2281
2357
|
f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=newsletter">Read the guide</a>;`)
|
|
2282
2358
|
]
|
|
2283
2359
|
}
|
|
2284
2360
|
},
|
|
2361
|
+
{
|
|
2362
|
+
id: "stock-email-line",
|
|
2363
|
+
name: "Stock email line",
|
|
2364
|
+
generation: 1,
|
|
2365
|
+
severity: "warn",
|
|
2366
|
+
surface: "copy",
|
|
2367
|
+
why: "'I hope this email finds you well', 'let me know if you have any questions', 'don't hesitate to contact us': the opener and closer of a form email. A client reads past them, and a model drafting an email writes them every time.",
|
|
2368
|
+
fix: "Cut the opener and start with the reason for writing. Replace the closer with the next step and who to ask: 'Ring me on 07700 900123 if the date does not work.'",
|
|
2369
|
+
detect: phrases(STOCK_EMAIL, quoted),
|
|
2370
|
+
fixtures: {
|
|
2371
|
+
flag: [
|
|
2372
|
+
f("emails/handover.md", "Hi,\n\nI hope this email finds you well! Certainly, here is the handover request. Let me know if you have any questions."),
|
|
2373
|
+
f("emails/quote.md", "Hope you're having a great week. The quote is attached."),
|
|
2374
|
+
f("content/contact.md", "Please don't hesitate to contact us about a rewire.")
|
|
2375
|
+
],
|
|
2376
|
+
pass: [
|
|
2377
|
+
f("emails/handover.md", "Hi Sam,\n\nThe handover pack is attached: certificates, the board schedule and the warranty. Ring me on 07700 900123 if anything is missing."),
|
|
2378
|
+
f("content/contact.md", "If you have any questions, ring Jo on 01280 700123.")
|
|
2379
|
+
]
|
|
2380
|
+
}
|
|
2381
|
+
},
|
|
2285
2382
|
{
|
|
2286
2383
|
id: "prompt-context",
|
|
2287
2384
|
name: "Prompt context",
|
|
@@ -4891,6 +4988,10 @@ var HOUSE = {
|
|
|
4891
4988
|
// New 2026-10-14 (founder): review first, like every new tell, until estate hits are read.
|
|
4892
4989
|
"sales-tagline": { tier: "review", label: "sales tagline, say what you will actually do" },
|
|
4893
4990
|
"outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
|
|
4991
|
+
// New 2026-10-19: review first, like every new tell, until estate hits are read.
|
|
4992
|
+
"stock-email-line": { tier: "review", label: "stock email line, start with the reason for writing and end with the next step" },
|
|
4993
|
+
// New 2026-10-19 (founder): review first, like every new tell, until estate hits are read.
|
|
4994
|
+
"matey-register": { tier: "review", label: "matey register, write plain and complete British English" },
|
|
4894
4995
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
4895
4996
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
4896
4997
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -6096,7 +6197,7 @@ var IMAGERY_TELLS = [
|
|
|
6096
6197
|
];
|
|
6097
6198
|
|
|
6098
6199
|
// src/character/check.ts
|
|
6099
|
-
var CATALOGUE_VERSION = "2026.10.
|
|
6200
|
+
var CATALOGUE_VERSION = "2026.10.4";
|
|
6100
6201
|
var CATALOGUE = [
|
|
6101
6202
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
6102
6203
|
...SPECIFICITY_COPY_TELLS,
|
package/dist/phone.cjs
CHANGED
|
@@ -33,8 +33,13 @@ __export(phone_exports, {
|
|
|
33
33
|
WIDE_WIDTHS: () => WIDE_WIDTHS,
|
|
34
34
|
bandOf: () => bandOf,
|
|
35
35
|
checkExceptions: () => checkExceptions,
|
|
36
|
+
cleanRoute: () => cleanRoute,
|
|
36
37
|
formatWidths: () => formatWidths,
|
|
38
|
+
hasReason: () => hasReason,
|
|
39
|
+
hasUnexcusedEntries: () => hasUnexcusedEntries,
|
|
40
|
+
judgeGrowth: () => judgeGrowth,
|
|
37
41
|
judgeMeasured: () => judgeMeasured,
|
|
42
|
+
judgeRoutes: () => judgeRoutes,
|
|
38
43
|
judgeStatic: () => judgeStatic,
|
|
39
44
|
mainActionFor: () => mainActionFor,
|
|
40
45
|
measurePhone: () => measurePhone,
|
|
@@ -45,8 +50,12 @@ __export(phone_exports, {
|
|
|
45
50
|
measureViewports: () => measureViewports,
|
|
46
51
|
mergeViewportFindings: () => mergeViewportFindings,
|
|
47
52
|
normaliseElement: () => normaliseElement,
|
|
53
|
+
parseBaseline: () => parseBaseline,
|
|
48
54
|
reasonOf: () => reasonOf,
|
|
49
55
|
routeFile: () => routeFile,
|
|
56
|
+
routeMatches: () => routeMatches,
|
|
57
|
+
routesFromManifest: () => routesFromManifest,
|
|
58
|
+
routesFromPages: () => routesFromPages,
|
|
50
59
|
scanPhoneFirst: () => scanPhoneFirst,
|
|
51
60
|
sweepWidths: () => sweepWidths,
|
|
52
61
|
writeBaseline: () => writeBaseline
|
|
@@ -62,13 +71,14 @@ var PHONE = {
|
|
|
62
71
|
isMobile: true,
|
|
63
72
|
hasTouch: true
|
|
64
73
|
};
|
|
65
|
-
var PHONE_FLOORS = { targetPx: 44, fieldFontPx: 16, textPx: 12 };
|
|
74
|
+
var PHONE_FLOORS = { targetPx: 44, fieldFontPx: 16, textPx: 12, fieldHeightPx: 48 };
|
|
66
75
|
var DEFAULT_MEASURED_DIR = "phone-first/measured";
|
|
67
76
|
var routeFile = (route) => `${route === "/" ? "home" : route.replace(/^\/+|\/+$/g, "").replace(/[^\w.-]+/g, "-")}.json`;
|
|
68
77
|
|
|
69
78
|
// src/phone/measure-in-page.ts
|
|
70
79
|
function measurePhoneInPage(floors) {
|
|
71
80
|
const out = [];
|
|
81
|
+
const fieldHeightPx = floors.fieldHeightPx ?? 48;
|
|
72
82
|
const only = floors.only;
|
|
73
83
|
const on = (c) => !only || only.includes(c);
|
|
74
84
|
const vw = document.documentElement.clientWidth;
|
|
@@ -112,6 +122,9 @@ function measurePhoneInPage(floors) {
|
|
|
112
122
|
if (!shown(e) || quiet(e) || e.tagName === "INPUT" && NOT_TEXT.has(e.type)) continue;
|
|
113
123
|
const px = parseFloat(getComputedStyle(e).fontSize);
|
|
114
124
|
if (px < floors.fieldFontPx) out.push({ check: "field", element: describe(e), detail: `set at ${px}px, under ${floors.fieldFontPx}px, so Safari on iOS zooms on focus` });
|
|
125
|
+
const box = e.getBoundingClientRect();
|
|
126
|
+
const height = Math.round(box.height);
|
|
127
|
+
if (box.width > 1 && height > 1 && height < fieldHeightPx) out.push({ check: "field", element: describe(e), detail: `${height}px tall, under ${fieldHeightPx}px (use min-h-12)` });
|
|
115
128
|
}
|
|
116
129
|
for (const e of on("text") ? Array.from(document.body.querySelectorAll("*")) : []) {
|
|
117
130
|
if (!shown(e) || quiet(e) || ["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"].includes(e.tagName)) continue;
|
|
@@ -419,17 +432,26 @@ function measureViewportInPage(arg) {
|
|
|
419
432
|
}
|
|
420
433
|
|
|
421
434
|
// src/phone/judge.ts
|
|
435
|
+
var hasReason = (entry) => typeof entry.reason === "string" && entry.reason.trim() !== "";
|
|
436
|
+
function parseBaseline(raw, where) {
|
|
437
|
+
const b = raw ?? {};
|
|
438
|
+
const entries = [...b.static ?? [], ...b.measured ?? []];
|
|
439
|
+
const bad = entries.find((e) => e.reason !== void 0 && typeof e.reason !== "string");
|
|
440
|
+
if (bad) throw new Error(`${where}: an entry has a "reason" that is not text: ${JSON.stringify(bad)}`);
|
|
441
|
+
return { ...b.note ? { note: b.note } : {}, static: b.static ?? [], measured: b.measured ?? [] };
|
|
442
|
+
}
|
|
422
443
|
var EMPTY_BASELINE = { static: [], measured: [] };
|
|
423
444
|
var MEDIA = /@(?:custom-)?media\b([^{;]*)/g;
|
|
424
445
|
var CAPS_WIDTH = /\bmax-(?:device-)?width\s*:|\bwidth\s*<|>=?\s*(?:device-)?width\b/i;
|
|
425
446
|
var VARIANT = /(?<![\w@-])max-(\[[^\]\s]+\]|[a-z0-9]+):/g;
|
|
426
447
|
var NOT_VARIANTS = /* @__PURE__ */ new Set(["width", "height", "lines", "resolution", "zoom", "inline-size", "block-size"]);
|
|
427
448
|
var OK = /mobile-first-ok\b:?(.*)/;
|
|
449
|
+
var VAR_OPACITY = /[^\s"'`]*var\([^)]*\)\]\/[0-9]+/g;
|
|
428
450
|
function reasonOf(line) {
|
|
429
451
|
const m = line?.match(OK);
|
|
430
452
|
return m ? m[1].replace(/\*\/|-->|[{}]/g, "").trim() : null;
|
|
431
453
|
}
|
|
432
|
-
function scanPhoneFirst(files, exceptions = []) {
|
|
454
|
+
function scanPhoneFirst(files, exceptions = [], options = {}) {
|
|
433
455
|
const excusedFiles = new Set(exceptions.filter((e) => e.reason?.trim()).map((e) => e.file));
|
|
434
456
|
const findings = [];
|
|
435
457
|
let excused = 0;
|
|
@@ -444,7 +466,18 @@ function scanPhoneFirst(files, exceptions = []) {
|
|
|
444
466
|
for (const m of text.matchAll(VARIANT)) {
|
|
445
467
|
if (!NOT_VARIANTS.has(m[1])) found.push({ line: lineOf(m.index), what: m[0] });
|
|
446
468
|
}
|
|
447
|
-
|
|
469
|
+
const silent = /* @__PURE__ */ new Set();
|
|
470
|
+
if (options.tailwind3?.(path)) {
|
|
471
|
+
for (const m of text.matchAll(VAR_OPACITY)) {
|
|
472
|
+
silent.add(found.push({ line: lineOf(m.index), what: `${m[0]} (Tailwind 3 emits no CSS for opacity on a var() colour)` }) - 1);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const order = found.map((f, i) => ({ ...f, silent: silent.has(i) })).sort((a, b) => a.line - b.line);
|
|
476
|
+
for (const { line, what, silent: cannotExcuse } of order) {
|
|
477
|
+
if (cannotExcuse) {
|
|
478
|
+
findings.push({ file: path, line, what });
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
448
481
|
const here = reasonOf(lines[line - 1]);
|
|
449
482
|
const above = reasonOf(lines[line - 2]);
|
|
450
483
|
if (here || above) {
|
|
@@ -549,17 +582,101 @@ var LABEL = {
|
|
|
549
582
|
tel: "phone number is not a link",
|
|
550
583
|
linelength: "line too long"
|
|
551
584
|
};
|
|
552
|
-
function writeBaseline(findings, routes, note) {
|
|
585
|
+
function writeBaseline(findings, routes, note, previous) {
|
|
553
586
|
const byFile = tally(findings, (f) => f.file);
|
|
554
587
|
const measured = routes.flatMap(
|
|
555
588
|
({ route, findings: findings2 }) => [...tally(findings2, (f) => measuredKey(route, f.check, normaliseElement(f.element))).values()].map(({ item, count }) => ({ route, check: item.check, element: normaliseElement(item.element), count }))
|
|
556
589
|
);
|
|
590
|
+
const staticReasons = new Map((previous?.static ?? []).filter(hasReason).map((b) => [b.file, b.reason]));
|
|
591
|
+
const measuredReasons = new Map((previous?.measured ?? []).filter(hasReason).map((b) => [measuredKey(b.route, b.check, b.element), b.reason]));
|
|
557
592
|
return {
|
|
558
593
|
...note ? { note } : {},
|
|
559
|
-
static: [...byFile.values()].map(({ item, count }) => ({ file: item.file, count, found: findings.filter((f) => f.file === item.file).map((f) => f.what) })).sort((a, b) => a.file.localeCompare(b.file)),
|
|
560
|
-
measured: measured.sort((a, b) => a.route.localeCompare(b.route) || a.check.localeCompare(b.check) || a.element.localeCompare(b.element))
|
|
594
|
+
static: [...byFile.values()].map(({ item, count }) => ({ file: item.file, count, found: findings.filter((f) => f.file === item.file).map((f) => f.what), ...staticReasons.has(item.file) ? { reason: staticReasons.get(item.file) } : {} })).sort((a, b) => a.file.localeCompare(b.file)),
|
|
595
|
+
measured: measured.map((m) => measuredReasons.has(measuredKey(m.route, m.check, m.element)) ? { ...m, reason: measuredReasons.get(measuredKey(m.route, m.check, m.element)) } : m).sort((a, b) => a.route.localeCompare(b.route) || a.check.localeCompare(b.check) || a.element.localeCompare(b.element))
|
|
561
596
|
};
|
|
562
597
|
}
|
|
598
|
+
function judgeGrowth(current, base, baseName = "main") {
|
|
599
|
+
const out = [];
|
|
600
|
+
const baseStatic = new Map(base.static.map((b) => [b.file, b.count]));
|
|
601
|
+
for (const b of current.static) {
|
|
602
|
+
const was = baseStatic.get(b.file) ?? 0;
|
|
603
|
+
if (b.count > was && !hasReason(b)) out.push(`${b.file}: ${was ? `${b.count} desktop-first rules, ${baseName} allows ${was}` : `${b.count} desktop-first rule${b.count === 1 ? "" : "s"} added to the baseline`}`);
|
|
604
|
+
}
|
|
605
|
+
const baseMeasured = new Map(base.measured.map((b) => [measuredKey(b.route, b.check, b.element), b.count]));
|
|
606
|
+
for (const b of current.measured) {
|
|
607
|
+
const was = baseMeasured.get(measuredKey(b.route, b.check, b.element)) ?? 0;
|
|
608
|
+
if (b.count > was && !hasReason(b)) out.push(`${b.route}: ${LABEL[b.check]} ${b.element}: ${was ? `${b.count} in the baseline, ${baseName} allows ${was}` : `added to the baseline (${b.count})`}`);
|
|
609
|
+
}
|
|
610
|
+
return out;
|
|
611
|
+
}
|
|
612
|
+
var hasUnexcusedEntries = (b) => [...b.static, ...b.measured].some((e) => !hasReason(e));
|
|
613
|
+
var PAGE_FILE = /(?:^|\/)page\.(?:tsx|ts|jsx|js|mdx)$/;
|
|
614
|
+
var INTERCEPTING = /^\(\.{1,3}\)/;
|
|
615
|
+
function routesFromPages(files) {
|
|
616
|
+
const routes = /* @__PURE__ */ new Set();
|
|
617
|
+
for (const file of files) {
|
|
618
|
+
const rel = file.replace(/\\/g, "/");
|
|
619
|
+
if (!PAGE_FILE.test(rel)) continue;
|
|
620
|
+
const dirs = rel.split("/").slice(0, -1);
|
|
621
|
+
if (dirs.some((d) => INTERCEPTING.test(d) || d.startsWith("_"))) continue;
|
|
622
|
+
const url = dirs.filter((d) => !/^\(.*\)$/.test(d) && !d.startsWith("@"));
|
|
623
|
+
routes.add(`/${url.join("/")}`);
|
|
624
|
+
}
|
|
625
|
+
return [...routes].sort();
|
|
626
|
+
}
|
|
627
|
+
function routesFromManifest(manifest) {
|
|
628
|
+
const routes = /* @__PURE__ */ new Set();
|
|
629
|
+
for (const [key, route] of Object.entries(manifest ?? {})) {
|
|
630
|
+
if (!/(?:^|\/)page$/.test(key) || typeof route !== "string") continue;
|
|
631
|
+
if (route.split("/").some((d) => d.startsWith("_"))) continue;
|
|
632
|
+
routes.add(route);
|
|
633
|
+
}
|
|
634
|
+
return [...routes].sort();
|
|
635
|
+
}
|
|
636
|
+
var segmentsOf = (route) => route.split("/").filter(Boolean);
|
|
637
|
+
var rank = (seg) => seg.startsWith("[[...") ? 3 : seg.startsWith("[...") ? 2 : seg.startsWith("[") ? 1 : 0;
|
|
638
|
+
var cleanRoute = (route) => {
|
|
639
|
+
const path = route.replace(/[?#].*$/, "").replace(/\/+$/, "");
|
|
640
|
+
return path === "" ? "/" : path;
|
|
641
|
+
};
|
|
642
|
+
function routeMatches(pattern, route) {
|
|
643
|
+
if (pattern === route) return true;
|
|
644
|
+
const want = segmentsOf(pattern);
|
|
645
|
+
const have = segmentsOf(route);
|
|
646
|
+
for (let i = 0; i < want.length; i++) {
|
|
647
|
+
const seg = want[i];
|
|
648
|
+
if (seg.startsWith("[[...")) return true;
|
|
649
|
+
if (seg.startsWith("[...")) return have.length > i;
|
|
650
|
+
if (have[i] === void 0) return false;
|
|
651
|
+
if (!seg.startsWith("[") && seg !== have[i]) return false;
|
|
652
|
+
}
|
|
653
|
+
return want.length === have.length;
|
|
654
|
+
}
|
|
655
|
+
var moreSpecific = (a, b) => {
|
|
656
|
+
const x = segmentsOf(a).map(rank);
|
|
657
|
+
const y = segmentsOf(b).map(rank);
|
|
658
|
+
for (let i = 0; i < Math.min(x.length, y.length); i++) if (x[i] !== y[i]) return x[i] < y[i];
|
|
659
|
+
return x.length > y.length;
|
|
660
|
+
};
|
|
661
|
+
var excludes = (entry, page) => entry.endsWith("/*") ? page === entry.slice(0, -2) || page.startsWith(entry.slice(0, -1)) : entry === page;
|
|
662
|
+
function judgeRoutes(pages, measuredRoutes, excluded = []) {
|
|
663
|
+
const problems = [];
|
|
664
|
+
excluded.forEach((e, i) => {
|
|
665
|
+
const at = `excludeRoutes[${i}]`;
|
|
666
|
+
const x = e;
|
|
667
|
+
if (typeof x?.route !== "string" || !x.route.startsWith("/")) problems.push(`${at}: names no route (start it with /)`);
|
|
668
|
+
else if (typeof x.reason !== "string" || !x.reason.trim()) problems.push(`${at} (${x.route}): no reason. Say why the gate cannot measure this page, or remove the entry.`);
|
|
669
|
+
else if (!pages.some((p) => excludes(x.route, p))) problems.push(`${at}: ${x.route} is not a page route of the app. Remove the entry.`);
|
|
670
|
+
});
|
|
671
|
+
const named = excluded.filter((e) => typeof e?.route === "string" && typeof e.reason === "string" && e.reason.trim());
|
|
672
|
+
const covered = /* @__PURE__ */ new Set();
|
|
673
|
+
for (const route of measuredRoutes.map(cleanRoute)) {
|
|
674
|
+
const hit = pages.filter((p) => routeMatches(p, route)).sort((a, b) => moreSpecific(a, b) ? -1 : moreSpecific(b, a) ? 1 : 0)[0];
|
|
675
|
+
if (hit) covered.add(hit);
|
|
676
|
+
}
|
|
677
|
+
for (const page of pages) if (named.some((e) => excludes(e.route, page))) covered.add(page);
|
|
678
|
+
return { missing: pages.filter((p) => !covered.has(p)), problems, covered: covered.size };
|
|
679
|
+
}
|
|
563
680
|
|
|
564
681
|
// src/phone/viewports.ts
|
|
565
682
|
var BANDS = [
|
|
@@ -603,16 +720,16 @@ var DEFAULT_THRESHOLDS = {
|
|
|
603
720
|
var mainActionFor = (config, route) => typeof config === "string" ? config : config ? config[route] ?? config["*"] : void 0;
|
|
604
721
|
function formatWidths(failing, tested) {
|
|
605
722
|
const plain = tested.filter((n) => /^\d+$/.test(n)).sort((a, b) => Number(a) - Number(b));
|
|
606
|
-
const
|
|
723
|
+
const rank2 = new Map(plain.map((name, i) => [name, i]));
|
|
607
724
|
const names = [...new Set(failing)];
|
|
608
725
|
const runs = [];
|
|
609
|
-
for (const name of names.filter((n) =>
|
|
726
|
+
for (const name of names.filter((n) => rank2.has(n)).sort((a, b) => rank2.get(a) - rank2.get(b))) {
|
|
610
727
|
const last = runs.at(-1);
|
|
611
|
-
if (last &&
|
|
728
|
+
if (last && rank2.get(name) === rank2.get(last.at(-1)) + 1) last.push(name);
|
|
612
729
|
else runs.push([name]);
|
|
613
730
|
}
|
|
614
731
|
const out = runs.map((r) => r.length > 1 ? `${r[0]}-${r.at(-1)}` : r[0]);
|
|
615
|
-
return [...out, ...names.filter((n) => !
|
|
732
|
+
return [...out, ...names.filter((n) => !rank2.has(n))].join(", ");
|
|
616
733
|
}
|
|
617
734
|
var keyOf = (f) => `${f.check}\0${f.element.replace(/("[^]*)$/, (t) => t.replace(/\d+/g, "#"))}`;
|
|
618
735
|
function mergeViewportFindings(perViewport, tested) {
|
|
@@ -777,7 +894,7 @@ async function measureSite(browser, options) {
|
|
|
777
894
|
async function measurePhone(page, options) {
|
|
778
895
|
if (!options.route.startsWith("/")) throw new Error(`measurePhone: route "${options.route}" should start with /`);
|
|
779
896
|
await page.evaluate(() => document.fonts?.ready.then(() => void 0), void 0);
|
|
780
|
-
const findings = await page.evaluate(measurePhoneInPage, PHONE_FLOORS);
|
|
897
|
+
const findings = await page.evaluate(measurePhoneInPage, { ...PHONE_FLOORS, ...options.floors ?? {} });
|
|
781
898
|
const viewport = page.viewportSize?.() ?? void 0;
|
|
782
899
|
const result = { route: options.route, ...viewport ? { viewport } : {}, findings };
|
|
783
900
|
const dir = options.out ?? DEFAULT_MEASURED_DIR;
|
|
@@ -801,8 +918,13 @@ async function measurePhone(page, options) {
|
|
|
801
918
|
WIDE_WIDTHS,
|
|
802
919
|
bandOf,
|
|
803
920
|
checkExceptions,
|
|
921
|
+
cleanRoute,
|
|
804
922
|
formatWidths,
|
|
923
|
+
hasReason,
|
|
924
|
+
hasUnexcusedEntries,
|
|
925
|
+
judgeGrowth,
|
|
805
926
|
judgeMeasured,
|
|
927
|
+
judgeRoutes,
|
|
806
928
|
judgeStatic,
|
|
807
929
|
mainActionFor,
|
|
808
930
|
measurePhone,
|
|
@@ -813,8 +935,12 @@ async function measurePhone(page, options) {
|
|
|
813
935
|
measureViewports,
|
|
814
936
|
mergeViewportFindings,
|
|
815
937
|
normaliseElement,
|
|
938
|
+
parseBaseline,
|
|
816
939
|
reasonOf,
|
|
817
940
|
routeFile,
|
|
941
|
+
routeMatches,
|
|
942
|
+
routesFromManifest,
|
|
943
|
+
routesFromPages,
|
|
818
944
|
scanPhoneFirst,
|
|
819
945
|
sweepWidths,
|
|
820
946
|
writeBaseline
|
package/dist/phone.d.cts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
interface SourceFile {
|
|
2
|
-
/** Repo-relative path. The extension decides how the file is read. */
|
|
3
|
-
path: string;
|
|
4
|
-
text: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
1
|
/**
|
|
8
2
|
* The in-page half of the phone-first gate.
|
|
9
3
|
*
|
|
@@ -23,6 +17,12 @@ interface PhoneFloors {
|
|
|
23
17
|
targetPx: number;
|
|
24
18
|
fieldFontPx: number;
|
|
25
19
|
textPx: number;
|
|
20
|
+
/**
|
|
21
|
+
* The height of a text field, select or textarea (portal standard rule 5:
|
|
22
|
+
* 48px inputs). Left out, 48. Below `targetPx` the field also fails the
|
|
23
|
+
* target check; this floor is what catches 44 to 47px.
|
|
24
|
+
*/
|
|
25
|
+
fieldHeightPx?: number;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* What a measure can find. The first five are the 375px floors (DOM-775); the
|
|
@@ -44,6 +44,12 @@ declare function measurePhoneInPage(floors: PhoneFloors & {
|
|
|
44
44
|
only?: PhoneCheck[];
|
|
45
45
|
}): RawPhoneFinding[];
|
|
46
46
|
|
|
47
|
+
interface SourceFile {
|
|
48
|
+
/** Repo-relative path. The extension decides how the file is read. */
|
|
49
|
+
path: string;
|
|
50
|
+
text: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
/**
|
|
48
54
|
* The judging half of the phone-first gate: pure text work, tested without a
|
|
49
55
|
* browser or a disk.
|
|
@@ -62,6 +68,11 @@ declare function measurePhoneInPage(floors: PhoneFloors & {
|
|
|
62
68
|
* the day it goes in while the existing ones stay listed in the open. The
|
|
63
69
|
* baseline only shrinks: a count higher than what is found fails as well
|
|
64
70
|
* ("lower the baseline"), so a fix cannot leave room for the next breach.
|
|
71
|
+
*
|
|
72
|
+
* Shrinking is also enforced against the base branch (DOM-1053): a baseline
|
|
73
|
+
* entry, or a higher count, that main's copy does not have fails, unless the
|
|
74
|
+
* entry carries a `reason` (a named exception: third-party UI, a
|
|
75
|
+
* destructive-only form).
|
|
65
76
|
*/
|
|
66
77
|
|
|
67
78
|
interface StaticFinding {
|
|
@@ -86,18 +97,30 @@ interface MeasuredRoute {
|
|
|
86
97
|
interface Baseline {
|
|
87
98
|
/** Why the baseline exists and which issue burns it down. Free text, for a person. */
|
|
88
99
|
note?: string;
|
|
100
|
+
/**
|
|
101
|
+
* `reason` makes an entry a named exception: it may be added, or raised,
|
|
102
|
+
* after the baseline went in. Absent on the faults that were there first.
|
|
103
|
+
*/
|
|
89
104
|
static: {
|
|
90
105
|
file: string;
|
|
91
106
|
count: number;
|
|
92
107
|
found?: string[];
|
|
108
|
+
reason?: string;
|
|
93
109
|
}[];
|
|
94
110
|
measured: {
|
|
95
111
|
route: string;
|
|
96
112
|
check: PhoneCheck;
|
|
97
113
|
element: string;
|
|
98
114
|
count: number;
|
|
115
|
+
reason?: string;
|
|
99
116
|
}[];
|
|
100
117
|
}
|
|
118
|
+
/** An entry is a named exception when its `reason` says something. */
|
|
119
|
+
declare const hasReason: (entry: {
|
|
120
|
+
reason?: unknown;
|
|
121
|
+
}) => boolean;
|
|
122
|
+
/** A baseline file, read. Old files (no `reason`, no `note`) read as before; a `reason` that is not text is an error. */
|
|
123
|
+
declare function parseBaseline(raw: unknown, where: string): Baseline;
|
|
101
124
|
declare const EMPTY_BASELINE: Baseline;
|
|
102
125
|
/** The reason in a `mobile-first-ok: <reason>` comment, "" for the marker with no reason, null for none. */
|
|
103
126
|
declare function reasonOf(line: string | undefined): string | null;
|
|
@@ -106,7 +129,10 @@ declare function reasonOf(line: string | undefined): string | null;
|
|
|
106
129
|
* `mobile-first-ok: <reason>` on the line or the line above, or the whole file
|
|
107
130
|
* in `exceptions` with a reason. A marker with no reason is itself a finding.
|
|
108
131
|
*/
|
|
109
|
-
declare function scanPhoneFirst(files: SourceFile[], exceptions?: FileException[]
|
|
132
|
+
declare function scanPhoneFirst(files: SourceFile[], exceptions?: FileException[], options?: {
|
|
133
|
+
/** True for a file in an app whose tailwindcss is v3: it is also scanned for `/NN` opacity on a `var()` colour. */
|
|
134
|
+
tailwind3?: (path: string) => boolean;
|
|
135
|
+
}): {
|
|
110
136
|
findings: StaticFinding[];
|
|
111
137
|
excused: number;
|
|
112
138
|
};
|
|
@@ -128,7 +154,48 @@ interface Judgement {
|
|
|
128
154
|
declare function judgeStatic(findings: StaticFinding[], baseline: Baseline): Judgement;
|
|
129
155
|
declare function judgeMeasured(routes: MeasuredRoute[], baseline: Baseline): Judgement;
|
|
130
156
|
/** The baseline that makes this run pass: what is there now, counted. */
|
|
131
|
-
declare function writeBaseline(findings: StaticFinding[], routes: MeasuredRoute[], note?: string): Baseline;
|
|
157
|
+
declare function writeBaseline(findings: StaticFinding[], routes: MeasuredRoute[], note?: string, previous?: Baseline): Baseline;
|
|
158
|
+
/**
|
|
159
|
+
* Entries the baseline holds that the base branch's copy does not, or holds
|
|
160
|
+
* at a lower count, and that name no reason. The baseline lists the faults
|
|
161
|
+
* that were there when the gate arrived; a new breach is fixed where it is.
|
|
162
|
+
* An entry with a `reason` is a named exception and may be added or raised.
|
|
163
|
+
*/
|
|
164
|
+
declare function judgeGrowth(current: Baseline, base: Baseline, baseName?: string): string[];
|
|
165
|
+
/** Whether the baseline holds anything a reason does not name: only then is the base branch's copy needed. */
|
|
166
|
+
declare const hasUnexcusedEntries: (b: Baseline) => boolean;
|
|
167
|
+
/**
|
|
168
|
+
* Page routes from the page files of a Next app directory, as paths relative
|
|
169
|
+
* to it (`portal/(shell)/invoices/[id]/page.tsx`). Route groups `(x)`, parallel
|
|
170
|
+
* slots `@x` and private folders `_x` are not part of the URL; an intercepting
|
|
171
|
+
* route `(.)x` is a second way into a page, not a page of its own.
|
|
172
|
+
*/
|
|
173
|
+
declare function routesFromPages(files: string[]): string[];
|
|
174
|
+
/** Page routes from Next's `app-path-routes-manifest.json` (`{ "/portal/page": "/portal" }`). Route handlers and internal `_` routes are not pages. */
|
|
175
|
+
declare function routesFromManifest(manifest: unknown): string[];
|
|
176
|
+
interface ExcludedRoute {
|
|
177
|
+
route: string;
|
|
178
|
+
reason: string;
|
|
179
|
+
}
|
|
180
|
+
/** A route as the gate names it: no query, no hash, no trailing slash. */
|
|
181
|
+
declare const cleanRoute: (route: string) => string;
|
|
182
|
+
/** Whether a measured route (`/portal/projects/p1`, or the pattern itself) is a page route (`/portal/projects/[id]`). */
|
|
183
|
+
declare function routeMatches(pattern: string, route: string): boolean;
|
|
184
|
+
interface RouteJudgement {
|
|
185
|
+
/** Page routes nothing measured and no exclusion covers: the gate fails on these. */
|
|
186
|
+
missing: string[];
|
|
187
|
+
/** Problems with `excludeRoutes` itself: no reason, or names no page. */
|
|
188
|
+
problems: string[];
|
|
189
|
+
/** Pages measured or excluded with a reason. */
|
|
190
|
+
covered: number;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Every page route of the app is in the gate's route list. A measured route
|
|
194
|
+
* counts for the most specific page it matches, so `/portal/projects/new` does
|
|
195
|
+
* not cover `/portal/projects/[id]`. A page that only redirects measures
|
|
196
|
+
* nothing: it is excluded with that reason.
|
|
197
|
+
*/
|
|
198
|
+
declare function judgeRoutes(pages: string[], measuredRoutes: string[], excluded?: ExcludedRoute[]): RouteJudgement;
|
|
132
199
|
|
|
133
200
|
/**
|
|
134
201
|
* What "every viewport" means (STANDARD.md section 10, DOM-787): the ladder
|
|
@@ -263,8 +330,13 @@ declare const PHONE: {
|
|
|
263
330
|
readonly isMobile: true;
|
|
264
331
|
readonly hasTouch: true;
|
|
265
332
|
};
|
|
266
|
-
/**
|
|
267
|
-
|
|
333
|
+
/**
|
|
334
|
+
* The house floors at phone width. A site may raise `textPx`, or lower
|
|
335
|
+
* `fieldHeightPx` with a reason; they are not per-repo taste. The field height
|
|
336
|
+
* is portal standard rule 5 (48px inputs); until DOM-1053 the gate held fields
|
|
337
|
+
* to the 44px target floor only.
|
|
338
|
+
*/
|
|
339
|
+
declare const PHONE_FLOORS: Required<PhoneFloors>;
|
|
268
340
|
/** Where `measurePhone` writes and `craft phone-first --measured` reads, unless told otherwise. */
|
|
269
341
|
declare const DEFAULT_MEASURED_DIR = "phone-first/measured";
|
|
270
342
|
/** A route as a file name: `/portal/invoices` is `portal-invoices.json`, `/` is `home.json`. */
|
|
@@ -309,7 +381,7 @@ interface MeasureViewportsOptions {
|
|
|
309
381
|
scope?: "all" | "phone" | "wide";
|
|
310
382
|
/** Override house thresholds. */
|
|
311
383
|
thresholds?: Partial<Thresholds>;
|
|
312
|
-
/** Override the 12 / 16 / 44px floors. The defaults are the house standard; a site may set `textPx` higher. */
|
|
384
|
+
/** Override the 12 / 16 / 44px floors and the 48px field height (`fieldHeightPx`). The defaults are the house standard; a site may set `textPx` higher. */
|
|
313
385
|
floors?: Partial<PhoneFloors>;
|
|
314
386
|
/** The main action's selector: one for all routes, or per route with `*` as the fallback. Absent: not checked. */
|
|
315
387
|
mainAction?: MainAction;
|
|
@@ -440,6 +512,8 @@ interface MeasurePhoneOptions {
|
|
|
440
512
|
route: string;
|
|
441
513
|
/** The folder to write `<route>.json` into. Default `phone-first/measured`, from the working directory. */
|
|
442
514
|
out?: string;
|
|
515
|
+
/** Override the 12 / 16 / 44 / 48px floors (text, field text, target, field height). The defaults are the house standard. */
|
|
516
|
+
floors?: Partial<PhoneFloors>;
|
|
443
517
|
}
|
|
444
518
|
/**
|
|
445
519
|
* Measure the page as it is now and write the result for `craft phone-first`.
|
|
@@ -448,4 +522,4 @@ interface MeasurePhoneOptions {
|
|
|
448
522
|
*/
|
|
449
523
|
declare function measurePhone(page: MinimalPage, options: MeasurePhoneOptions): Promise<MeasuredRoute>;
|
|
450
524
|
|
|
451
|
-
export { BANDS, type Band, type Baseline, DEFAULT_MEASURED_DIR, DEFAULT_SWEEP, DEFAULT_THRESHOLDS, DESKTOP, EMPTY_BASELINE, 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, 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, measureRoute, measureSite, measureViewportInPage, measureViewports, mergeViewportFindings, normaliseElement, reasonOf, routeFile, scanPhoneFirst, sweepWidths, writeBaseline };
|
|
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 };
|