@omniharmonic/openspace-cli 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agents-md-63S3T7CX.js → agents-md-OWDSHBGF.js} +2 -2
- package/dist/bin.js +3 -3
- package/dist/{chunk-SY5NDWS2.js → chunk-GMKHKHUR.js} +127 -25
- package/dist/chunk-GMKHKHUR.js.map +1 -0
- package/dist/{chunk-TDLUJHWB.js → chunk-PBN36VUE.js} +1965 -122
- package/dist/chunk-PBN36VUE.js.map +1 -0
- package/dist/{chunk-N7WFQYFE.js → chunk-YAPLQ4OA.js} +24 -1
- package/dist/chunk-YAPLQ4OA.js.map +1 -0
- package/dist/{crawl-YPDLNAN7.js → crawl-WQQDFI7W.js} +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -3
- package/dist/starter/site/AGENTS.md +1 -1
- package/dist/templates/darkroom/content/pages/about.json +2 -2
- package/dist/templates/encore/content/pages/home.json +1 -1
- package/dist/templates/folio/content/pages/home.json +2 -2
- package/dist/templates/gather/content/pages/home.json +2 -2
- package/dist/templates/harbor/content/pages/home.json +1 -1
- package/dist/templates/hearth/content/pages/home.json +6 -6
- package/dist/templates/inkwell/content/pages/about.json +2 -2
- package/dist/templates/inkwell/content/pages/books.json +1 -1
- package/dist/templates/keystone/content/pages/about.json +2 -2
- package/dist/templates/kiln/content/pages/home.json +7 -5
- package/dist/templates/ledger/content/pages/about.json +4 -4
- package/dist/templates/vow/content/pages/home.json +1 -1
- package/package.json +8 -8
- package/dist/chunk-N7WFQYFE.js.map +0 -1
- package/dist/chunk-SY5NDWS2.js.map +0 -1
- package/dist/chunk-TDLUJHWB.js.map +0 -1
- /package/dist/{agents-md-63S3T7CX.js.map → agents-md-OWDSHBGF.js.map} +0 -0
- /package/dist/{crawl-YPDLNAN7.js.map → crawl-WQQDFI7W.js.map} +0 -0
|
@@ -22,14 +22,16 @@ import {
|
|
|
22
22
|
platformLabel,
|
|
23
23
|
sectionsFrom,
|
|
24
24
|
sectionsFromLayoutHtml,
|
|
25
|
+
signupAsksName,
|
|
25
26
|
sitePath,
|
|
26
27
|
socialNetwork,
|
|
27
28
|
stripSuffix,
|
|
28
29
|
stripTags,
|
|
30
|
+
subscribersAdvice,
|
|
29
31
|
text,
|
|
30
32
|
themeFromCss,
|
|
31
33
|
videoUrl
|
|
32
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-YAPLQ4OA.js";
|
|
33
35
|
import {
|
|
34
36
|
BREAKPOINT_INFO,
|
|
35
37
|
BUTTON_STYLES,
|
|
@@ -130,6 +132,8 @@ import {
|
|
|
130
132
|
parseColor,
|
|
131
133
|
parseComponentPreviewPath,
|
|
132
134
|
parseCss,
|
|
135
|
+
phoneColumns,
|
|
136
|
+
phonePadding,
|
|
133
137
|
plainText,
|
|
134
138
|
presetCatalog,
|
|
135
139
|
presetSampleStrings,
|
|
@@ -147,7 +151,7 @@ import {
|
|
|
147
151
|
toHex,
|
|
148
152
|
toJsonSchema,
|
|
149
153
|
toMarkdown
|
|
150
|
-
} from "./chunk-
|
|
154
|
+
} from "./chunk-GMKHKHUR.js";
|
|
151
155
|
import {
|
|
152
156
|
__commonJS,
|
|
153
157
|
__export,
|
|
@@ -7788,10 +7792,10 @@ function withPages(doc, pages) {
|
|
|
7788
7792
|
// ../schema/src/secrets.ts
|
|
7789
7793
|
function shannonEntropy(value) {
|
|
7790
7794
|
if (!value) return 0;
|
|
7791
|
-
const
|
|
7792
|
-
for (const c of value)
|
|
7795
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
7796
|
+
for (const c of value) counts2.set(c, (counts2.get(c) ?? 0) + 1);
|
|
7793
7797
|
let h = 0;
|
|
7794
|
-
for (const n of
|
|
7798
|
+
for (const n of counts2.values()) {
|
|
7795
7799
|
const p = n / value.length;
|
|
7796
7800
|
h -= p * Math.log2(p);
|
|
7797
7801
|
}
|
|
@@ -12600,7 +12604,15 @@ function autoMobileLayout(section, options = {}) {
|
|
|
12600
12604
|
}
|
|
12601
12605
|
};
|
|
12602
12606
|
for (const pl of [...placed]) placeLayered(pl.id);
|
|
12603
|
-
|
|
12607
|
+
let stacking = readingOrder(pending.filter((it) => !hostOf.has(it.id)));
|
|
12608
|
+
if (options.textFirst) {
|
|
12609
|
+
const isText2 = (id) => {
|
|
12610
|
+
const b = blocks2.get(id);
|
|
12611
|
+
return !!b && isTextBlock(b);
|
|
12612
|
+
};
|
|
12613
|
+
stacking = [...stacking.filter((i) => isText2(i.id)), ...stacking.filter((i) => !isText2(i.id))];
|
|
12614
|
+
}
|
|
12615
|
+
for (const { id, rect: d } of stacking) {
|
|
12604
12616
|
if (placedById.has(id)) continue;
|
|
12605
12617
|
const block4 = blocks2.get(id);
|
|
12606
12618
|
const m = { x: 1, y: cursor, w: cols, h: mobileHeight(block4, d, width) };
|
|
@@ -12641,6 +12653,690 @@ function autoMobileLayoutOps(doc, page, sectionId, options = {}) {
|
|
|
12641
12653
|
return ops;
|
|
12642
12654
|
}
|
|
12643
12655
|
|
|
12656
|
+
// ../lint/src/phone.ts
|
|
12657
|
+
var PHONE = 360;
|
|
12658
|
+
var GLYPH = 0.48;
|
|
12659
|
+
var GLYPH_CAPS = 0.62;
|
|
12660
|
+
function* sectionsOf2(doc, pageId) {
|
|
12661
|
+
const pages = pageId ? [doc.pages[pageId]].filter((p) => !!p) : Object.values(doc.pages);
|
|
12662
|
+
for (const p of pages)
|
|
12663
|
+
for (const s2 of p.sections) if (!s2.hidden) yield { page: p.id, section: s2 };
|
|
12664
|
+
yield { page: FOOTER, section: doc.site.footer };
|
|
12665
|
+
}
|
|
12666
|
+
function phonePx(value) {
|
|
12667
|
+
if (!value) return void 0;
|
|
12668
|
+
const v = value.trim();
|
|
12669
|
+
const m = /^clamp\(\s*([\d.]+)(rem|px)/.exec(v) ?? /^([\d.]+)(rem|px)$/.exec(v);
|
|
12670
|
+
if (!m) return void 0;
|
|
12671
|
+
return Number(m[1]) * (m[2] === "rem" ? 16 : 1);
|
|
12672
|
+
}
|
|
12673
|
+
var words2 = (s2) => s2.match(/[\p{L}\p{N}]{2,}/gu)?.length ?? 0;
|
|
12674
|
+
var PhoneCtx = class {
|
|
12675
|
+
constructor(doc, options) {
|
|
12676
|
+
this.doc = doc;
|
|
12677
|
+
this.options = options;
|
|
12678
|
+
this.meta = opMeta(options);
|
|
12679
|
+
this.root = compileStyles(doc.styles).tokens.vars.root;
|
|
12680
|
+
const density = doc.styles.choices.density;
|
|
12681
|
+
this.space = SPACE_SCALES[density ?? "comfortable"] ?? SPACE_SCALES.comfortable;
|
|
12682
|
+
this.gutter = phonePx(this.root["--os-section-gutter"]) ?? 24;
|
|
12683
|
+
}
|
|
12684
|
+
doc;
|
|
12685
|
+
options;
|
|
12686
|
+
issues = [];
|
|
12687
|
+
meta;
|
|
12688
|
+
root;
|
|
12689
|
+
space;
|
|
12690
|
+
gutter;
|
|
12691
|
+
add(i) {
|
|
12692
|
+
this.issues.push(issue(i));
|
|
12693
|
+
}
|
|
12694
|
+
section({ page, section }) {
|
|
12695
|
+
if (section.layout.mode === "stack") this.columns(page, section);
|
|
12696
|
+
else this.grid(page, section, section.layout);
|
|
12697
|
+
this.padding(page, section);
|
|
12698
|
+
this.tallPhoto(page, section);
|
|
12699
|
+
for (const b of section.blocks) if (!b.hidden) this.headingWords(page, section, b);
|
|
12700
|
+
}
|
|
12701
|
+
/** Text columns kept side by side on phones (an explicit phone column setting). */
|
|
12702
|
+
columns(page, section) {
|
|
12703
|
+
const layout = section.layout;
|
|
12704
|
+
if (layout.mode !== "stack") return;
|
|
12705
|
+
const cols = phoneColumns(layout.columns);
|
|
12706
|
+
if (cols.length < 2) return;
|
|
12707
|
+
const content3 = PHONE - 2 * this.gutter;
|
|
12708
|
+
const squeezed = cols.some((span, i) => {
|
|
12709
|
+
const longest = section.blocks.filter((b) => !b.hidden && (b.column ?? 0) === i && isTextBlock(b)).reduce((n, b) => Math.max(n, words2(blockText(b))), 0);
|
|
12710
|
+
return longest >= 25 && span / STACK_UNITS * content3 < 170;
|
|
12711
|
+
});
|
|
12712
|
+
if (!squeezed) return;
|
|
12713
|
+
this.add({
|
|
12714
|
+
rule: "phone-columns",
|
|
12715
|
+
severity: "warning",
|
|
12716
|
+
page,
|
|
12717
|
+
section: section.id,
|
|
12718
|
+
message: `On phones this section keeps ${cols.length} columns side by side, so its text is squeezed into a narrow strip.`,
|
|
12719
|
+
fix: "Stack the columns on phones (one under the other).",
|
|
12720
|
+
autofix: {
|
|
12721
|
+
label: "Stack the columns on phones",
|
|
12722
|
+
ops: [
|
|
12723
|
+
{
|
|
12724
|
+
op: "section.update",
|
|
12725
|
+
...this.meta(),
|
|
12726
|
+
page,
|
|
12727
|
+
section: section.id,
|
|
12728
|
+
set: { "layout.columns.m": [STACK_UNITS] }
|
|
12729
|
+
}
|
|
12730
|
+
]
|
|
12731
|
+
}
|
|
12732
|
+
});
|
|
12733
|
+
}
|
|
12734
|
+
/** Grid sections: missing or partial phone layouts, text squeezed into narrow phone columns. */
|
|
12735
|
+
grid(page, section, layout) {
|
|
12736
|
+
const items = Object.entries(layout.items);
|
|
12737
|
+
if (!items.length) return;
|
|
12738
|
+
const placed = items.filter(([, i]) => i.m).length;
|
|
12739
|
+
const blocks2 = new Map(section.blocks.map((b) => [b.id, b]));
|
|
12740
|
+
const fill = () => autoMobileLayoutOps(this.doc, page, section.id, this.options);
|
|
12741
|
+
if (placed > 0 && placed < items.length) {
|
|
12742
|
+
this.add({
|
|
12743
|
+
rule: "phone-layout-missing",
|
|
12744
|
+
severity: "warning",
|
|
12745
|
+
page,
|
|
12746
|
+
section: section.id,
|
|
12747
|
+
message: `${items.length - placed === 1 ? "A block was" : `${items.length - placed} blocks were`} added on desktop but not placed for phones, so on a phone ${items.length - placed === 1 ? "it shows" : "they show"} at the bottom of the section.`,
|
|
12748
|
+
fix: "Place the new blocks in reading order on phones (your other phone placements stay).",
|
|
12749
|
+
autofix: { label: "Place them for phones", ops: fill() }
|
|
12750
|
+
});
|
|
12751
|
+
} else if (placed === 0 && items.length > 1) {
|
|
12752
|
+
const fallback2 = fallbackMobileRects(layout);
|
|
12753
|
+
const stacked = Object.entries(fallback2).sort(([, a], [, b]) => a.y - b.y).map(([id]) => id);
|
|
12754
|
+
const reading = readingOrder(items.map(([id, i]) => ({ id, rect: i.d }))).map((i) => i.id);
|
|
12755
|
+
if (stacked.join() !== reading.join())
|
|
12756
|
+
this.add({
|
|
12757
|
+
rule: "phone-layout-order",
|
|
12758
|
+
severity: "warning",
|
|
12759
|
+
page,
|
|
12760
|
+
section: section.id,
|
|
12761
|
+
message: "On phones this section's blocks would stack in a confusing order (by where each one starts, not how the section reads).",
|
|
12762
|
+
fix: "Lay the section out for phones in reading order.",
|
|
12763
|
+
autofix: { label: "Lay it out for phones", ops: fill() }
|
|
12764
|
+
});
|
|
12765
|
+
}
|
|
12766
|
+
for (const [id, item2] of items) {
|
|
12767
|
+
const b = blocks2.get(id);
|
|
12768
|
+
if (!item2.m || !b || b.hidden || !isTextBlock(b)) continue;
|
|
12769
|
+
const n = words2(blockText(b));
|
|
12770
|
+
const width = item2.m.w / 8 * (PHONE - 2 * this.gutter);
|
|
12771
|
+
if (n < 8 || width >= 150 || width >= 120 && n < 25) continue;
|
|
12772
|
+
const restack = autoMobileLayout(section, { reset: true });
|
|
12773
|
+
this.add({
|
|
12774
|
+
rule: "phone-narrow-text",
|
|
12775
|
+
severity: "warning",
|
|
12776
|
+
page,
|
|
12777
|
+
section: section.id,
|
|
12778
|
+
block: id,
|
|
12779
|
+
message: `On phones this ${b.type} block is only about ${Math.round(width)}px wide, so its text is squeezed into a narrow strip.`,
|
|
12780
|
+
fix: "Re-stack the section for phones so every block is full width.",
|
|
12781
|
+
...restack ? {
|
|
12782
|
+
autofix: { label: "Re-stack for phones", ops: this.placeOps(page, section, restack) }
|
|
12783
|
+
} : {}
|
|
12784
|
+
});
|
|
12785
|
+
}
|
|
12786
|
+
}
|
|
12787
|
+
placeOps(page, section, items) {
|
|
12788
|
+
return Object.entries(items).flatMap(
|
|
12789
|
+
([block4, item2]) => item2.m ? [
|
|
12790
|
+
{
|
|
12791
|
+
op: "grid.place",
|
|
12792
|
+
...this.meta(),
|
|
12793
|
+
page,
|
|
12794
|
+
section: section.id,
|
|
12795
|
+
block: block4,
|
|
12796
|
+
bp: "m",
|
|
12797
|
+
rect: item2.m
|
|
12798
|
+
}
|
|
12799
|
+
] : []
|
|
12800
|
+
);
|
|
12801
|
+
}
|
|
12802
|
+
/** A phone padding so large the screen is mostly empty space. */
|
|
12803
|
+
padding(page, section) {
|
|
12804
|
+
const pad3 = section.style.padding;
|
|
12805
|
+
if (!pad3) return;
|
|
12806
|
+
const token = phonePadding(pad3);
|
|
12807
|
+
const px3 = this.space[token] ?? 0;
|
|
12808
|
+
if (px3 <= 100) return;
|
|
12809
|
+
this.add({
|
|
12810
|
+
rule: "phone-padding",
|
|
12811
|
+
severity: "warning",
|
|
12812
|
+
page,
|
|
12813
|
+
section: section.id,
|
|
12814
|
+
message: `On phones this section has ${px3}px of empty space above and below it, which pushes its content off the screen.`,
|
|
12815
|
+
fix: "Use less padding on phones (your desktop spacing stays).",
|
|
12816
|
+
autofix: {
|
|
12817
|
+
label: "Tighten the spacing on phones",
|
|
12818
|
+
ops: [
|
|
12819
|
+
{
|
|
12820
|
+
op: "section.update",
|
|
12821
|
+
...this.meta(),
|
|
12822
|
+
page,
|
|
12823
|
+
section: section.id,
|
|
12824
|
+
set: { "style.padding.m": "lg" }
|
|
12825
|
+
}
|
|
12826
|
+
]
|
|
12827
|
+
}
|
|
12828
|
+
});
|
|
12829
|
+
}
|
|
12830
|
+
/** A photo section taller than most of a phone screen (the words and button get pushed down). */
|
|
12831
|
+
tallPhoto(page, section) {
|
|
12832
|
+
const bg = section.style.background;
|
|
12833
|
+
const mh = section.style.minHeight;
|
|
12834
|
+
if (!mh || !bg || bg.type !== "image" && bg.type !== "video") return;
|
|
12835
|
+
const m = mh.m ?? mh.t ?? mh.d;
|
|
12836
|
+
const vh = /^(\d+)(?:vh|svh|dvh|lvh)$/.exec(m);
|
|
12837
|
+
const px3 = /^(\d+)px$/.exec(m);
|
|
12838
|
+
if (!(vh && Number(vh[1]) > 75) && !(px3 && Number(px3[1]) > 640)) return;
|
|
12839
|
+
this.add({
|
|
12840
|
+
rule: "phone-tall-photo",
|
|
12841
|
+
severity: "warning",
|
|
12842
|
+
page,
|
|
12843
|
+
section: section.id,
|
|
12844
|
+
message: `On phones this photo section is at least ${m} tall, so the photo fills most of the screen and pushes everything else down.`,
|
|
12845
|
+
fix: "Make it a little shorter on phones (70% of the screen); desktop keeps its height.",
|
|
12846
|
+
autofix: {
|
|
12847
|
+
label: "Shorten it on phones",
|
|
12848
|
+
ops: [
|
|
12849
|
+
{
|
|
12850
|
+
op: "section.update",
|
|
12851
|
+
...this.meta(),
|
|
12852
|
+
page,
|
|
12853
|
+
section: section.id,
|
|
12854
|
+
set: { "style.minHeight.m": "70svh" }
|
|
12855
|
+
}
|
|
12856
|
+
]
|
|
12857
|
+
}
|
|
12858
|
+
});
|
|
12859
|
+
}
|
|
12860
|
+
/** Heading words that can't fit on one phone line at the heading's phone size. */
|
|
12861
|
+
headingWords(page, section, b) {
|
|
12862
|
+
if (b.type !== "heading") return;
|
|
12863
|
+
const fields = b.fields;
|
|
12864
|
+
const size2 = fields.phoneSize && fields.phoneSize !== "auto" ? fields.phoneSize : b.style?.typography?.heading ?? (fields.size && fields.size !== "auto" ? fields.size : fields.level ?? "h2");
|
|
12865
|
+
const px3 = this.typePx(size2);
|
|
12866
|
+
if (!px3) return;
|
|
12867
|
+
const width = PHONE - 2 * this.gutter - this.blockPad(b);
|
|
12868
|
+
const caps = this.root[`--os-type-${size2}-text-transform`] === "uppercase";
|
|
12869
|
+
const longest = (fields.text ?? "").split(/[\s\-‐–—/]+/).reduce((a, w) => w.length > a.length ? w : a, "");
|
|
12870
|
+
const est = longest.length * px3 * (caps || longest === longest.toUpperCase() ? GLYPH_CAPS : GLYPH);
|
|
12871
|
+
if (est <= width * 1.1) return;
|
|
12872
|
+
const smaller = this.fittingSize(longest, width, caps);
|
|
12873
|
+
this.add({
|
|
12874
|
+
rule: "phone-long-word",
|
|
12875
|
+
severity: "warning",
|
|
12876
|
+
page,
|
|
12877
|
+
section: section.id,
|
|
12878
|
+
block: b.id,
|
|
12879
|
+
message: `\u201C${longest}\u201D is too long to fit on one line of a phone at this heading size, so it would break in the middle of the word.`,
|
|
12880
|
+
fix: smaller ? "Use a smaller heading size on phones (desktop keeps its size), or a shorter word." : "Use a shorter word or phrase.",
|
|
12881
|
+
...smaller ? {
|
|
12882
|
+
autofix: {
|
|
12883
|
+
label: "Use a smaller size on phones",
|
|
12884
|
+
ops: [
|
|
12885
|
+
{
|
|
12886
|
+
op: "block.update",
|
|
12887
|
+
...this.meta(),
|
|
12888
|
+
page,
|
|
12889
|
+
block: b.id,
|
|
12890
|
+
set: { phoneSize: smaller }
|
|
12891
|
+
}
|
|
12892
|
+
]
|
|
12893
|
+
}
|
|
12894
|
+
} : {}
|
|
12895
|
+
});
|
|
12896
|
+
}
|
|
12897
|
+
typePx(preset) {
|
|
12898
|
+
const px3 = phonePx(this.root[`--os-type-${preset}-font-size`]);
|
|
12899
|
+
if (!px3) return void 0;
|
|
12900
|
+
const content3 = PHONE - 2 * this.gutter;
|
|
12901
|
+
if (preset === "display") return Math.min(px3, 0.16 * content3);
|
|
12902
|
+
if (preset === "h1") return Math.min(px3, 0.12 * content3);
|
|
12903
|
+
return px3;
|
|
12904
|
+
}
|
|
12905
|
+
blockPad(b) {
|
|
12906
|
+
const pad3 = b.style?.padding;
|
|
12907
|
+
const token = pad3 ? pad3.m ?? pad3.t ?? pad3.d : b.style?.colorTheme ? "md" : void 0;
|
|
12908
|
+
return token ? 2 * (this.space[token] ?? 0) : 0;
|
|
12909
|
+
}
|
|
12910
|
+
fittingSize(word, width, caps) {
|
|
12911
|
+
for (const s2 of ["h1", "h2", "h3", "h4"]) {
|
|
12912
|
+
const px3 = this.typePx(s2);
|
|
12913
|
+
if (px3 && word.length * px3 * (caps ? GLYPH_CAPS : GLYPH) <= width) return s2;
|
|
12914
|
+
}
|
|
12915
|
+
return void 0;
|
|
12916
|
+
}
|
|
12917
|
+
/** A header logo that leaves no room for the menu button on phones. */
|
|
12918
|
+
header() {
|
|
12919
|
+
const h = this.doc.site.header.logoHeight;
|
|
12920
|
+
if (!h) return;
|
|
12921
|
+
const m = h.m ?? h.t ?? h.d;
|
|
12922
|
+
if (m <= 64) return;
|
|
12923
|
+
this.add({
|
|
12924
|
+
rule: "phone-logo",
|
|
12925
|
+
severity: "warning",
|
|
12926
|
+
message: `The logo is ${m}px tall on phones, which makes the header take up a big part of a phone screen.`,
|
|
12927
|
+
fix: "Use a smaller logo on phones (around 40px; desktop keeps its size).",
|
|
12928
|
+
autofix: {
|
|
12929
|
+
label: "Use a 40px logo on phones",
|
|
12930
|
+
ops: [{ op: "site.update", ...this.meta(), set: { "header.logoHeight.m": 40 } }]
|
|
12931
|
+
}
|
|
12932
|
+
});
|
|
12933
|
+
}
|
|
12934
|
+
/** Custom CSS or components that fix something wider than a phone. */
|
|
12935
|
+
css() {
|
|
12936
|
+
const sources = [];
|
|
12937
|
+
if (this.doc.site.customCss)
|
|
12938
|
+
sources.push({ css: this.doc.site.customCss, where: "Your custom CSS" });
|
|
12939
|
+
for (const [name, c] of Object.entries(this.doc.components ?? {}))
|
|
12940
|
+
if (c.css) sources.push({ css: c.css, where: `The \u201C${name}\u201D component`, component: name });
|
|
12941
|
+
for (const s2 of sources) {
|
|
12942
|
+
const wide = wideRules(s2.css);
|
|
12943
|
+
if (!wide.length) continue;
|
|
12944
|
+
this.add({
|
|
12945
|
+
rule: "phone-fixed-width",
|
|
12946
|
+
severity: "warning",
|
|
12947
|
+
key: s2.component ?? "custom-css",
|
|
12948
|
+
message: `${s2.where} sets a fixed width (${wide.slice(0, 2).join(", ")}) that is wider than a phone screen, so the page may scroll sideways or be cut off on phones.`,
|
|
12949
|
+
fix: "Use max-width (or a percentage) instead of a fixed width, or add a phone rule (@media (max-width: 767px))."
|
|
12950
|
+
});
|
|
12951
|
+
}
|
|
12952
|
+
}
|
|
12953
|
+
};
|
|
12954
|
+
function wideRules(css) {
|
|
12955
|
+
let plain3 = css;
|
|
12956
|
+
for (let guard = 0; guard < 50; guard++) {
|
|
12957
|
+
const m = /@media[^{]*min-width\s*:\s*(\d+)/i.exec(plain3);
|
|
12958
|
+
if (!m || Number(m[1]) < 400) break;
|
|
12959
|
+
const start = plain3.indexOf("{", m.index);
|
|
12960
|
+
let depth = 0;
|
|
12961
|
+
let end = start;
|
|
12962
|
+
for (; end < plain3.length; end++) {
|
|
12963
|
+
if (plain3[end] === "{") depth++;
|
|
12964
|
+
else if (plain3[end] === "}" && --depth === 0) break;
|
|
12965
|
+
}
|
|
12966
|
+
plain3 = plain3.slice(0, m.index) + plain3.slice(end + 1);
|
|
12967
|
+
}
|
|
12968
|
+
const out = [];
|
|
12969
|
+
for (const m of plain3.matchAll(/(?:^|[;{\s])((?:min-)?width)\s*:\s*([\d.]+)(px|rem|em)\b/gi)) {
|
|
12970
|
+
const px3 = Number(m[2]) * (m[3]?.toLowerCase() === "px" ? 1 : 16);
|
|
12971
|
+
if (px3 > 340) out.push(`${m[1]}: ${m[2]}${m[3]}`);
|
|
12972
|
+
}
|
|
12973
|
+
return out;
|
|
12974
|
+
}
|
|
12975
|
+
function phoneCheck(doc, pageId, options = {}) {
|
|
12976
|
+
const ctx = new PhoneCtx(doc, options);
|
|
12977
|
+
for (const w of sectionsOf2(doc, pageId)) ctx.section(w);
|
|
12978
|
+
ctx.header();
|
|
12979
|
+
ctx.css();
|
|
12980
|
+
return sortIssues(ctx.issues);
|
|
12981
|
+
}
|
|
12982
|
+
var SEVERITY = {
|
|
12983
|
+
"page-overflow": "error",
|
|
12984
|
+
"cut-off": "error",
|
|
12985
|
+
overlap: "error",
|
|
12986
|
+
"photo-contrast": "error",
|
|
12987
|
+
"low-contrast": "error",
|
|
12988
|
+
"broken-word": "warning",
|
|
12989
|
+
"big-heading": "warning",
|
|
12990
|
+
"small-text": "warning",
|
|
12991
|
+
"tiny-text": "warning",
|
|
12992
|
+
"narrow-text": "warning",
|
|
12993
|
+
"tap-target": "warning",
|
|
12994
|
+
"edge-text": "warning",
|
|
12995
|
+
"header-crowded": "warning",
|
|
12996
|
+
"loose-padding": "warning",
|
|
12997
|
+
"tall-image": "info",
|
|
12998
|
+
"heavy-crop": "info",
|
|
12999
|
+
"long-footer": "info",
|
|
13000
|
+
"cta-below-fold": "warning"
|
|
13001
|
+
};
|
|
13002
|
+
var MESSAGE = {
|
|
13003
|
+
"page-overflow": (_f, w) => [
|
|
13004
|
+
`On phones the page is wider than the screen, so it scrolls sideways${w}.`,
|
|
13005
|
+
"Something has a fixed width wider than a phone. Re-stack the section for phones or make the element narrower."
|
|
13006
|
+
],
|
|
13007
|
+
"cut-off": (_f, w) => [
|
|
13008
|
+
`On phones something${w} is cut off at the edge of the screen.`,
|
|
13009
|
+
"Re-stack the section for phones, or make the element narrower."
|
|
13010
|
+
],
|
|
13011
|
+
overlap: (_f, w) => [
|
|
13012
|
+
`On phones two pieces of text${w} are drawn on top of each other, so neither can be read.`,
|
|
13013
|
+
"Re-stack the section for phones so the blocks sit one under the other."
|
|
13014
|
+
],
|
|
13015
|
+
"photo-contrast": (f, w) => [
|
|
13016
|
+
`On phones the text \u201C${f.text ?? ""}\u201D${w} sits on a photo where it can drop to ${f.value}:1 contrast (it needs ${f.limit}:1), so it is hard to read on bright or dark parts of the photo.`,
|
|
13017
|
+
"Darken (or lighten) the photo overlay, or put the text on a solid panel."
|
|
13018
|
+
],
|
|
13019
|
+
"low-contrast": (f, w) => [
|
|
13020
|
+
`On phones the text \u201C${f.text ?? ""}\u201D${w} has only ${f.value}:1 contrast with its background (it needs ${f.limit}:1).`,
|
|
13021
|
+
"Use a color theme instead of a custom color, or a darker/lighter text color."
|
|
13022
|
+
],
|
|
13023
|
+
"broken-word": (f, w) => [
|
|
13024
|
+
`On phones a word in the heading \u201C${f.text ?? ""}\u201D${w} doesn't fit on one line and breaks in the middle.`,
|
|
13025
|
+
"Use a smaller heading size on phones, or a shorter word."
|
|
13026
|
+
],
|
|
13027
|
+
"big-heading": (f, w) => [
|
|
13028
|
+
`On phones the heading \u201C${f.text ?? ""}\u201D${w} is so large it fills most of the screen.`,
|
|
13029
|
+
"Use a smaller heading size on phones, or a shorter heading."
|
|
13030
|
+
],
|
|
13031
|
+
"small-text": (f, w) => [
|
|
13032
|
+
`On phones the text${w} is ${f.value}px \u2014 too small to read comfortably (${f.limit === 14 ? "14px is the minimum for small print" : "16px is the minimum for reading text"}).`,
|
|
13033
|
+
f.limit === 14 ? "Use the body text style for it, or shorten it to a note." : "Use the body text style for paragraphs instead of the small style."
|
|
13034
|
+
],
|
|
13035
|
+
"tiny-text": (f, w) => [
|
|
13036
|
+
`On phones the label \u201C${f.text ?? ""}\u201D${w} is only ${f.value}px tall, too small to read.`,
|
|
13037
|
+
"Make it at least 12px."
|
|
13038
|
+
],
|
|
13039
|
+
"narrow-text": (_f, w) => [
|
|
13040
|
+
`On phones the text${w} is squeezed into a very narrow column.`,
|
|
13041
|
+
"Stack the columns on phones so the text is full width."
|
|
13042
|
+
],
|
|
13043
|
+
"tap-target": (f, w) => [
|
|
13044
|
+
`On phones the link or button \u201C${f.text ?? ""}\u201D${w} is small and close to another one, so it's easy to tap the wrong one.`,
|
|
13045
|
+
"Make it a button, or put more space around it."
|
|
13046
|
+
],
|
|
13047
|
+
"edge-text": (_f, w) => [
|
|
13048
|
+
`On phones the text${w} touches the edge of the screen.`,
|
|
13049
|
+
"Keep text inside the page margins (don't let text blocks bleed to the edge)."
|
|
13050
|
+
],
|
|
13051
|
+
"header-crowded": () => [
|
|
13052
|
+
"On phones the header is crowded: its logo, title and buttons overlap or wrap.",
|
|
13053
|
+
"Use a smaller logo on phones or a shorter site title."
|
|
13054
|
+
],
|
|
13055
|
+
"loose-padding": (f, w) => [
|
|
13056
|
+
`On phones there is ${f.value}px of empty space above or below${w || " a section"}.`,
|
|
13057
|
+
"Use less padding on phones."
|
|
13058
|
+
],
|
|
13059
|
+
"tall-image": (_f, w) => [
|
|
13060
|
+
`On phones an image${w} is taller than the whole screen.`,
|
|
13061
|
+
"Crop it to a landscape or square shape, or set a focus point."
|
|
13062
|
+
],
|
|
13063
|
+
"heavy-crop": (_f, w) => [
|
|
13064
|
+
`On phones most of a photo${w} is cropped away.`,
|
|
13065
|
+
"Set the photo's focus point so the important part stays in view on phones."
|
|
13066
|
+
],
|
|
13067
|
+
"cta-below-fold": (f) => [
|
|
13068
|
+
`On phones the main button (\u201C${f.text ?? ""}\u201D) is about ${Math.round(f.value ?? 0)}px down the page, below the first screen \u2014 visitors have to scroll before they see what to do.`,
|
|
13069
|
+
"Put the words and button first on phones (photos after them), or use a shorter headline."
|
|
13070
|
+
],
|
|
13071
|
+
"long-footer": () => [
|
|
13072
|
+
"On phones the footer is several screens long.",
|
|
13073
|
+
"Keep the footer to the essentials (a few links, contact details, social icons)."
|
|
13074
|
+
]
|
|
13075
|
+
};
|
|
13076
|
+
function locate(doc, pageId, f) {
|
|
13077
|
+
const lookup = (id) => {
|
|
13078
|
+
if (!id) return void 0;
|
|
13079
|
+
const pages = [pageId, FOOTER];
|
|
13080
|
+
for (const p of pages) {
|
|
13081
|
+
const hit = findSection(doc, p, id);
|
|
13082
|
+
if (hit) return { page: p, section: hit.section };
|
|
13083
|
+
}
|
|
13084
|
+
return void 0;
|
|
13085
|
+
};
|
|
13086
|
+
const sec = lookup(f.section);
|
|
13087
|
+
if (sec) {
|
|
13088
|
+
const block4 = f.block ? sec.section.blocks.find((b) => b.id === f.block) : void 0;
|
|
13089
|
+
return { page: sec.page, section: sec.section, ...block4 ? { block: block4 } : {} };
|
|
13090
|
+
}
|
|
13091
|
+
if (f.area === "footer") return { page: FOOTER, section: doc.site.footer };
|
|
13092
|
+
return { page: pageId };
|
|
13093
|
+
}
|
|
13094
|
+
function fixFor(at, f, meta) {
|
|
13095
|
+
const { section, block: block4, page } = at;
|
|
13096
|
+
if (!section) return void 0;
|
|
13097
|
+
const restack = (label) => {
|
|
13098
|
+
if (section.layout.mode === "grid") {
|
|
13099
|
+
const items = autoMobileLayout(section, { reset: true });
|
|
13100
|
+
if (!items) return void 0;
|
|
13101
|
+
const ops = Object.entries(items).flatMap(
|
|
13102
|
+
([b, item2]) => item2.m ? [
|
|
13103
|
+
{
|
|
13104
|
+
op: "grid.place",
|
|
13105
|
+
...meta(),
|
|
13106
|
+
page,
|
|
13107
|
+
section: section.id,
|
|
13108
|
+
block: b,
|
|
13109
|
+
bp: "m",
|
|
13110
|
+
rect: item2.m
|
|
13111
|
+
}
|
|
13112
|
+
] : []
|
|
13113
|
+
);
|
|
13114
|
+
return ops.length ? { label, ops } : void 0;
|
|
13115
|
+
}
|
|
13116
|
+
if (phoneColumns(section.layout.columns).length > 1)
|
|
13117
|
+
return {
|
|
13118
|
+
label: "Stack the columns on phones",
|
|
13119
|
+
ops: [
|
|
13120
|
+
{
|
|
13121
|
+
op: "section.update",
|
|
13122
|
+
...meta(),
|
|
13123
|
+
page,
|
|
13124
|
+
section: section.id,
|
|
13125
|
+
set: { "layout.columns.m": [STACK_UNITS] }
|
|
13126
|
+
}
|
|
13127
|
+
]
|
|
13128
|
+
};
|
|
13129
|
+
return void 0;
|
|
13130
|
+
};
|
|
13131
|
+
switch (f.kind) {
|
|
13132
|
+
case "overlap":
|
|
13133
|
+
case "cut-off":
|
|
13134
|
+
case "page-overflow":
|
|
13135
|
+
case "narrow-text":
|
|
13136
|
+
case "edge-text":
|
|
13137
|
+
return f.component ? void 0 : restack("Re-stack for phones");
|
|
13138
|
+
case "photo-contrast": {
|
|
13139
|
+
const bg = section.style.background;
|
|
13140
|
+
if (bg && (bg.type === "image" || bg.type === "video")) {
|
|
13141
|
+
const now = bg.overlay?.opacity ?? 0;
|
|
13142
|
+
if (now >= 0.85) break;
|
|
13143
|
+
const target = Math.min(0.85, Math.ceil((now + 0.15) * 20) / 20);
|
|
13144
|
+
return {
|
|
13145
|
+
label: `Raise the photo overlay to ${Math.round(target * 100)}%`,
|
|
13146
|
+
ops: [
|
|
13147
|
+
{
|
|
13148
|
+
op: "section.update",
|
|
13149
|
+
...meta(),
|
|
13150
|
+
page,
|
|
13151
|
+
section: section.id,
|
|
13152
|
+
set: {
|
|
13153
|
+
"style.background.overlay": {
|
|
13154
|
+
color: bg.overlay?.color ?? "background",
|
|
13155
|
+
opacity: target
|
|
13156
|
+
}
|
|
13157
|
+
}
|
|
13158
|
+
}
|
|
13159
|
+
]
|
|
13160
|
+
};
|
|
13161
|
+
}
|
|
13162
|
+
if (block4 && isTextBlock(block4) && !block4.style?.colorTheme)
|
|
13163
|
+
return {
|
|
13164
|
+
label: "Put the text on a panel",
|
|
13165
|
+
ops: [
|
|
13166
|
+
{
|
|
13167
|
+
op: "block.update",
|
|
13168
|
+
...meta(),
|
|
13169
|
+
page,
|
|
13170
|
+
block: block4.id,
|
|
13171
|
+
style: {
|
|
13172
|
+
...block4.style ?? {},
|
|
13173
|
+
colorTheme: section.style.colorTheme ?? "light",
|
|
13174
|
+
padding: block4.style?.padding ?? { d: "lg", m: "md" }
|
|
13175
|
+
}
|
|
13176
|
+
}
|
|
13177
|
+
]
|
|
13178
|
+
};
|
|
13179
|
+
return void 0;
|
|
13180
|
+
}
|
|
13181
|
+
case "broken-word":
|
|
13182
|
+
case "big-heading": {
|
|
13183
|
+
if (block4?.type !== "heading") return void 0;
|
|
13184
|
+
const fields = block4.fields;
|
|
13185
|
+
const order = ["display", "h1", "h2", "h3", "h4"];
|
|
13186
|
+
const current2 = fields.phoneSize && fields.phoneSize !== "auto" ? fields.phoneSize : fields.size && fields.size !== "auto" ? fields.size : fields.level ?? "h2";
|
|
13187
|
+
const next = order[order.indexOf(current2) + 1];
|
|
13188
|
+
if (!next) return void 0;
|
|
13189
|
+
return {
|
|
13190
|
+
label: "Use a smaller size on phones",
|
|
13191
|
+
ops: [
|
|
13192
|
+
{
|
|
13193
|
+
op: "block.update",
|
|
13194
|
+
...meta(),
|
|
13195
|
+
page,
|
|
13196
|
+
block: block4.id,
|
|
13197
|
+
set: { phoneSize: next }
|
|
13198
|
+
}
|
|
13199
|
+
]
|
|
13200
|
+
};
|
|
13201
|
+
}
|
|
13202
|
+
case "cta-below-fold": {
|
|
13203
|
+
if (section.layout.mode === "grid") {
|
|
13204
|
+
const items = autoMobileLayout(section, { reset: true, textFirst: true });
|
|
13205
|
+
if (!items) return void 0;
|
|
13206
|
+
return {
|
|
13207
|
+
label: "Put the words first on phones",
|
|
13208
|
+
ops: Object.entries(items).flatMap(
|
|
13209
|
+
([b, item2]) => item2.m ? [
|
|
13210
|
+
{
|
|
13211
|
+
op: "grid.place",
|
|
13212
|
+
...meta(),
|
|
13213
|
+
page,
|
|
13214
|
+
section: section.id,
|
|
13215
|
+
block: b,
|
|
13216
|
+
bp: "m",
|
|
13217
|
+
rect: item2.m
|
|
13218
|
+
}
|
|
13219
|
+
] : []
|
|
13220
|
+
)
|
|
13221
|
+
};
|
|
13222
|
+
}
|
|
13223
|
+
const col = section.blocks.find((b) => b.type === "button")?.column ?? 0;
|
|
13224
|
+
const count2 = section.layout.columns.d.length;
|
|
13225
|
+
if (count2 < 2 || col === 0) return void 0;
|
|
13226
|
+
const order = [col, ...Array.from({ length: count2 }, (_, i) => i).filter((i) => i !== col)];
|
|
13227
|
+
return {
|
|
13228
|
+
label: "Put the words first on phones",
|
|
13229
|
+
ops: [
|
|
13230
|
+
{
|
|
13231
|
+
op: "section.update",
|
|
13232
|
+
...meta(),
|
|
13233
|
+
page,
|
|
13234
|
+
section: section.id,
|
|
13235
|
+
set: { "layout.order.m": order }
|
|
13236
|
+
}
|
|
13237
|
+
]
|
|
13238
|
+
};
|
|
13239
|
+
}
|
|
13240
|
+
case "loose-padding":
|
|
13241
|
+
return {
|
|
13242
|
+
label: "Tighten the spacing on phones",
|
|
13243
|
+
ops: [
|
|
13244
|
+
{
|
|
13245
|
+
op: "section.update",
|
|
13246
|
+
...meta(),
|
|
13247
|
+
page,
|
|
13248
|
+
section: section.id,
|
|
13249
|
+
set: { "style.padding.m": "lg" }
|
|
13250
|
+
}
|
|
13251
|
+
]
|
|
13252
|
+
};
|
|
13253
|
+
default:
|
|
13254
|
+
return void 0;
|
|
13255
|
+
}
|
|
13256
|
+
return void 0;
|
|
13257
|
+
}
|
|
13258
|
+
function phoneIssuesFromProbe(doc, pageId, results, options = {}) {
|
|
13259
|
+
const meta = opMeta(options);
|
|
13260
|
+
const out = /* @__PURE__ */ new Map();
|
|
13261
|
+
const sorted = [...results].sort((a, b) => a.width - b.width);
|
|
13262
|
+
for (const r of sorted)
|
|
13263
|
+
for (const f of r.findings) {
|
|
13264
|
+
const at = locate(doc, pageId, f);
|
|
13265
|
+
if (f.kind === "cta-below-fold" && at.section?.preset && !at.section.preset.startsWith("hero/"))
|
|
13266
|
+
continue;
|
|
13267
|
+
const sectionName = at.section && at.page !== FOOTER ? " in this section" : at.page === FOOTER ? " in the footer" : "";
|
|
13268
|
+
const where2 = f.component ? ` in the \u201C${f.component}\u201D component` : sectionName;
|
|
13269
|
+
const [message, fix] = MESSAGE[f.kind](f, where2);
|
|
13270
|
+
const key2 = `${f.kind}:${at.block?.id ?? f.block ?? ""}:${f.kind === "tap-target" || f.kind === "photo-contrast" ? f.text ?? "" : ""}`;
|
|
13271
|
+
const i = issue({
|
|
13272
|
+
rule: `phone-${f.kind}`,
|
|
13273
|
+
severity: SEVERITY[f.kind],
|
|
13274
|
+
page: at.page,
|
|
13275
|
+
...at.section ? { section: at.section.id } : {},
|
|
13276
|
+
...at.block ? { block: at.block.id } : {},
|
|
13277
|
+
key: `${r.width}`,
|
|
13278
|
+
message,
|
|
13279
|
+
fix
|
|
13280
|
+
});
|
|
13281
|
+
const id = `${i.rule}:${i.page}:${i.section ?? ""}:${key2}`;
|
|
13282
|
+
if (out.has(id)) continue;
|
|
13283
|
+
const autofix = fixFor(at, f, meta);
|
|
13284
|
+
out.set(id, { ...i, id: id.replace(/:+$/, ""), ...autofix ? { autofix } : {} });
|
|
13285
|
+
}
|
|
13286
|
+
return sortIssues([...out.values()]);
|
|
13287
|
+
}
|
|
13288
|
+
function fixForPhones(doc, opts = {}) {
|
|
13289
|
+
let registry3;
|
|
13290
|
+
const getRegistry = () => {
|
|
13291
|
+
registry3 ??= blocksRegistry();
|
|
13292
|
+
return registry3;
|
|
13293
|
+
};
|
|
13294
|
+
const apply2 = opts.validateOp ?? ((d, op2) => {
|
|
13295
|
+
const r = validate(d, op2, { trusted: true, registry: getRegistry() });
|
|
13296
|
+
return r.ok ? r.doc : null;
|
|
13297
|
+
});
|
|
13298
|
+
let cur = doc;
|
|
13299
|
+
const ops = [];
|
|
13300
|
+
const fixed = [];
|
|
13301
|
+
const tried = /* @__PURE__ */ new Set();
|
|
13302
|
+
const run2 = (i) => {
|
|
13303
|
+
if (!i.autofix || tried.has(i.id)) return;
|
|
13304
|
+
tried.add(i.id);
|
|
13305
|
+
let work = cur;
|
|
13306
|
+
for (const op2 of i.autofix.ops) {
|
|
13307
|
+
const next = apply2(work, op2);
|
|
13308
|
+
if (!next) return;
|
|
13309
|
+
work = next;
|
|
13310
|
+
}
|
|
13311
|
+
cur = work;
|
|
13312
|
+
ops.push(...i.autofix.ops);
|
|
13313
|
+
fixed.push({ id: i.id, label: i.autofix.label });
|
|
13314
|
+
};
|
|
13315
|
+
for (let guard = 0; guard < 200; guard++) {
|
|
13316
|
+
const next = phoneCheck(cur, opts.pageId, opts).find((i) => i.autofix && !tried.has(i.id));
|
|
13317
|
+
if (!next) break;
|
|
13318
|
+
run2(next);
|
|
13319
|
+
}
|
|
13320
|
+
const restacked = /* @__PURE__ */ new Set();
|
|
13321
|
+
for (const i of opts.rendered ?? []) {
|
|
13322
|
+
if (!i.autofix) continue;
|
|
13323
|
+
const key2 = `${i.section}:${i.autofix.label}`;
|
|
13324
|
+
if (restacked.has(key2)) {
|
|
13325
|
+
tried.add(i.id);
|
|
13326
|
+
fixed.push({ id: i.id, label: i.autofix.label });
|
|
13327
|
+
continue;
|
|
13328
|
+
}
|
|
13329
|
+
const before = fixed.length;
|
|
13330
|
+
run2(i);
|
|
13331
|
+
if (fixed.length > before) restacked.add(key2);
|
|
13332
|
+
}
|
|
13333
|
+
const remaining = [
|
|
13334
|
+
...phoneCheck(cur, opts.pageId, opts),
|
|
13335
|
+
...(opts.rendered ?? []).filter((i) => !tried.has(i.id))
|
|
13336
|
+
];
|
|
13337
|
+
return { ops, fixed, doc: cur, remaining };
|
|
13338
|
+
}
|
|
13339
|
+
|
|
12644
13340
|
// ../lint/src/photo-contrast.ts
|
|
12645
13341
|
var SOLID_PRIMARY = /* @__PURE__ */ new Set(["solid", "pill-solid"]);
|
|
12646
13342
|
function richChecks(doc, out) {
|
|
@@ -12679,6 +13375,11 @@ function photoTextChecks(block4, buttons) {
|
|
|
12679
13375
|
if (f.note) out.push({ role: "text-muted", label: "Button note", min: 4.5 });
|
|
12680
13376
|
break;
|
|
12681
13377
|
}
|
|
13378
|
+
case "quote":
|
|
13379
|
+
case "countdown":
|
|
13380
|
+
out.push({ role: "text", label: "Text", min: 4.5 });
|
|
13381
|
+
out.push({ role: "text-muted", label: "Secondary text", min: 4.5 });
|
|
13382
|
+
break;
|
|
12682
13383
|
default:
|
|
12683
13384
|
out.push({ role: "text", label: "Text", min: 4.5 });
|
|
12684
13385
|
}
|
|
@@ -12732,6 +13433,7 @@ function designCheck(doc, pageId, options = {}) {
|
|
|
12732
13433
|
ctx.container(FOOTER, [doc.site.footer], { footer: true });
|
|
12733
13434
|
ctx.issues.push(...linkIssues(doc, pageId, options, ctx.meta));
|
|
12734
13435
|
ctx.issues.push(...componentIssues(doc, pageId));
|
|
13436
|
+
ctx.issues.push(...phoneCheck(doc, pageId, options));
|
|
12735
13437
|
for (const w of hostingWarnings(doc, pageId))
|
|
12736
13438
|
ctx.issues.push(
|
|
12737
13439
|
issue({
|
|
@@ -13407,9 +14109,25 @@ var Ctx = class {
|
|
|
13407
14109
|
return this.tokens.palette[m[1]][Number(m[2])];
|
|
13408
14110
|
return /^(?:#|oklch|oklab|rgb|hsl|lab|lch)/.test(value) ? value : void 0;
|
|
13409
14111
|
}
|
|
14112
|
+
/**
|
|
14113
|
+
* A custom component (an imported or uploaded design) whose template draws a button from a
|
|
14114
|
+
* filled link field: that's a call to action too (wave 4: "no clear call to action" on a hero
|
|
14115
|
+
* with two buttons).
|
|
14116
|
+
*/
|
|
14117
|
+
componentHasButton(b) {
|
|
14118
|
+
if (!b.type.startsWith("component/")) return false;
|
|
14119
|
+
const def = this.doc.components?.[b.type.slice("component/".length)];
|
|
14120
|
+
if (!def || !/\{\{\s*(?:button|#link)\s/.test(def.template)) return false;
|
|
14121
|
+
const fields = b.fields;
|
|
14122
|
+
return Object.entries(def.fields).some(
|
|
14123
|
+
([k, f]) => f.kind === "link" && fields[k] !== void 0 && fields[k] !== null
|
|
14124
|
+
);
|
|
14125
|
+
}
|
|
13410
14126
|
cta(page, sections) {
|
|
13411
14127
|
if (sections.length === 0) return;
|
|
13412
|
-
const has3 = sections.some(
|
|
14128
|
+
const has3 = sections.some(
|
|
14129
|
+
(s2) => s2.blocks.some((b) => ACTION_BLOCKS.has(b.type) || this.componentHasButton(b))
|
|
14130
|
+
);
|
|
13413
14131
|
const navButton = this.doc.site.nav.some((n) => n.type === "button" && !n.hidden);
|
|
13414
14132
|
if (has3 || navButton) return;
|
|
13415
14133
|
this.add({
|
|
@@ -13483,6 +14201,457 @@ function plain(node2) {
|
|
|
13483
14201
|
return parts.join("");
|
|
13484
14202
|
}
|
|
13485
14203
|
|
|
14204
|
+
// ../lint/src/phone-probe.ts
|
|
14205
|
+
var PHONE_LIMITS = {
|
|
14206
|
+
/** Apple HIG / Material: 44 CSS px square. */
|
|
14207
|
+
tapTarget: 44,
|
|
14208
|
+
/** Nothing smaller than this, however isolated. */
|
|
14209
|
+
tapTargetMin: 16,
|
|
14210
|
+
/** Reading text (paragraphs, lists, quotes). */
|
|
14211
|
+
bodyText: 16,
|
|
14212
|
+
/** Any text (labels, eyebrows, captions). */
|
|
14213
|
+
anyText: 12,
|
|
14214
|
+
/** Text never closer than this to the screen edge. */
|
|
14215
|
+
edge: 12,
|
|
14216
|
+
/** Heading font size as a share of the viewport width. */
|
|
14217
|
+
headingShare: 0.16,
|
|
14218
|
+
/** Heading block height as a share of the viewport height. */
|
|
14219
|
+
headingScreen: 0.6,
|
|
14220
|
+
/** Images taller than this share of the viewport height. */
|
|
14221
|
+
imageScreen: 1.05,
|
|
14222
|
+
/** Section padding (each side) above which a phone shows "a lot of empty space". */
|
|
14223
|
+
loosePadding: 128,
|
|
14224
|
+
/** Text columns narrower than this (px) with more than a few words. */
|
|
14225
|
+
narrowColumn: 130,
|
|
14226
|
+
/** Visible share of a photo below which it counts as heavily cropped. */
|
|
14227
|
+
crop: 0.34
|
|
14228
|
+
};
|
|
14229
|
+
var PHONE_WIDTHS = [360, 390];
|
|
14230
|
+
var PHONE_HEIGHT = 780;
|
|
14231
|
+
var PHONE_PROBE_SCRIPT = `(async () => {
|
|
14232
|
+
const L = ${JSON.stringify(PHONE_LIMITS)};
|
|
14233
|
+
const vw = document.documentElement.clientWidth || window.innerWidth;
|
|
14234
|
+
// The phone screen's height (an editor canvas may be as tall as the page: it says so here).
|
|
14235
|
+
const vh = Number(window.__osPhoneScreenHeight) || window.innerHeight;
|
|
14236
|
+
const viewH = window.innerHeight;
|
|
14237
|
+
const findings = [];
|
|
14238
|
+
const seen = new Set();
|
|
14239
|
+
const style = document.createElement("style");
|
|
14240
|
+
// elementsFromPoint skips pointer-events:none layers (photo backgrounds, overlays).
|
|
14241
|
+
style.textContent = "*,*::before,*::after{pointer-events:auto!important;scroll-behavior:auto!important}";
|
|
14242
|
+
document.head.appendChild(style);
|
|
14243
|
+
const cs = (el) => getComputedStyle(el);
|
|
14244
|
+
const txt = (el) => (el.textContent || "").replace(/\\s+/g, " ").trim();
|
|
14245
|
+
const short = (s) => (s.length > 60 ? s.slice(0, 59) + "\u2026" : s);
|
|
14246
|
+
const where = (el) => {
|
|
14247
|
+
const out = {};
|
|
14248
|
+
const blk = el.closest("[data-os-block]");
|
|
14249
|
+
const sec = el.closest("[data-os-section]");
|
|
14250
|
+
if (blk && blk.getAttribute("data-os-id")) out.block = blk.getAttribute("data-os-id");
|
|
14251
|
+
if (sec && sec.getAttribute("data-os-id")) out.section = sec.getAttribute("data-os-id");
|
|
14252
|
+
const comp = el.closest("[data-os-component]");
|
|
14253
|
+
if (comp) out.component = comp.getAttribute("data-os-component") || "component";
|
|
14254
|
+
out.area = el.closest(".os-menu") ? "menu" : el.closest("[data-os-header],.os-header,.os-announcement") ? "header" : el.closest(".os-footer,footer") ? "footer" : el.closest(".os-popup,dialog") ? "other" : "main";
|
|
14255
|
+
return out;
|
|
14256
|
+
};
|
|
14257
|
+
const add = (kind, el, extra) => {
|
|
14258
|
+
const w = where(el);
|
|
14259
|
+
const key = kind + "|" + (w.block || "") + "|" + (w.section || "") + "|" + (extra && extra.key ? extra.key : "") + "|" + (w.block || w.section ? "" : el.tagName + txt(el).slice(0, 30));
|
|
14260
|
+
if (seen.has(key)) return;
|
|
14261
|
+
seen.add(key);
|
|
14262
|
+
const f = { kind, ...w, tag: el.tagName.toLowerCase() };
|
|
14263
|
+
const cls = (e) => (e && typeof e.className === "string" ? e.className.split(/\\s+/).filter((c) => c.startsWith("os-") && !/^os-(theme|align|talign|malign)/.test(c)).slice(0, 2).join(".") : "");
|
|
14264
|
+
const where2 = [cls(el), cls(el.parentElement), cls(el.parentElement && el.parentElement.parentElement)].filter(Boolean).join(" < ");
|
|
14265
|
+
if (where2) f.path = where2.slice(0, 120);
|
|
14266
|
+
const t = txt(el);
|
|
14267
|
+
if (t) f.text = short(t);
|
|
14268
|
+
if (extra) { if (extra.value !== undefined) f.value = Math.round(extra.value * 100) / 100; if (extra.limit !== undefined) f.limit = extra.limit; }
|
|
14269
|
+
findings.push(f);
|
|
14270
|
+
};
|
|
14271
|
+
const hiddenByStyle = (el) => {
|
|
14272
|
+
for (let e = el; e && e !== document.documentElement; e = e.parentElement) {
|
|
14273
|
+
const s = cs(e);
|
|
14274
|
+
if (s.display === "none" || s.visibility === "hidden" || s.visibility === "collapse" || Number(s.opacity) === 0) return true;
|
|
14275
|
+
if (e.hasAttribute("hidden") || e.getAttribute("aria-hidden") === "true") return true;
|
|
14276
|
+
if (e.classList && (e.classList.contains("os-visually-hidden") || e.classList.contains("os-skip-link") || e.classList.contains("os-hp") || e.classList.contains("os-empty"))) return true;
|
|
14277
|
+
// Editor-only notes and Studio's own canvas chrome are not part of the published page.
|
|
14278
|
+
if (e.hasAttribute && e.hasAttribute("data-os-editor")) return true;
|
|
14279
|
+
if (e.tagName === "DIALOG" && !e.open) return true;
|
|
14280
|
+
if (e.tagName === "DETAILS" && !e.open && e !== el && !(el.closest("summary") && el.closest("summary").parentElement === e)) return true;
|
|
14281
|
+
}
|
|
14282
|
+
return false;
|
|
14283
|
+
};
|
|
14284
|
+
const visible = (el) => {
|
|
14285
|
+
const r = el.getBoundingClientRect();
|
|
14286
|
+
if (r.width < 1 || r.height < 1) return false;
|
|
14287
|
+
return !hiddenByStyle(el);
|
|
14288
|
+
};
|
|
14289
|
+
/** Inside something that scrolls sideways on purpose (carousels, tab bars, marquees, code). */
|
|
14290
|
+
const inScroller = (el) => {
|
|
14291
|
+
for (let e = el.parentElement; e && e !== document.body; e = e.parentElement) {
|
|
14292
|
+
const ox = cs(e).overflowX;
|
|
14293
|
+
if (ox === "auto" || ox === "scroll") return true;
|
|
14294
|
+
if (e.classList.contains("os-marquee") || e.classList.contains("os-carousel")) return true;
|
|
14295
|
+
}
|
|
14296
|
+
return false;
|
|
14297
|
+
};
|
|
14298
|
+
/** The nearest ancestor that clips horizontally (overflow hidden/clip), if any. */
|
|
14299
|
+
const clipper = (el) => {
|
|
14300
|
+
for (let e = el.parentElement; e && e !== document.body; e = e.parentElement) {
|
|
14301
|
+
const ox = cs(e).overflowX;
|
|
14302
|
+
if (ox === "hidden" || ox === "clip") return e;
|
|
14303
|
+
}
|
|
14304
|
+
return null;
|
|
14305
|
+
};
|
|
14306
|
+
/** Bounding box of an element's own text (not hidden child spans). */
|
|
14307
|
+
const ownRect = (el) => {
|
|
14308
|
+
let l = Infinity, t = Infinity, r = -Infinity, b = -Infinity;
|
|
14309
|
+
for (const n of el.childNodes) {
|
|
14310
|
+
if (n.nodeType !== 3 || !n.textContent.trim()) continue;
|
|
14311
|
+
const range = document.createRange();
|
|
14312
|
+
range.selectNodeContents(n);
|
|
14313
|
+
for (const x of range.getClientRects()) {
|
|
14314
|
+
if (x.width < 0.5) continue;
|
|
14315
|
+
l = Math.min(l, x.left); t = Math.min(t, x.top); r = Math.max(r, x.right); b = Math.max(b, x.bottom);
|
|
14316
|
+
}
|
|
14317
|
+
}
|
|
14318
|
+
return l === Infinity ? { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 } : { left: l, top: t, right: r, bottom: b, width: r - l, height: b - t };
|
|
14319
|
+
};
|
|
14320
|
+
const ownText = (el) => { let s = ""; for (const n of el.childNodes) if (n.nodeType === 3) s += n.textContent; return s.replace(/\\s+/g, " ").trim(); };
|
|
14321
|
+
const decorative = (el) => !!el.closest(".os-section__bg,.os-img,.os-section__overlay,[aria-hidden=true],.os-marquee,.os-header__cart-count");
|
|
14322
|
+
|
|
14323
|
+
// Text leaves: elements with their own (non-blank) text.
|
|
14324
|
+
const all = Array.from(document.body.querySelectorAll("*"));
|
|
14325
|
+
const SKIP = new Set(["SCRIPT","STYLE","NOSCRIPT","TEMPLATE","SVG","PATH","OPTION","HEAD","TITLE","META","LINK","BR"]);
|
|
14326
|
+
const leaves = [];
|
|
14327
|
+
const leafSet = new Set();
|
|
14328
|
+
for (const el of all) {
|
|
14329
|
+
if (SKIP.has(el.tagName.toUpperCase())) continue;
|
|
14330
|
+
let own = "";
|
|
14331
|
+
for (const n of el.childNodes) if (n.nodeType === 3) own += n.textContent;
|
|
14332
|
+
if (!own.trim()) continue;
|
|
14333
|
+
if (!visible(el)) continue;
|
|
14334
|
+
leaves.push(el);
|
|
14335
|
+
leafSet.add(el);
|
|
14336
|
+
}
|
|
14337
|
+
|
|
14338
|
+
// --- sideways scrolling and cut-off content ---------------------------------------------
|
|
14339
|
+
const scrollWidth = Math.max(document.documentElement.scrollWidth, document.body ? document.body.scrollWidth : 0);
|
|
14340
|
+
const content = all.filter((el) => {
|
|
14341
|
+
const t = el.tagName;
|
|
14342
|
+
return (t === "IMG" || t === "IFRAME" || t === "VIDEO" || t === "TABLE" || t === "PRE" || t === "INPUT" || t === "SELECT" || t === "TEXTAREA" || t === "BUTTON" || t === "A" || t === "svg" || t === "SVG" || el.classList.contains("os-img") || el.classList.contains("os-block") || leafSet.has(el));
|
|
14343
|
+
});
|
|
14344
|
+
let overflowCulprit = null;
|
|
14345
|
+
for (const el of content) {
|
|
14346
|
+
if (el.tagName === "IMG" || el.tagName === "svg" || el.tagName === "SVG") continue; // cropped by their frame on purpose
|
|
14347
|
+
if (!visible(el) || decorative(el) && !el.classList.contains("os-img") || inScroller(el)) continue;
|
|
14348
|
+
if (el.classList.contains("os-img") && el.closest(".os-section__bg")) continue;
|
|
14349
|
+
const r = leafSet.has(el) && !el.classList.contains("os-block") ? ownRect(el) : el.getBoundingClientRect();
|
|
14350
|
+
if (r.width < 1) continue;
|
|
14351
|
+
const over = Math.max(r.right - vw, -r.left);
|
|
14352
|
+
if (over <= 2) continue;
|
|
14353
|
+
// Full-bleed media is drawn edge to edge on purpose.
|
|
14354
|
+
if ((el.tagName === "IMG" || el.classList.contains("os-img") || el.tagName === "VIDEO") && r.left >= -2 && r.right <= vw + 2) continue;
|
|
14355
|
+
const clip = clipper(el);
|
|
14356
|
+
const clipRect = clip ? clip.getBoundingClientRect() : null;
|
|
14357
|
+
if (clipRect && clipRect.left >= -2 && clipRect.right <= vw + 2) {
|
|
14358
|
+
// Clipped by a box inside the screen: is the element cut by it?
|
|
14359
|
+
if (r.right > clipRect.right + 2 || r.left < clipRect.left - 2) {
|
|
14360
|
+
add("cut-off", el, { value: over, key: el.tagName });
|
|
14361
|
+
}
|
|
14362
|
+
} else if (!overflowCulprit || r.right > overflowCulprit.getBoundingClientRect().right) overflowCulprit = el;
|
|
14363
|
+
}
|
|
14364
|
+
if (scrollWidth > vw + 1) add("page-overflow", overflowCulprit || document.body, { value: scrollWidth - vw });
|
|
14365
|
+
|
|
14366
|
+
// --- headings: broken words and huge sizes ------------------------------------------------
|
|
14367
|
+
const heads = all.filter((el) => /^H[1-6]$/.test(el.tagName) || el.classList.contains("os-type-display") || el.classList.contains("os-type-h1") || el.classList.contains("os-header__title"));
|
|
14368
|
+
for (const h of heads) {
|
|
14369
|
+
if (!visible(h)) continue;
|
|
14370
|
+
const s = cs(h);
|
|
14371
|
+
const size = parseFloat(s.fontSize);
|
|
14372
|
+
const r = h.getBoundingClientRect();
|
|
14373
|
+
if (size > vw * L.headingShare || r.height > vh * L.headingScreen) add("big-heading", h, { value: size, limit: Math.round(vw * L.headingShare) });
|
|
14374
|
+
// A word broken across lines: one word, more than one line box.
|
|
14375
|
+
const walker = document.createTreeWalker(h, NodeFilter.SHOW_TEXT);
|
|
14376
|
+
let broken = false;
|
|
14377
|
+
for (let n = walker.nextNode(); n && !broken; n = walker.nextNode()) {
|
|
14378
|
+
const text = n.textContent || "";
|
|
14379
|
+
const re = /[^\\s\\-\u2010\u2013\u2014/]+/g;
|
|
14380
|
+
let m;
|
|
14381
|
+
while ((m = re.exec(text)) && !broken) {
|
|
14382
|
+
if (m[0].length < 4) continue;
|
|
14383
|
+
const range = document.createRange();
|
|
14384
|
+
range.setStart(n, m.index);
|
|
14385
|
+
range.setEnd(n, m.index + m[0].length);
|
|
14386
|
+
const rects = Array.from(range.getClientRects()).filter((x) => x.width > 0.5);
|
|
14387
|
+
const tops = new Set(rects.map((x) => Math.round(x.top)));
|
|
14388
|
+
if (tops.size > 1) broken = true;
|
|
14389
|
+
else if (rects.length && h.getBoundingClientRect().right + 1 < Math.max(...rects.map((x) => x.right))) broken = true;
|
|
14390
|
+
}
|
|
14391
|
+
}
|
|
14392
|
+
if (broken) add("broken-word", h, { value: size });
|
|
14393
|
+
}
|
|
14394
|
+
|
|
14395
|
+
// --- text size, narrow columns, edges ------------------------------------------------------
|
|
14396
|
+
const READING = new Set(["P","LI","DD","BLOCKQUOTE","TD","FIGCAPTION"]);
|
|
14397
|
+
// The site's small-print size (notes, captions), measured once.
|
|
14398
|
+
const smallProbe = document.createElement("span");
|
|
14399
|
+
smallProbe.className = "os-type-small";
|
|
14400
|
+
smallProbe.textContent = "x";
|
|
14401
|
+
smallProbe.style.cssText = "position:absolute;visibility:hidden";
|
|
14402
|
+
(document.querySelector("main") || document.body).appendChild(smallProbe);
|
|
14403
|
+
const smallPx = parseFloat(cs(smallProbe).fontSize) || 14;
|
|
14404
|
+
smallProbe.remove();
|
|
14405
|
+
for (const el of leaves) {
|
|
14406
|
+
if (decorative(el) && !el.closest(".os-img figcaption")) continue;
|
|
14407
|
+
const s = cs(el);
|
|
14408
|
+
const size = parseFloat(s.fontSize);
|
|
14409
|
+
const t = ownText(el);
|
|
14410
|
+
// Reading text: block-level paragraphs and list items (inline code or links are part of them).
|
|
14411
|
+
const inline = s.display.startsWith("inline");
|
|
14412
|
+
const reading = !inline && (READING.has(el.tagName) || (!!el.closest(".os-rich") && !el.closest("h1,h2,h3,h4,h5,h6,.os-type-eyebrow,small,label,button,.os-button")));
|
|
14413
|
+
// Body text must be 16px. Text styled as small print (notes, captions) may be 14px.
|
|
14414
|
+
const smallPrint = !!el.closest(".os-type-small,small,figcaption,.os-muted") || Math.abs(size - smallPx) < 0.5;
|
|
14415
|
+
if (reading && t.length >= 60 && (smallPrint ? size < 13.5 : size < L.bodyText - 0.5)) add("small-text", el, { value: size, limit: smallPrint ? 14 : L.bodyText });
|
|
14416
|
+
else if (size < L.anyText - 0.5 && t.length > 1) add("tiny-text", el, { value: size, limit: L.anyText });
|
|
14417
|
+
const r = el.getBoundingClientRect();
|
|
14418
|
+
if (!el.closest("h1,h2,h3,h4,h5,h6") && (t.match(/[A-Za-z\\u00C0-\\uFFFF]{2,}/g) || []).length >= 6 && r.width < L.narrowColumn && !inScroller(el)) add("narrow-text", el, { value: r.width, limit: L.narrowColumn });
|
|
14419
|
+
if (!inScroller(el) && r.right <= vw + 2 && r.left >= -2) {
|
|
14420
|
+
// The text itself (not its box) against the screen edges.
|
|
14421
|
+
const tr = ownRect(el);
|
|
14422
|
+
if (tr.width > 0 && (tr.left < L.edge - 0.5 || tr.right > vw - L.edge + 0.5) && !el.closest(".os-announcement")) add("edge-text", el, { value: Math.min(tr.left, vw - tr.right), limit: L.edge });
|
|
14423
|
+
}
|
|
14424
|
+
}
|
|
14425
|
+
|
|
14426
|
+
// --- tap targets ---------------------------------------------------------------------------
|
|
14427
|
+
// (A map's legal attribution link is exempt: it's required, tiny by convention and rarely tapped.)
|
|
14428
|
+
const targets = all.filter((el) => el.matches("a[href],button,input:not([type=hidden]),select,textarea,summary,[role=button],[role=tab]") && visible(el) && !el.closest(".os-hp,.os-map__attribution"));
|
|
14429
|
+
const proseLink = (el) => el.tagName === "A" && !el.classList.contains("os-button") && !!el.parentElement && el.parentElement.closest("p,li,dd,blockquote,td,figcaption") && txt(el.parentElement.closest("p,li,dd,blockquote,td,figcaption")).length > txt(el).length + 10;
|
|
14430
|
+
/** A "stretched" link (an absolutely positioned ::after) is as big as the card it covers. */
|
|
14431
|
+
const hitRect = (el) => {
|
|
14432
|
+
const after = getComputedStyle(el, "::after");
|
|
14433
|
+
if (after && after.content && after.content !== "none" && after.position === "absolute") {
|
|
14434
|
+
for (let e = el.parentElement; e && e !== document.body; e = e.parentElement)
|
|
14435
|
+
if (cs(e).position !== "static") return e.getBoundingClientRect();
|
|
14436
|
+
}
|
|
14437
|
+
return el.getBoundingClientRect();
|
|
14438
|
+
};
|
|
14439
|
+
const rects = targets.map(hitRect);
|
|
14440
|
+
// What a neighbour visibly occupies (its padding is tap area, not something to collide with).
|
|
14441
|
+
const inner = targets.map((el, i) => {
|
|
14442
|
+
const r = rects[i], st = cs(el), b = el.getBoundingClientRect();
|
|
14443
|
+
if (Math.abs(r.width - b.width) > 0.5 || Math.abs(r.height - b.height) > 0.5) return r; // a stretched link
|
|
14444
|
+
const pt = parseFloat(st.paddingTop) || 0, pb = parseFloat(st.paddingBottom) || 0;
|
|
14445
|
+
return { left: r.left, right: r.right, top: r.top + pt, bottom: r.bottom - pb };
|
|
14446
|
+
});
|
|
14447
|
+
targets.forEach((el, i) => {
|
|
14448
|
+
if (proseLink(el)) return;
|
|
14449
|
+
if (el.type === "checkbox" || el.type === "radio") { const lab = el.closest("label"); if (lab) return; }
|
|
14450
|
+
const r = rects[i];
|
|
14451
|
+
if (r.width >= L.tapTarget - 0.5 && r.height >= L.tapTarget - 0.5) return;
|
|
14452
|
+
// Too short to tap reliably (44px), or so narrow and close to another target that it's easy to miss.
|
|
14453
|
+
const tiny = r.height < L.tapTarget - 0.5 || r.width < L.tapTargetMin - 0.5;
|
|
14454
|
+
// Tall enough: is it so narrow that a neighbour on the same line sits inside its 44px-wide zone?
|
|
14455
|
+
const cx = r.left + r.width / 2, half = L.tapTarget / 2;
|
|
14456
|
+
const zone = { left: cx - half, right: cx + half, top: r.top + r.height * 0.25, bottom: r.bottom - r.height * 0.25 };
|
|
14457
|
+
let crowded = false;
|
|
14458
|
+
targets.forEach((o, j) => {
|
|
14459
|
+
if (j === i || crowded || o.contains(el) || el.contains(o)) return;
|
|
14460
|
+
const q = inner[j];
|
|
14461
|
+
if (q.left < zone.right && q.right > zone.left && q.top < zone.bottom && q.bottom > zone.top) crowded = true;
|
|
14462
|
+
});
|
|
14463
|
+
if (tiny || crowded) add("tap-target", el, { value: Math.min(r.width, r.height), limit: L.tapTarget, key: txt(el).slice(0, 20) || el.getAttribute("aria-label") || String(i) });
|
|
14464
|
+
});
|
|
14465
|
+
|
|
14466
|
+
// --- text over text --------------------------------------------------------------------------
|
|
14467
|
+
const blockOf = (el) => el.closest("[data-os-block]") || el.closest("[data-os-id]");
|
|
14468
|
+
/** Line boxes of an element's own text (a drop cap or a tall first letter stays within its line). */
|
|
14469
|
+
const lineRects = (el) => {
|
|
14470
|
+
const out = [];
|
|
14471
|
+
for (const n of el.childNodes) {
|
|
14472
|
+
if (n.nodeType !== 3 || !n.textContent.trim()) continue;
|
|
14473
|
+
const range = document.createRange();
|
|
14474
|
+
range.selectNodeContents(n);
|
|
14475
|
+
for (const x of range.getClientRects()) if (x.width >= 0.5) out.push(x);
|
|
14476
|
+
}
|
|
14477
|
+
return out;
|
|
14478
|
+
};
|
|
14479
|
+
const boxes = leaves.filter((el) => !decorative(el) && ownText(el).length > 1).map((el) => ({ el, r: ownRect(el), lines: lineRects(el), b: blockOf(el) }));
|
|
14480
|
+
const linesMeet = (a, b) => a.lines.some((x) => b.lines.some((y) => Math.min(x.right, y.right) - Math.max(x.left, y.left) > 4 && Math.min(x.bottom, y.bottom) - Math.max(x.top, y.top) > Math.max(8, 0.3 * Math.min(x.height, y.height))));
|
|
14481
|
+
for (let i = 0; i < boxes.length; i++)
|
|
14482
|
+
for (let j = i + 1; j < boxes.length; j++) {
|
|
14483
|
+
const a = boxes[i], b = boxes[j];
|
|
14484
|
+
if (!a.b || !b.b || a.b === b.b || a.b.contains(b.b) || b.b.contains(a.b)) continue;
|
|
14485
|
+
const w = Math.min(a.r.right, b.r.right) - Math.max(a.r.left, b.r.left);
|
|
14486
|
+
const hgt = Math.min(a.r.bottom, b.r.bottom) - Math.max(a.r.top, b.r.top);
|
|
14487
|
+
if (w > 4 && hgt > 4 && linesMeet(a, b)) add("overlap", b.el, { key: a.b.getAttribute("data-os-id") || String(i) });
|
|
14488
|
+
}
|
|
14489
|
+
|
|
14490
|
+
// --- contrast (text over photos and on solid colors) ---------------------------------------
|
|
14491
|
+
const canvas = document.createElement("canvas");
|
|
14492
|
+
canvas.width = canvas.height = 1;
|
|
14493
|
+
const ctx2d = canvas.getContext("2d", { willReadFrequently: true });
|
|
14494
|
+
const colors = new Map();
|
|
14495
|
+
/** Any CSS color (rgb, color(), oklab, color-mix results\u2026) as 0\u2013255 RGB + alpha. */
|
|
14496
|
+
const parse = (c) => {
|
|
14497
|
+
if (!c || c === "transparent") return null;
|
|
14498
|
+
if (colors.has(c)) return colors.get(c);
|
|
14499
|
+
let out = null;
|
|
14500
|
+
const m = /^rgba?\\(([^)]+)\\)$/.exec(c);
|
|
14501
|
+
if (m) {
|
|
14502
|
+
const p = m[1].split(/[ ,/]+/).filter(Boolean).map(Number);
|
|
14503
|
+
out = { r: p[0], g: p[1], b: p[2], a: p.length > 3 ? p[3] : 1 };
|
|
14504
|
+
} else if (ctx2d) {
|
|
14505
|
+
const a = /\\/\\s*([\\d.]+)(%?)\\s*\\)$/.exec(c);
|
|
14506
|
+
const alpha = a ? Number(a[1]) / (a[2] ? 100 : 1) : 1;
|
|
14507
|
+
ctx2d.clearRect(0, 0, 1, 1);
|
|
14508
|
+
ctx2d.fillStyle = "#000";
|
|
14509
|
+
ctx2d.fillStyle = c.replace(/\\/\\s*[\\d.]+%?\\s*\\)$/, ")");
|
|
14510
|
+
ctx2d.fillRect(0, 0, 1, 1);
|
|
14511
|
+
const d = ctx2d.getImageData(0, 0, 1, 1).data;
|
|
14512
|
+
out = { r: d[0], g: d[1], b: d[2], a: alpha };
|
|
14513
|
+
}
|
|
14514
|
+
colors.set(c, out);
|
|
14515
|
+
return out;
|
|
14516
|
+
};
|
|
14517
|
+
const lin = (v) => { v /= 255; return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
|
|
14518
|
+
const lum = (c) => 0.2126 * lin(c.r) + 0.7152 * lin(c.g) + 0.0722 * lin(c.b);
|
|
14519
|
+
const ratio = (a, b) => { const x = lum(a), y = lum(b); return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05); };
|
|
14520
|
+
const over = (top, base) => ({ r: top.r * top.a + base.r * (1 - top.a), g: top.g * top.a + base.g * (1 - top.a), b: top.b * top.a + base.b * (1 - top.a), a: 1 });
|
|
14521
|
+
const opacityOf = (el) => { let o = 1; for (let e = el; e && e !== document.documentElement; e = e.parentElement) o *= Number(cs(e).opacity); return o; };
|
|
14522
|
+
const PHOTO = new Set(["IMG","VIDEO","CANVAS","PICTURE","IFRAME"]);
|
|
14523
|
+
const pageBg = parse(cs(document.body).backgroundColor) || { r: 255, g: 255, b: 255, a: 1 };
|
|
14524
|
+
const checkPoint = (el, x, y) => {
|
|
14525
|
+
const stack = document.elementsFromPoint(x, y);
|
|
14526
|
+
const at = stack.indexOf(el);
|
|
14527
|
+
if (at < 0) return null;
|
|
14528
|
+
const layers = [];
|
|
14529
|
+
let photo = false, base = null;
|
|
14530
|
+
for (const e of stack.slice(at)) {
|
|
14531
|
+
if (PHOTO.has(e.tagName)) { photo = true; break; }
|
|
14532
|
+
const s = cs(e);
|
|
14533
|
+
if (/url\\(/.test(s.backgroundImage)) { photo = true; break; }
|
|
14534
|
+
if (s.backgroundImage && s.backgroundImage !== "none") return null; // gradients: checked from the document
|
|
14535
|
+
const bg = parse(s.backgroundColor);
|
|
14536
|
+
if (bg && bg.a > 0) {
|
|
14537
|
+
const a = bg.a * opacityOf(e);
|
|
14538
|
+
if (a >= 0.98) { base = { ...bg, a: 1 }; break; }
|
|
14539
|
+
layers.push({ ...bg, a });
|
|
14540
|
+
}
|
|
14541
|
+
}
|
|
14542
|
+
return { photo, base: base || (photo ? null : pageBg), layers };
|
|
14543
|
+
};
|
|
14544
|
+
for (const el of leaves) {
|
|
14545
|
+
if (decorative(el) || ownText(el).length < 2) continue;
|
|
14546
|
+
const s = cs(el);
|
|
14547
|
+
const fg = parse(s.color);
|
|
14548
|
+
if (!fg || fg.a === 0) continue;
|
|
14549
|
+
const size = parseFloat(s.fontSize);
|
|
14550
|
+
const bold = Number(s.fontWeight) >= 700;
|
|
14551
|
+
const need = size >= 24 || (bold && size >= 18.66) ? 3 : 4.5;
|
|
14552
|
+
const r = ownRect(el);
|
|
14553
|
+
if (r.width < 1) continue;
|
|
14554
|
+
const sy = window.scrollY;
|
|
14555
|
+
const pts = [[r.left + Math.min(8, r.width / 2), r.top + sy + r.height / 2], [r.left + r.width / 2, r.top + sy + r.height / 2]];
|
|
14556
|
+
let worst = Infinity, photo = false;
|
|
14557
|
+
for (const [px, docY] of pts) {
|
|
14558
|
+
if (docY < window.scrollY + 80 || docY > window.scrollY + viewH - 20) window.scrollTo(0, Math.max(0, docY - viewH / 2));
|
|
14559
|
+
const py = docY - window.scrollY;
|
|
14560
|
+
if (py < 0 || py > viewH) continue;
|
|
14561
|
+
const hit = checkPoint(el, Math.max(0, Math.min(vw - 1, px)), py);
|
|
14562
|
+
if (!hit) continue;
|
|
14563
|
+
const bases = hit.photo ? [{ r: 0, g: 0, b: 0, a: 1 }, { r: 255, g: 255, b: 255, a: 1 }] : [hit.base];
|
|
14564
|
+
for (const b0 of bases) {
|
|
14565
|
+
let c = b0;
|
|
14566
|
+
for (const l of hit.layers.slice().reverse()) c = over(l, c);
|
|
14567
|
+
const text = fg.a < 1 ? over(fg, c) : fg;
|
|
14568
|
+
worst = Math.min(worst, ratio(text, c));
|
|
14569
|
+
}
|
|
14570
|
+
photo = photo || hit.photo;
|
|
14571
|
+
}
|
|
14572
|
+
if (worst < need - 0.05) add(photo ? "photo-contrast" : "low-contrast", el, { value: worst, limit: need });
|
|
14573
|
+
}
|
|
14574
|
+
window.scrollTo(0, 0);
|
|
14575
|
+
|
|
14576
|
+
// --- images --------------------------------------------------------------------------------
|
|
14577
|
+
for (const box of document.querySelectorAll(".os-img,iframe,video")) {
|
|
14578
|
+
if (!visible(box) || box.closest(".os-section__bg,.os-header,.os-lightbox,.os-carousel__track,.os-marquee")) continue;
|
|
14579
|
+
const r = box.getBoundingClientRect();
|
|
14580
|
+
if (r.height > vh * L.imageScreen) add("tall-image", box, { value: r.height, limit: Math.round(vh * L.imageScreen) });
|
|
14581
|
+
const img = box.querySelector && box.querySelector("img");
|
|
14582
|
+
if (img && img.naturalWidth > 0 && cs(img).objectFit === "cover") {
|
|
14583
|
+
const ir = img.getBoundingClientRect();
|
|
14584
|
+
const nat = img.naturalWidth / img.naturalHeight, shown = ir.width / Math.max(1, ir.height);
|
|
14585
|
+
const share = Math.min(nat / shown, shown / nat);
|
|
14586
|
+
if (share < L.crop && !box.style.getPropertyValue("--focal") && !img.style.getPropertyValue("--focal")) add("heavy-crop", box, { value: share, limit: L.crop });
|
|
14587
|
+
}
|
|
14588
|
+
}
|
|
14589
|
+
for (const bg of document.querySelectorAll(".os-section__bg img")) {
|
|
14590
|
+
const sec = bg.closest("[data-os-section]");
|
|
14591
|
+
if (!sec || !visible(sec) || bg.naturalWidth === 0) continue;
|
|
14592
|
+
const r = sec.getBoundingClientRect();
|
|
14593
|
+
const nat = bg.naturalWidth / bg.naturalHeight, shown = r.width / Math.max(1, r.height);
|
|
14594
|
+
const share = Math.min(nat / shown, shown / nat);
|
|
14595
|
+
const focal = (bg.getAttribute("style") || "") + (bg.closest("[style*='--focal']") ? "focal" : "");
|
|
14596
|
+
if (share < L.crop && !/--focal|focal/.test(focal)) add("heavy-crop", sec, { value: share, limit: L.crop, key: "bg" });
|
|
14597
|
+
}
|
|
14598
|
+
|
|
14599
|
+
// --- section rhythm, header, footer ---------------------------------------------------------
|
|
14600
|
+
for (const sec of document.querySelectorAll("main [data-os-section]")) {
|
|
14601
|
+
if (!visible(sec)) continue;
|
|
14602
|
+
const s = cs(sec);
|
|
14603
|
+
const pad = Math.max(parseFloat(s.paddingTop), parseFloat(s.paddingBottom));
|
|
14604
|
+
const header = document.querySelector(".os-header");
|
|
14605
|
+
const overlayExtra = sec.classList.contains("os-section--first") && document.querySelector(".has-overlay-header") && header ? header.getBoundingClientRect().height : 0;
|
|
14606
|
+
const top = parseFloat(s.paddingTop) - overlayExtra, bottom = parseFloat(s.paddingBottom);
|
|
14607
|
+
const minh = parseFloat(s.minHeight) || 0;
|
|
14608
|
+
if (Math.max(top, bottom) > L.loosePadding && minh < vh * 0.5) add("loose-padding", sec, { value: Math.max(top, bottom), limit: L.loosePadding });
|
|
14609
|
+
void pad;
|
|
14610
|
+
}
|
|
14611
|
+
const bar = document.querySelector(".os-header__bar");
|
|
14612
|
+
if (bar && visible(bar)) {
|
|
14613
|
+
const parts = Array.from(bar.children).filter(visible);
|
|
14614
|
+
let crowded = bar.getBoundingClientRect().height > 120;
|
|
14615
|
+
for (let i = 0; i < parts.length && !crowded; i++)
|
|
14616
|
+
for (let j = i + 1; j < parts.length; j++) {
|
|
14617
|
+
const a = parts[i].getBoundingClientRect(), b = parts[j].getBoundingClientRect();
|
|
14618
|
+
if (Math.min(a.right, b.right) - Math.max(a.left, b.left) > 2 && Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top) > 2) { crowded = true; break; }
|
|
14619
|
+
}
|
|
14620
|
+
if (bar.scrollWidth > bar.clientWidth + 2) crowded = true;
|
|
14621
|
+
if (crowded) add("header-crowded", bar, { value: bar.getBoundingClientRect().height });
|
|
14622
|
+
}
|
|
14623
|
+
// The first section's main action: visible without scrolling?
|
|
14624
|
+
const first = Array.from(document.querySelectorAll("main [data-os-section]")).find(visible);
|
|
14625
|
+
// Only a first section with photos or video (a hero) can push its words and button down.
|
|
14626
|
+
if (first && first.querySelector("img,video,.os-section__bg")) {
|
|
14627
|
+
// A form's own submit button sits at its end, wherever that is; look at call-to-action buttons.
|
|
14628
|
+
const cta = Array.from(first.querySelectorAll(".os-button--primary")).find((b) => visible(b) && !b.closest("form,.os-form,.os-newsletter"));
|
|
14629
|
+
if (cta) {
|
|
14630
|
+
const top = cta.getBoundingClientRect().top + window.scrollY;
|
|
14631
|
+
if (top + Math.min(44, cta.getBoundingClientRect().height) > vh) add("cta-below-fold", cta, { value: top, limit: vh });
|
|
14632
|
+
}
|
|
14633
|
+
}
|
|
14634
|
+
const footer = document.querySelector(".os-footer");
|
|
14635
|
+
if (footer && visible(footer) && footer.getBoundingClientRect().height > vh * 2.5) add("long-footer", footer, { value: footer.getBoundingClientRect().height, limit: Math.round(vh * 2.5) });
|
|
14636
|
+
|
|
14637
|
+
style.remove();
|
|
14638
|
+
return { width: vw, height: vh, scrollWidth, findings };
|
|
14639
|
+
})()`;
|
|
14640
|
+
var PHONE_PREPARE_SCRIPT = `(async () => {
|
|
14641
|
+
for (const img of Array.from(document.images)) { img.loading = "eager"; img.decoding = "sync"; }
|
|
14642
|
+
document.documentElement.classList.remove("os-reveal-ready");
|
|
14643
|
+
const step = Math.max(300, Math.floor(window.innerHeight * 0.8));
|
|
14644
|
+
for (let y = 0, i = 0; y < document.documentElement.scrollHeight && i < 80; y += step, i++) {
|
|
14645
|
+
window.scrollTo(0, y);
|
|
14646
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
14647
|
+
}
|
|
14648
|
+
window.scrollTo(0, 0);
|
|
14649
|
+
const done = Promise.all(Array.from(document.images).map((img) => img.complete ? null :
|
|
14650
|
+
new Promise((r) => { img.addEventListener("load", r, { once: true }); img.addEventListener("error", r, { once: true }); })));
|
|
14651
|
+
await Promise.race([done, new Promise((r) => setTimeout(r, 5000))]);
|
|
14652
|
+
if (document.fonts) await Promise.race([document.fonts.ready, new Promise((r) => setTimeout(r, 3000))]);
|
|
14653
|
+
})()`;
|
|
14654
|
+
|
|
13486
14655
|
// ../lint/src/seo.ts
|
|
13487
14656
|
function seoCheck(page, doc, options = {}) {
|
|
13488
14657
|
const meta = opMeta(options);
|
|
@@ -13575,12 +14744,12 @@ function seoCheck(page, doc, options = {}) {
|
|
|
13575
14744
|
} : {}
|
|
13576
14745
|
});
|
|
13577
14746
|
}
|
|
13578
|
-
const
|
|
13579
|
-
if (!page.seo?.noindex &&
|
|
14747
|
+
const words4 = blocks2.map(blockText).join(" ").split(/\s+/).filter(Boolean).length;
|
|
14748
|
+
if (!page.seo?.noindex && words4 < 80) {
|
|
13580
14749
|
add({
|
|
13581
14750
|
rule: "seo-thin-content",
|
|
13582
14751
|
severity: "info",
|
|
13583
|
-
message: `The page has about ${
|
|
14752
|
+
message: `The page has about ${words4} words of text; search engines have little to go on.`,
|
|
13584
14753
|
fix: "Add a short paragraph that explains what's on the page in your own words."
|
|
13585
14754
|
});
|
|
13586
14755
|
}
|
|
@@ -14206,6 +15375,20 @@ async function runCheck(root, options = {}) {
|
|
|
14206
15375
|
});
|
|
14207
15376
|
}
|
|
14208
15377
|
}
|
|
15378
|
+
if (doc) {
|
|
15379
|
+
try {
|
|
15380
|
+
for (const i of phoneCheck(doc)) {
|
|
15381
|
+
if (i.severity === "info") continue;
|
|
15382
|
+
add({
|
|
15383
|
+
severity: "warning",
|
|
15384
|
+
rule: "phone",
|
|
15385
|
+
file: i.page && i.page !== "footer" ? pageFiles2[i.page] ?? PAGES_DIR : i.page === "footer" ? SITE_FILE : SITE_FILE,
|
|
15386
|
+
message: `${i.message} ${i.autofix ? `Fix: \`openspace check --phone --fix\` (${i.autofix.label.toLowerCase()}).` : i.fix}`
|
|
15387
|
+
});
|
|
15388
|
+
}
|
|
15389
|
+
} catch {
|
|
15390
|
+
}
|
|
15391
|
+
}
|
|
14209
15392
|
const mediaOnDisk = /* @__PURE__ */ new Map();
|
|
14210
15393
|
for (const rel of await listFiles(root, "media")) {
|
|
14211
15394
|
const bytes = (await stat(join2(root, rel))).size;
|
|
@@ -14515,7 +15698,7 @@ function setupChecklist(sq, build) {
|
|
|
14515
15698
|
id: "newsletter",
|
|
14516
15699
|
kind: "newsletter",
|
|
14517
15700
|
title: "Bring your newsletter subscribers",
|
|
14518
|
-
detail:
|
|
15701
|
+
detail: subscribersAdvice(sq.platform),
|
|
14519
15702
|
where: newsletter.where,
|
|
14520
15703
|
status: "todo"
|
|
14521
15704
|
});
|
|
@@ -15654,13 +16837,50 @@ var PROBE_SCRIPT = String.raw`(async () => {
|
|
|
15654
16837
|
res.textLeft = Math.round(Math.min(...measures.map((x) => x.left)));
|
|
15655
16838
|
}
|
|
15656
16839
|
// Columns: text items whose vertical spans overlap, at distinct left edges.
|
|
15657
|
-
const
|
|
15658
|
-
|
|
15659
|
-
|
|
15660
|
-
const
|
|
15661
|
-
|
|
15662
|
-
|
|
16840
|
+
const itemEls = [...sec.querySelectorAll(textSel + ", h2, img")].filter((e) => visible(e) && (e.tagName === "IMG" ? e.getBoundingClientRect().width > 60 : ownText(e).length >= 12));
|
|
16841
|
+
const columnsOf = (items) => {
|
|
16842
|
+
let n = 1;
|
|
16843
|
+
for (const a of items) {
|
|
16844
|
+
const mid = a.top + Math.min(a.height, 40) / 2;
|
|
16845
|
+
const lefts = [];
|
|
16846
|
+
for (const b of items) if (b.top <= mid && b.bottom >= mid && !lefts.some((l) => Math.abs(l - b.left) < 40)) lefts.push(b.left);
|
|
16847
|
+
n = Math.max(n, Math.min(6, lefts.length));
|
|
16848
|
+
}
|
|
16849
|
+
return n;
|
|
16850
|
+
};
|
|
16851
|
+
res.columns = columnsOf(itemEls.map(rect));
|
|
16852
|
+
// Corrected score only (the strict v3 count above is kept): columns a visitor can see.
|
|
16853
|
+
// A box that clips content running sideways out of it (a scroll-snap slideshow, a map's
|
|
16854
|
+
// tiles) is one picture on screen, however many slides or tiles it holds; anything else
|
|
16855
|
+
// counts when at least half of it (or half the viewport's width) is inside the viewport.
|
|
16856
|
+
// (A clearfix row with
|
|
16857
|
+
// overflow: hidden doesn't overflow sideways, so its columns stay columns.)
|
|
16858
|
+
const clipBox = (el) => {
|
|
16859
|
+
for (let e = el.parentElement; e && e !== document.body && e !== doc; e = e.parentElement) {
|
|
16860
|
+
const st = cs(e);
|
|
16861
|
+
if ((st.overflowX !== "visible" || st.overflow !== "visible") && e.scrollWidth > e.clientWidth + 2) return e;
|
|
16862
|
+
}
|
|
16863
|
+
return null;
|
|
16864
|
+
};
|
|
16865
|
+
const shownRects = [];
|
|
16866
|
+
const boxes = new Set();
|
|
16867
|
+
for (const el of itemEls) {
|
|
16868
|
+
const box = clipBox(el);
|
|
16869
|
+
if (box) {
|
|
16870
|
+
if (boxes.has(box)) continue;
|
|
16871
|
+
boxes.add(box);
|
|
16872
|
+
const br = rect(box);
|
|
16873
|
+
const L = Math.max(0, br.left), R = Math.min(vw, br.right);
|
|
16874
|
+
if (R - L > 2) shownRects.push({ ...br, left: L, right: R, width: R - L });
|
|
16875
|
+
continue;
|
|
16876
|
+
}
|
|
16877
|
+
const r = el.getBoundingClientRect();
|
|
16878
|
+
const L = Math.max(0, r.left), R = Math.min(vw, r.right);
|
|
16879
|
+
// Half of it on screen — or half the screen (a wide cover photo cropped at both sides).
|
|
16880
|
+
if (R - L >= Math.min(r.width, vw) * 0.5) shownRects.push(rect(el));
|
|
15663
16881
|
}
|
|
16882
|
+
const shown = columnsOf(shownRects);
|
|
16883
|
+
if (shown !== res.columns) res.columnsOnScreen = shown;
|
|
15664
16884
|
// The section's parts and where they sit (to recreate it as a component when needed).
|
|
15665
16885
|
if (!sec.querySelector("form, input, textarea, select, iframe, video, canvas, embed, object")) {
|
|
15666
16886
|
const parts = [];
|
|
@@ -15756,6 +16976,8 @@ var PROBE_SCRIPT = String.raw`(async () => {
|
|
|
15756
16976
|
const inMain = (el) => !chrome(el) && !el.closest("footer, .os-footer");
|
|
15757
16977
|
const styleOfAll = (sel) => [...document.querySelectorAll(sel)].filter((e) => visible(e) && inMain(e) && ownText(e).length > 1).map(textOf);
|
|
15758
16978
|
const presets = {};
|
|
16979
|
+
// Measurements only the corrected score uses (the strict v3 ones above stay as they were).
|
|
16980
|
+
const corrections = {};
|
|
15759
16981
|
const h1s = styleOfAll("h1");
|
|
15760
16982
|
if (h1s.length) presets.h1 = h1s.sort((a, b) => b.size - a.size)[0];
|
|
15761
16983
|
const h2 = median(styleOfAll("h2"));
|
|
@@ -15806,6 +17028,21 @@ var PROBE_SCRIPT = String.raw`(async () => {
|
|
|
15806
17028
|
if (!links.length) links = [...header.querySelectorAll("a")].filter(linkOk);
|
|
15807
17029
|
const mid = links[Math.floor(links.length / 2)];
|
|
15808
17030
|
if (mid) presets.nav = textOf(mid);
|
|
17031
|
+
else {
|
|
17032
|
+
// Corrected score only: "[class*=brand]" above is meant for logo boxes, but it also
|
|
17033
|
+
// matches a header's colour theme (OpenSpace's "os-theme-brand" sidebar). A class naming a
|
|
17034
|
+
// theme isn't a logo box: look again without it.
|
|
17035
|
+
const logoBox = (a) => {
|
|
17036
|
+
for (let e = a; e && e.nodeType === 1; e = e.parentElement)
|
|
17037
|
+
if ([...e.classList].some((c) => /brand/i.test(c) && !/theme[-_]?brand/i.test(c))) return true;
|
|
17038
|
+
return false;
|
|
17039
|
+
};
|
|
17040
|
+
const linkOk2 = (a) => visible(a) && ownText(a).length > 1 && ownText(a).length < 30 && toHex(cs(a).backgroundColor) === undefined && !a.matches(".os-header__brand, .os-button, [rel=home]") && !a.closest(".os-header__brand, [class*=site-title]") && !logoBox(a) && !homeLink(a);
|
|
17041
|
+
let again = navEl ? [...navEl.querySelectorAll("a")].filter(linkOk2) : [];
|
|
17042
|
+
if (!again.length) again = [...header.querySelectorAll("a")].filter(linkOk2);
|
|
17043
|
+
const mid2 = again[Math.floor(again.length / 2)];
|
|
17044
|
+
if (mid2) corrections.nav = textOf(mid2);
|
|
17045
|
+
}
|
|
15809
17046
|
// The site title set as text (a wordmark), not a logo image.
|
|
15810
17047
|
// The innermost element holding the title text (an OpenSpace title span, not its link).
|
|
15811
17048
|
const brandOk = (e) => visible(e) && ownText(e).length > 1 && ownText(e).length <= 40 && !e.querySelector("img, svg");
|
|
@@ -15915,6 +17152,7 @@ var PROBE_SCRIPT = String.raw`(async () => {
|
|
|
15915
17152
|
...(buttons ? { buttons } : {}),
|
|
15916
17153
|
...(eyebrows.length ? { eyebrowRule: out.filter((s) => s.rule).length >= Math.max(1, eyebrows.length / 2) } : {}),
|
|
15917
17154
|
layout,
|
|
17155
|
+
...(Object.keys(corrections).length ? { corrections } : {}),
|
|
15918
17156
|
sections: out,
|
|
15919
17157
|
height: doc.scrollHeight,
|
|
15920
17158
|
fonts,
|
|
@@ -19648,15 +20886,15 @@ function truncate(v, l = 50, o = "...") {
|
|
|
19648
20886
|
return v;
|
|
19649
20887
|
return str5.substring(0, l - o.length) + o;
|
|
19650
20888
|
}
|
|
19651
|
-
function truncatewords(v,
|
|
20889
|
+
function truncatewords(v, words4 = 15, o = "...") {
|
|
19652
20890
|
const str5 = stringify(v);
|
|
19653
20891
|
o = stringify(o);
|
|
19654
20892
|
this.context.memoryLimit.use(str5.length + o.length);
|
|
19655
20893
|
const arr = str5.split(/\s+/);
|
|
19656
|
-
if (
|
|
19657
|
-
|
|
19658
|
-
let ret = arr.slice(0,
|
|
19659
|
-
if (arr.length >=
|
|
20894
|
+
if (words4 <= 0)
|
|
20895
|
+
words4 = 1;
|
|
20896
|
+
let ret = arr.slice(0, words4).join(" ");
|
|
20897
|
+
if (arr.length >= words4)
|
|
19660
20898
|
ret += o;
|
|
19661
20899
|
return ret;
|
|
19662
20900
|
}
|
|
@@ -28493,28 +29731,28 @@ function gfmFootnoteHtml(options) {
|
|
|
28493
29731
|
},
|
|
28494
29732
|
gfmFootnoteCallString(token) {
|
|
28495
29733
|
let calls = this.getData("gfmFootnoteCallOrder");
|
|
28496
|
-
let
|
|
29734
|
+
let counts2 = this.getData("gfmFootnoteCallCounts");
|
|
28497
29735
|
const id = normalizeIdentifier(this.sliceSerialize(token));
|
|
28498
29736
|
let counter;
|
|
28499
29737
|
this.resume();
|
|
28500
29738
|
if (!calls) this.setData("gfmFootnoteCallOrder", calls = []);
|
|
28501
|
-
if (!
|
|
29739
|
+
if (!counts2) this.setData("gfmFootnoteCallCounts", counts2 = {});
|
|
28502
29740
|
const index = calls.indexOf(id);
|
|
28503
29741
|
const safeId = sanitizeUri(id.toLowerCase());
|
|
28504
29742
|
if (index === -1) {
|
|
28505
29743
|
calls.push(id);
|
|
28506
|
-
|
|
29744
|
+
counts2[id] = 1;
|
|
28507
29745
|
counter = calls.length;
|
|
28508
29746
|
} else {
|
|
28509
|
-
|
|
29747
|
+
counts2[id]++;
|
|
28510
29748
|
counter = index + 1;
|
|
28511
29749
|
}
|
|
28512
|
-
const reuseCounter =
|
|
29750
|
+
const reuseCounter = counts2[id];
|
|
28513
29751
|
this.tag('<sup><a href="#' + clobberPrefix + "fn-" + safeId + '" id="' + clobberPrefix + "fnref-" + safeId + (reuseCounter > 1 ? "-" + reuseCounter : "") + '" data-footnote-ref="" aria-describedby="footnote-label">' + String(counter) + "</a></sup>");
|
|
28514
29752
|
},
|
|
28515
29753
|
null() {
|
|
28516
29754
|
const calls = this.getData("gfmFootnoteCallOrder") || [];
|
|
28517
|
-
const
|
|
29755
|
+
const counts2 = this.getData("gfmFootnoteCallCounts") || {};
|
|
28518
29756
|
const definitions = this.getData("gfmFootnoteDefinitions") || {};
|
|
28519
29757
|
let index = -1;
|
|
28520
29758
|
if (calls.length > 0) {
|
|
@@ -28530,7 +29768,7 @@ function gfmFootnoteHtml(options) {
|
|
|
28530
29768
|
const safeId = sanitizeUri(id.toLowerCase());
|
|
28531
29769
|
let referenceIndex = 0;
|
|
28532
29770
|
const references = [];
|
|
28533
|
-
while (++referenceIndex <=
|
|
29771
|
+
while (++referenceIndex <= counts2[id]) {
|
|
28534
29772
|
references.push('<a href="#' + clobberPrefix + "fnref-" + safeId + (referenceIndex > 1 ? "-" + referenceIndex : "") + '" data-footnote-backref="" aria-label="' + this.encode(typeof backLabel === "string" ? backLabel : backLabel(index, referenceIndex)) + '" class="data-footnote-backref">\u21A9' + (referenceIndex > 1 ? "<sup>" + referenceIndex + "</sup>" : "") + "</a>");
|
|
28535
29773
|
}
|
|
28536
29774
|
const reference = references.join(" ");
|
|
@@ -30947,7 +32185,7 @@ var typeOf = (o) => {
|
|
|
30947
32185
|
for (const k of TYPE_KEYS) if (typeof o[k] === "string") return o[k];
|
|
30948
32186
|
return void 0;
|
|
30949
32187
|
};
|
|
30950
|
-
function
|
|
32188
|
+
function sectionsOf3(o) {
|
|
30951
32189
|
if (!isObj3(o)) return void 0;
|
|
30952
32190
|
for (const k of SECTION_KEYS) {
|
|
30953
32191
|
const v = o[k];
|
|
@@ -30978,7 +32216,7 @@ async function findStructuredPages(source, files) {
|
|
|
30978
32216
|
data = p.data;
|
|
30979
32217
|
body = p.body;
|
|
30980
32218
|
}
|
|
30981
|
-
if (
|
|
32219
|
+
if (sectionsOf3(data)) out.push({ path: f, data, ...body ? { body } : {} });
|
|
30982
32220
|
}
|
|
30983
32221
|
return out;
|
|
30984
32222
|
}
|
|
@@ -31094,6 +32332,7 @@ function formFrom(v, ctx) {
|
|
|
31094
32332
|
if (signupOnly)
|
|
31095
32333
|
return {
|
|
31096
32334
|
kind: "newsletter",
|
|
32335
|
+
...fields.some((f) => f.type !== "email") ? { askName: true } : {},
|
|
31097
32336
|
...button ? { buttonLabel: plain2(button) } : {},
|
|
31098
32337
|
...str3(v.email_placeholder) ? { placeholder: str3(v.email_placeholder) } : {},
|
|
31099
32338
|
...filled(v.success_message) ? { success: filled(v.success_message) } : {}
|
|
@@ -31545,7 +32784,7 @@ ${siteBg ? `body{background-color:${siteBg};}` : ""}`
|
|
|
31545
32784
|
placeholders,
|
|
31546
32785
|
fg: fgFor
|
|
31547
32786
|
};
|
|
31548
|
-
const raw2 =
|
|
32787
|
+
const raw2 = sectionsOf3(file.data) ?? [];
|
|
31549
32788
|
const sections = [];
|
|
31550
32789
|
for (const s2 of raw2) {
|
|
31551
32790
|
typesSeen.set(typeOf(s2) ?? "?", (typesSeen.get(typeOf(s2) ?? "?") ?? 0) + 1);
|
|
@@ -41057,13 +42296,14 @@ function fontCredit(a, b, o, i) {
|
|
|
41057
42296
|
const d = metricDistance(ma, mb);
|
|
41058
42297
|
return d <= 0.04 ? 0.85 : d <= 0.07 ? 0.7 : d <= 0.11 ? 0.5 : d <= 0.16 ? 0.25 : 0;
|
|
41059
42298
|
}
|
|
42299
|
+
var SCORER = "v3.1-corrected";
|
|
41060
42300
|
var norm3 = (s2) => (s2 ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
41061
|
-
var
|
|
42301
|
+
var words3 = (s2) => new Set(
|
|
41062
42302
|
s2.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2)
|
|
41063
42303
|
);
|
|
41064
42304
|
var jaccard = (a, b) => {
|
|
41065
|
-
const A =
|
|
41066
|
-
const B =
|
|
42305
|
+
const A = words3(a);
|
|
42306
|
+
const B = words3(b);
|
|
41067
42307
|
if (!A.size || !B.size) return 0;
|
|
41068
42308
|
let n = 0;
|
|
41069
42309
|
for (const w of A) if (B.has(w)) n++;
|
|
@@ -41104,7 +42344,12 @@ var ROLE_OF = {
|
|
|
41104
42344
|
nav: "label",
|
|
41105
42345
|
wordmark: "label"
|
|
41106
42346
|
};
|
|
41107
|
-
function
|
|
42347
|
+
function presetOf(p, k, mode) {
|
|
42348
|
+
const v = p.presets[k];
|
|
42349
|
+
if (v || mode === "strict" || k !== "nav") return v;
|
|
42350
|
+
return p.corrections?.nav;
|
|
42351
|
+
}
|
|
42352
|
+
function typeChecks(o, i, tol, mode = "corrected") {
|
|
41108
42353
|
const out = [];
|
|
41109
42354
|
const mobile = o.viewport < 700;
|
|
41110
42355
|
const sizeKey = mobile ? "sizeMobile" : "size";
|
|
@@ -41120,8 +42365,8 @@ function typeChecks(o, i, tol) {
|
|
|
41120
42365
|
"nav",
|
|
41121
42366
|
"wordmark"
|
|
41122
42367
|
]) {
|
|
41123
|
-
const a = o
|
|
41124
|
-
const b = i
|
|
42368
|
+
const a = presetOf(o, k, mode);
|
|
42369
|
+
const b = presetOf(i, k, mode);
|
|
41125
42370
|
if (!a) continue;
|
|
41126
42371
|
if (k === "nav" && mobile) continue;
|
|
41127
42372
|
if ((k === "h1" || k === "h2" || k === "h3") && a.case === "uppercase" && a.size < 16) continue;
|
|
@@ -41266,12 +42511,12 @@ function matchSections(o, i) {
|
|
|
41266
42511
|
});
|
|
41267
42512
|
o.forEach((s2, k) => {
|
|
41268
42513
|
if (out[k] !== void 0) return;
|
|
41269
|
-
const A =
|
|
42514
|
+
const A = words3(`${s2.heading?.text ?? ""} ${s2.words ?? s2.text}`);
|
|
41270
42515
|
if (A.size < 4) return;
|
|
41271
42516
|
let best = -1;
|
|
41272
42517
|
let score = 0.6;
|
|
41273
42518
|
i.forEach((t, n) => {
|
|
41274
|
-
const B =
|
|
42519
|
+
const B = words3(`${t.heading?.text ?? ""} ${t.words ?? t.text}`);
|
|
41275
42520
|
let hit = 0;
|
|
41276
42521
|
for (const w of A) if (B.has(w)) hit++;
|
|
41277
42522
|
if (hit / A.size > score) {
|
|
@@ -41299,7 +42544,10 @@ function matchSections(o, i) {
|
|
|
41299
42544
|
});
|
|
41300
42545
|
return out;
|
|
41301
42546
|
}
|
|
41302
|
-
|
|
42547
|
+
var columnsOf2 = (s2, mode) => mode === "corrected" ? s2.columnsOnScreen ?? s2.columns : s2.columns;
|
|
42548
|
+
function sectionChecks(a0, b0, tol, o, i, mode = "corrected") {
|
|
42549
|
+
const a = { ...a0, columns: columnsOf2(a0, mode) };
|
|
42550
|
+
const b = { ...b0, columns: columnsOf2(b0, mode) };
|
|
41303
42551
|
const out = [];
|
|
41304
42552
|
const add = (c) => out.push({ area: "section", ...c });
|
|
41305
42553
|
if (a.background && b.background && !a.backgroundImage) {
|
|
@@ -41436,56 +42684,159 @@ function withVisual(p, visual) {
|
|
|
41436
42684
|
const base = p.exact ?? structure;
|
|
41437
42685
|
const v = Math.max(0, Math.min(1, visual));
|
|
41438
42686
|
const exact = (1 - VISUAL_WEIGHT) * base + VISUAL_WEIGHT * 100 * v;
|
|
42687
|
+
const strictExact = p.strict ? (1 - VISUAL_WEIGHT) * p.strict.checks + VISUAL_WEIGHT * 100 * v : void 0;
|
|
41439
42688
|
return {
|
|
41440
42689
|
...p,
|
|
41441
42690
|
structure,
|
|
41442
42691
|
visual: Math.round(v * 1e3) / 1e3,
|
|
41443
42692
|
exact,
|
|
41444
|
-
score: Math.round(exact)
|
|
41445
|
-
|
|
42693
|
+
score: Math.round(exact),
|
|
42694
|
+
...p.strict && strictExact !== void 0 ? { strict: { ...p.strict, exact: strictExact, score: Math.round(strictExact) } } : {}
|
|
42695
|
+
};
|
|
42696
|
+
}
|
|
42697
|
+
var pictureOnly = (s2) => !s2.heading && (s2.text ?? "").replace(/\s+/g, "").length < 3 && Boolean(s2.image || s2.backgroundImage);
|
|
42698
|
+
function pictureHost(k, match, o, i) {
|
|
42699
|
+
let pk = k - 1;
|
|
42700
|
+
while (pk >= 0 && match[pk] === void 0) pk--;
|
|
42701
|
+
let nk = k + 1;
|
|
42702
|
+
while (nk < match.length && match[nk] === void 0) nk++;
|
|
42703
|
+
const lo = pk >= 0 ? match[pk] : 0;
|
|
42704
|
+
const hi = nk < match.length ? match[nk] : i.sections.length - 1;
|
|
42705
|
+
const at = (o.sections[k]?.top ?? 0) / Math.max(1, o.height);
|
|
42706
|
+
let best;
|
|
42707
|
+
let gap = Number.POSITIVE_INFINITY;
|
|
42708
|
+
for (let n = Math.min(lo, hi); n <= Math.max(lo, hi); n++) {
|
|
42709
|
+
const t = i.sections[n];
|
|
42710
|
+
if (!t || !(t.image || t.backgroundImage)) continue;
|
|
42711
|
+
const d = Math.abs(t.top / Math.max(1, i.height) - at);
|
|
42712
|
+
if (d < gap) {
|
|
42713
|
+
gap = d;
|
|
42714
|
+
best = n;
|
|
42715
|
+
}
|
|
42716
|
+
}
|
|
42717
|
+
return best;
|
|
41446
42718
|
}
|
|
41447
|
-
function
|
|
41448
|
-
const
|
|
41449
|
-
|
|
42719
|
+
function comparePage(original, imported, tol, mode, notes) {
|
|
42720
|
+
const checks = [
|
|
42721
|
+
...typeChecks(original, imported, tol, mode),
|
|
42722
|
+
...layoutChecks(original, imported)
|
|
42723
|
+
];
|
|
42724
|
+
if (notes && original.viewport >= 700 && !original.presets.nav !== !presetOf(original, "nav", mode))
|
|
42725
|
+
notes.push(`The original's navigation sits in a header themed "brand": read as navigation.`);
|
|
42726
|
+
if (notes && !imported.presets.nav && presetOf(imported, "nav", mode) && original.viewport >= 700)
|
|
42727
|
+
notes.push(
|
|
42728
|
+
`The new site's navigation sits in a header themed "brand" (os-theme-brand): read as navigation, not as a logo.`
|
|
42729
|
+
);
|
|
41450
42730
|
const match = matchSections(original.sections, imported.sections);
|
|
42731
|
+
const phone = original.viewport < 700;
|
|
41451
42732
|
const sections = original.sections.map((s2, k) => {
|
|
41452
42733
|
const j = match[k];
|
|
41453
42734
|
const other = j === void 0 ? void 0 : imported.sections[j];
|
|
41454
|
-
|
|
41455
|
-
|
|
41456
|
-
|
|
41457
|
-
|
|
41458
|
-
|
|
41459
|
-
|
|
41460
|
-
|
|
41461
|
-
|
|
41462
|
-
|
|
42735
|
+
let c;
|
|
42736
|
+
let shared;
|
|
42737
|
+
if (other) {
|
|
42738
|
+
c = sectionChecks(s2, other, tol, original, imported, mode);
|
|
42739
|
+
if (notes && mode === "corrected") {
|
|
42740
|
+
const was = [s2.columns, other.columns];
|
|
42741
|
+
const now = [columnsOf2(s2, mode), columnsOf2(other, mode)];
|
|
42742
|
+
if (was[0] !== was[1] !== (now[0] !== now[1]))
|
|
42743
|
+
notes.push(
|
|
42744
|
+
`Section ${k + 1}: columns counted on screen only (${was[0]} \u2192 ${was[1]} counted as ${now[0]} \u2192 ${now[1]}; what's off screen or inside one slideshow or map isn't a column).`
|
|
42745
|
+
);
|
|
41463
42746
|
}
|
|
41464
|
-
|
|
42747
|
+
} else if (mode === "corrected" && phone && pictureOnly(s2)) {
|
|
42748
|
+
shared = pictureHost(k, match, original, imported);
|
|
42749
|
+
c = [];
|
|
42750
|
+
} else c = [];
|
|
42751
|
+
if (shared !== void 0) {
|
|
42752
|
+
const host = imported.sections[shared];
|
|
42753
|
+
notes?.push(
|
|
42754
|
+
`Section ${k + 1} (a picture alone on a phone) is the picture of section ${shared + 1}, which holds it with its words.`
|
|
42755
|
+
);
|
|
42756
|
+
c = [
|
|
42757
|
+
{
|
|
42758
|
+
area: "section",
|
|
42759
|
+
what: "Present",
|
|
42760
|
+
original: "(a picture)",
|
|
42761
|
+
imported: `with its words in section ${shared + 1}`,
|
|
42762
|
+
ok: true,
|
|
42763
|
+
weight: 4
|
|
42764
|
+
}
|
|
42765
|
+
];
|
|
42766
|
+
const sa = s2.image?.aspect;
|
|
42767
|
+
const sb = host.image?.aspect;
|
|
42768
|
+
if (sa && sb) {
|
|
42769
|
+
const r = sb / sa;
|
|
42770
|
+
const same = r > 0.85 && r < 1.18;
|
|
42771
|
+
c.push({
|
|
42772
|
+
area: "section",
|
|
42773
|
+
what: "Image crop",
|
|
42774
|
+
original: `${sa}:1`,
|
|
42775
|
+
imported: `${sb}:1`,
|
|
42776
|
+
ok: same,
|
|
42777
|
+
...same ? {} : { fix: "Use the original's image proportions (the block's aspect ratio)." },
|
|
42778
|
+
weight: 1
|
|
42779
|
+
});
|
|
42780
|
+
}
|
|
42781
|
+
} else if (!other)
|
|
42782
|
+
c = [
|
|
42783
|
+
{
|
|
42784
|
+
area: "section",
|
|
42785
|
+
what: "Present",
|
|
42786
|
+
original: s2.heading?.text ?? s2.text.slice(0, 40),
|
|
42787
|
+
imported: "no matching section",
|
|
42788
|
+
ok: false,
|
|
42789
|
+
fix: "Bring this section across (or check it was merged into another).",
|
|
42790
|
+
weight: 4
|
|
42791
|
+
}
|
|
42792
|
+
];
|
|
42793
|
+
const at = j ?? shared;
|
|
41465
42794
|
return {
|
|
41466
42795
|
index: k,
|
|
41467
42796
|
...s2.heading ? { heading: s2.heading.text } : {},
|
|
41468
|
-
...
|
|
42797
|
+
...at !== void 0 ? { importedIndex: at } : {},
|
|
41469
42798
|
checks: c,
|
|
41470
42799
|
score: scoreOf(c)
|
|
41471
42800
|
};
|
|
41472
42801
|
});
|
|
41473
42802
|
const all = [...checks, ...sections.flatMap((s2) => s2.checks)];
|
|
42803
|
+
return { checks, sections, exact: exactScore(all) };
|
|
42804
|
+
}
|
|
42805
|
+
function compareLooks(original, imported, path2, opts = {}) {
|
|
42806
|
+
const tol = opts.sizeTolerance ?? 0.12;
|
|
42807
|
+
const notes = [];
|
|
42808
|
+
const page = comparePage(original, imported, tol, "corrected", notes);
|
|
42809
|
+
const strict = comparePage(original, imported, tol, "strict");
|
|
41474
42810
|
return {
|
|
41475
42811
|
path: path2,
|
|
41476
42812
|
original: original.url,
|
|
41477
42813
|
imported: imported.url,
|
|
41478
42814
|
viewport: original.viewport,
|
|
41479
|
-
score:
|
|
41480
|
-
structure:
|
|
41481
|
-
exact:
|
|
41482
|
-
checks,
|
|
41483
|
-
sections
|
|
42815
|
+
score: Math.round(page.exact),
|
|
42816
|
+
structure: Math.round(page.exact),
|
|
42817
|
+
exact: page.exact,
|
|
42818
|
+
checks: page.checks,
|
|
42819
|
+
sections: page.sections,
|
|
42820
|
+
strict: {
|
|
42821
|
+
score: Math.round(strict.exact),
|
|
42822
|
+
exact: strict.exact,
|
|
42823
|
+
structure: Math.round(strict.exact),
|
|
42824
|
+
checks: strict.exact
|
|
42825
|
+
},
|
|
42826
|
+
...notes.length ? { corrections: notes } : {}
|
|
41484
42827
|
};
|
|
41485
42828
|
}
|
|
41486
42829
|
function fidelityReport(pages) {
|
|
41487
|
-
const
|
|
41488
|
-
|
|
42830
|
+
const mean = (f) => pages.length ? pages.reduce((n, p) => n + f(p), 0) / pages.length : 100;
|
|
42831
|
+
const exact = mean((p) => p.exact ?? p.score);
|
|
42832
|
+
const strict = mean((p) => p.strict?.exact ?? p.exact ?? p.score);
|
|
42833
|
+
return {
|
|
42834
|
+
score: Math.round(exact),
|
|
42835
|
+
exact: Math.round(exact * 100) / 100,
|
|
42836
|
+
strict: { score: Math.round(strict), exact: Math.round(strict * 100) / 100 },
|
|
42837
|
+
scorer: SCORER,
|
|
42838
|
+
pages
|
|
42839
|
+
};
|
|
41489
42840
|
}
|
|
41490
42841
|
var cell = (s2) => s2.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
41491
42842
|
function fidelityMarkdown(report, opts = {}) {
|
|
@@ -41493,7 +42844,11 @@ function fidelityMarkdown(report, opts = {}) {
|
|
|
41493
42844
|
"# Visual fidelity",
|
|
41494
42845
|
"",
|
|
41495
42846
|
`Overall match: **${report.score}/100** \u2014 fonts, type sizes, spacing and each section measured in a browser on both sites (75%), and how alike the pages look as pictures (25%). How it's scored: docs/qa/import-fidelity.md.`,
|
|
41496
|
-
""
|
|
42847
|
+
"",
|
|
42848
|
+
...report.strict ? [
|
|
42849
|
+
`Scored two ways, side by side: **${report.score}/100 corrected**, ${report.strict.score}/100 by the strict v3 scorer. The corrected score reads three things the strict one gets wrong (navigation in a header themed "brand", off-screen slides counted as columns, a picture-only band on a phone); the pages below list where it did.`,
|
|
42850
|
+
""
|
|
42851
|
+
] : []
|
|
41497
42852
|
];
|
|
41498
42853
|
if (opts.tuning) {
|
|
41499
42854
|
lines.push(
|
|
@@ -41508,7 +42863,12 @@ function fidelityMarkdown(report, opts = {}) {
|
|
|
41508
42863
|
);
|
|
41509
42864
|
}
|
|
41510
42865
|
for (const p of report.pages) {
|
|
41511
|
-
lines.push(
|
|
42866
|
+
lines.push(
|
|
42867
|
+
`## ${p.path} at ${p.viewport}px \u2014 ${p.score}/100${p.strict && p.strict.score !== p.score ? ` (strict v3: ${p.strict.score}/100)` : ""}`,
|
|
42868
|
+
""
|
|
42869
|
+
);
|
|
42870
|
+
if (p.corrections?.length)
|
|
42871
|
+
lines.push("Corrected scoring on this page:", "", ...p.corrections.map((c) => `- ${c}`), "");
|
|
41512
42872
|
if (p.visual !== void 0)
|
|
41513
42873
|
lines.push(
|
|
41514
42874
|
`Checks ${p.structure ?? p.score}/100 \xB7 looks alike ${Math.round(p.visual * 100)}/100 (screenshots compared).`,
|
|
@@ -41795,9 +43155,9 @@ function htmlToRichText(html, rewrite2 = (h) => h) {
|
|
|
41795
43155
|
)
|
|
41796
43156
|
});
|
|
41797
43157
|
}
|
|
41798
|
-
const
|
|
41799
|
-
for (const a of ctx.aligns)
|
|
41800
|
-
const top = [...
|
|
43158
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
43159
|
+
for (const a of ctx.aligns) counts2.set(a, (counts2.get(a) ?? 0) + 1);
|
|
43160
|
+
const top = [...counts2.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
41801
43161
|
return {
|
|
41802
43162
|
doc,
|
|
41803
43163
|
...top === "center" ? { align: "center" } : top === "right" ? { align: "end" } : {}
|
|
@@ -42266,7 +43626,7 @@ async function mapBlock(ctx, sq, opts = {}) {
|
|
|
42266
43626
|
ctx.notices.push({
|
|
42267
43627
|
kind: "form",
|
|
42268
43628
|
where: ctx.where,
|
|
42269
|
-
message: ctx.platform && ctx.platform !== "Squarespace" ? `Newsletter signup recreated. Subscribers are not moved: export your list from ${ctx.platform} (or your email service) and import it in the Hub.` : "Newsletter signup recreated. Squarespace Email Campaigns / Mailchimp subscribers are not moved: export your list from Squarespace and import it in the Hub."
|
|
43629
|
+
message: ctx.platform && /^(?:GitHub Pages|Jekyll|Hugo|Eleventy|the original site)/.test(ctx.platform) ? "Newsletter signup recreated. The old site didn't keep a subscriber list itself: if its form sent sign-ups to an email service, export the list there and import it in the Hub." : ctx.platform && ctx.platform !== "Squarespace" ? `Newsletter signup recreated. Subscribers are not moved: export your list from ${ctx.platform} (or your email service) and import it in the Hub.` : "Newsletter signup recreated. Squarespace Email Campaigns / Mailchimp subscribers are not moved: export your list from Squarespace and import it in the Hub."
|
|
42270
43630
|
});
|
|
42271
43631
|
return one(
|
|
42272
43632
|
makeBlock(
|
|
@@ -42275,6 +43635,7 @@ async function mapBlock(ctx, sq, opts = {}) {
|
|
|
42275
43635
|
{
|
|
42276
43636
|
...sq.heading ? { heading: clip2(sq.heading, 80) } : {},
|
|
42277
43637
|
...sq.description ? { description: clip2(sq.description, 240) } : {},
|
|
43638
|
+
...sq.askName ? { askName: true } : {},
|
|
42278
43639
|
...sq.buttonLabel ? { buttonLabel: clip2(sq.buttonLabel, 30) } : {},
|
|
42279
43640
|
...sq.placeholder ? { placeholder: clip2(sq.placeholder, 60) } : {},
|
|
42280
43641
|
...sq.success ? { successMessage: clip2(sq.success, 200) } : {}
|
|
@@ -44048,7 +45409,7 @@ The same rules power Studio's built-in Design check. Follow them so pages look d
|
|
|
44048
45409
|
6. **Two typefaces maximum.** Change the pairing in Site Styles rather than per block.
|
|
44049
45410
|
7. **Never invent facts, testimonials, prices or credentials.** Ask the owner.
|
|
44050
45411
|
8. **Alt text describes the image's purpose,** not just its contents; decorative images are marked decorative.
|
|
44051
|
-
9. **
|
|
45412
|
+
9. **Great on phones, every time.** Most visitors use a phone. Before any publish run phone_check (it measures every page as a phone at 360 and 390 px where a browser is available), then fix_for_phones (phone-only fixes; desktop stays as designed), until there are zero phone errors and warnings. The bar: the hero's headline and main button on the first screen, no sideways scrolling or overlapping text, reading text 16px or larger, tap targets 44px or larger, text never squeezed into narrow columns, blocks in reading order, readable text over photos, no photo taller than the screen.
|
|
44052
45413
|
10. **Prefer fewer, stronger sections** over many thin ones.
|
|
44053
45414
|
11. **Custom components for bespoke moments only.** When no preset can express a distinctive section (an editorial hero, a layered collage, a horizontal story), use a custom component (list_components \u2192 create_component or insert_component from the Designer components library). Expose every word, image and link as a field (owners edit fields, never code), style only with theme tokens (var(--os-color-\u2026), var(--os-font-\u2026), var(--os-space-\u2026)), use the 767/768/1199/1200 breakpoints, honour reduced motion, and look at it with preview_component before showing the owner.
|
|
44054
45415
|
|
|
@@ -44067,12 +45428,12 @@ var ITERATION_GUIDANCE = `# Design iteration loop
|
|
|
44067
45428
|
Repeat until the page is right, keeping each pass small:
|
|
44068
45429
|
|
|
44069
45430
|
1. **Inspect** \u2014 get_page (detail "outline") for ids and copy; get_styles for the current look.
|
|
44070
|
-
2. **Check** \u2014 design_check on the page. Fix errors first (apply_fix for autofixable issues), then warnings that matter.
|
|
45431
|
+
2. **Check** \u2014 design_check and phone_check on the page. Fix errors first (apply_fix for autofixable issues, fix_for_phones for phone issues), then warnings that matter.
|
|
44071
45432
|
3. **Edit** \u2014 one intent per pass: replace demo copy (update_block), swap a section for a better preset (insert_section + delete_section), adjust a section's color theme for rhythm (update_section colorTheme), or restyle globally (set_styles / apply_template_look).
|
|
44072
45433
|
4. **Look** \u2014 screenshot the page (or just the section you changed) at desktop and mobile; use render_page when screenshots are unavailable.
|
|
44073
45434
|
5. **Judge** \u2014 hierarchy (one H1, clear headline), one primary action per screen, rhythm of themes, copy within preset ranges, images consistent, nothing cramped on mobile. Then go back to 2.
|
|
44074
45435
|
|
|
44075
|
-
Finish with design_check showing zero errors and get_draft_diff to summarise the work for the owner.
|
|
45436
|
+
Finish with design_check showing zero errors, phone_check showing zero phone errors and warnings, and get_draft_diff to summarise the work for the owner.
|
|
44076
45437
|
`;
|
|
44077
45438
|
|
|
44078
45439
|
// ../mcp-tools/src/images.ts
|
|
@@ -44492,7 +45853,7 @@ function renderPreviewHtml(doc, pageId, opts = {}) {
|
|
|
44492
45853
|
const html = renderPageHtml({
|
|
44493
45854
|
doc,
|
|
44494
45855
|
pageId: target,
|
|
44495
|
-
mode: "preview",
|
|
45856
|
+
mode: opts.production ? "production" : "preview",
|
|
44496
45857
|
basePath: opts.basePath ?? "",
|
|
44497
45858
|
behaviorsUrl: opts.behaviorsUrl,
|
|
44498
45859
|
extraHead: blocksStyleTag()
|
|
@@ -45650,10 +47011,10 @@ var listSectionPresets = defineTool({
|
|
|
45650
47011
|
let list5 = presetCatalog.slice();
|
|
45651
47012
|
if (input.category) list5 = list5.filter((p) => p.category === input.category);
|
|
45652
47013
|
if (input.query) {
|
|
45653
|
-
const
|
|
47014
|
+
const words4 = input.query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
45654
47015
|
list5 = list5.filter((p) => {
|
|
45655
47016
|
const hay = `${p.id} ${p.label} ${p.description} ${p.tags.join(" ")} ${p.ai.whenToUse}`.toLowerCase();
|
|
45656
|
-
return
|
|
47017
|
+
return words4.every((w) => hay.includes(w));
|
|
45657
47018
|
});
|
|
45658
47019
|
}
|
|
45659
47020
|
const full = input.detail === "full";
|
|
@@ -46012,12 +47373,12 @@ var designCheckTool = defineTool({
|
|
|
46012
47373
|
const issues = runChecks(doc, pageId, input.checks ?? CHECKS, ctx.now()).filter(
|
|
46013
47374
|
(i) => RANK[i.severity] <= min
|
|
46014
47375
|
);
|
|
46015
|
-
const
|
|
46016
|
-
for (const i of issues)
|
|
47376
|
+
const counts2 = { error: 0, warning: 0, info: 0 };
|
|
47377
|
+
for (const i of issues) counts2[i.severity]++;
|
|
46017
47378
|
return {
|
|
46018
47379
|
data: {
|
|
46019
47380
|
...pageId ? { page: pageId } : {},
|
|
46020
|
-
counts,
|
|
47381
|
+
counts: counts2,
|
|
46021
47382
|
issues: issues.map((i) => ({
|
|
46022
47383
|
id: i.id,
|
|
46023
47384
|
rule: i.rule,
|
|
@@ -46078,9 +47439,9 @@ var applyFix = defineTool({
|
|
|
46078
47439
|
});
|
|
46079
47440
|
}
|
|
46080
47441
|
const remaining = runChecks(doc, pageId, CHECKS, ctx.now());
|
|
46081
|
-
const
|
|
46082
|
-
for (const i of remaining)
|
|
46083
|
-
return { data: { applied, skipped, remaining:
|
|
47442
|
+
const counts2 = { error: 0, warning: 0, info: 0 };
|
|
47443
|
+
for (const i of remaining) counts2[i.severity]++;
|
|
47444
|
+
return { data: { applied, skipped, remaining: counts2 } };
|
|
46084
47445
|
}
|
|
46085
47446
|
});
|
|
46086
47447
|
var preview = defineTool({
|
|
@@ -47206,6 +48567,143 @@ var pageTools = [
|
|
|
47206
48567
|
listPosts
|
|
47207
48568
|
];
|
|
47208
48569
|
|
|
48570
|
+
// ../mcp-tools/src/tools/phone.ts
|
|
48571
|
+
var PHONE_PAGE_LIMIT = 12;
|
|
48572
|
+
var stripMeta2 = (op2) => {
|
|
48573
|
+
const { id: _i, actor: _a3, ts: _t, ...body } = op2;
|
|
48574
|
+
return body;
|
|
48575
|
+
};
|
|
48576
|
+
async function measure(ctx, siteId, doc, pages, wantRendered) {
|
|
48577
|
+
const now = ctx.now();
|
|
48578
|
+
const statics = phoneCheck(doc, pages.length === 1 ? pages[0]?.id : void 0, {
|
|
48579
|
+
actor: "phone-check",
|
|
48580
|
+
now
|
|
48581
|
+
});
|
|
48582
|
+
const store = ctx.store;
|
|
48583
|
+
if (!wantRendered || !store.capabilities.canMeasurePhone || !store.measurePhone)
|
|
48584
|
+
return {
|
|
48585
|
+
issues: statics,
|
|
48586
|
+
rendered: false,
|
|
48587
|
+
measuredPages: [],
|
|
48588
|
+
...wantRendered ? {
|
|
48589
|
+
note: store.capabilities.publishNote?.includes("git") ? "Only the rules that don't need a browser ran. For the full phone check install Playwright next to the CLI (npm i -D playwright && npx playwright install chromium) and restart the MCP server." : "Only the rules that don't need a browser ran here; screenshot the page on mobile to look it over."
|
|
48590
|
+
} : {}
|
|
48591
|
+
};
|
|
48592
|
+
const measured = pages.slice(0, PHONE_PAGE_LIMIT);
|
|
48593
|
+
const rendered2 = [];
|
|
48594
|
+
let note;
|
|
48595
|
+
for (const page of measured) {
|
|
48596
|
+
const html = renderPreviewHtml(doc, page.id, { production: true });
|
|
48597
|
+
const results = [];
|
|
48598
|
+
try {
|
|
48599
|
+
for (const width of PHONE_WIDTHS)
|
|
48600
|
+
results.push(await store.measurePhone(siteId, { pageId: page.id, width, html }));
|
|
48601
|
+
} catch (e) {
|
|
48602
|
+
note = `The browser check stopped at \u201C${page.title}\u201D: ${e instanceof Error ? e.message : String(e)}`;
|
|
48603
|
+
break;
|
|
48604
|
+
}
|
|
48605
|
+
rendered2.push(...phoneIssuesFromProbe(doc, page.id, results, { actor: "phone-check", now }));
|
|
48606
|
+
}
|
|
48607
|
+
if (!note && pages.length > measured.length)
|
|
48608
|
+
note = `Measured the first ${measured.length} of ${pages.length} pages; run phone_check with page for the others.`;
|
|
48609
|
+
const seen = /* @__PURE__ */ new Set();
|
|
48610
|
+
const deduped = rendered2.filter((i) => {
|
|
48611
|
+
if (i.page !== "footer") return true;
|
|
48612
|
+
const key2 = `${i.rule}|${i.message}`;
|
|
48613
|
+
if (seen.has(key2)) return false;
|
|
48614
|
+
seen.add(key2);
|
|
48615
|
+
return true;
|
|
48616
|
+
});
|
|
48617
|
+
return {
|
|
48618
|
+
issues: sortIssues([...statics, ...deduped]),
|
|
48619
|
+
rendered: true,
|
|
48620
|
+
measuredPages: measured.map((p) => p.id),
|
|
48621
|
+
...note ? { note } : {}
|
|
48622
|
+
};
|
|
48623
|
+
}
|
|
48624
|
+
var pagesFor = (doc, page) => page ? [resolvePage(doc, page).page] : Object.values(doc.pages);
|
|
48625
|
+
var describe2 = (i) => ({
|
|
48626
|
+
id: i.id,
|
|
48627
|
+
rule: i.rule,
|
|
48628
|
+
severity: i.severity,
|
|
48629
|
+
...i.page ? { page: i.page } : {},
|
|
48630
|
+
...i.section ? { section: i.section } : {},
|
|
48631
|
+
...i.block ? { block: i.block } : {},
|
|
48632
|
+
message: i.message,
|
|
48633
|
+
fix: i.fix,
|
|
48634
|
+
autofix: i.autofix?.label ?? null
|
|
48635
|
+
});
|
|
48636
|
+
var counts = (issues) => {
|
|
48637
|
+
const c = { error: 0, warning: 0, info: 0 };
|
|
48638
|
+
for (const i of issues) c[i.severity]++;
|
|
48639
|
+
return c;
|
|
48640
|
+
};
|
|
48641
|
+
var phoneCheckTool = defineTool({
|
|
48642
|
+
name: "phone_check",
|
|
48643
|
+
title: "Phone check",
|
|
48644
|
+
description: "Check how the site works on phones: the rules that need no browser (sections not laid out for phones, columns squeezed side by side, padding that empties the screen, heading words too long for a phone line, tall photo sections, fixed widths in custom CSS) plus, where a browser is available, a real phone layout at 360 and 390 px (sideways scrolling, cut-off or overlapping text, text too small, buttons too small to tap, text over photos, the main button below the first screen). Run it before every publish; fix with fix_for_phones. Aim for zero errors and warnings.",
|
|
48645
|
+
input: external_exports.object({
|
|
48646
|
+
site: siteArg,
|
|
48647
|
+
page: external_exports.string().optional().describe("Page id/slug/title (default: every page)."),
|
|
48648
|
+
rendered: external_exports.boolean().optional().describe("Measure in a real browser when available (default true).")
|
|
48649
|
+
}),
|
|
48650
|
+
annotations: { readOnlyHint: true },
|
|
48651
|
+
async handler(ctx, input) {
|
|
48652
|
+
const siteId = await ctx.siteId(input.site);
|
|
48653
|
+
const doc = await ctx.doc(siteId);
|
|
48654
|
+
const r = await measure(ctx, siteId, doc, pagesFor(doc, input.page), input.rendered !== false);
|
|
48655
|
+
const c = counts(r.issues);
|
|
48656
|
+
return {
|
|
48657
|
+
data: {
|
|
48658
|
+
rendered: r.rendered,
|
|
48659
|
+
...r.note ? { note: r.note } : {},
|
|
48660
|
+
counts: c,
|
|
48661
|
+
issues: r.issues.map(describe2),
|
|
48662
|
+
next: c.error + c.warning === 0 ? "Looks great on phones." : r.issues.some((i) => i.autofix) ? "Run fix_for_phones to fix everything that can be fixed automatically, then phone_check again; handle the rest by editing." : "Edit the content as each fix says, then run phone_check again."
|
|
48663
|
+
}
|
|
48664
|
+
};
|
|
48665
|
+
}
|
|
48666
|
+
});
|
|
48667
|
+
var fixForPhonesTool = defineTool({
|
|
48668
|
+
name: "fix_for_phones",
|
|
48669
|
+
title: "Fix everything for phones",
|
|
48670
|
+
description: "Apply every automatic phone fix in one change (phone-only settings: phone layouts in reading order, words first in heroes, stacked columns, lighter phone padding, shorter photo sections, smaller phone heading sizes, stronger photo overlays). Desktop stays as designed. Re-measures and returns what's left.",
|
|
48671
|
+
input: external_exports.object({
|
|
48672
|
+
site: siteArg,
|
|
48673
|
+
page: external_exports.string().optional().describe("Limit to a page (default: every page).")
|
|
48674
|
+
}),
|
|
48675
|
+
annotations: {},
|
|
48676
|
+
async handler(ctx, input) {
|
|
48677
|
+
const siteId = await ctx.siteId(input.site);
|
|
48678
|
+
let doc = await ctx.doc(siteId);
|
|
48679
|
+
const pages = pagesFor(doc, input.page);
|
|
48680
|
+
const before = await measure(ctx, siteId, doc, pages, true);
|
|
48681
|
+
const r = fixForPhones(doc, {
|
|
48682
|
+
...input.page ? { pageId: pages[0]?.id } : {},
|
|
48683
|
+
actor: "phone-check",
|
|
48684
|
+
now: ctx.now(),
|
|
48685
|
+
rendered: before.issues
|
|
48686
|
+
});
|
|
48687
|
+
if (r.ops.length) {
|
|
48688
|
+
const applied = await ctx.apply(siteId, r.ops.map(stripMeta2));
|
|
48689
|
+
doc = applied.doc;
|
|
48690
|
+
}
|
|
48691
|
+
const after = await measure(ctx, siteId, doc, pagesFor(doc, input.page), true);
|
|
48692
|
+
const labels = /* @__PURE__ */ new Map();
|
|
48693
|
+
for (const f of r.fixed) labels.set(f.label, (labels.get(f.label) ?? 0) + 1);
|
|
48694
|
+
return {
|
|
48695
|
+
data: {
|
|
48696
|
+
fixed: [...labels].map(([label, n]) => n > 1 ? `${label} (${n}\xD7)` : label),
|
|
48697
|
+
rendered: after.rendered,
|
|
48698
|
+
remaining: counts(after.issues),
|
|
48699
|
+
issues: after.issues.filter((i) => i.severity !== "info").map(describe2),
|
|
48700
|
+
next: after.issues.filter((i) => i.severity !== "info").length === 0 ? "Looks great on phones." : "These need an edit (no automatic fix): follow each fix, then run phone_check again."
|
|
48701
|
+
}
|
|
48702
|
+
};
|
|
48703
|
+
}
|
|
48704
|
+
});
|
|
48705
|
+
var phoneTools = [phoneCheckTool, fixForPhonesTool];
|
|
48706
|
+
|
|
47209
48707
|
// ../mcp-tools/src/tools/publish.ts
|
|
47210
48708
|
var getDraftDiff = defineTool({
|
|
47211
48709
|
name: "get_draft_diff",
|
|
@@ -47351,6 +48849,7 @@ var ALL_TOOLS = [
|
|
|
47351
48849
|
...pageTools,
|
|
47352
48850
|
...designSystemTools,
|
|
47353
48851
|
...designTools,
|
|
48852
|
+
...phoneTools,
|
|
47354
48853
|
...componentTools,
|
|
47355
48854
|
...publishTools
|
|
47356
48855
|
];
|
|
@@ -48142,6 +49641,25 @@ function squarespaceRenditions(u, width, height, canResize = true) {
|
|
|
48142
49641
|
];
|
|
48143
49642
|
}
|
|
48144
49643
|
|
|
49644
|
+
// ../import/src/phone.ts
|
|
49645
|
+
function phoneReady(result) {
|
|
49646
|
+
let fixed;
|
|
49647
|
+
try {
|
|
49648
|
+
fixed = fixForPhones(result.doc, { actor: "import", now: 0 });
|
|
49649
|
+
} catch {
|
|
49650
|
+
return result;
|
|
49651
|
+
}
|
|
49652
|
+
if (!fixed.ops.length) return result;
|
|
49653
|
+
const labels = [...new Set(fixed.fixed.map((f) => f.label.toLowerCase()))];
|
|
49654
|
+
const line = `
|
|
49655
|
+
## Phones
|
|
49656
|
+
|
|
49657
|
+
Made ${fixed.fixed.length} phone-only adjustment${fixed.fixed.length === 1 ? "" : "s"} so the site reads well on phones (${labels.join(", ")}). The desktop design is unchanged.
|
|
49658
|
+
`;
|
|
49659
|
+
return { ...result, doc: fixed.doc, report: `${result.report.trimEnd()}
|
|
49660
|
+
${line}` };
|
|
49661
|
+
}
|
|
49662
|
+
|
|
48145
49663
|
// ../import/src/report.ts
|
|
48146
49664
|
var NOTICE_TITLES = {
|
|
48147
49665
|
"unsupported-block": "Blocks that need a look",
|
|
@@ -48277,17 +49795,17 @@ function renderReport(r) {
|
|
|
48277
49795
|
any = true;
|
|
48278
49796
|
L2.push(`### ${NOTICE_TITLES[kind]}`);
|
|
48279
49797
|
L2.push("");
|
|
48280
|
-
const
|
|
49798
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
48281
49799
|
for (const n of list5) {
|
|
48282
|
-
const e =
|
|
49800
|
+
const e = counts2.get(n.message) ?? { n: 0, where: /* @__PURE__ */ new Set() };
|
|
48283
49801
|
e.n++;
|
|
48284
49802
|
e.where.add(n.where);
|
|
48285
|
-
|
|
49803
|
+
counts2.set(n.message, e);
|
|
48286
49804
|
}
|
|
48287
49805
|
let shown = 0;
|
|
48288
|
-
for (const [message, e] of
|
|
49806
|
+
for (const [message, e] of counts2) {
|
|
48289
49807
|
if (++shown > 15) {
|
|
48290
|
-
L2.push(`- \u2026 and ${
|
|
49808
|
+
L2.push(`- \u2026 and ${counts2.size - 15} more like these.`);
|
|
48291
49809
|
break;
|
|
48292
49810
|
}
|
|
48293
49811
|
const where2 = [...e.where];
|
|
@@ -48680,7 +50198,7 @@ async function crawlWordPress(opts) {
|
|
|
48680
50198
|
...it.excerptHtml ? { caption: it.excerptHtml } : {}
|
|
48681
50199
|
});
|
|
48682
50200
|
const entries = [];
|
|
48683
|
-
const
|
|
50201
|
+
const pathOf2 = (link2, fallback2) => (link2 ? sitePath(link2, base) : void 0) ?? fallback2;
|
|
48684
50202
|
if (rest) {
|
|
48685
50203
|
const [pages2, posts] = [
|
|
48686
50204
|
await listAll("wp/v2/pages", "_embed=1&orderby=menu_order&order=asc"),
|
|
@@ -48707,7 +50225,7 @@ async function crawlWordPress(opts) {
|
|
|
48707
50225
|
}
|
|
48708
50226
|
entries.push({
|
|
48709
50227
|
type,
|
|
48710
|
-
path:
|
|
50228
|
+
path: pathOf2(link2, `/${slug}`),
|
|
48711
50229
|
slug,
|
|
48712
50230
|
title: decodeEntities2(rendered(p.title)) || slug || "Untitled",
|
|
48713
50231
|
html: rendered(p.content),
|
|
@@ -48774,7 +50292,7 @@ async function crawlWordPress(opts) {
|
|
|
48774
50292
|
});
|
|
48775
50293
|
}
|
|
48776
50294
|
if (!rest && !wxr && home) {
|
|
48777
|
-
const { crawlGeneric: crawlGeneric2 } = await import("./crawl-
|
|
50295
|
+
const { crawlGeneric: crawlGeneric2 } = await import("./crawl-WQQDFI7W.js");
|
|
48778
50296
|
const site2 = await crawlGeneric2({
|
|
48779
50297
|
url: base,
|
|
48780
50298
|
fetcher: f,
|
|
@@ -49058,6 +50576,7 @@ function componentFromBlueprint(input) {
|
|
|
49058
50576
|
".gx__img picture, .gx__img img { display: block; width: 100%; height: 100%; object-fit: cover; }"
|
|
49059
50577
|
];
|
|
49060
50578
|
const phone = [];
|
|
50579
|
+
const named = [];
|
|
49061
50580
|
let headingSeen = false;
|
|
49062
50581
|
const role = (family) => {
|
|
49063
50582
|
if (norm5(family) && norm5(family) === norm5(input.roles.label)) return "label";
|
|
@@ -49123,6 +50642,7 @@ function componentFromBlueprint(input) {
|
|
|
49123
50642
|
const key3 = `image${n}`;
|
|
49124
50643
|
fields[key3] = { kind: "image", label: `Picture ${n}` };
|
|
49125
50644
|
values[key3] = { src: it.src, alt: (it.alt ?? "").slice(0, 300) };
|
|
50645
|
+
named.push({ role: "picture", text: it.alt ?? "", keys: [key3] });
|
|
49126
50646
|
parts.push(
|
|
49127
50647
|
`<div class="${cls} gx__img">{{image ${key3} sizes="(max-width: 767px) 100vw, ${Math.max(10, Math.round(it.box.w / 1440 * 100))}vw"}}</div>`
|
|
49128
50648
|
);
|
|
@@ -49141,6 +50661,7 @@ function componentFromBlueprint(input) {
|
|
|
49141
50661
|
};
|
|
49142
50662
|
values[label] = (it.text ?? "").slice(0, 60);
|
|
49143
50663
|
if (!target) {
|
|
50664
|
+
named.push({ role: it.kind, text: it.text ?? "", keys: [label] });
|
|
49144
50665
|
parts.push(`<p class="${cls}">{{${label}}}</p>`);
|
|
49145
50666
|
} else {
|
|
49146
50667
|
const lk = `link${n}`;
|
|
@@ -49149,6 +50670,7 @@ function componentFromBlueprint(input) {
|
|
|
49149
50670
|
label: `${it.kind === "button" ? "Button" : "Link"} ${n} goes to`
|
|
49150
50671
|
};
|
|
49151
50672
|
values[lk] = target;
|
|
50673
|
+
named.push({ role: it.kind, text: it.text ?? "", keys: [label, lk] });
|
|
49152
50674
|
parts.push(
|
|
49153
50675
|
it.kind === "button" ? `<div class="${cls}">{{button ${lk} label=${label} variant="primary"}}</div>` : `<p class="${cls}">{{#link ${lk}}}{{${label}}}{{/link}}</p>`
|
|
49154
50676
|
);
|
|
@@ -49170,6 +50692,7 @@ function componentFromBlueprint(input) {
|
|
|
49170
50692
|
maxLength: 3e3
|
|
49171
50693
|
};
|
|
49172
50694
|
values[key2] = doc;
|
|
50695
|
+
named.push({ role: textRole(it), text: it.text ?? "", keys: [key2] });
|
|
49173
50696
|
parts.push(`<div class="${cls}">{{rich ${key2}}}</div>`);
|
|
49174
50697
|
css.push(`.gx__i${n} { ${place}; ${typeCss(it.style, false)}; }`);
|
|
49175
50698
|
phone.push(`.gx__i${n} { ${typeCss(it.style, true)}; }`);
|
|
@@ -49183,6 +50706,7 @@ function componentFromBlueprint(input) {
|
|
|
49183
50706
|
...it.kind === "text" ? { multiline: true } : {}
|
|
49184
50707
|
};
|
|
49185
50708
|
values[key2] = text7;
|
|
50709
|
+
named.push({ role: it.kind === "heading" ? "heading" : textRole(it), text: text7, keys: [key2] });
|
|
49186
50710
|
const tag = it.kind === "heading" ? headingSeen ? "h3" : "h2" : "p";
|
|
49187
50711
|
if (it.kind === "heading") headingSeen = true;
|
|
49188
50712
|
parts.push(`<${tag} class="${cls}">{{${key2}}}</${tag}>`);
|
|
@@ -49190,6 +50714,14 @@ function componentFromBlueprint(input) {
|
|
|
49190
50714
|
phone.push(`.gx__i${n} { ${typeCss(it.style, true)}; }`);
|
|
49191
50715
|
});
|
|
49192
50716
|
if (!parts.length) return void 0;
|
|
50717
|
+
for (const [i, label] of partLabels(named).entries()) {
|
|
50718
|
+
const part = named[i];
|
|
50719
|
+
const [first2, second] = part.keys;
|
|
50720
|
+
const f = fields[first2];
|
|
50721
|
+
if (f) fields[first2] = { ...f, label };
|
|
50722
|
+
const l = second ? fields[second] : void 0;
|
|
50723
|
+
if (second && l) fields[second] = { ...l, label: `${label} goes to` };
|
|
50724
|
+
}
|
|
49193
50725
|
css.push(
|
|
49194
50726
|
`@media (max-width: 767px) {
|
|
49195
50727
|
.gx { display: flex; flex-direction: column; gap: var(--os-space-sm); }
|
|
@@ -49212,6 +50744,53 @@ ${parts.map((p) => ` ${p}`).join("\n")}
|
|
|
49212
50744
|
};
|
|
49213
50745
|
return { def, fields: values };
|
|
49214
50746
|
}
|
|
50747
|
+
function textRole(it) {
|
|
50748
|
+
const words4 = (it.text ?? "").trim().split(/\s+/).filter(Boolean).length;
|
|
50749
|
+
const caps = it.style?.case === "uppercase" || /^[^a-z]*[A-Z][^a-z]*$/.test(it.text ?? "");
|
|
50750
|
+
return words4 <= 5 && (caps || (it.style?.size ?? 16) <= 14) ? "small" : "paragraph";
|
|
50751
|
+
}
|
|
50752
|
+
var ROLE_LABEL = {
|
|
50753
|
+
heading: "Heading",
|
|
50754
|
+
small: "Small label",
|
|
50755
|
+
paragraph: "Paragraph",
|
|
50756
|
+
picture: "Picture",
|
|
50757
|
+
button: "Button",
|
|
50758
|
+
link: "Link"
|
|
50759
|
+
};
|
|
50760
|
+
function snippet(text7) {
|
|
50761
|
+
const words4 = text7.replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
|
|
50762
|
+
let out = "";
|
|
50763
|
+
for (const w of words4) {
|
|
50764
|
+
if (`${out} ${w}`.trim().length > 24) break;
|
|
50765
|
+
out = `${out} ${w}`.trim();
|
|
50766
|
+
}
|
|
50767
|
+
if (!out && words4[0]) out = words4[0].slice(0, 22);
|
|
50768
|
+
if (!out) return "";
|
|
50769
|
+
const cased = /[a-z]/.test(out) ? out : out.charAt(0) + out.slice(1).toLowerCase();
|
|
50770
|
+
return `${cased}${out.length < text7.trim().length ? "\u2026" : ""}`;
|
|
50771
|
+
}
|
|
50772
|
+
function partLabels(parts) {
|
|
50773
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
50774
|
+
for (const p of parts) counts2.set(p.role, (counts2.get(p.role) ?? 0) + 1);
|
|
50775
|
+
const used = /* @__PURE__ */ new Set();
|
|
50776
|
+
const seen = /* @__PURE__ */ new Map();
|
|
50777
|
+
return parts.map((p) => {
|
|
50778
|
+
const nth = (seen.get(p.role) ?? 0) + 1;
|
|
50779
|
+
seen.set(p.role, nth);
|
|
50780
|
+
const base = ROLE_LABEL[p.role];
|
|
50781
|
+
let label;
|
|
50782
|
+
if ((counts2.get(p.role) ?? 0) === 1) label = base;
|
|
50783
|
+
else if (p.role === "button" && nth === 1) label = "Main button";
|
|
50784
|
+
else {
|
|
50785
|
+
const words4 = p.role === "picture" ? "" : snippet(p.text);
|
|
50786
|
+
label = words4 ? `${base} \u201C${words4}\u201D` : `${base} ${nth}`;
|
|
50787
|
+
}
|
|
50788
|
+
let unique = label;
|
|
50789
|
+
for (let k = 2; used.has(unique); k++) unique = `${label} ${k}`;
|
|
50790
|
+
used.add(unique);
|
|
50791
|
+
return unique.slice(0, 60);
|
|
50792
|
+
});
|
|
50793
|
+
}
|
|
49215
50794
|
function richDoc(runs, origin) {
|
|
49216
50795
|
if (!runs?.length) return void 0;
|
|
49217
50796
|
const content3 = [];
|
|
@@ -49742,8 +51321,8 @@ function tuneRound(input, measured, groups = TUNE_GROUPS, opts = {}) {
|
|
|
49742
51321
|
if (b.type !== "text" || done) continue;
|
|
49743
51322
|
const doc2 = b.fields.content;
|
|
49744
51323
|
for (const node2 of doc2?.content ?? []) {
|
|
49745
|
-
const
|
|
49746
|
-
if (node2.type !== "heading" || norm6(
|
|
51324
|
+
const words4 = (node2.content ?? []).map((x) => x.text ?? "").join("");
|
|
51325
|
+
if (node2.type !== "heading" || norm6(words4) !== norm6(i.text)) continue;
|
|
49747
51326
|
if (pick === "lead" || node2.attrs?.preset === pick) break;
|
|
49748
51327
|
node2.attrs = { ...node2.attrs ?? {}, preset: pick };
|
|
49749
51328
|
done = true;
|
|
@@ -49896,7 +51475,12 @@ function tuneRound(input, measured, groups = TUNE_GROUPS, opts = {}) {
|
|
|
49896
51475
|
const theme = themes?.[old.style.colorTheme ?? "light"];
|
|
49897
51476
|
const gen = componentFromBlueprint({
|
|
49898
51477
|
name,
|
|
49899
|
-
label:
|
|
51478
|
+
label: sectionLabel(
|
|
51479
|
+
s2.heading ?? o.heading?.text,
|
|
51480
|
+
o.blueprint,
|
|
51481
|
+
page.title || (page.slug ? page.slug : "Home"),
|
|
51482
|
+
s2.index + 1
|
|
51483
|
+
),
|
|
49900
51484
|
section: o,
|
|
49901
51485
|
roles,
|
|
49902
51486
|
...theme ? {
|
|
@@ -49957,6 +51541,14 @@ function setRole(doc, theme, role, value) {
|
|
|
49957
51541
|
doc.styles.overrides = overrides;
|
|
49958
51542
|
return true;
|
|
49959
51543
|
}
|
|
51544
|
+
function sectionLabel(heading2, blueprint, pageTitle, n) {
|
|
51545
|
+
const first2 = heading2?.trim() || blueprint?.find((i) => i.kind === "heading" && i.text?.trim())?.text || blueprint?.find((i) => i.kind === "text" && i.text?.trim())?.text;
|
|
51546
|
+
const words4 = (first2 ?? "").replace(/\s+/g, " ").trim();
|
|
51547
|
+
if (!words4) return `${pageTitle} section ${n}`.slice(0, 60);
|
|
51548
|
+
const short = words4.length > 40 ? `${words4.slice(0, 40).replace(/\s+\S*$/, "")}\u2026` : words4;
|
|
51549
|
+
const cased = /[a-z]/.test(short) ? short : short.charAt(0) + short.slice(1).toLowerCase();
|
|
51550
|
+
return cased.slice(0, 60);
|
|
51551
|
+
}
|
|
49960
51552
|
|
|
49961
51553
|
// ../import/src/index.ts
|
|
49962
51554
|
var REPORT_FILE = "IMPORT-REPORT.md";
|
|
@@ -50166,6 +51758,9 @@ async function detectSite(url, opts = {}) {
|
|
|
50166
51758
|
return { ...detectPlatform(r.text, { "content-type": r.contentType }), url: r.url || base };
|
|
50167
51759
|
}
|
|
50168
51760
|
async function importSite(opts) {
|
|
51761
|
+
return phoneReady(await importAnySite(opts));
|
|
51762
|
+
}
|
|
51763
|
+
async function importAnySite(opts) {
|
|
50169
51764
|
let detection;
|
|
50170
51765
|
let platform;
|
|
50171
51766
|
if (opts.repoDir) return importGitHub(opts);
|
|
@@ -50228,7 +51823,7 @@ async function renderAgentsMd(root, siteTitle) {
|
|
|
50228
51823
|
} catch {
|
|
50229
51824
|
}
|
|
50230
51825
|
}
|
|
50231
|
-
const { generateAgentsMd: generateAgentsMd2 } = await import("./agents-md-
|
|
51826
|
+
const { generateAgentsMd: generateAgentsMd2 } = await import("./agents-md-OWDSHBGF.js");
|
|
50232
51827
|
return generateAgentsMd2(title ? { siteTitle: title } : {});
|
|
50233
51828
|
}
|
|
50234
51829
|
async function writeAgentsMd(root, siteTitle) {
|
|
@@ -50507,7 +52102,7 @@ function compareFidelityTool(opts) {
|
|
|
50507
52102
|
[],
|
|
50508
52103
|
"Install Playwright next to the CLI (npm i -D playwright && npx playwright install chromium) and restart the MCP server."
|
|
50509
52104
|
);
|
|
50510
|
-
const widths = input.device === "desktop" ? [DESKTOP] : input.device === "mobile" ? [
|
|
52105
|
+
const widths = input.device === "desktop" ? [DESKTOP] : input.device === "mobile" ? [PHONE2] : [DESKTOP, PHONE2];
|
|
50511
52106
|
const run2 = opts.compare ?? runCompare;
|
|
50512
52107
|
const r = await run2(u.toString(), opts.siteRoot, {
|
|
50513
52108
|
...input.pages?.length ? { pages: input.pages } : {},
|
|
@@ -50710,8 +52305,8 @@ var DEFAULTS = {
|
|
|
50710
52305
|
products: []
|
|
50711
52306
|
};
|
|
50712
52307
|
var FILE = {
|
|
50713
|
-
rss: { file: "rss.xml", label: "RSS", mime: "application/rss+xml" },
|
|
50714
|
-
atom: { file: "atom.xml", label: "Atom", mime: "application/atom+xml" },
|
|
52308
|
+
rss: { file: "rss.xml", label: "RSS feed", mime: "application/rss+xml" },
|
|
52309
|
+
atom: { file: "atom.xml", label: "Atom feed", mime: "application/atom+xml" },
|
|
50715
52310
|
ical: { file: "calendar.ics", label: "Add to calendar (iCal)", mime: "text/calendar" },
|
|
50716
52311
|
podcast: { file: "podcast.xml", label: "Podcast feed", mime: "application/rss+xml" }
|
|
50717
52312
|
};
|
|
@@ -51526,7 +53121,11 @@ async function startPreviewServer(root, load, opts = {}) {
|
|
|
51526
53121
|
page = Object.values(view.pages).find((p) => p.slug === slug);
|
|
51527
53122
|
}
|
|
51528
53123
|
if (page) {
|
|
51529
|
-
const html = renderPreviewHtml(view, page.id, {
|
|
53124
|
+
const html = renderPreviewHtml(view, page.id, {
|
|
53125
|
+
behaviorsUrl: "/_os/behaviors",
|
|
53126
|
+
// `?os_mode=production`: exactly as published (the Phone check measures this).
|
|
53127
|
+
production: url.searchParams.get("os_mode") === "production"
|
|
53128
|
+
});
|
|
51530
53129
|
res.writeHead(200, {
|
|
51531
53130
|
"content-type": "text/html; charset=utf-8",
|
|
51532
53131
|
"cache-control": "no-store"
|
|
@@ -51590,6 +53189,7 @@ var FileSiteStore = class _FileSiteStore {
|
|
|
51590
53189
|
canPublish: store.hasGit,
|
|
51591
53190
|
canRollback: store.hasGit,
|
|
51592
53191
|
canScreenshot: Boolean(options.screenshotter),
|
|
53192
|
+
canMeasurePhone: Boolean(options.screenshotter),
|
|
51593
53193
|
publishNote: store.hasGit ? "Local site in git: publishing commits content/ and media/ on the current branch; push it to deploy." : "Local site without git: edits are saved to the files; there is no publish step."
|
|
51594
53194
|
};
|
|
51595
53195
|
return store;
|
|
@@ -51898,6 +53498,16 @@ Unpublished work saved as ${tag}`,
|
|
|
51898
53498
|
const url = await this.previewUrl(siteId, path2);
|
|
51899
53499
|
return this.options.screenshotter.capture(url, request);
|
|
51900
53500
|
}
|
|
53501
|
+
async measurePhone(siteId, request) {
|
|
53502
|
+
this.check(siteId);
|
|
53503
|
+
if (!this.options.screenshotter)
|
|
53504
|
+
throw new ToolError("The browser phone check is not available.", "unsupported");
|
|
53505
|
+
const doc = await loadSiteDocument(this.root);
|
|
53506
|
+
const page = doc.pages[request.pageId];
|
|
53507
|
+
const path2 = page?.slug ? `/${page.slug}/` : "/";
|
|
53508
|
+
const url = await this.previewUrl(siteId, `${path2}?os_mode=production`);
|
|
53509
|
+
return this.options.screenshotter.measure(url, request.width);
|
|
53510
|
+
}
|
|
51901
53511
|
async agentsMd() {
|
|
51902
53512
|
try {
|
|
51903
53513
|
return await readFile9(join11(this.root, "AGENTS.md"), "utf8");
|
|
@@ -52013,6 +53623,32 @@ function createScreenshotter(chromium) {
|
|
|
52013
53623
|
await page.close().catch(() => void 0);
|
|
52014
53624
|
}
|
|
52015
53625
|
},
|
|
53626
|
+
async measure(url, width) {
|
|
53627
|
+
browser ??= launch2();
|
|
53628
|
+
let b;
|
|
53629
|
+
try {
|
|
53630
|
+
b = await browser;
|
|
53631
|
+
} catch (e) {
|
|
53632
|
+
browser = void 0;
|
|
53633
|
+
throw e;
|
|
53634
|
+
}
|
|
53635
|
+
const context = await b.newContext({
|
|
53636
|
+
viewport: { width, height: PHONE_HEIGHT },
|
|
53637
|
+
deviceScaleFactor: 2,
|
|
53638
|
+
isMobile: true,
|
|
53639
|
+
hasTouch: true,
|
|
53640
|
+
reducedMotion: "reduce"
|
|
53641
|
+
});
|
|
53642
|
+
const page = await context.newPage();
|
|
53643
|
+
try {
|
|
53644
|
+
await page.goto(url, { waitUntil: "load", timeout: 3e4 });
|
|
53645
|
+
await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => void 0);
|
|
53646
|
+
await page.evaluate(PHONE_PREPARE_SCRIPT).catch(() => void 0);
|
|
53647
|
+
return await page.evaluate(PHONE_PROBE_SCRIPT);
|
|
53648
|
+
} finally {
|
|
53649
|
+
await context.close().catch(() => void 0);
|
|
53650
|
+
}
|
|
53651
|
+
},
|
|
52016
53652
|
async close() {
|
|
52017
53653
|
if (!browser) return;
|
|
52018
53654
|
const b = await browser.catch(() => void 0);
|
|
@@ -52232,6 +53868,147 @@ function rebrand(value, from, to) {
|
|
|
52232
53868
|
return changed ? out : null;
|
|
52233
53869
|
}
|
|
52234
53870
|
|
|
53871
|
+
// src/phone.ts
|
|
53872
|
+
var pathOf = (p) => p.slug ? `/${p.slug}/` : "/";
|
|
53873
|
+
async function measureSite(root, load, doc, shooter, pages, widths, now) {
|
|
53874
|
+
const server = await startPreviewServer(root, load);
|
|
53875
|
+
const out = /* @__PURE__ */ new Map();
|
|
53876
|
+
try {
|
|
53877
|
+
for (const page of pages) {
|
|
53878
|
+
const results = [];
|
|
53879
|
+
for (const w of widths)
|
|
53880
|
+
results.push(await shooter.measure(`${server.url}${pathOf(page)}?os_mode=production`, w));
|
|
53881
|
+
out.set(page.id, phoneIssuesFromProbe(doc, page.id, results, { now, actor: "phone-check" }));
|
|
53882
|
+
}
|
|
53883
|
+
} finally {
|
|
53884
|
+
await server.close();
|
|
53885
|
+
}
|
|
53886
|
+
return out;
|
|
53887
|
+
}
|
|
53888
|
+
async function runPhoneCheck(root, opts = {}) {
|
|
53889
|
+
const now = opts.now ?? Date.now();
|
|
53890
|
+
const widths = opts.widths ?? PHONE_WIDTHS;
|
|
53891
|
+
let doc = await loadSiteDocument(root);
|
|
53892
|
+
const pick = (d) => Object.values(d.pages).filter(
|
|
53893
|
+
(p) => !opts.pages?.length || opts.pages.includes(p.id) || opts.pages.includes(p.slug)
|
|
53894
|
+
);
|
|
53895
|
+
let shooter = opts.screenshotter;
|
|
53896
|
+
let note;
|
|
53897
|
+
let owned = false;
|
|
53898
|
+
if (!shooter && opts.rendered !== false) {
|
|
53899
|
+
const pw = await loadPlaywright();
|
|
53900
|
+
if ("error" in pw)
|
|
53901
|
+
note = "The browser check needs Playwright: run `npm i -D playwright && npx playwright install chromium` in the site folder. Only the rules that don't need a browser ran.";
|
|
53902
|
+
else {
|
|
53903
|
+
shooter = createScreenshotter(pw.chromium);
|
|
53904
|
+
owned = true;
|
|
53905
|
+
}
|
|
53906
|
+
}
|
|
53907
|
+
let current2 = doc;
|
|
53908
|
+
const load = async () => current2;
|
|
53909
|
+
const measure2 = async () => shooter && opts.rendered !== false ? measureSite(root, load, current2, shooter, pick(current2), widths, now).catch((e) => {
|
|
53910
|
+
note = `The browser check couldn't run: ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`;
|
|
53911
|
+
return /* @__PURE__ */ new Map();
|
|
53912
|
+
}) : /* @__PURE__ */ new Map();
|
|
53913
|
+
try {
|
|
53914
|
+
let rendered2 = await measure2();
|
|
53915
|
+
const fixed = [];
|
|
53916
|
+
const written = [];
|
|
53917
|
+
if (opts.fix) {
|
|
53918
|
+
const r = fixForPhones(doc, {
|
|
53919
|
+
now,
|
|
53920
|
+
actor: "phone-check",
|
|
53921
|
+
rendered: [...rendered2.values()].flat()
|
|
53922
|
+
});
|
|
53923
|
+
if (r.ops.length) {
|
|
53924
|
+
doc = r.doc;
|
|
53925
|
+
current2 = doc;
|
|
53926
|
+
const w = await writeSiteDocument(root, doc);
|
|
53927
|
+
written.push(...w.written);
|
|
53928
|
+
fixed.push(...r.fixed);
|
|
53929
|
+
rendered2 = await measure2();
|
|
53930
|
+
}
|
|
53931
|
+
}
|
|
53932
|
+
const statics = phoneCheck(doc, void 0, { now, actor: "phone-check" });
|
|
53933
|
+
const pages = pick(doc).map((p) => ({
|
|
53934
|
+
page: p.id,
|
|
53935
|
+
title: p.title,
|
|
53936
|
+
path: pathOf(p),
|
|
53937
|
+
issues: sortIssues([
|
|
53938
|
+
...statics.filter((i) => i.page === p.id),
|
|
53939
|
+
...(rendered2.get(p.id) ?? []).filter((i) => i.page === p.id)
|
|
53940
|
+
])
|
|
53941
|
+
}));
|
|
53942
|
+
const elsewhere = [
|
|
53943
|
+
...statics.filter((i) => !i.page || !doc.pages[i.page]),
|
|
53944
|
+
// Footer findings repeat on every page: report each once.
|
|
53945
|
+
...dedupe([...rendered2.values()].flat().filter((i) => i.page === "footer"))
|
|
53946
|
+
];
|
|
53947
|
+
const issues = sortIssues([...elsewhere, ...pages.flatMap((p) => p.issues)]);
|
|
53948
|
+
const counts2 = { error: 0, warning: 0, info: 0 };
|
|
53949
|
+
for (const i of issues) counts2[i.severity]++;
|
|
53950
|
+
return {
|
|
53951
|
+
rendered: !!shooter && opts.rendered !== false && !note,
|
|
53952
|
+
...note ? { note } : {},
|
|
53953
|
+
issues,
|
|
53954
|
+
pages,
|
|
53955
|
+
counts: counts2,
|
|
53956
|
+
fixed,
|
|
53957
|
+
written
|
|
53958
|
+
};
|
|
53959
|
+
} finally {
|
|
53960
|
+
if (owned) await shooter?.close();
|
|
53961
|
+
}
|
|
53962
|
+
}
|
|
53963
|
+
function dedupe(issues) {
|
|
53964
|
+
const seen = /* @__PURE__ */ new Set();
|
|
53965
|
+
return issues.filter((i) => {
|
|
53966
|
+
const key2 = `${i.rule}|${i.message}`;
|
|
53967
|
+
if (seen.has(key2)) return false;
|
|
53968
|
+
seen.add(key2);
|
|
53969
|
+
return true;
|
|
53970
|
+
});
|
|
53971
|
+
}
|
|
53972
|
+
function formatPhoneReport(r, paint2) {
|
|
53973
|
+
const lines = [];
|
|
53974
|
+
lines.push(paint2(1, r.rendered ? "Phone check (measured at 360 and 390 px)" : "Phone check"));
|
|
53975
|
+
if (r.note) lines.push(paint2(33, r.note));
|
|
53976
|
+
if (r.fixed.length) {
|
|
53977
|
+
lines.push(
|
|
53978
|
+
paint2(32, `Fixed for phones: ${r.fixed.length} change${r.fixed.length === 1 ? "" : "s"}.`)
|
|
53979
|
+
);
|
|
53980
|
+
for (const f of uniqueLabels(r.fixed)) lines.push(` \u2713 ${f}`);
|
|
53981
|
+
}
|
|
53982
|
+
const groups = /* @__PURE__ */ new Map();
|
|
53983
|
+
for (const i of r.issues) {
|
|
53984
|
+
const page = r.pages.find((p) => p.page === i.page);
|
|
53985
|
+
const key2 = page ? `${page.title} (${page.path})` : i.page === "footer" ? "Footer" : "Whole site";
|
|
53986
|
+
groups.set(key2, [...groups.get(key2) ?? [], i]);
|
|
53987
|
+
}
|
|
53988
|
+
for (const [where2, list5] of groups) {
|
|
53989
|
+
lines.push("", paint2(1, where2));
|
|
53990
|
+
for (const i of list5) {
|
|
53991
|
+
const sev = i.severity === "error" ? paint2(31, "error ") : i.severity === "warning" ? paint2(33, "warning") : "info ";
|
|
53992
|
+
lines.push(` ${sev} ${i.message}`);
|
|
53993
|
+
lines.push(` ${i.autofix ? `Fix for phones: ${i.autofix.label}.` : i.fix}`);
|
|
53994
|
+
}
|
|
53995
|
+
}
|
|
53996
|
+
const { error: error2, warning, info } = r.counts;
|
|
53997
|
+
lines.push("");
|
|
53998
|
+
lines.push(
|
|
53999
|
+
error2 + warning === 0 ? paint2(
|
|
54000
|
+
32,
|
|
54001
|
+
`Looks great on phones${info ? ` (${info} suggestion${info === 1 ? "" : "s"})` : ""}.`
|
|
54002
|
+
) : `${error2} error${error2 === 1 ? "" : "s"}, ${warning} warning${warning === 1 ? "" : "s"} on phones.${r.issues.some((i) => i.autofix) && !r.fixed.length ? " Run `openspace check --phone --fix` to fix what can be fixed automatically." : ""}`
|
|
54003
|
+
);
|
|
54004
|
+
return lines.join("\n");
|
|
54005
|
+
}
|
|
54006
|
+
function uniqueLabels(fixed) {
|
|
54007
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
54008
|
+
for (const f of fixed) counts2.set(f.label, (counts2.get(f.label) ?? 0) + 1);
|
|
54009
|
+
return [...counts2].map(([l, n]) => n > 1 ? `${l} (${n}\xD7)` : l);
|
|
54010
|
+
}
|
|
54011
|
+
|
|
52235
54012
|
// src/wow-first.ts
|
|
52236
54013
|
var WOW_FIRST_MESSAGE = "New and imported sites always start on free GitHub Pages, so the first version goes live without any extra accounts. Shops, donations, member areas and password pages show a friendly \u201Copening soon\u201D note until you switch them on in Studio \u2192 Finish setting up (it moves the site to Cloudflare for you, with no downtime).";
|
|
52237
54014
|
function assertGitHubPagesStart(target) {
|
|
@@ -52410,6 +54187,7 @@ async function runImportSite(io, url, dirArg, opts = {}) {
|
|
|
52410
54187
|
await writeShell(dir, existing.deploy?.target ?? "github-pages", slug);
|
|
52411
54188
|
await writeImport(dir, result);
|
|
52412
54189
|
await writeAgentsMd(dir, result.doc.site.title);
|
|
54190
|
+
await phoneFixRendered(dir, opts, io);
|
|
52413
54191
|
const check2 = await runCheck(dir);
|
|
52414
54192
|
const r2 = { dir, result, check: check2 };
|
|
52415
54193
|
if (opts.summary) await writeSummary(resolve6(io.cwd, opts.summary), r2);
|
|
@@ -52427,11 +54205,21 @@ async function runImportSite(io, url, dirArg, opts = {}) {
|
|
|
52427
54205
|
await rm6(join15(dir, entry), { recursive: true, force: true });
|
|
52428
54206
|
throw e;
|
|
52429
54207
|
}
|
|
54208
|
+
await phoneFixRendered(dir, opts, io);
|
|
52430
54209
|
const check = await runCheck(dir);
|
|
52431
54210
|
const r = { dir, result, check };
|
|
52432
54211
|
if (opts.summary) await writeSummary(resolve6(io.cwd, opts.summary), r);
|
|
52433
54212
|
return r;
|
|
52434
54213
|
}
|
|
54214
|
+
async function phoneFixRendered(dir, opts, io) {
|
|
54215
|
+
if (!opts.render) return;
|
|
54216
|
+
try {
|
|
54217
|
+
const r = await runPhoneCheck(dir, { fix: true });
|
|
54218
|
+
if (r.fixed.length) io.err(` Fixed ${r.fixed.length} thing(s) for phones.`);
|
|
54219
|
+
} catch (e) {
|
|
54220
|
+
io.err(` Couldn't check the pages on a phone: ${e.message.split("\n")[0]}`);
|
|
54221
|
+
}
|
|
54222
|
+
}
|
|
52435
54223
|
function importCounts(r) {
|
|
52436
54224
|
const { result, check } = r;
|
|
52437
54225
|
return {
|
|
@@ -56698,8 +58486,8 @@ var escapeText3 = (s2) => s2.replace(/&/g, "&").replace(/</g, "<").replac
|
|
|
56698
58486
|
var escapeAttr2 = (s2) => s2.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/\{\{/g, "{{");
|
|
56699
58487
|
var UTILITY = /^(?:display|italic|muted|measure|body|lead|text|small|large|wrap|wrapper|inner|container|row|col|cols|grid|flex|block|inline|content|section|item|items|list|link|links|mt|mb|my|mx|px|py|pt|pb|pl|pr|ml|mr|m|p|d|g|is|has|js|u|sr|visually|astro|w|h|max|min|font|bg|color|center|left|right|hidden|show|active|reveal|fade|animate|anim|in|out|copy|art|wf|w|elementor|wp|x|y|z|lg|md|sm|xl|xs|xxl|space|gap|align|justify|order|relative|absolute|fixed|sticky|overflow|shadow|rounded|border|ring|opacity|transition|duration|ease|delay|transform|scale|rotate|translate|uppercase|lowercase|capitalize|tracking|leading|truncate|break|whitespace|select|cursor|pointer|z)$/;
|
|
56700
58488
|
function camel(s2) {
|
|
56701
|
-
const
|
|
56702
|
-
const out =
|
|
58489
|
+
const words4 = s2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean).map((w) => w.toLowerCase());
|
|
58490
|
+
const out = words4.map((w, i) => i === 0 ? w : w.charAt(0).toUpperCase() + w.slice(1)).join("").replace(/^[^a-z]+/, "");
|
|
56703
58491
|
return out.slice(0, 32);
|
|
56704
58492
|
}
|
|
56705
58493
|
function semanticName(el) {
|
|
@@ -56742,9 +58530,34 @@ function nameFor(el, fallback2 = "text") {
|
|
|
56742
58530
|
if (!el) return fallback2;
|
|
56743
58531
|
return semanticName(el) ?? TAG_NAME[tagOf(el)] ?? fallback2;
|
|
56744
58532
|
}
|
|
56745
|
-
|
|
56746
|
-
|
|
56747
|
-
|
|
58533
|
+
var PLAIN_WORDS = {
|
|
58534
|
+
cta: "button",
|
|
58535
|
+
btn: "button",
|
|
58536
|
+
img: "picture",
|
|
58537
|
+
image: "picture",
|
|
58538
|
+
pic: "picture",
|
|
58539
|
+
photo: "picture",
|
|
58540
|
+
desc: "description",
|
|
58541
|
+
txt: "text",
|
|
58542
|
+
href: "link",
|
|
58543
|
+
url: "link",
|
|
58544
|
+
kicker: "small label",
|
|
58545
|
+
eyebrow: "small label",
|
|
58546
|
+
overline: "small label",
|
|
58547
|
+
lede: "intro",
|
|
58548
|
+
lead: "intro",
|
|
58549
|
+
subhead: "subheading",
|
|
58550
|
+
bg: "background",
|
|
58551
|
+
nav: "menu"
|
|
58552
|
+
};
|
|
58553
|
+
function humanize2(key2, kind) {
|
|
58554
|
+
const words4 = key2.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Za-z])(\d)/g, "$1 $2").replace(/(\d)([A-Za-z])/g, "$1 $2").toLowerCase().split(/[\s_-]+/).filter(Boolean).map((w) => PLAIN_WORDS[w] ?? w);
|
|
58555
|
+
const linky = words4.some((w) => w === "button" || w === "link");
|
|
58556
|
+
if (linky && words4.length > 1 && words4[words4.length - 1] === "label")
|
|
58557
|
+
words4[words4.length - 1] = "text";
|
|
58558
|
+
if (kind === "link" && words4[words4.length - 1] !== "link") words4.push("goes to");
|
|
58559
|
+
const out = words4.join(" ") || "text";
|
|
58560
|
+
return (out.charAt(0).toUpperCase() + out.slice(1)).slice(0, 60);
|
|
56748
58561
|
}
|
|
56749
58562
|
var RESERVED2 = /* @__PURE__ */ new Set([
|
|
56750
58563
|
"rich",
|
|
@@ -56786,7 +58599,7 @@ var FieldScope = class {
|
|
|
56786
58599
|
}
|
|
56787
58600
|
add(base, spec, value) {
|
|
56788
58601
|
const key2 = this.key(base);
|
|
56789
|
-
const label = humanize2(key2);
|
|
58602
|
+
const label = humanize2(key2, spec.kind);
|
|
56790
58603
|
this.fields[key2] = { ...spec, label };
|
|
56791
58604
|
this.values[key2] = value;
|
|
56792
58605
|
return key2;
|
|
@@ -57061,10 +58874,10 @@ var SectionConverter = class {
|
|
|
57061
58874
|
async shapes(els, w) {
|
|
57062
58875
|
const map2 = /* @__PURE__ */ new Map();
|
|
57063
58876
|
const sig = (e) => `${tagOf(e)}.${classes(e).sort().join(".")}`;
|
|
57064
|
-
const
|
|
57065
|
-
for (const e of els)
|
|
58877
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
58878
|
+
for (const e of els) counts2.set(sig(e), (counts2.get(sig(e)) ?? 0) + 1);
|
|
57066
58879
|
for (const e of els) {
|
|
57067
|
-
if ((
|
|
58880
|
+
if ((counts2.get(sig(e)) ?? 0) < 2) continue;
|
|
57068
58881
|
const scope = new FieldScope();
|
|
57069
58882
|
const tpl = await this.gen(e, scope, { ...w, listDepth: w.listDepth + 1 });
|
|
57070
58883
|
const kinds = Object.entries(scope.fields).map(([k, f]) => `${k}:${f.kind}`).join(",");
|
|
@@ -58028,7 +59841,11 @@ function tokensFromPage(css, fontLinks) {
|
|
|
58028
59841
|
const btn = step(/class:(?:btn|button)\b/i);
|
|
58029
59842
|
if (btn) type.button = btn;
|
|
58030
59843
|
if (Object.keys(type).length) tokens.type = type;
|
|
58031
|
-
|
|
59844
|
+
const nColors = Object.keys(tokens.colors ?? {}).length;
|
|
59845
|
+
const nType = Object.keys(tokens.type ?? {}).length;
|
|
59846
|
+
const pl = (k, w) => `${k} ${w}${k === 1 ? "" : "s"}`;
|
|
59847
|
+
const note = `Read your page\u2019s styles: ${pl(nColors, "colour")}${families.length ? `, its fonts (${families.slice(0, 4).join(", ")})` : ""}${nType ? ` and ${pl(nType, "text style")}` : ""}.`;
|
|
59848
|
+
return { tokens, notes: [note], families };
|
|
58032
59849
|
}
|
|
58033
59850
|
function linkFamilies(hrefs) {
|
|
58034
59851
|
const out = [];
|
|
@@ -58074,6 +59891,9 @@ function siteHref(bundle, fromPath, href, pagePaths) {
|
|
|
58074
59891
|
return resolved;
|
|
58075
59892
|
}
|
|
58076
59893
|
async function importHtmlBundle(bundle, opts) {
|
|
59894
|
+
return phoneReady(await importBundle(bundle, opts));
|
|
59895
|
+
}
|
|
59896
|
+
async function importBundle(bundle, opts) {
|
|
58077
59897
|
const progress = opts.onProgress ?? (() => void 0);
|
|
58078
59898
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
58079
59899
|
const notices = [];
|
|
@@ -58234,7 +60054,7 @@ ${await r.text()}`;
|
|
|
58234
60054
|
}
|
|
58235
60055
|
const docTitle = text(find(home.root, (e) => e.tagName === "title"));
|
|
58236
60056
|
const wordmark = brand ? text(brand).replace(/\s+/g, " ").trim() : "";
|
|
58237
|
-
const siteTitle = (
|
|
60057
|
+
const siteTitle = ((wordmark && wordmark.length <= 60 ? titleCase(wordmark) : void 0) ?? opts.title ?? homePage.siteName ?? (docTitle.split(/\s+[|·—–-]\s+/)[0] || "My site")).slice(0, 120);
|
|
58238
60058
|
const logoImg = brand ? find(brand, (e) => e.tagName === "img") : void 0;
|
|
58239
60059
|
const logoRef = logoImg ? resolveRef(bundle, attr(logoImg, "src") ?? "", home.path) : void 0;
|
|
58240
60060
|
const segsByPage = /* @__PURE__ */ new Map();
|
|
@@ -58298,6 +60118,7 @@ ${await r.text()}`;
|
|
|
58298
60118
|
const placeholder = email ? attr(email, "placeholder") : void 0;
|
|
58299
60119
|
blocks2.push({
|
|
58300
60120
|
kind: "newsletter",
|
|
60121
|
+
...signupAsksName(f) ? { askName: true } : {},
|
|
58301
60122
|
...label3 ? { buttonLabel: label3.slice(0, 40) } : {},
|
|
58302
60123
|
...placeholder ? { placeholder } : {}
|
|
58303
60124
|
});
|
|
@@ -58661,8 +60482,8 @@ function componentNameFor(el, pagePath2) {
|
|
|
58661
60482
|
)
|
|
58662
60483
|
);
|
|
58663
60484
|
const heading2 = find(el, (e) => /^h[1-3]$/.test(e.tagName));
|
|
58664
|
-
const
|
|
58665
|
-
const raw2 = (id && id.length > 2 && !/^(?:top|main|content|section\d*)$/i.test(id) ? id : void 0) ?? cls ?? (
|
|
60485
|
+
const words4 = text(heading2).split(/\s+/).slice(0, 3).join(" ");
|
|
60486
|
+
const raw2 = (id && id.length > 2 && !/^(?:top|main|content|section\d*)$/i.test(id) ? id : void 0) ?? cls ?? (words4 || el.tagName);
|
|
58666
60487
|
const slug = raw2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^(\d)/, "s-$1").slice(0, 30).replace(/-+$/, "");
|
|
58667
60488
|
const prefix = pagePath2 === "/" ? "" : `${pagePath2.split("/").filter(Boolean).pop()}-`;
|
|
58668
60489
|
return `${prefix}${slug || "section"}`.replace(/^[^a-z]+/, "s-").slice(0, 36).replace(/-+$/, "");
|
|
@@ -59302,7 +61123,7 @@ async function runMcp(io, dir, opts = {}) {
|
|
|
59302
61123
|
// src/fidelity.ts
|
|
59303
61124
|
var DESKTOP = 1440;
|
|
59304
61125
|
var COMPONENT_RETRY_BELOW = 75;
|
|
59305
|
-
var
|
|
61126
|
+
var PHONE2 = 390;
|
|
59306
61127
|
async function launch() {
|
|
59307
61128
|
const pw = await loadPlaywright();
|
|
59308
61129
|
if ("error" in pw) throw new CliError(pw.error);
|
|
@@ -59345,7 +61166,7 @@ async function lookProber() {
|
|
|
59345
61166
|
const out = {};
|
|
59346
61167
|
for (const [key2, width] of [
|
|
59347
61168
|
["desktop", DESKTOP],
|
|
59348
|
-
["mobile",
|
|
61169
|
+
["mobile", PHONE2]
|
|
59349
61170
|
]) {
|
|
59350
61171
|
const page = await open(browser, url, width);
|
|
59351
61172
|
try {
|
|
@@ -59494,7 +61315,7 @@ async function runCompare(original, dir, opts = {}, log = () => void 0) {
|
|
|
59494
61315
|
await mkdir8(out, { recursive: true });
|
|
59495
61316
|
if (!opts.out) await writeFile10(join18(out, ".gitignore"), "*\n");
|
|
59496
61317
|
const pages = (opts.pages?.length ? opts.pages : defaultPages(doc)).map((p) => `/${slugOf2(p)}`);
|
|
59497
|
-
const widths = opts.widths ?? [DESKTOP,
|
|
61318
|
+
const widths = opts.widths ?? [DESKTOP, PHONE2];
|
|
59498
61319
|
const server = await startPreviewServer(root, () => loadSiteDocument(root));
|
|
59499
61320
|
const browser = await launch();
|
|
59500
61321
|
const oldPath = /* @__PURE__ */ new Map();
|
|
@@ -59514,7 +61335,7 @@ async function runCompare(original, dir, opts = {}, log = () => void 0) {
|
|
|
59514
61335
|
await page.close().catch(() => void 0);
|
|
59515
61336
|
}
|
|
59516
61337
|
};
|
|
59517
|
-
const
|
|
61338
|
+
const measure2 = async () => {
|
|
59518
61339
|
const comparisons = [];
|
|
59519
61340
|
const measured = [];
|
|
59520
61341
|
const shots = /* @__PURE__ */ new Map();
|
|
@@ -59559,7 +61380,7 @@ async function runCompare(original, dir, opts = {}, log = () => void 0) {
|
|
|
59559
61380
|
let best;
|
|
59560
61381
|
let tuning;
|
|
59561
61382
|
try {
|
|
59562
|
-
best = await
|
|
61383
|
+
best = await measure2();
|
|
59563
61384
|
const first2 = fidelityReport(best.comparisons);
|
|
59564
61385
|
const rounds = Math.max(0, Math.min(5, opts.tune ?? 0));
|
|
59565
61386
|
if (rounds && best.measured.length) {
|
|
@@ -59610,7 +61431,7 @@ async function runCompare(original, dir, opts = {}, log = () => void 0) {
|
|
|
59610
61431
|
await writeSiteDocument(root, bestDoc);
|
|
59611
61432
|
continue;
|
|
59612
61433
|
}
|
|
59613
|
-
const next = await
|
|
61434
|
+
const next = await measure2();
|
|
59614
61435
|
const score = fidelityReport(next.comparisons).exact ?? 0;
|
|
59615
61436
|
const better = score > bestScore + 0.1;
|
|
59616
61437
|
log(
|
|
@@ -59728,10 +61549,17 @@ function fidelitySummary(report, tuning, thumbs) {
|
|
|
59728
61549
|
const before = new Map(
|
|
59729
61550
|
(tuning?.before.pages ?? []).map((p) => [`${p.path}@${p.viewport}`, p.score])
|
|
59730
61551
|
);
|
|
61552
|
+
const strictBefore = new Map(
|
|
61553
|
+
(tuning?.before.pages ?? []).map((p) => [`${p.path}@${p.viewport}`, p.strict?.score])
|
|
61554
|
+
);
|
|
59731
61555
|
return {
|
|
59732
61556
|
version: 1,
|
|
59733
61557
|
score: report.score,
|
|
61558
|
+
// The same pages by the strict v3 scorer, side by side (additive: older readers ignore it).
|
|
61559
|
+
...report.strict ? { strict: report.strict.score } : {},
|
|
61560
|
+
...report.scorer ? { scorer: report.scorer } : {},
|
|
59734
61561
|
...tuning ? { before: tuning.before.score, changes: tuning.changes.slice(0, 40) } : {},
|
|
61562
|
+
...tuning?.before.strict ? { strictBefore: tuning.before.strict.score } : {},
|
|
59735
61563
|
...tuning?.withoutComponents !== void 0 ? {
|
|
59736
61564
|
withoutComponents: tuning.withoutComponents,
|
|
59737
61565
|
...tuning.withComponents !== void 0 ? { withComponents: tuning.withComponents } : {}
|
|
@@ -59740,11 +61568,15 @@ function fidelitySummary(report, tuning, thumbs) {
|
|
|
59740
61568
|
const key2 = `${p.path}@${p.viewport}`;
|
|
59741
61569
|
const b = before.get(key2);
|
|
59742
61570
|
const worst = [...p.checks, ...p.sections.flatMap((s2) => s2.checks)].filter((c) => !c.ok && !c.credit).sort((x, y) => y.weight - x.weight).slice(0, 3).map((c) => c.what);
|
|
61571
|
+
const sb = strictBefore.get(key2);
|
|
59743
61572
|
return {
|
|
59744
61573
|
path: p.path,
|
|
59745
61574
|
device: p.viewport < 700 ? "phone" : "desktop",
|
|
59746
61575
|
score: p.score,
|
|
61576
|
+
...p.strict ? { strict: p.strict.score } : {},
|
|
61577
|
+
...p.corrections?.length ? { corrections: p.corrections.slice(0, 6) } : {},
|
|
59747
61578
|
...b !== void 0 ? { before: b } : {},
|
|
61579
|
+
...sb !== void 0 ? { strictBefore: sb } : {},
|
|
59748
61580
|
...thumbs[key2] ? { image: thumbs[key2] } : {},
|
|
59749
61581
|
...worst.length ? { differs: worst } : {}
|
|
59750
61582
|
};
|
|
@@ -59788,6 +61620,7 @@ async function addFidelityToSummary(file, r) {
|
|
|
59788
61620
|
const summary = JSON.parse(await readFile14(file, "utf8"));
|
|
59789
61621
|
summary.fidelity = {
|
|
59790
61622
|
score: r.report.score,
|
|
61623
|
+
...r.report.strict ? { strict: r.report.strict.score } : {},
|
|
59791
61624
|
...r.tuning ? { before: r.tuning.before.score } : {}
|
|
59792
61625
|
};
|
|
59793
61626
|
await writeFile10(file, `${JSON.stringify(summary, null, 2)}
|
|
@@ -59796,8 +61629,9 @@ async function addFidelityToSummary(file, r) {
|
|
|
59796
61629
|
}
|
|
59797
61630
|
}
|
|
59798
61631
|
function compareSummary(r) {
|
|
61632
|
+
const strict = r.report.strict ? r.tuning?.before.strict ? ` Strict v3 scorer: ${r.tuning.before.strict.score}/100 \u2192 ${r.report.strict.score}/100 (the corrected score reads three things v3 gets wrong; see FIDELITY-REPORT.md).` : ` Strict v3 scorer: ${r.report.strict.score}/100 (the corrected score reads three things v3 gets wrong; see FIDELITY-REPORT.md).` : "";
|
|
59799
61633
|
const lines = [
|
|
59800
|
-
r.tuning ? `Visual match with the original: ${r.tuning.before.score}/100 \u2192 ${r.report.score}/100 after ${r.tuning.rounds} tuning round(s)${r.tuning.withoutComponents !== void 0 ? ` (${r.tuning.withoutComponents}/100 before recreating sections as custom components${r.tuning.withComponents !== void 0 ? "" : "; none kept"})` : ""}
|
|
61634
|
+
r.tuning ? `Visual match with the original: ${r.tuning.before.score}/100 \u2192 ${r.report.score}/100 after ${r.tuning.rounds} tuning round(s)${r.tuning.withoutComponents !== void 0 ? ` (${r.tuning.withoutComponents}/100 before recreating sections as custom components${r.tuning.withComponents !== void 0 ? "" : "; none kept"})` : ""}.${strict}` : `Visual match with the original: ${r.report.score}/100.${strict}`
|
|
59801
61635
|
];
|
|
59802
61636
|
for (const p of r.report.pages) {
|
|
59803
61637
|
const off = [
|
|
@@ -59805,7 +61639,7 @@ function compareSummary(r) {
|
|
|
59805
61639
|
...p.sections.filter((s2) => s2.score < 80).map((s2) => `section ${s2.index + 1}${s2.heading ? ` "${s2.heading.slice(0, 30)}"` : ""}`)
|
|
59806
61640
|
];
|
|
59807
61641
|
lines.push(
|
|
59808
|
-
` ${p.path} ${p.viewport}px: ${p.score}/100${off.length ? ` \u2014 differs: ${off.slice(0, 6).join(", ")}${off.length > 6 ? "\u2026" : ""}` : ""}`
|
|
61642
|
+
` ${p.path} ${p.viewport}px: ${p.score}/100${p.strict && p.strict.score !== p.score ? ` (strict v3 ${p.strict.score})` : ""}${off.length ? ` \u2014 differs: ${off.slice(0, 6).join(", ")}${off.length > 6 ? "\u2026" : ""}` : ""}`
|
|
59809
61643
|
);
|
|
59810
61644
|
}
|
|
59811
61645
|
lines.push(` Report and side-by-side images: ${join18(r.out, "FIDELITY-REPORT.md")}`);
|
|
@@ -60025,14 +61859,23 @@ async function runMigrate(root, opts = {}) {
|
|
|
60025
61859
|
var VERSION = "0.1.0";
|
|
60026
61860
|
function createProgram(io, setExit, deps = {}) {
|
|
60027
61861
|
const program = new Command("openspace").description("Check, preview, migrate, scaffold and serve OpenSpace sites to Claude (MCP).").version(VERSION).configureOutput({ writeOut: (s2) => io.out(s2.trimEnd()), writeErr: (s2) => io.err(s2.trimEnd()) }).exitOverride();
|
|
60028
|
-
program.command("check").argument("[dir]", "site folder (default: the current folder)").description("Validate content: schemas, links, images, size budgets and secrets.").option("--ci", "CI mode: also print GitHub Actions annotations").option("--json", "print the report as JSON").option("--media-warn-mb <n>", "media folder warning threshold in MB", Number).option("--media-fail-mb <n>", "media folder failure threshold in MB", Number).action(
|
|
61862
|
+
program.command("check").argument("[dir]", "site folder (default: the current folder)").description("Validate content: schemas, links, images, size budgets and secrets.").option("--ci", "CI mode: also print GitHub Actions annotations").option("--json", "print the report as JSON").option("--media-warn-mb <n>", "media folder warning threshold in MB", Number).option("--media-fail-mb <n>", "media folder failure threshold in MB", Number).option("--phone", "also run the Phone check in a real browser (Playwright) at 360 and 390 px").option("--fix", "with --phone: fix everything for phones (writes the content files)").action(
|
|
60029
61863
|
async (dir, opts) => {
|
|
61864
|
+
let phone;
|
|
61865
|
+
if (opts.phone || opts.fix) {
|
|
61866
|
+
phone = await runPhoneCheck(resolve10(io.cwd, dir ?? "."), { fix: opts.fix });
|
|
61867
|
+
if (!opts.json) {
|
|
61868
|
+
io.out(formatPhoneReport(phone, (code3, s2) => paint(colorEnabled(io), code3, s2)));
|
|
61869
|
+
io.out("");
|
|
61870
|
+
}
|
|
61871
|
+
if (phone.counts.error) setExit(1);
|
|
61872
|
+
}
|
|
60030
61873
|
const budgets = {};
|
|
60031
61874
|
if (opts.mediaWarnMb !== void 0) budgets.mediaWarnBytes = opts.mediaWarnMb * 1024 * 1024;
|
|
60032
61875
|
if (opts.mediaFailMb !== void 0) budgets.mediaFailBytes = opts.mediaFailMb * 1024 * 1024;
|
|
60033
61876
|
const report = await runCheck(resolve10(io.cwd, dir ?? "."), { budgets });
|
|
60034
61877
|
if (opts.json) {
|
|
60035
|
-
io.out(JSON.stringify(report, null, 2));
|
|
61878
|
+
io.out(JSON.stringify(phone ? { ...report, phone } : report, null, 2));
|
|
60036
61879
|
} else {
|
|
60037
61880
|
if (opts.ci) for (const line of githubAnnotations(report)) io.out(line);
|
|
60038
61881
|
const color = colorEnabled(io);
|
|
@@ -60388,4 +62231,4 @@ export {
|
|
|
60388
62231
|
createProgram,
|
|
60389
62232
|
run
|
|
60390
62233
|
};
|
|
60391
|
-
//# sourceMappingURL=chunk-
|
|
62234
|
+
//# sourceMappingURL=chunk-PBN36VUE.js.map
|