@omniharmonic/openspace-cli 0.1.14 → 0.1.16
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-JDVCOOGD.js} +1990 -129
- package/dist/chunk-JDVCOOGD.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) } : {}
|
|
@@ -43913,13 +45274,24 @@ var ToolContext = class {
|
|
|
43913
45274
|
const allowed = await this.allowedSites();
|
|
43914
45275
|
if (input) {
|
|
43915
45276
|
if (allowed.includes(input)) return input;
|
|
45277
|
+
const wanted = input.trim().toLowerCase();
|
|
45278
|
+
const named = (await this.store.listSites()).filter(
|
|
45279
|
+
(s2) => allowed.includes(s2.id) && s2.title.trim().toLowerCase() === wanted
|
|
45280
|
+
);
|
|
45281
|
+
if (named.length === 1 && named[0]) return named[0].id;
|
|
45282
|
+
const explained = await this.store.explainUnreachable?.(input);
|
|
45283
|
+
if (explained) throw explained;
|
|
43916
45284
|
throw new ToolError(
|
|
43917
45285
|
`This connection can't reach site "${input}". Sites available: ${allowed.join(", ") || "none"}.`,
|
|
43918
45286
|
"forbidden"
|
|
43919
45287
|
);
|
|
43920
45288
|
}
|
|
43921
45289
|
if (allowed.length === 1) return allowed[0];
|
|
43922
|
-
if (!allowed.length)
|
|
45290
|
+
if (!allowed.length) {
|
|
45291
|
+
const explained = await this.store.explainUnreachable?.(void 0);
|
|
45292
|
+
if (explained) throw explained;
|
|
45293
|
+
throw new ToolError("This connection has no sites.", "forbidden");
|
|
45294
|
+
}
|
|
43923
45295
|
throw new ToolError(
|
|
43924
45296
|
`Several sites are available; pass "site" (one of: ${allowed.join(", ")}).`,
|
|
43925
45297
|
"invalid_arguments"
|
|
@@ -44048,7 +45420,7 @@ The same rules power Studio's built-in Design check. Follow them so pages look d
|
|
|
44048
45420
|
6. **Two typefaces maximum.** Change the pairing in Site Styles rather than per block.
|
|
44049
45421
|
7. **Never invent facts, testimonials, prices or credentials.** Ask the owner.
|
|
44050
45422
|
8. **Alt text describes the image's purpose,** not just its contents; decorative images are marked decorative.
|
|
44051
|
-
9. **
|
|
45423
|
+
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
45424
|
10. **Prefer fewer, stronger sections** over many thin ones.
|
|
44053
45425
|
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
45426
|
|
|
@@ -44067,12 +45439,12 @@ var ITERATION_GUIDANCE = `# Design iteration loop
|
|
|
44067
45439
|
Repeat until the page is right, keeping each pass small:
|
|
44068
45440
|
|
|
44069
45441
|
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.
|
|
45442
|
+
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
45443
|
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
45444
|
4. **Look** \u2014 screenshot the page (or just the section you changed) at desktop and mobile; use render_page when screenshots are unavailable.
|
|
44073
45445
|
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
45446
|
|
|
44075
|
-
Finish with design_check showing zero errors and get_draft_diff to summarise the work for the owner.
|
|
45447
|
+
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
45448
|
`;
|
|
44077
45449
|
|
|
44078
45450
|
// ../mcp-tools/src/images.ts
|
|
@@ -44454,11 +45826,11 @@ ${ITERATION_GUIDANCE}
|
|
|
44454
45826
|
Do at most five passes, and after each pass say in one line what you changed and what you saw in the screenshot. Stop when design_check shows no errors and the page reads clearly on desktop and mobile.`
|
|
44455
45827
|
}
|
|
44456
45828
|
];
|
|
44457
|
-
function getPrompt(name, args = {}) {
|
|
44458
|
-
const p =
|
|
45829
|
+
function getPrompt(name, args = {}, prompts = PROMPTS) {
|
|
45830
|
+
const p = prompts.find((x) => x.name === name);
|
|
44459
45831
|
if (!p)
|
|
44460
45832
|
throw new ToolError(
|
|
44461
|
-
`Unknown prompt "${name}". Prompts: ${
|
|
45833
|
+
`Unknown prompt "${name}". Prompts: ${prompts.map((x) => x.name).join(", ")}.`,
|
|
44462
45834
|
"not_found"
|
|
44463
45835
|
);
|
|
44464
45836
|
const missing = p.arguments.filter((x) => x.required && !args[x.name]?.trim()).map((x) => x.name);
|
|
@@ -44492,7 +45864,7 @@ function renderPreviewHtml(doc, pageId, opts = {}) {
|
|
|
44492
45864
|
const html = renderPageHtml({
|
|
44493
45865
|
doc,
|
|
44494
45866
|
pageId: target,
|
|
44495
|
-
mode: "preview",
|
|
45867
|
+
mode: opts.production ? "production" : "preview",
|
|
44496
45868
|
basePath: opts.basePath ?? "",
|
|
44497
45869
|
behaviorsUrl: opts.behaviorsUrl,
|
|
44498
45870
|
extraHead: blocksStyleTag()
|
|
@@ -45650,10 +47022,10 @@ var listSectionPresets = defineTool({
|
|
|
45650
47022
|
let list5 = presetCatalog.slice();
|
|
45651
47023
|
if (input.category) list5 = list5.filter((p) => p.category === input.category);
|
|
45652
47024
|
if (input.query) {
|
|
45653
|
-
const
|
|
47025
|
+
const words4 = input.query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
45654
47026
|
list5 = list5.filter((p) => {
|
|
45655
47027
|
const hay = `${p.id} ${p.label} ${p.description} ${p.tags.join(" ")} ${p.ai.whenToUse}`.toLowerCase();
|
|
45656
|
-
return
|
|
47028
|
+
return words4.every((w) => hay.includes(w));
|
|
45657
47029
|
});
|
|
45658
47030
|
}
|
|
45659
47031
|
const full = input.detail === "full";
|
|
@@ -46012,12 +47384,12 @@ var designCheckTool = defineTool({
|
|
|
46012
47384
|
const issues = runChecks(doc, pageId, input.checks ?? CHECKS, ctx.now()).filter(
|
|
46013
47385
|
(i) => RANK[i.severity] <= min
|
|
46014
47386
|
);
|
|
46015
|
-
const
|
|
46016
|
-
for (const i of issues)
|
|
47387
|
+
const counts2 = { error: 0, warning: 0, info: 0 };
|
|
47388
|
+
for (const i of issues) counts2[i.severity]++;
|
|
46017
47389
|
return {
|
|
46018
47390
|
data: {
|
|
46019
47391
|
...pageId ? { page: pageId } : {},
|
|
46020
|
-
counts,
|
|
47392
|
+
counts: counts2,
|
|
46021
47393
|
issues: issues.map((i) => ({
|
|
46022
47394
|
id: i.id,
|
|
46023
47395
|
rule: i.rule,
|
|
@@ -46078,9 +47450,9 @@ var applyFix = defineTool({
|
|
|
46078
47450
|
});
|
|
46079
47451
|
}
|
|
46080
47452
|
const remaining = runChecks(doc, pageId, CHECKS, ctx.now());
|
|
46081
|
-
const
|
|
46082
|
-
for (const i of remaining)
|
|
46083
|
-
return { data: { applied, skipped, remaining:
|
|
47453
|
+
const counts2 = { error: 0, warning: 0, info: 0 };
|
|
47454
|
+
for (const i of remaining) counts2[i.severity]++;
|
|
47455
|
+
return { data: { applied, skipped, remaining: counts2 } };
|
|
46084
47456
|
}
|
|
46085
47457
|
});
|
|
46086
47458
|
var preview = defineTool({
|
|
@@ -47206,6 +48578,143 @@ var pageTools = [
|
|
|
47206
48578
|
listPosts
|
|
47207
48579
|
];
|
|
47208
48580
|
|
|
48581
|
+
// ../mcp-tools/src/tools/phone.ts
|
|
48582
|
+
var PHONE_PAGE_LIMIT = 12;
|
|
48583
|
+
var stripMeta2 = (op2) => {
|
|
48584
|
+
const { id: _i, actor: _a3, ts: _t, ...body } = op2;
|
|
48585
|
+
return body;
|
|
48586
|
+
};
|
|
48587
|
+
async function measure(ctx, siteId, doc, pages, wantRendered) {
|
|
48588
|
+
const now = ctx.now();
|
|
48589
|
+
const statics = phoneCheck(doc, pages.length === 1 ? pages[0]?.id : void 0, {
|
|
48590
|
+
actor: "phone-check",
|
|
48591
|
+
now
|
|
48592
|
+
});
|
|
48593
|
+
const store = ctx.store;
|
|
48594
|
+
if (!wantRendered || !store.capabilities.canMeasurePhone || !store.measurePhone)
|
|
48595
|
+
return {
|
|
48596
|
+
issues: statics,
|
|
48597
|
+
rendered: false,
|
|
48598
|
+
measuredPages: [],
|
|
48599
|
+
...wantRendered ? {
|
|
48600
|
+
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."
|
|
48601
|
+
} : {}
|
|
48602
|
+
};
|
|
48603
|
+
const measured = pages.slice(0, PHONE_PAGE_LIMIT);
|
|
48604
|
+
const rendered2 = [];
|
|
48605
|
+
let note;
|
|
48606
|
+
for (const page of measured) {
|
|
48607
|
+
const html = renderPreviewHtml(doc, page.id, { production: true });
|
|
48608
|
+
const results = [];
|
|
48609
|
+
try {
|
|
48610
|
+
for (const width of PHONE_WIDTHS)
|
|
48611
|
+
results.push(await store.measurePhone(siteId, { pageId: page.id, width, html }));
|
|
48612
|
+
} catch (e) {
|
|
48613
|
+
note = `The browser check stopped at \u201C${page.title}\u201D: ${e instanceof Error ? e.message : String(e)}`;
|
|
48614
|
+
break;
|
|
48615
|
+
}
|
|
48616
|
+
rendered2.push(...phoneIssuesFromProbe(doc, page.id, results, { actor: "phone-check", now }));
|
|
48617
|
+
}
|
|
48618
|
+
if (!note && pages.length > measured.length)
|
|
48619
|
+
note = `Measured the first ${measured.length} of ${pages.length} pages; run phone_check with page for the others.`;
|
|
48620
|
+
const seen = /* @__PURE__ */ new Set();
|
|
48621
|
+
const deduped = rendered2.filter((i) => {
|
|
48622
|
+
if (i.page !== "footer") return true;
|
|
48623
|
+
const key2 = `${i.rule}|${i.message}`;
|
|
48624
|
+
if (seen.has(key2)) return false;
|
|
48625
|
+
seen.add(key2);
|
|
48626
|
+
return true;
|
|
48627
|
+
});
|
|
48628
|
+
return {
|
|
48629
|
+
issues: sortIssues([...statics, ...deduped]),
|
|
48630
|
+
rendered: true,
|
|
48631
|
+
measuredPages: measured.map((p) => p.id),
|
|
48632
|
+
...note ? { note } : {}
|
|
48633
|
+
};
|
|
48634
|
+
}
|
|
48635
|
+
var pagesFor = (doc, page) => page ? [resolvePage(doc, page).page] : Object.values(doc.pages);
|
|
48636
|
+
var describe2 = (i) => ({
|
|
48637
|
+
id: i.id,
|
|
48638
|
+
rule: i.rule,
|
|
48639
|
+
severity: i.severity,
|
|
48640
|
+
...i.page ? { page: i.page } : {},
|
|
48641
|
+
...i.section ? { section: i.section } : {},
|
|
48642
|
+
...i.block ? { block: i.block } : {},
|
|
48643
|
+
message: i.message,
|
|
48644
|
+
fix: i.fix,
|
|
48645
|
+
autofix: i.autofix?.label ?? null
|
|
48646
|
+
});
|
|
48647
|
+
var counts = (issues) => {
|
|
48648
|
+
const c = { error: 0, warning: 0, info: 0 };
|
|
48649
|
+
for (const i of issues) c[i.severity]++;
|
|
48650
|
+
return c;
|
|
48651
|
+
};
|
|
48652
|
+
var phoneCheckTool = defineTool({
|
|
48653
|
+
name: "phone_check",
|
|
48654
|
+
title: "Phone check",
|
|
48655
|
+
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.",
|
|
48656
|
+
input: external_exports.object({
|
|
48657
|
+
site: siteArg,
|
|
48658
|
+
page: external_exports.string().optional().describe("Page id/slug/title (default: every page)."),
|
|
48659
|
+
rendered: external_exports.boolean().optional().describe("Measure in a real browser when available (default true).")
|
|
48660
|
+
}),
|
|
48661
|
+
annotations: { readOnlyHint: true },
|
|
48662
|
+
async handler(ctx, input) {
|
|
48663
|
+
const siteId = await ctx.siteId(input.site);
|
|
48664
|
+
const doc = await ctx.doc(siteId);
|
|
48665
|
+
const r = await measure(ctx, siteId, doc, pagesFor(doc, input.page), input.rendered !== false);
|
|
48666
|
+
const c = counts(r.issues);
|
|
48667
|
+
return {
|
|
48668
|
+
data: {
|
|
48669
|
+
rendered: r.rendered,
|
|
48670
|
+
...r.note ? { note: r.note } : {},
|
|
48671
|
+
counts: c,
|
|
48672
|
+
issues: r.issues.map(describe2),
|
|
48673
|
+
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."
|
|
48674
|
+
}
|
|
48675
|
+
};
|
|
48676
|
+
}
|
|
48677
|
+
});
|
|
48678
|
+
var fixForPhonesTool = defineTool({
|
|
48679
|
+
name: "fix_for_phones",
|
|
48680
|
+
title: "Fix everything for phones",
|
|
48681
|
+
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.",
|
|
48682
|
+
input: external_exports.object({
|
|
48683
|
+
site: siteArg,
|
|
48684
|
+
page: external_exports.string().optional().describe("Limit to a page (default: every page).")
|
|
48685
|
+
}),
|
|
48686
|
+
annotations: {},
|
|
48687
|
+
async handler(ctx, input) {
|
|
48688
|
+
const siteId = await ctx.siteId(input.site);
|
|
48689
|
+
let doc = await ctx.doc(siteId);
|
|
48690
|
+
const pages = pagesFor(doc, input.page);
|
|
48691
|
+
const before = await measure(ctx, siteId, doc, pages, true);
|
|
48692
|
+
const r = fixForPhones(doc, {
|
|
48693
|
+
...input.page ? { pageId: pages[0]?.id } : {},
|
|
48694
|
+
actor: "phone-check",
|
|
48695
|
+
now: ctx.now(),
|
|
48696
|
+
rendered: before.issues
|
|
48697
|
+
});
|
|
48698
|
+
if (r.ops.length) {
|
|
48699
|
+
const applied = await ctx.apply(siteId, r.ops.map(stripMeta2));
|
|
48700
|
+
doc = applied.doc;
|
|
48701
|
+
}
|
|
48702
|
+
const after = await measure(ctx, siteId, doc, pagesFor(doc, input.page), true);
|
|
48703
|
+
const labels = /* @__PURE__ */ new Map();
|
|
48704
|
+
for (const f of r.fixed) labels.set(f.label, (labels.get(f.label) ?? 0) + 1);
|
|
48705
|
+
return {
|
|
48706
|
+
data: {
|
|
48707
|
+
fixed: [...labels].map(([label, n]) => n > 1 ? `${label} (${n}\xD7)` : label),
|
|
48708
|
+
rendered: after.rendered,
|
|
48709
|
+
remaining: counts(after.issues),
|
|
48710
|
+
issues: after.issues.filter((i) => i.severity !== "info").map(describe2),
|
|
48711
|
+
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."
|
|
48712
|
+
}
|
|
48713
|
+
};
|
|
48714
|
+
}
|
|
48715
|
+
});
|
|
48716
|
+
var phoneTools = [phoneCheckTool, fixForPhonesTool];
|
|
48717
|
+
|
|
47209
48718
|
// ../mcp-tools/src/tools/publish.ts
|
|
47210
48719
|
var getDraftDiff = defineTool({
|
|
47211
48720
|
name: "get_draft_diff",
|
|
@@ -47351,6 +48860,7 @@ var ALL_TOOLS = [
|
|
|
47351
48860
|
...pageTools,
|
|
47352
48861
|
...designSystemTools,
|
|
47353
48862
|
...designTools,
|
|
48863
|
+
...phoneTools,
|
|
47354
48864
|
...componentTools,
|
|
47355
48865
|
...publishTools
|
|
47356
48866
|
];
|
|
@@ -47396,14 +48906,21 @@ var OpenSpaceMcpServer = class {
|
|
|
47396
48906
|
this.initialized = Boolean(options.assumeInitialized);
|
|
47397
48907
|
for (const t of ALL_TOOLS) this.tools.set(t.name, t);
|
|
47398
48908
|
for (const t of options.tools ?? []) this.tools.set(t.name, t);
|
|
48909
|
+
const extra = options.prompts ?? [];
|
|
48910
|
+
this.prompts = [...PROMPTS.filter((p) => !extra.some((e) => e.name === p.name)), ...extra];
|
|
47399
48911
|
}
|
|
47400
48912
|
options;
|
|
47401
48913
|
ctx;
|
|
47402
48914
|
tools = /* @__PURE__ */ new Map();
|
|
48915
|
+
prompts;
|
|
47403
48916
|
clientName;
|
|
47404
48917
|
initialized;
|
|
47405
48918
|
protocolVersion;
|
|
47406
48919
|
closed = false;
|
|
48920
|
+
/** Prompts served by this instance. */
|
|
48921
|
+
promptList() {
|
|
48922
|
+
return [...this.prompts];
|
|
48923
|
+
}
|
|
47407
48924
|
/** Tool definitions served by this instance. */
|
|
47408
48925
|
toolList() {
|
|
47409
48926
|
return [...this.tools.values()];
|
|
@@ -47525,7 +49042,7 @@ var OpenSpaceMcpServer = class {
|
|
|
47525
49042
|
}
|
|
47526
49043
|
case "prompts/list":
|
|
47527
49044
|
return {
|
|
47528
|
-
prompts:
|
|
49045
|
+
prompts: this.prompts.map((p) => ({
|
|
47529
49046
|
name: p.name,
|
|
47530
49047
|
title: p.title,
|
|
47531
49048
|
description: p.description,
|
|
@@ -47536,7 +49053,7 @@ var OpenSpaceMcpServer = class {
|
|
|
47536
49053
|
if (typeof params.name !== "string")
|
|
47537
49054
|
throw new RpcError(ErrorCode.InvalidParams, "name is required");
|
|
47538
49055
|
const args = isRecord5(params.arguments) ? Object.fromEntries(Object.entries(params.arguments).map(([k, v]) => [k, String(v)])) : {};
|
|
47539
|
-
return getPrompt(params.name, args);
|
|
49056
|
+
return getPrompt(params.name, args, this.prompts);
|
|
47540
49057
|
}
|
|
47541
49058
|
case "completion/complete":
|
|
47542
49059
|
return { completion: { values: [], hasMore: false } };
|
|
@@ -47564,7 +49081,7 @@ var OpenSpaceMcpServer = class {
|
|
|
47564
49081
|
logging: {}
|
|
47565
49082
|
},
|
|
47566
49083
|
serverInfo: { name: SERVER_NAME, title: "OpenSpace", version: SERVER_VERSION },
|
|
47567
|
-
instructions: SERVER_INSTRUCTIONS
|
|
49084
|
+
instructions: this.options.instructions ?? SERVER_INSTRUCTIONS
|
|
47568
49085
|
};
|
|
47569
49086
|
}
|
|
47570
49087
|
/** Name the client reported in `initialize`. */
|
|
@@ -48142,6 +49659,25 @@ function squarespaceRenditions(u, width, height, canResize = true) {
|
|
|
48142
49659
|
];
|
|
48143
49660
|
}
|
|
48144
49661
|
|
|
49662
|
+
// ../import/src/phone.ts
|
|
49663
|
+
function phoneReady(result) {
|
|
49664
|
+
let fixed;
|
|
49665
|
+
try {
|
|
49666
|
+
fixed = fixForPhones(result.doc, { actor: "import", now: 0 });
|
|
49667
|
+
} catch {
|
|
49668
|
+
return result;
|
|
49669
|
+
}
|
|
49670
|
+
if (!fixed.ops.length) return result;
|
|
49671
|
+
const labels = [...new Set(fixed.fixed.map((f) => f.label.toLowerCase()))];
|
|
49672
|
+
const line = `
|
|
49673
|
+
## Phones
|
|
49674
|
+
|
|
49675
|
+
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.
|
|
49676
|
+
`;
|
|
49677
|
+
return { ...result, doc: fixed.doc, report: `${result.report.trimEnd()}
|
|
49678
|
+
${line}` };
|
|
49679
|
+
}
|
|
49680
|
+
|
|
48145
49681
|
// ../import/src/report.ts
|
|
48146
49682
|
var NOTICE_TITLES = {
|
|
48147
49683
|
"unsupported-block": "Blocks that need a look",
|
|
@@ -48277,17 +49813,17 @@ function renderReport(r) {
|
|
|
48277
49813
|
any = true;
|
|
48278
49814
|
L2.push(`### ${NOTICE_TITLES[kind]}`);
|
|
48279
49815
|
L2.push("");
|
|
48280
|
-
const
|
|
49816
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
48281
49817
|
for (const n of list5) {
|
|
48282
|
-
const e =
|
|
49818
|
+
const e = counts2.get(n.message) ?? { n: 0, where: /* @__PURE__ */ new Set() };
|
|
48283
49819
|
e.n++;
|
|
48284
49820
|
e.where.add(n.where);
|
|
48285
|
-
|
|
49821
|
+
counts2.set(n.message, e);
|
|
48286
49822
|
}
|
|
48287
49823
|
let shown = 0;
|
|
48288
|
-
for (const [message, e] of
|
|
49824
|
+
for (const [message, e] of counts2) {
|
|
48289
49825
|
if (++shown > 15) {
|
|
48290
|
-
L2.push(`- \u2026 and ${
|
|
49826
|
+
L2.push(`- \u2026 and ${counts2.size - 15} more like these.`);
|
|
48291
49827
|
break;
|
|
48292
49828
|
}
|
|
48293
49829
|
const where2 = [...e.where];
|
|
@@ -48680,7 +50216,7 @@ async function crawlWordPress(opts) {
|
|
|
48680
50216
|
...it.excerptHtml ? { caption: it.excerptHtml } : {}
|
|
48681
50217
|
});
|
|
48682
50218
|
const entries = [];
|
|
48683
|
-
const
|
|
50219
|
+
const pathOf2 = (link2, fallback2) => (link2 ? sitePath(link2, base) : void 0) ?? fallback2;
|
|
48684
50220
|
if (rest) {
|
|
48685
50221
|
const [pages2, posts] = [
|
|
48686
50222
|
await listAll("wp/v2/pages", "_embed=1&orderby=menu_order&order=asc"),
|
|
@@ -48707,7 +50243,7 @@ async function crawlWordPress(opts) {
|
|
|
48707
50243
|
}
|
|
48708
50244
|
entries.push({
|
|
48709
50245
|
type,
|
|
48710
|
-
path:
|
|
50246
|
+
path: pathOf2(link2, `/${slug}`),
|
|
48711
50247
|
slug,
|
|
48712
50248
|
title: decodeEntities2(rendered(p.title)) || slug || "Untitled",
|
|
48713
50249
|
html: rendered(p.content),
|
|
@@ -48774,7 +50310,7 @@ async function crawlWordPress(opts) {
|
|
|
48774
50310
|
});
|
|
48775
50311
|
}
|
|
48776
50312
|
if (!rest && !wxr && home) {
|
|
48777
|
-
const { crawlGeneric: crawlGeneric2 } = await import("./crawl-
|
|
50313
|
+
const { crawlGeneric: crawlGeneric2 } = await import("./crawl-WQQDFI7W.js");
|
|
48778
50314
|
const site2 = await crawlGeneric2({
|
|
48779
50315
|
url: base,
|
|
48780
50316
|
fetcher: f,
|
|
@@ -49058,6 +50594,7 @@ function componentFromBlueprint(input) {
|
|
|
49058
50594
|
".gx__img picture, .gx__img img { display: block; width: 100%; height: 100%; object-fit: cover; }"
|
|
49059
50595
|
];
|
|
49060
50596
|
const phone = [];
|
|
50597
|
+
const named = [];
|
|
49061
50598
|
let headingSeen = false;
|
|
49062
50599
|
const role = (family) => {
|
|
49063
50600
|
if (norm5(family) && norm5(family) === norm5(input.roles.label)) return "label";
|
|
@@ -49123,6 +50660,7 @@ function componentFromBlueprint(input) {
|
|
|
49123
50660
|
const key3 = `image${n}`;
|
|
49124
50661
|
fields[key3] = { kind: "image", label: `Picture ${n}` };
|
|
49125
50662
|
values[key3] = { src: it.src, alt: (it.alt ?? "").slice(0, 300) };
|
|
50663
|
+
named.push({ role: "picture", text: it.alt ?? "", keys: [key3] });
|
|
49126
50664
|
parts.push(
|
|
49127
50665
|
`<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
50666
|
);
|
|
@@ -49141,6 +50679,7 @@ function componentFromBlueprint(input) {
|
|
|
49141
50679
|
};
|
|
49142
50680
|
values[label] = (it.text ?? "").slice(0, 60);
|
|
49143
50681
|
if (!target) {
|
|
50682
|
+
named.push({ role: it.kind, text: it.text ?? "", keys: [label] });
|
|
49144
50683
|
parts.push(`<p class="${cls}">{{${label}}}</p>`);
|
|
49145
50684
|
} else {
|
|
49146
50685
|
const lk = `link${n}`;
|
|
@@ -49149,6 +50688,7 @@ function componentFromBlueprint(input) {
|
|
|
49149
50688
|
label: `${it.kind === "button" ? "Button" : "Link"} ${n} goes to`
|
|
49150
50689
|
};
|
|
49151
50690
|
values[lk] = target;
|
|
50691
|
+
named.push({ role: it.kind, text: it.text ?? "", keys: [label, lk] });
|
|
49152
50692
|
parts.push(
|
|
49153
50693
|
it.kind === "button" ? `<div class="${cls}">{{button ${lk} label=${label} variant="primary"}}</div>` : `<p class="${cls}">{{#link ${lk}}}{{${label}}}{{/link}}</p>`
|
|
49154
50694
|
);
|
|
@@ -49170,6 +50710,7 @@ function componentFromBlueprint(input) {
|
|
|
49170
50710
|
maxLength: 3e3
|
|
49171
50711
|
};
|
|
49172
50712
|
values[key2] = doc;
|
|
50713
|
+
named.push({ role: textRole(it), text: it.text ?? "", keys: [key2] });
|
|
49173
50714
|
parts.push(`<div class="${cls}">{{rich ${key2}}}</div>`);
|
|
49174
50715
|
css.push(`.gx__i${n} { ${place}; ${typeCss(it.style, false)}; }`);
|
|
49175
50716
|
phone.push(`.gx__i${n} { ${typeCss(it.style, true)}; }`);
|
|
@@ -49183,6 +50724,7 @@ function componentFromBlueprint(input) {
|
|
|
49183
50724
|
...it.kind === "text" ? { multiline: true } : {}
|
|
49184
50725
|
};
|
|
49185
50726
|
values[key2] = text7;
|
|
50727
|
+
named.push({ role: it.kind === "heading" ? "heading" : textRole(it), text: text7, keys: [key2] });
|
|
49186
50728
|
const tag = it.kind === "heading" ? headingSeen ? "h3" : "h2" : "p";
|
|
49187
50729
|
if (it.kind === "heading") headingSeen = true;
|
|
49188
50730
|
parts.push(`<${tag} class="${cls}">{{${key2}}}</${tag}>`);
|
|
@@ -49190,6 +50732,14 @@ function componentFromBlueprint(input) {
|
|
|
49190
50732
|
phone.push(`.gx__i${n} { ${typeCss(it.style, true)}; }`);
|
|
49191
50733
|
});
|
|
49192
50734
|
if (!parts.length) return void 0;
|
|
50735
|
+
for (const [i, label] of partLabels(named).entries()) {
|
|
50736
|
+
const part = named[i];
|
|
50737
|
+
const [first2, second] = part.keys;
|
|
50738
|
+
const f = fields[first2];
|
|
50739
|
+
if (f) fields[first2] = { ...f, label };
|
|
50740
|
+
const l = second ? fields[second] : void 0;
|
|
50741
|
+
if (second && l) fields[second] = { ...l, label: `${label} goes to` };
|
|
50742
|
+
}
|
|
49193
50743
|
css.push(
|
|
49194
50744
|
`@media (max-width: 767px) {
|
|
49195
50745
|
.gx { display: flex; flex-direction: column; gap: var(--os-space-sm); }
|
|
@@ -49212,6 +50762,53 @@ ${parts.map((p) => ` ${p}`).join("\n")}
|
|
|
49212
50762
|
};
|
|
49213
50763
|
return { def, fields: values };
|
|
49214
50764
|
}
|
|
50765
|
+
function textRole(it) {
|
|
50766
|
+
const words4 = (it.text ?? "").trim().split(/\s+/).filter(Boolean).length;
|
|
50767
|
+
const caps = it.style?.case === "uppercase" || /^[^a-z]*[A-Z][^a-z]*$/.test(it.text ?? "");
|
|
50768
|
+
return words4 <= 5 && (caps || (it.style?.size ?? 16) <= 14) ? "small" : "paragraph";
|
|
50769
|
+
}
|
|
50770
|
+
var ROLE_LABEL = {
|
|
50771
|
+
heading: "Heading",
|
|
50772
|
+
small: "Small label",
|
|
50773
|
+
paragraph: "Paragraph",
|
|
50774
|
+
picture: "Picture",
|
|
50775
|
+
button: "Button",
|
|
50776
|
+
link: "Link"
|
|
50777
|
+
};
|
|
50778
|
+
function snippet(text7) {
|
|
50779
|
+
const words4 = text7.replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
|
|
50780
|
+
let out = "";
|
|
50781
|
+
for (const w of words4) {
|
|
50782
|
+
if (`${out} ${w}`.trim().length > 24) break;
|
|
50783
|
+
out = `${out} ${w}`.trim();
|
|
50784
|
+
}
|
|
50785
|
+
if (!out && words4[0]) out = words4[0].slice(0, 22);
|
|
50786
|
+
if (!out) return "";
|
|
50787
|
+
const cased = /[a-z]/.test(out) ? out : out.charAt(0) + out.slice(1).toLowerCase();
|
|
50788
|
+
return `${cased}${out.length < text7.trim().length ? "\u2026" : ""}`;
|
|
50789
|
+
}
|
|
50790
|
+
function partLabels(parts) {
|
|
50791
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
50792
|
+
for (const p of parts) counts2.set(p.role, (counts2.get(p.role) ?? 0) + 1);
|
|
50793
|
+
const used = /* @__PURE__ */ new Set();
|
|
50794
|
+
const seen = /* @__PURE__ */ new Map();
|
|
50795
|
+
return parts.map((p) => {
|
|
50796
|
+
const nth = (seen.get(p.role) ?? 0) + 1;
|
|
50797
|
+
seen.set(p.role, nth);
|
|
50798
|
+
const base = ROLE_LABEL[p.role];
|
|
50799
|
+
let label;
|
|
50800
|
+
if ((counts2.get(p.role) ?? 0) === 1) label = base;
|
|
50801
|
+
else if (p.role === "button" && nth === 1) label = "Main button";
|
|
50802
|
+
else {
|
|
50803
|
+
const words4 = p.role === "picture" ? "" : snippet(p.text);
|
|
50804
|
+
label = words4 ? `${base} \u201C${words4}\u201D` : `${base} ${nth}`;
|
|
50805
|
+
}
|
|
50806
|
+
let unique = label;
|
|
50807
|
+
for (let k = 2; used.has(unique); k++) unique = `${label} ${k}`;
|
|
50808
|
+
used.add(unique);
|
|
50809
|
+
return unique.slice(0, 60);
|
|
50810
|
+
});
|
|
50811
|
+
}
|
|
49215
50812
|
function richDoc(runs, origin) {
|
|
49216
50813
|
if (!runs?.length) return void 0;
|
|
49217
50814
|
const content3 = [];
|
|
@@ -49742,8 +51339,8 @@ function tuneRound(input, measured, groups = TUNE_GROUPS, opts = {}) {
|
|
|
49742
51339
|
if (b.type !== "text" || done) continue;
|
|
49743
51340
|
const doc2 = b.fields.content;
|
|
49744
51341
|
for (const node2 of doc2?.content ?? []) {
|
|
49745
|
-
const
|
|
49746
|
-
if (node2.type !== "heading" || norm6(
|
|
51342
|
+
const words4 = (node2.content ?? []).map((x) => x.text ?? "").join("");
|
|
51343
|
+
if (node2.type !== "heading" || norm6(words4) !== norm6(i.text)) continue;
|
|
49747
51344
|
if (pick === "lead" || node2.attrs?.preset === pick) break;
|
|
49748
51345
|
node2.attrs = { ...node2.attrs ?? {}, preset: pick };
|
|
49749
51346
|
done = true;
|
|
@@ -49896,7 +51493,12 @@ function tuneRound(input, measured, groups = TUNE_GROUPS, opts = {}) {
|
|
|
49896
51493
|
const theme = themes?.[old.style.colorTheme ?? "light"];
|
|
49897
51494
|
const gen = componentFromBlueprint({
|
|
49898
51495
|
name,
|
|
49899
|
-
label:
|
|
51496
|
+
label: sectionLabel(
|
|
51497
|
+
s2.heading ?? o.heading?.text,
|
|
51498
|
+
o.blueprint,
|
|
51499
|
+
page.title || (page.slug ? page.slug : "Home"),
|
|
51500
|
+
s2.index + 1
|
|
51501
|
+
),
|
|
49900
51502
|
section: o,
|
|
49901
51503
|
roles,
|
|
49902
51504
|
...theme ? {
|
|
@@ -49957,6 +51559,14 @@ function setRole(doc, theme, role, value) {
|
|
|
49957
51559
|
doc.styles.overrides = overrides;
|
|
49958
51560
|
return true;
|
|
49959
51561
|
}
|
|
51562
|
+
function sectionLabel(heading2, blueprint, pageTitle, n) {
|
|
51563
|
+
const first2 = heading2?.trim() || blueprint?.find((i) => i.kind === "heading" && i.text?.trim())?.text || blueprint?.find((i) => i.kind === "text" && i.text?.trim())?.text;
|
|
51564
|
+
const words4 = (first2 ?? "").replace(/\s+/g, " ").trim();
|
|
51565
|
+
if (!words4) return `${pageTitle} section ${n}`.slice(0, 60);
|
|
51566
|
+
const short = words4.length > 40 ? `${words4.slice(0, 40).replace(/\s+\S*$/, "")}\u2026` : words4;
|
|
51567
|
+
const cased = /[a-z]/.test(short) ? short : short.charAt(0) + short.slice(1).toLowerCase();
|
|
51568
|
+
return cased.slice(0, 60);
|
|
51569
|
+
}
|
|
49960
51570
|
|
|
49961
51571
|
// ../import/src/index.ts
|
|
49962
51572
|
var REPORT_FILE = "IMPORT-REPORT.md";
|
|
@@ -50166,6 +51776,9 @@ async function detectSite(url, opts = {}) {
|
|
|
50166
51776
|
return { ...detectPlatform(r.text, { "content-type": r.contentType }), url: r.url || base };
|
|
50167
51777
|
}
|
|
50168
51778
|
async function importSite(opts) {
|
|
51779
|
+
return phoneReady(await importAnySite(opts));
|
|
51780
|
+
}
|
|
51781
|
+
async function importAnySite(opts) {
|
|
50169
51782
|
let detection;
|
|
50170
51783
|
let platform;
|
|
50171
51784
|
if (opts.repoDir) return importGitHub(opts);
|
|
@@ -50228,7 +51841,7 @@ async function renderAgentsMd(root, siteTitle) {
|
|
|
50228
51841
|
} catch {
|
|
50229
51842
|
}
|
|
50230
51843
|
}
|
|
50231
|
-
const { generateAgentsMd: generateAgentsMd2 } = await import("./agents-md-
|
|
51844
|
+
const { generateAgentsMd: generateAgentsMd2 } = await import("./agents-md-OWDSHBGF.js");
|
|
50232
51845
|
return generateAgentsMd2(title ? { siteTitle: title } : {});
|
|
50233
51846
|
}
|
|
50234
51847
|
async function writeAgentsMd(root, siteTitle) {
|
|
@@ -50507,7 +52120,7 @@ function compareFidelityTool(opts) {
|
|
|
50507
52120
|
[],
|
|
50508
52121
|
"Install Playwright next to the CLI (npm i -D playwright && npx playwright install chromium) and restart the MCP server."
|
|
50509
52122
|
);
|
|
50510
|
-
const widths = input.device === "desktop" ? [DESKTOP] : input.device === "mobile" ? [
|
|
52123
|
+
const widths = input.device === "desktop" ? [DESKTOP] : input.device === "mobile" ? [PHONE2] : [DESKTOP, PHONE2];
|
|
50511
52124
|
const run2 = opts.compare ?? runCompare;
|
|
50512
52125
|
const r = await run2(u.toString(), opts.siteRoot, {
|
|
50513
52126
|
...input.pages?.length ? { pages: input.pages } : {},
|
|
@@ -50710,8 +52323,8 @@ var DEFAULTS = {
|
|
|
50710
52323
|
products: []
|
|
50711
52324
|
};
|
|
50712
52325
|
var FILE = {
|
|
50713
|
-
rss: { file: "rss.xml", label: "RSS", mime: "application/rss+xml" },
|
|
50714
|
-
atom: { file: "atom.xml", label: "Atom", mime: "application/atom+xml" },
|
|
52326
|
+
rss: { file: "rss.xml", label: "RSS feed", mime: "application/rss+xml" },
|
|
52327
|
+
atom: { file: "atom.xml", label: "Atom feed", mime: "application/atom+xml" },
|
|
50715
52328
|
ical: { file: "calendar.ics", label: "Add to calendar (iCal)", mime: "text/calendar" },
|
|
50716
52329
|
podcast: { file: "podcast.xml", label: "Podcast feed", mime: "application/rss+xml" }
|
|
50717
52330
|
};
|
|
@@ -51526,7 +53139,11 @@ async function startPreviewServer(root, load, opts = {}) {
|
|
|
51526
53139
|
page = Object.values(view.pages).find((p) => p.slug === slug);
|
|
51527
53140
|
}
|
|
51528
53141
|
if (page) {
|
|
51529
|
-
const html = renderPreviewHtml(view, page.id, {
|
|
53142
|
+
const html = renderPreviewHtml(view, page.id, {
|
|
53143
|
+
behaviorsUrl: "/_os/behaviors",
|
|
53144
|
+
// `?os_mode=production`: exactly as published (the Phone check measures this).
|
|
53145
|
+
production: url.searchParams.get("os_mode") === "production"
|
|
53146
|
+
});
|
|
51530
53147
|
res.writeHead(200, {
|
|
51531
53148
|
"content-type": "text/html; charset=utf-8",
|
|
51532
53149
|
"cache-control": "no-store"
|
|
@@ -51590,6 +53207,7 @@ var FileSiteStore = class _FileSiteStore {
|
|
|
51590
53207
|
canPublish: store.hasGit,
|
|
51591
53208
|
canRollback: store.hasGit,
|
|
51592
53209
|
canScreenshot: Boolean(options.screenshotter),
|
|
53210
|
+
canMeasurePhone: Boolean(options.screenshotter),
|
|
51593
53211
|
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
53212
|
};
|
|
51595
53213
|
return store;
|
|
@@ -51898,6 +53516,16 @@ Unpublished work saved as ${tag}`,
|
|
|
51898
53516
|
const url = await this.previewUrl(siteId, path2);
|
|
51899
53517
|
return this.options.screenshotter.capture(url, request);
|
|
51900
53518
|
}
|
|
53519
|
+
async measurePhone(siteId, request) {
|
|
53520
|
+
this.check(siteId);
|
|
53521
|
+
if (!this.options.screenshotter)
|
|
53522
|
+
throw new ToolError("The browser phone check is not available.", "unsupported");
|
|
53523
|
+
const doc = await loadSiteDocument(this.root);
|
|
53524
|
+
const page = doc.pages[request.pageId];
|
|
53525
|
+
const path2 = page?.slug ? `/${page.slug}/` : "/";
|
|
53526
|
+
const url = await this.previewUrl(siteId, `${path2}?os_mode=production`);
|
|
53527
|
+
return this.options.screenshotter.measure(url, request.width);
|
|
53528
|
+
}
|
|
51901
53529
|
async agentsMd() {
|
|
51902
53530
|
try {
|
|
51903
53531
|
return await readFile9(join11(this.root, "AGENTS.md"), "utf8");
|
|
@@ -52013,6 +53641,32 @@ function createScreenshotter(chromium) {
|
|
|
52013
53641
|
await page.close().catch(() => void 0);
|
|
52014
53642
|
}
|
|
52015
53643
|
},
|
|
53644
|
+
async measure(url, width) {
|
|
53645
|
+
browser ??= launch2();
|
|
53646
|
+
let b;
|
|
53647
|
+
try {
|
|
53648
|
+
b = await browser;
|
|
53649
|
+
} catch (e) {
|
|
53650
|
+
browser = void 0;
|
|
53651
|
+
throw e;
|
|
53652
|
+
}
|
|
53653
|
+
const context = await b.newContext({
|
|
53654
|
+
viewport: { width, height: PHONE_HEIGHT },
|
|
53655
|
+
deviceScaleFactor: 2,
|
|
53656
|
+
isMobile: true,
|
|
53657
|
+
hasTouch: true,
|
|
53658
|
+
reducedMotion: "reduce"
|
|
53659
|
+
});
|
|
53660
|
+
const page = await context.newPage();
|
|
53661
|
+
try {
|
|
53662
|
+
await page.goto(url, { waitUntil: "load", timeout: 3e4 });
|
|
53663
|
+
await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => void 0);
|
|
53664
|
+
await page.evaluate(PHONE_PREPARE_SCRIPT).catch(() => void 0);
|
|
53665
|
+
return await page.evaluate(PHONE_PROBE_SCRIPT);
|
|
53666
|
+
} finally {
|
|
53667
|
+
await context.close().catch(() => void 0);
|
|
53668
|
+
}
|
|
53669
|
+
},
|
|
52016
53670
|
async close() {
|
|
52017
53671
|
if (!browser) return;
|
|
52018
53672
|
const b = await browser.catch(() => void 0);
|
|
@@ -52232,6 +53886,147 @@ function rebrand(value, from, to) {
|
|
|
52232
53886
|
return changed ? out : null;
|
|
52233
53887
|
}
|
|
52234
53888
|
|
|
53889
|
+
// src/phone.ts
|
|
53890
|
+
var pathOf = (p) => p.slug ? `/${p.slug}/` : "/";
|
|
53891
|
+
async function measureSite(root, load, doc, shooter, pages, widths, now) {
|
|
53892
|
+
const server = await startPreviewServer(root, load);
|
|
53893
|
+
const out = /* @__PURE__ */ new Map();
|
|
53894
|
+
try {
|
|
53895
|
+
for (const page of pages) {
|
|
53896
|
+
const results = [];
|
|
53897
|
+
for (const w of widths)
|
|
53898
|
+
results.push(await shooter.measure(`${server.url}${pathOf(page)}?os_mode=production`, w));
|
|
53899
|
+
out.set(page.id, phoneIssuesFromProbe(doc, page.id, results, { now, actor: "phone-check" }));
|
|
53900
|
+
}
|
|
53901
|
+
} finally {
|
|
53902
|
+
await server.close();
|
|
53903
|
+
}
|
|
53904
|
+
return out;
|
|
53905
|
+
}
|
|
53906
|
+
async function runPhoneCheck(root, opts = {}) {
|
|
53907
|
+
const now = opts.now ?? Date.now();
|
|
53908
|
+
const widths = opts.widths ?? PHONE_WIDTHS;
|
|
53909
|
+
let doc = await loadSiteDocument(root);
|
|
53910
|
+
const pick = (d) => Object.values(d.pages).filter(
|
|
53911
|
+
(p) => !opts.pages?.length || opts.pages.includes(p.id) || opts.pages.includes(p.slug)
|
|
53912
|
+
);
|
|
53913
|
+
let shooter = opts.screenshotter;
|
|
53914
|
+
let note;
|
|
53915
|
+
let owned = false;
|
|
53916
|
+
if (!shooter && opts.rendered !== false) {
|
|
53917
|
+
const pw = await loadPlaywright();
|
|
53918
|
+
if ("error" in pw)
|
|
53919
|
+
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.";
|
|
53920
|
+
else {
|
|
53921
|
+
shooter = createScreenshotter(pw.chromium);
|
|
53922
|
+
owned = true;
|
|
53923
|
+
}
|
|
53924
|
+
}
|
|
53925
|
+
let current2 = doc;
|
|
53926
|
+
const load = async () => current2;
|
|
53927
|
+
const measure2 = async () => shooter && opts.rendered !== false ? measureSite(root, load, current2, shooter, pick(current2), widths, now).catch((e) => {
|
|
53928
|
+
note = `The browser check couldn't run: ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`;
|
|
53929
|
+
return /* @__PURE__ */ new Map();
|
|
53930
|
+
}) : /* @__PURE__ */ new Map();
|
|
53931
|
+
try {
|
|
53932
|
+
let rendered2 = await measure2();
|
|
53933
|
+
const fixed = [];
|
|
53934
|
+
const written = [];
|
|
53935
|
+
if (opts.fix) {
|
|
53936
|
+
const r = fixForPhones(doc, {
|
|
53937
|
+
now,
|
|
53938
|
+
actor: "phone-check",
|
|
53939
|
+
rendered: [...rendered2.values()].flat()
|
|
53940
|
+
});
|
|
53941
|
+
if (r.ops.length) {
|
|
53942
|
+
doc = r.doc;
|
|
53943
|
+
current2 = doc;
|
|
53944
|
+
const w = await writeSiteDocument(root, doc);
|
|
53945
|
+
written.push(...w.written);
|
|
53946
|
+
fixed.push(...r.fixed);
|
|
53947
|
+
rendered2 = await measure2();
|
|
53948
|
+
}
|
|
53949
|
+
}
|
|
53950
|
+
const statics = phoneCheck(doc, void 0, { now, actor: "phone-check" });
|
|
53951
|
+
const pages = pick(doc).map((p) => ({
|
|
53952
|
+
page: p.id,
|
|
53953
|
+
title: p.title,
|
|
53954
|
+
path: pathOf(p),
|
|
53955
|
+
issues: sortIssues([
|
|
53956
|
+
...statics.filter((i) => i.page === p.id),
|
|
53957
|
+
...(rendered2.get(p.id) ?? []).filter((i) => i.page === p.id)
|
|
53958
|
+
])
|
|
53959
|
+
}));
|
|
53960
|
+
const elsewhere = [
|
|
53961
|
+
...statics.filter((i) => !i.page || !doc.pages[i.page]),
|
|
53962
|
+
// Footer findings repeat on every page: report each once.
|
|
53963
|
+
...dedupe([...rendered2.values()].flat().filter((i) => i.page === "footer"))
|
|
53964
|
+
];
|
|
53965
|
+
const issues = sortIssues([...elsewhere, ...pages.flatMap((p) => p.issues)]);
|
|
53966
|
+
const counts2 = { error: 0, warning: 0, info: 0 };
|
|
53967
|
+
for (const i of issues) counts2[i.severity]++;
|
|
53968
|
+
return {
|
|
53969
|
+
rendered: !!shooter && opts.rendered !== false && !note,
|
|
53970
|
+
...note ? { note } : {},
|
|
53971
|
+
issues,
|
|
53972
|
+
pages,
|
|
53973
|
+
counts: counts2,
|
|
53974
|
+
fixed,
|
|
53975
|
+
written
|
|
53976
|
+
};
|
|
53977
|
+
} finally {
|
|
53978
|
+
if (owned) await shooter?.close();
|
|
53979
|
+
}
|
|
53980
|
+
}
|
|
53981
|
+
function dedupe(issues) {
|
|
53982
|
+
const seen = /* @__PURE__ */ new Set();
|
|
53983
|
+
return issues.filter((i) => {
|
|
53984
|
+
const key2 = `${i.rule}|${i.message}`;
|
|
53985
|
+
if (seen.has(key2)) return false;
|
|
53986
|
+
seen.add(key2);
|
|
53987
|
+
return true;
|
|
53988
|
+
});
|
|
53989
|
+
}
|
|
53990
|
+
function formatPhoneReport(r, paint2) {
|
|
53991
|
+
const lines = [];
|
|
53992
|
+
lines.push(paint2(1, r.rendered ? "Phone check (measured at 360 and 390 px)" : "Phone check"));
|
|
53993
|
+
if (r.note) lines.push(paint2(33, r.note));
|
|
53994
|
+
if (r.fixed.length) {
|
|
53995
|
+
lines.push(
|
|
53996
|
+
paint2(32, `Fixed for phones: ${r.fixed.length} change${r.fixed.length === 1 ? "" : "s"}.`)
|
|
53997
|
+
);
|
|
53998
|
+
for (const f of uniqueLabels(r.fixed)) lines.push(` \u2713 ${f}`);
|
|
53999
|
+
}
|
|
54000
|
+
const groups = /* @__PURE__ */ new Map();
|
|
54001
|
+
for (const i of r.issues) {
|
|
54002
|
+
const page = r.pages.find((p) => p.page === i.page);
|
|
54003
|
+
const key2 = page ? `${page.title} (${page.path})` : i.page === "footer" ? "Footer" : "Whole site";
|
|
54004
|
+
groups.set(key2, [...groups.get(key2) ?? [], i]);
|
|
54005
|
+
}
|
|
54006
|
+
for (const [where2, list5] of groups) {
|
|
54007
|
+
lines.push("", paint2(1, where2));
|
|
54008
|
+
for (const i of list5) {
|
|
54009
|
+
const sev = i.severity === "error" ? paint2(31, "error ") : i.severity === "warning" ? paint2(33, "warning") : "info ";
|
|
54010
|
+
lines.push(` ${sev} ${i.message}`);
|
|
54011
|
+
lines.push(` ${i.autofix ? `Fix for phones: ${i.autofix.label}.` : i.fix}`);
|
|
54012
|
+
}
|
|
54013
|
+
}
|
|
54014
|
+
const { error: error2, warning, info } = r.counts;
|
|
54015
|
+
lines.push("");
|
|
54016
|
+
lines.push(
|
|
54017
|
+
error2 + warning === 0 ? paint2(
|
|
54018
|
+
32,
|
|
54019
|
+
`Looks great on phones${info ? ` (${info} suggestion${info === 1 ? "" : "s"})` : ""}.`
|
|
54020
|
+
) : `${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." : ""}`
|
|
54021
|
+
);
|
|
54022
|
+
return lines.join("\n");
|
|
54023
|
+
}
|
|
54024
|
+
function uniqueLabels(fixed) {
|
|
54025
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
54026
|
+
for (const f of fixed) counts2.set(f.label, (counts2.get(f.label) ?? 0) + 1);
|
|
54027
|
+
return [...counts2].map(([l, n]) => n > 1 ? `${l} (${n}\xD7)` : l);
|
|
54028
|
+
}
|
|
54029
|
+
|
|
52235
54030
|
// src/wow-first.ts
|
|
52236
54031
|
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
54032
|
function assertGitHubPagesStart(target) {
|
|
@@ -52410,6 +54205,7 @@ async function runImportSite(io, url, dirArg, opts = {}) {
|
|
|
52410
54205
|
await writeShell(dir, existing.deploy?.target ?? "github-pages", slug);
|
|
52411
54206
|
await writeImport(dir, result);
|
|
52412
54207
|
await writeAgentsMd(dir, result.doc.site.title);
|
|
54208
|
+
await phoneFixRendered(dir, opts, io);
|
|
52413
54209
|
const check2 = await runCheck(dir);
|
|
52414
54210
|
const r2 = { dir, result, check: check2 };
|
|
52415
54211
|
if (opts.summary) await writeSummary(resolve6(io.cwd, opts.summary), r2);
|
|
@@ -52427,11 +54223,21 @@ async function runImportSite(io, url, dirArg, opts = {}) {
|
|
|
52427
54223
|
await rm6(join15(dir, entry), { recursive: true, force: true });
|
|
52428
54224
|
throw e;
|
|
52429
54225
|
}
|
|
54226
|
+
await phoneFixRendered(dir, opts, io);
|
|
52430
54227
|
const check = await runCheck(dir);
|
|
52431
54228
|
const r = { dir, result, check };
|
|
52432
54229
|
if (opts.summary) await writeSummary(resolve6(io.cwd, opts.summary), r);
|
|
52433
54230
|
return r;
|
|
52434
54231
|
}
|
|
54232
|
+
async function phoneFixRendered(dir, opts, io) {
|
|
54233
|
+
if (!opts.render) return;
|
|
54234
|
+
try {
|
|
54235
|
+
const r = await runPhoneCheck(dir, { fix: true });
|
|
54236
|
+
if (r.fixed.length) io.err(` Fixed ${r.fixed.length} thing(s) for phones.`);
|
|
54237
|
+
} catch (e) {
|
|
54238
|
+
io.err(` Couldn't check the pages on a phone: ${e.message.split("\n")[0]}`);
|
|
54239
|
+
}
|
|
54240
|
+
}
|
|
52435
54241
|
function importCounts(r) {
|
|
52436
54242
|
const { result, check } = r;
|
|
52437
54243
|
return {
|
|
@@ -56698,8 +58504,8 @@ var escapeText3 = (s2) => s2.replace(/&/g, "&").replace(/</g, "<").replac
|
|
|
56698
58504
|
var escapeAttr2 = (s2) => s2.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/\{\{/g, "{{");
|
|
56699
58505
|
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
58506
|
function camel(s2) {
|
|
56701
|
-
const
|
|
56702
|
-
const out =
|
|
58507
|
+
const words4 = s2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean).map((w) => w.toLowerCase());
|
|
58508
|
+
const out = words4.map((w, i) => i === 0 ? w : w.charAt(0).toUpperCase() + w.slice(1)).join("").replace(/^[^a-z]+/, "");
|
|
56703
58509
|
return out.slice(0, 32);
|
|
56704
58510
|
}
|
|
56705
58511
|
function semanticName(el) {
|
|
@@ -56742,9 +58548,34 @@ function nameFor(el, fallback2 = "text") {
|
|
|
56742
58548
|
if (!el) return fallback2;
|
|
56743
58549
|
return semanticName(el) ?? TAG_NAME[tagOf(el)] ?? fallback2;
|
|
56744
58550
|
}
|
|
56745
|
-
|
|
56746
|
-
|
|
56747
|
-
|
|
58551
|
+
var PLAIN_WORDS = {
|
|
58552
|
+
cta: "button",
|
|
58553
|
+
btn: "button",
|
|
58554
|
+
img: "picture",
|
|
58555
|
+
image: "picture",
|
|
58556
|
+
pic: "picture",
|
|
58557
|
+
photo: "picture",
|
|
58558
|
+
desc: "description",
|
|
58559
|
+
txt: "text",
|
|
58560
|
+
href: "link",
|
|
58561
|
+
url: "link",
|
|
58562
|
+
kicker: "small label",
|
|
58563
|
+
eyebrow: "small label",
|
|
58564
|
+
overline: "small label",
|
|
58565
|
+
lede: "intro",
|
|
58566
|
+
lead: "intro",
|
|
58567
|
+
subhead: "subheading",
|
|
58568
|
+
bg: "background",
|
|
58569
|
+
nav: "menu"
|
|
58570
|
+
};
|
|
58571
|
+
function humanize2(key2, kind) {
|
|
58572
|
+
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);
|
|
58573
|
+
const linky = words4.some((w) => w === "button" || w === "link");
|
|
58574
|
+
if (linky && words4.length > 1 && words4[words4.length - 1] === "label")
|
|
58575
|
+
words4[words4.length - 1] = "text";
|
|
58576
|
+
if (kind === "link" && words4[words4.length - 1] !== "link") words4.push("goes to");
|
|
58577
|
+
const out = words4.join(" ") || "text";
|
|
58578
|
+
return (out.charAt(0).toUpperCase() + out.slice(1)).slice(0, 60);
|
|
56748
58579
|
}
|
|
56749
58580
|
var RESERVED2 = /* @__PURE__ */ new Set([
|
|
56750
58581
|
"rich",
|
|
@@ -56786,7 +58617,7 @@ var FieldScope = class {
|
|
|
56786
58617
|
}
|
|
56787
58618
|
add(base, spec, value) {
|
|
56788
58619
|
const key2 = this.key(base);
|
|
56789
|
-
const label = humanize2(key2);
|
|
58620
|
+
const label = humanize2(key2, spec.kind);
|
|
56790
58621
|
this.fields[key2] = { ...spec, label };
|
|
56791
58622
|
this.values[key2] = value;
|
|
56792
58623
|
return key2;
|
|
@@ -57061,10 +58892,10 @@ var SectionConverter = class {
|
|
|
57061
58892
|
async shapes(els, w) {
|
|
57062
58893
|
const map2 = /* @__PURE__ */ new Map();
|
|
57063
58894
|
const sig = (e) => `${tagOf(e)}.${classes(e).sort().join(".")}`;
|
|
57064
|
-
const
|
|
57065
|
-
for (const e of els)
|
|
58895
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
58896
|
+
for (const e of els) counts2.set(sig(e), (counts2.get(sig(e)) ?? 0) + 1);
|
|
57066
58897
|
for (const e of els) {
|
|
57067
|
-
if ((
|
|
58898
|
+
if ((counts2.get(sig(e)) ?? 0) < 2) continue;
|
|
57068
58899
|
const scope = new FieldScope();
|
|
57069
58900
|
const tpl = await this.gen(e, scope, { ...w, listDepth: w.listDepth + 1 });
|
|
57070
58901
|
const kinds = Object.entries(scope.fields).map(([k, f]) => `${k}:${f.kind}`).join(",");
|
|
@@ -58028,7 +59859,11 @@ function tokensFromPage(css, fontLinks) {
|
|
|
58028
59859
|
const btn = step(/class:(?:btn|button)\b/i);
|
|
58029
59860
|
if (btn) type.button = btn;
|
|
58030
59861
|
if (Object.keys(type).length) tokens.type = type;
|
|
58031
|
-
|
|
59862
|
+
const nColors = Object.keys(tokens.colors ?? {}).length;
|
|
59863
|
+
const nType = Object.keys(tokens.type ?? {}).length;
|
|
59864
|
+
const pl = (k, w) => `${k} ${w}${k === 1 ? "" : "s"}`;
|
|
59865
|
+
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")}` : ""}.`;
|
|
59866
|
+
return { tokens, notes: [note], families };
|
|
58032
59867
|
}
|
|
58033
59868
|
function linkFamilies(hrefs) {
|
|
58034
59869
|
const out = [];
|
|
@@ -58074,6 +59909,9 @@ function siteHref(bundle, fromPath, href, pagePaths) {
|
|
|
58074
59909
|
return resolved;
|
|
58075
59910
|
}
|
|
58076
59911
|
async function importHtmlBundle(bundle, opts) {
|
|
59912
|
+
return phoneReady(await importBundle(bundle, opts));
|
|
59913
|
+
}
|
|
59914
|
+
async function importBundle(bundle, opts) {
|
|
58077
59915
|
const progress = opts.onProgress ?? (() => void 0);
|
|
58078
59916
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
58079
59917
|
const notices = [];
|
|
@@ -58234,7 +60072,7 @@ ${await r.text()}`;
|
|
|
58234
60072
|
}
|
|
58235
60073
|
const docTitle = text(find(home.root, (e) => e.tagName === "title"));
|
|
58236
60074
|
const wordmark = brand ? text(brand).replace(/\s+/g, " ").trim() : "";
|
|
58237
|
-
const siteTitle = (
|
|
60075
|
+
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
60076
|
const logoImg = brand ? find(brand, (e) => e.tagName === "img") : void 0;
|
|
58239
60077
|
const logoRef = logoImg ? resolveRef(bundle, attr(logoImg, "src") ?? "", home.path) : void 0;
|
|
58240
60078
|
const segsByPage = /* @__PURE__ */ new Map();
|
|
@@ -58298,6 +60136,7 @@ ${await r.text()}`;
|
|
|
58298
60136
|
const placeholder = email ? attr(email, "placeholder") : void 0;
|
|
58299
60137
|
blocks2.push({
|
|
58300
60138
|
kind: "newsletter",
|
|
60139
|
+
...signupAsksName(f) ? { askName: true } : {},
|
|
58301
60140
|
...label3 ? { buttonLabel: label3.slice(0, 40) } : {},
|
|
58302
60141
|
...placeholder ? { placeholder } : {}
|
|
58303
60142
|
});
|
|
@@ -58661,8 +60500,8 @@ function componentNameFor(el, pagePath2) {
|
|
|
58661
60500
|
)
|
|
58662
60501
|
);
|
|
58663
60502
|
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 ?? (
|
|
60503
|
+
const words4 = text(heading2).split(/\s+/).slice(0, 3).join(" ");
|
|
60504
|
+
const raw2 = (id && id.length > 2 && !/^(?:top|main|content|section\d*)$/i.test(id) ? id : void 0) ?? cls ?? (words4 || el.tagName);
|
|
58666
60505
|
const slug = raw2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^(\d)/, "s-$1").slice(0, 30).replace(/-+$/, "");
|
|
58667
60506
|
const prefix = pagePath2 === "/" ? "" : `${pagePath2.split("/").filter(Boolean).pop()}-`;
|
|
58668
60507
|
return `${prefix}${slug || "section"}`.replace(/^[^a-z]+/, "s-").slice(0, 36).replace(/-+$/, "");
|
|
@@ -59302,7 +61141,7 @@ async function runMcp(io, dir, opts = {}) {
|
|
|
59302
61141
|
// src/fidelity.ts
|
|
59303
61142
|
var DESKTOP = 1440;
|
|
59304
61143
|
var COMPONENT_RETRY_BELOW = 75;
|
|
59305
|
-
var
|
|
61144
|
+
var PHONE2 = 390;
|
|
59306
61145
|
async function launch() {
|
|
59307
61146
|
const pw = await loadPlaywright();
|
|
59308
61147
|
if ("error" in pw) throw new CliError(pw.error);
|
|
@@ -59345,7 +61184,7 @@ async function lookProber() {
|
|
|
59345
61184
|
const out = {};
|
|
59346
61185
|
for (const [key2, width] of [
|
|
59347
61186
|
["desktop", DESKTOP],
|
|
59348
|
-
["mobile",
|
|
61187
|
+
["mobile", PHONE2]
|
|
59349
61188
|
]) {
|
|
59350
61189
|
const page = await open(browser, url, width);
|
|
59351
61190
|
try {
|
|
@@ -59494,7 +61333,7 @@ async function runCompare(original, dir, opts = {}, log = () => void 0) {
|
|
|
59494
61333
|
await mkdir8(out, { recursive: true });
|
|
59495
61334
|
if (!opts.out) await writeFile10(join18(out, ".gitignore"), "*\n");
|
|
59496
61335
|
const pages = (opts.pages?.length ? opts.pages : defaultPages(doc)).map((p) => `/${slugOf2(p)}`);
|
|
59497
|
-
const widths = opts.widths ?? [DESKTOP,
|
|
61336
|
+
const widths = opts.widths ?? [DESKTOP, PHONE2];
|
|
59498
61337
|
const server = await startPreviewServer(root, () => loadSiteDocument(root));
|
|
59499
61338
|
const browser = await launch();
|
|
59500
61339
|
const oldPath = /* @__PURE__ */ new Map();
|
|
@@ -59514,7 +61353,7 @@ async function runCompare(original, dir, opts = {}, log = () => void 0) {
|
|
|
59514
61353
|
await page.close().catch(() => void 0);
|
|
59515
61354
|
}
|
|
59516
61355
|
};
|
|
59517
|
-
const
|
|
61356
|
+
const measure2 = async () => {
|
|
59518
61357
|
const comparisons = [];
|
|
59519
61358
|
const measured = [];
|
|
59520
61359
|
const shots = /* @__PURE__ */ new Map();
|
|
@@ -59559,7 +61398,7 @@ async function runCompare(original, dir, opts = {}, log = () => void 0) {
|
|
|
59559
61398
|
let best;
|
|
59560
61399
|
let tuning;
|
|
59561
61400
|
try {
|
|
59562
|
-
best = await
|
|
61401
|
+
best = await measure2();
|
|
59563
61402
|
const first2 = fidelityReport(best.comparisons);
|
|
59564
61403
|
const rounds = Math.max(0, Math.min(5, opts.tune ?? 0));
|
|
59565
61404
|
if (rounds && best.measured.length) {
|
|
@@ -59610,7 +61449,7 @@ async function runCompare(original, dir, opts = {}, log = () => void 0) {
|
|
|
59610
61449
|
await writeSiteDocument(root, bestDoc);
|
|
59611
61450
|
continue;
|
|
59612
61451
|
}
|
|
59613
|
-
const next = await
|
|
61452
|
+
const next = await measure2();
|
|
59614
61453
|
const score = fidelityReport(next.comparisons).exact ?? 0;
|
|
59615
61454
|
const better = score > bestScore + 0.1;
|
|
59616
61455
|
log(
|
|
@@ -59728,10 +61567,17 @@ function fidelitySummary(report, tuning, thumbs) {
|
|
|
59728
61567
|
const before = new Map(
|
|
59729
61568
|
(tuning?.before.pages ?? []).map((p) => [`${p.path}@${p.viewport}`, p.score])
|
|
59730
61569
|
);
|
|
61570
|
+
const strictBefore = new Map(
|
|
61571
|
+
(tuning?.before.pages ?? []).map((p) => [`${p.path}@${p.viewport}`, p.strict?.score])
|
|
61572
|
+
);
|
|
59731
61573
|
return {
|
|
59732
61574
|
version: 1,
|
|
59733
61575
|
score: report.score,
|
|
61576
|
+
// The same pages by the strict v3 scorer, side by side (additive: older readers ignore it).
|
|
61577
|
+
...report.strict ? { strict: report.strict.score } : {},
|
|
61578
|
+
...report.scorer ? { scorer: report.scorer } : {},
|
|
59734
61579
|
...tuning ? { before: tuning.before.score, changes: tuning.changes.slice(0, 40) } : {},
|
|
61580
|
+
...tuning?.before.strict ? { strictBefore: tuning.before.strict.score } : {},
|
|
59735
61581
|
...tuning?.withoutComponents !== void 0 ? {
|
|
59736
61582
|
withoutComponents: tuning.withoutComponents,
|
|
59737
61583
|
...tuning.withComponents !== void 0 ? { withComponents: tuning.withComponents } : {}
|
|
@@ -59740,11 +61586,15 @@ function fidelitySummary(report, tuning, thumbs) {
|
|
|
59740
61586
|
const key2 = `${p.path}@${p.viewport}`;
|
|
59741
61587
|
const b = before.get(key2);
|
|
59742
61588
|
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);
|
|
61589
|
+
const sb = strictBefore.get(key2);
|
|
59743
61590
|
return {
|
|
59744
61591
|
path: p.path,
|
|
59745
61592
|
device: p.viewport < 700 ? "phone" : "desktop",
|
|
59746
61593
|
score: p.score,
|
|
61594
|
+
...p.strict ? { strict: p.strict.score } : {},
|
|
61595
|
+
...p.corrections?.length ? { corrections: p.corrections.slice(0, 6) } : {},
|
|
59747
61596
|
...b !== void 0 ? { before: b } : {},
|
|
61597
|
+
...sb !== void 0 ? { strictBefore: sb } : {},
|
|
59748
61598
|
...thumbs[key2] ? { image: thumbs[key2] } : {},
|
|
59749
61599
|
...worst.length ? { differs: worst } : {}
|
|
59750
61600
|
};
|
|
@@ -59788,6 +61638,7 @@ async function addFidelityToSummary(file, r) {
|
|
|
59788
61638
|
const summary = JSON.parse(await readFile14(file, "utf8"));
|
|
59789
61639
|
summary.fidelity = {
|
|
59790
61640
|
score: r.report.score,
|
|
61641
|
+
...r.report.strict ? { strict: r.report.strict.score } : {},
|
|
59791
61642
|
...r.tuning ? { before: r.tuning.before.score } : {}
|
|
59792
61643
|
};
|
|
59793
61644
|
await writeFile10(file, `${JSON.stringify(summary, null, 2)}
|
|
@@ -59796,8 +61647,9 @@ async function addFidelityToSummary(file, r) {
|
|
|
59796
61647
|
}
|
|
59797
61648
|
}
|
|
59798
61649
|
function compareSummary(r) {
|
|
61650
|
+
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
61651
|
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"})` : ""}
|
|
61652
|
+
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
61653
|
];
|
|
59802
61654
|
for (const p of r.report.pages) {
|
|
59803
61655
|
const off = [
|
|
@@ -59805,7 +61657,7 @@ function compareSummary(r) {
|
|
|
59805
61657
|
...p.sections.filter((s2) => s2.score < 80).map((s2) => `section ${s2.index + 1}${s2.heading ? ` "${s2.heading.slice(0, 30)}"` : ""}`)
|
|
59806
61658
|
];
|
|
59807
61659
|
lines.push(
|
|
59808
|
-
` ${p.path} ${p.viewport}px: ${p.score}/100${off.length ? ` \u2014 differs: ${off.slice(0, 6).join(", ")}${off.length > 6 ? "\u2026" : ""}` : ""}`
|
|
61660
|
+
` ${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
61661
|
);
|
|
59810
61662
|
}
|
|
59811
61663
|
lines.push(` Report and side-by-side images: ${join18(r.out, "FIDELITY-REPORT.md")}`);
|
|
@@ -60025,14 +61877,23 @@ async function runMigrate(root, opts = {}) {
|
|
|
60025
61877
|
var VERSION = "0.1.0";
|
|
60026
61878
|
function createProgram(io, setExit, deps = {}) {
|
|
60027
61879
|
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(
|
|
61880
|
+
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
61881
|
async (dir, opts) => {
|
|
61882
|
+
let phone;
|
|
61883
|
+
if (opts.phone || opts.fix) {
|
|
61884
|
+
phone = await runPhoneCheck(resolve10(io.cwd, dir ?? "."), { fix: opts.fix });
|
|
61885
|
+
if (!opts.json) {
|
|
61886
|
+
io.out(formatPhoneReport(phone, (code3, s2) => paint(colorEnabled(io), code3, s2)));
|
|
61887
|
+
io.out("");
|
|
61888
|
+
}
|
|
61889
|
+
if (phone.counts.error) setExit(1);
|
|
61890
|
+
}
|
|
60030
61891
|
const budgets = {};
|
|
60031
61892
|
if (opts.mediaWarnMb !== void 0) budgets.mediaWarnBytes = opts.mediaWarnMb * 1024 * 1024;
|
|
60032
61893
|
if (opts.mediaFailMb !== void 0) budgets.mediaFailBytes = opts.mediaFailMb * 1024 * 1024;
|
|
60033
61894
|
const report = await runCheck(resolve10(io.cwd, dir ?? "."), { budgets });
|
|
60034
61895
|
if (opts.json) {
|
|
60035
|
-
io.out(JSON.stringify(report, null, 2));
|
|
61896
|
+
io.out(JSON.stringify(phone ? { ...report, phone } : report, null, 2));
|
|
60036
61897
|
} else {
|
|
60037
61898
|
if (opts.ci) for (const line of githubAnnotations(report)) io.out(line);
|
|
60038
61899
|
const color = colorEnabled(io);
|
|
@@ -60388,4 +62249,4 @@ export {
|
|
|
60388
62249
|
createProgram,
|
|
60389
62250
|
run
|
|
60390
62251
|
};
|
|
60391
|
-
//# sourceMappingURL=chunk-
|
|
62252
|
+
//# sourceMappingURL=chunk-JDVCOOGD.js.map
|