@omniharmonic/openspace-cli 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agents-md-QBMFXTIQ.js → agents-md-OWDSHBGF.js} +2 -2
- package/dist/bin.js +3 -3
- package/dist/{chunk-IXN2CNGL.js → chunk-GMKHKHUR.js} +206 -39
- package/dist/chunk-GMKHKHUR.js.map +1 -0
- package/dist/{chunk-B5C7S4S5.js → chunk-PBN36VUE.js} +2775 -184
- package/dist/chunk-PBN36VUE.js.map +1 -0
- package/dist/{chunk-EFSGZWAO.js → chunk-YAPLQ4OA.js} +155 -16
- package/dist/chunk-YAPLQ4OA.js.map +1 -0
- package/dist/{crawl-TAADG5BR.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 +4 -4
- 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-B5C7S4S5.js.map +0 -1
- package/dist/chunk-EFSGZWAO.js.map +0 -1
- package/dist/chunk-IXN2CNGL.js.map +0 -1
- /package/dist/{agents-md-QBMFXTIQ.js.map → agents-md-OWDSHBGF.js.map} +0 -0
- /package/dist/{crawl-TAADG5BR.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
|
});
|
|
@@ -15069,7 +16252,7 @@ function nearestOpenFont(family, metrics) {
|
|
|
15069
16252
|
let best;
|
|
15070
16253
|
for (const [name, cat, xHeight, capHeight, width] of list5) {
|
|
15071
16254
|
let d = metricDistance(metrics, { xHeight, capHeight, width });
|
|
15072
|
-
if (eq.includes(name)) d -=
|
|
16255
|
+
if (eq.includes(name)) d -= 4e-3 * (eq.length - eq.indexOf(name));
|
|
15073
16256
|
if (!best || d < (best.distance ?? Number.POSITIVE_INFINITY))
|
|
15074
16257
|
best = { family: name, category: cat, via: "metrics", distance: Math.max(0, d) };
|
|
15075
16258
|
}
|
|
@@ -15169,6 +16352,11 @@ async function fitGoogleWeights(font, fetchImpl = fetch) {
|
|
|
15169
16352
|
// ../import/src/fidelity/look.ts
|
|
15170
16353
|
var norm2 = (s2) => (s2 ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
15171
16354
|
var GENERIC = /^(?:serif|sans-serif|monospace|system-ui|ui-\w+|-apple-system|blinkmacsystemfont)$/i;
|
|
16355
|
+
var colorGap = (a, b) => {
|
|
16356
|
+
const p = (h) => [1, 3, 5].map((i) => Number.parseInt(h.slice(i, i + 2), 16) || 0);
|
|
16357
|
+
const [x, y] = [p(a), p(b)];
|
|
16358
|
+
return Math.sqrt(x.reduce((n, v, i) => n + (v - (y[i] ?? 0)) ** 2, 0));
|
|
16359
|
+
};
|
|
15172
16360
|
var round = (n, d = 1) => Math.round(n * 10 ** d) / 10 ** d;
|
|
15173
16361
|
function stepOf(t, mobile, opts = {}) {
|
|
15174
16362
|
return {
|
|
@@ -15241,7 +16429,12 @@ function lookFromProbe(d, m) {
|
|
|
15241
16429
|
scale[p] = { ...scale[p], font: "heading", ...w >= 100 && w <= 900 ? { weight: w } : {} };
|
|
15242
16430
|
}
|
|
15243
16431
|
}
|
|
15244
|
-
if (
|
|
16432
|
+
if (P.display)
|
|
16433
|
+
scale.display = {
|
|
16434
|
+
...stepOf(P.display, M.display, { weight: true }),
|
|
16435
|
+
font: roleOf(P.display)
|
|
16436
|
+
};
|
|
16437
|
+
else if (scale.h1) scale.display = { ...scale.h1 };
|
|
15245
16438
|
if (P.lead) scale.lead = { ...stepOf(P.lead, M.lead), font: roleOf(P.lead) };
|
|
15246
16439
|
if (P.small) scale.small = stepOf(P.small, M.small);
|
|
15247
16440
|
if (P.eyebrow)
|
|
@@ -15263,6 +16456,17 @@ function lookFromProbe(d, m) {
|
|
|
15263
16456
|
const mobileLeft = m?.layout.left;
|
|
15264
16457
|
if (L2.contentWidth && L2.contentWidth >= 480 && L2.contentWidth <= 2400 && L2.contentWidth < d.viewport - 40)
|
|
15265
16458
|
layout.contentWidth = Math.round(L2.contentWidth / 8) * 8;
|
|
16459
|
+
const page = d.pageBackground;
|
|
16460
|
+
const panels = d.sections.filter(
|
|
16461
|
+
(s2) => page && s2.background && s2.left !== void 0 && s2.width !== void 0 && s2.left >= 24 && s2.left + s2.width <= d.viewport - 24 && s2.raised === true && colorGap(s2.background, page) >= 10
|
|
16462
|
+
);
|
|
16463
|
+
if (page && d.viewport >= 768 && panels.length && panels.length * 2 >= d.sections.length) {
|
|
16464
|
+
layout.surface = "card";
|
|
16465
|
+
layout.pageBackground = page;
|
|
16466
|
+
const measures = panels.map((s2) => s2.textWidth).filter((w2) => !!w2);
|
|
16467
|
+
const w = measures.sort((a, b) => a - b)[Math.floor(measures.length / 2)];
|
|
16468
|
+
if (w && w >= 480 && w <= 1600) layout.contentWidth = Math.round(w / 8) * 8;
|
|
16469
|
+
}
|
|
15266
16470
|
if (L2.sectionPadding && L2.sectionPadding >= 16 && L2.sectionPadding <= 320)
|
|
15267
16471
|
layout.sectionPadding = Math.round(L2.sectionPadding);
|
|
15268
16472
|
if (layout.sectionPadding && m?.layout.sectionPadding && m.layout.sectionPadding <= 240)
|
|
@@ -15304,6 +16508,17 @@ function lookFromProbe(d, m) {
|
|
|
15304
16508
|
...d.eyebrowRule ? { eyebrowRule: true } : {},
|
|
15305
16509
|
...d.fonts && Object.keys(d.fonts).length ? { metrics: { ...m?.fonts, ...d.fonts } } : {},
|
|
15306
16510
|
...hosts.length ? { fontHosts: hosts } : {},
|
|
16511
|
+
...d.logoHeight && d.logoHeight >= 16 ? {
|
|
16512
|
+
logoHeight: {
|
|
16513
|
+
d: Math.min(240, d.logoHeight),
|
|
16514
|
+
...m?.logoHeight && m.logoHeight >= 16 ? { m: Math.min(240, m.logoHeight) } : {}
|
|
16515
|
+
}
|
|
16516
|
+
} : {},
|
|
16517
|
+
...d.footerBackground ? { footerBackground: d.footerBackground } : {},
|
|
16518
|
+
...d.footerColumns && d.footerColumns >= 2 && d.footerColumns <= 4 ? {
|
|
16519
|
+
footerColumns: d.footerColumns,
|
|
16520
|
+
...d.footerText && d.footerText.size <= 14 && (d.footerText.case === "uppercase" || d.footerText.letterSpacing >= 0.08) ? { footerLabels: true } : {}
|
|
16521
|
+
} : {},
|
|
15307
16522
|
...d.sidebar ? {
|
|
15308
16523
|
sidebar: {
|
|
15309
16524
|
side: d.sidebar.side,
|
|
@@ -15331,6 +16546,13 @@ function applyLook(theme, look, source, mode = "override") {
|
|
|
15331
16546
|
theme.headerLayout = "sidebar";
|
|
15332
16547
|
if (look.sidebar.background) theme.headerBackground = look.sidebar.background;
|
|
15333
16548
|
}
|
|
16549
|
+
if (look.logoHeight && !keep(theme.logoHeight)) theme.logoHeight = look.logoHeight;
|
|
16550
|
+
if (look.footerBackground && !keep(theme.footerBackground))
|
|
16551
|
+
theme.footerBackground = look.footerBackground;
|
|
16552
|
+
if (look.footerColumns && !keep(theme.footerColumns)) {
|
|
16553
|
+
theme.footerColumns = look.footerColumns;
|
|
16554
|
+
if (look.footerLabels) theme.footerLabels = true;
|
|
16555
|
+
}
|
|
15334
16556
|
if (look.metrics) theme.fontMetrics = { ...look.metrics, ...theme.fontMetrics ?? {} };
|
|
15335
16557
|
if (look.fontHosts)
|
|
15336
16558
|
theme.fontHosts = [.../* @__PURE__ */ new Set([...theme.fontHosts ?? [], ...look.fontHosts])];
|
|
@@ -15541,7 +16763,15 @@ var PROBE_SCRIPT = String.raw`(async () => {
|
|
|
15541
16763
|
const er = e.getBoundingClientRect();
|
|
15542
16764
|
return visible(e) && er.width >= r.width * 0.9 && er.height >= r.height * 0.7;
|
|
15543
16765
|
});
|
|
15544
|
-
const res = { index, top: Math.round(r.top), height: Math.round(r.height), background: bgOf(sec.els ? sec.els[0] : sec), columns: 1, italicLines: 0, text: ownText(sec).slice(0, 120), words: ownText(sec).slice(0, 20000) };
|
|
16766
|
+
const res = { index, top: Math.round(r.top), height: Math.round(r.height), left: Math.round(r.left), width: Math.round(r.width), background: bgOf(sec.els ? sec.els[0] : sec), columns: 1, italicLines: 0, text: ownText(sec).slice(0, 120), words: ownText(sec).slice(0, 20000) };
|
|
16767
|
+
// A raised panel: the section's own box draws a shadow or a border (a theme's content card).
|
|
16768
|
+
// (Its own box, or a panel around it narrower than the page: an article card around a band.)
|
|
16769
|
+
for (let e = sec.els ? sec.els[0] : sec, k = 0; e && e !== document.body && k < 5; e = e.parentElement, k++) {
|
|
16770
|
+
const st = cs(e);
|
|
16771
|
+
const er = e.getBoundingClientRect();
|
|
16772
|
+
if (k > 0 && er.width >= vw * 0.95) break;
|
|
16773
|
+
if ((st.boxShadow && st.boxShadow !== "none") || (k === 0 && !sec.els && parseFloat(st.borderTopWidth) >= 1)) { res.raised = true; break; }
|
|
16774
|
+
}
|
|
15545
16775
|
if (bgEl || coverImg) res.backgroundImage = true;
|
|
15546
16776
|
const h = [...sec.querySelectorAll(headSel)].find((e) => visible(e) && ownText(e).length > 1);
|
|
15547
16777
|
if (h) {
|
|
@@ -15600,18 +16830,79 @@ var PROBE_SCRIPT = String.raw`(async () => {
|
|
|
15600
16830
|
res.body = best.t;
|
|
15601
16831
|
if (best.t.color) res.textColor = best.t.color;
|
|
15602
16832
|
}
|
|
16833
|
+
// How wide the running text is set (the measure), and where it starts.
|
|
16834
|
+
const measures = paras.map((p) => rect(p)).filter((x) => x.width > 120);
|
|
16835
|
+
if (measures.length) {
|
|
16836
|
+
res.textWidth = Math.round(Math.max(...measures.map((x) => x.width)));
|
|
16837
|
+
res.textLeft = Math.round(Math.min(...measures.map((x) => x.left)));
|
|
16838
|
+
}
|
|
15603
16839
|
// Columns: text items whose vertical spans overlap, at distinct left edges.
|
|
15604
|
-
const
|
|
15605
|
-
|
|
15606
|
-
|
|
15607
|
-
const
|
|
15608
|
-
|
|
15609
|
-
|
|
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));
|
|
15610
16881
|
}
|
|
16882
|
+
const shown = columnsOf(shownRects);
|
|
16883
|
+
if (shown !== res.columns) res.columnsOnScreen = shown;
|
|
15611
16884
|
// The section's parts and where they sit (to recreate it as a component when needed).
|
|
15612
16885
|
if (!sec.querySelector("form, input, textarea, select, iframe, video, canvas, embed, object")) {
|
|
15613
16886
|
const parts = [];
|
|
15614
16887
|
const inPart = (e) => parts.some((p) => p.el.contains(e));
|
|
16888
|
+
// Inline links and emphasis inside a paragraph (kept as rich text when it's recreated).
|
|
16889
|
+
const runsOf = (el) => {
|
|
16890
|
+
const out = [];
|
|
16891
|
+
const walk = (n, m) => {
|
|
16892
|
+
for (const c of n.childNodes) {
|
|
16893
|
+
if (c.nodeType === 3) { const t = (c.textContent || "").replace(/\s+/g, " "); if (t) out.push({ t, ...m }); }
|
|
16894
|
+
else if (c.nodeType === 1 && cs(c).display !== "none") {
|
|
16895
|
+
const mm = { ...m };
|
|
16896
|
+
if (c.tagName === "A" && c.getAttribute("href")) mm.href = c.href;
|
|
16897
|
+
if (/^(STRONG|B)$/.test(c.tagName)) mm.b = 1;
|
|
16898
|
+
if (/^(EM|I)$/.test(c.tagName)) mm.i = 1;
|
|
16899
|
+
if (c.tagName === "BR") out.push({ t: "\n" }); else walk(c, mm);
|
|
16900
|
+
}
|
|
16901
|
+
}
|
|
16902
|
+
};
|
|
16903
|
+
walk(el, {});
|
|
16904
|
+
return out.length <= 80 && out.some((r) => r.href || r.b || r.i) ? out : undefined;
|
|
16905
|
+
};
|
|
15615
16906
|
const btnLike = (e) => { const st = cs(e); return (toHex(st.backgroundColor) || parseFloat(st.borderTopWidth) >= 1) && parseFloat(st.paddingLeft) >= 8; };
|
|
15616
16907
|
const els = [...sec.querySelectorAll("h1, h2, h3, h4, h5, h6, p, li, blockquote, img, a, button")].filter((e) => visible(e));
|
|
15617
16908
|
for (const e of els) {
|
|
@@ -15630,10 +16921,34 @@ var PROBE_SCRIPT = String.raw`(async () => {
|
|
|
15630
16921
|
parts.push({ el: e, item: { kind: btnLike(e) ? "button" : "link", text: t, href: e.getAttribute("href") || "", box: at, style: { ...textOf(e), align: cs(e).textAlign } } });
|
|
15631
16922
|
} else {
|
|
15632
16923
|
if (!t || e.querySelector("p, li, h1, h2, h3, h4, h5, h6, img")) continue;
|
|
15633
|
-
|
|
16924
|
+
const runs = runsOf(e);
|
|
16925
|
+
parts.push({ el: e, item: { kind: "text", text: t.slice(0, 1500), box: at, style: { ...textOf(e), align: cs(e).textAlign }, ...(runs ? { runs } : {}) } });
|
|
16926
|
+
}
|
|
16927
|
+
}
|
|
16928
|
+
// Accent rules (thin bars, <hr>) and panels (boxes with their own fill or border) behind
|
|
16929
|
+
// the words: recreated on theme colors.
|
|
16930
|
+
const secBg = res.background;
|
|
16931
|
+
let decor = 0;
|
|
16932
|
+
for (const e of sec.querySelectorAll("hr, div, span, section, article, aside, figure, li")) {
|
|
16933
|
+
if (decor >= 6 || !visible(e) || parts.some((p) => p.el === e)) continue;
|
|
16934
|
+
const b = rect(e);
|
|
16935
|
+
const st = cs(e);
|
|
16936
|
+
const fill = toHex(st.backgroundColor);
|
|
16937
|
+
const bw = parseFloat(st.borderTopWidth) || 0;
|
|
16938
|
+
const line = e.tagName === "HR" || (b.height <= 4 && b.width >= 16 && (fill || bw >= 1));
|
|
16939
|
+
if (line) {
|
|
16940
|
+
parts.push({ el: e, item: { kind: "rule", box: { x: Math.round(b.left), y: Math.round(b.top), w: Math.round(b.width), h: Math.max(1, Math.round(e.tagName === "HR" ? bw || 1 : b.height)) }, color: fill || toHex(st.borderTopColor) } });
|
|
16941
|
+
decor++;
|
|
16942
|
+
continue;
|
|
16943
|
+
}
|
|
16944
|
+
const own = fill && fill !== secBg;
|
|
16945
|
+
const framed = bw >= 1 && st.borderTopStyle !== "none" && parseFloat(st.borderLeftWidth) >= 1;
|
|
16946
|
+
if ((own || framed) && b.width >= 120 && b.height >= 60 && b.width < r.width * 0.95 && !e.querySelector("img, video, iframe") && ownText(e)) {
|
|
16947
|
+
parts.push({ el: e, item: { kind: "panel", box: { x: Math.round(b.left), y: Math.round(b.top), w: Math.round(b.width), h: Math.round(b.height) }, ...(own ? { color: fill } : {}), ...(framed ? { border: { width: Math.round(bw), color: toHex(st.borderTopColor) } } : {}), radius: parseFloat(st.borderTopLeftRadius) || 0 } });
|
|
16948
|
+
decor++;
|
|
15634
16949
|
}
|
|
15635
16950
|
}
|
|
15636
|
-
if (parts.length && parts.length <=
|
|
16951
|
+
if (parts.length && parts.length <= 36) res.blueprint = parts.map((p) => p.item);
|
|
15637
16952
|
}
|
|
15638
16953
|
// Where the section's own content starts and ends (for the spacing between sections).
|
|
15639
16954
|
const content = [...sec.querySelectorAll(textSel + ", h1, h2, img, picture, video, iframe, form, button, a")].filter((e) => visible(e) && (/^(IMG|PICTURE|VIDEO|IFRAME|FORM)$/.test(e.tagName) || ownText(e).length > 1)).map(rect);
|
|
@@ -15661,12 +16976,28 @@ var PROBE_SCRIPT = String.raw`(async () => {
|
|
|
15661
16976
|
const inMain = (el) => !chrome(el) && !el.closest("footer, .os-footer");
|
|
15662
16977
|
const styleOfAll = (sel) => [...document.querySelectorAll(sel)].filter((e) => visible(e) && inMain(e) && ownText(e).length > 1).map(textOf);
|
|
15663
16978
|
const presets = {};
|
|
16979
|
+
// Measurements only the corrected score uses (the strict v3 ones above stay as they were).
|
|
16980
|
+
const corrections = {};
|
|
15664
16981
|
const h1s = styleOfAll("h1");
|
|
15665
16982
|
if (h1s.length) presets.h1 = h1s.sort((a, b) => b.size - a.size)[0];
|
|
15666
16983
|
const h2 = median(styleOfAll("h2"));
|
|
15667
16984
|
if (h2) presets.h2 = h2;
|
|
15668
16985
|
const h3 = median(styleOfAll("h3"));
|
|
15669
16986
|
if (h3) presets.h3 = h3;
|
|
16987
|
+
// A display line: the page's largest short text, well above its headings (a banner's title
|
|
16988
|
+
// set in a paragraph). Read when importing; the comparison doesn't check it.
|
|
16989
|
+
{
|
|
16990
|
+
const ref = Math.max(presets.h1 ? presets.h1.size : 0, presets.h2 ? presets.h2.size : 0, 16);
|
|
16991
|
+
let best;
|
|
16992
|
+
for (const e of document.querySelectorAll("p, strong, b, span, h1, h2, h3, div")) {
|
|
16993
|
+
if (!visible(e) || !inMain(e)) continue;
|
|
16994
|
+
const t = ownText(e);
|
|
16995
|
+
if (t.length < 3 || t.length > 60 || [...e.children].some((k) => ownText(k) === t && k.tagName !== "BR" && !/^(STRONG|B|EM|I|SPAN)$/.test(k.tagName))) continue;
|
|
16996
|
+
const st = textOf(e);
|
|
16997
|
+
if (st.size >= ref * 1.3 && (!best || st.size > best.size)) best = st;
|
|
16998
|
+
}
|
|
16999
|
+
if (best) presets.display = best;
|
|
17000
|
+
}
|
|
15670
17001
|
const tally = new Map();
|
|
15671
17002
|
for (const { el, t } of all) {
|
|
15672
17003
|
const k = t.family + "|" + t.size;
|
|
@@ -15697,6 +17028,21 @@ var PROBE_SCRIPT = String.raw`(async () => {
|
|
|
15697
17028
|
if (!links.length) links = [...header.querySelectorAll("a")].filter(linkOk);
|
|
15698
17029
|
const mid = links[Math.floor(links.length / 2)];
|
|
15699
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
|
+
}
|
|
15700
17046
|
// The site title set as text (a wordmark), not a logo image.
|
|
15701
17047
|
// The innermost element holding the title text (an OpenSpace title span, not its link).
|
|
15702
17048
|
const brandOk = (e) => visible(e) && ownText(e).length > 1 && ownText(e).length <= 40 && !e.querySelector("img, svg");
|
|
@@ -15717,7 +17063,7 @@ var PROBE_SCRIPT = String.raw`(async () => {
|
|
|
15717
17063
|
if (btn) {
|
|
15718
17064
|
presets.button = textOf(btn);
|
|
15719
17065
|
const s = cs(btn);
|
|
15720
|
-
buttons = { radius: parseFloat(s.borderTopLeftRadius) || 0, background: toHex(s.backgroundColor), border: parseFloat(s.borderTopWidth) || 0, paddingX: parseFloat(s.paddingLeft) || 0, text: ownText(btn).slice(0, 40), element: btn.tagName.toLowerCase() + (btn.className && typeof btn.className === "string" ? "." + btn.className.trim().split(/\s+/).slice(0, 2).join(".") : "") };
|
|
17066
|
+
buttons = { radius: parseFloat(s.borderTopLeftRadius) || 0, background: toHex(s.backgroundColor), border: parseFloat(s.borderTopWidth) || 0, paddingX: parseFloat(s.paddingLeft) || 0, text: ownText(btn).slice(0, 40), element: btn.tagName.toLowerCase() + (btn.className && typeof btn.className === "string" ? "." + btn.className.trim().split(/\s+/).slice(0, 2).join(".") : ""), ...(inMain(btn) ? {} : { chrome: true }) };
|
|
15721
17067
|
}
|
|
15722
17068
|
|
|
15723
17069
|
// --- layout ---------------------------------------------------------------------------------
|
|
@@ -15806,10 +17152,40 @@ var PROBE_SCRIPT = String.raw`(async () => {
|
|
|
15806
17152
|
...(buttons ? { buttons } : {}),
|
|
15807
17153
|
...(eyebrows.length ? { eyebrowRule: out.filter((s) => s.rule).length >= Math.max(1, eyebrows.length / 2) } : {}),
|
|
15808
17154
|
layout,
|
|
17155
|
+
...(Object.keys(corrections).length ? { corrections } : {}),
|
|
15809
17156
|
sections: out,
|
|
15810
17157
|
height: doc.scrollHeight,
|
|
15811
17158
|
fonts,
|
|
15812
17159
|
fontRequests,
|
|
17160
|
+
pageBackground: toHex(cs(document.body).backgroundColor) || toHex(cs(doc).backgroundColor) || "#ffffff",
|
|
17161
|
+
...(() => {
|
|
17162
|
+
// The header's logo picture (its shown height) and the footer's own color.
|
|
17163
|
+
const out = {};
|
|
17164
|
+
const head = document.querySelector(".os-header, header, [role=banner], #header");
|
|
17165
|
+
const logo = head && [...head.querySelectorAll("img, svg")].find((e) => visible(e) && e.getBoundingClientRect().height >= 16 && e.getBoundingClientRect().width >= 24);
|
|
17166
|
+
if (logo) out.logoHeight = Math.round(logo.getBoundingClientRect().height);
|
|
17167
|
+
const foot = [...document.querySelectorAll(".os-footer, footer, [role=contentinfo], #footer")].filter(visible).pop();
|
|
17168
|
+
if (foot) {
|
|
17169
|
+
const c = toHex(cs(foot).backgroundColor) || [...foot.querySelectorAll("*")].map((e) => toHex(cs(e).backgroundColor)).find(Boolean);
|
|
17170
|
+
if (c) out.footerBackground = c;
|
|
17171
|
+
// Its first line of words: how many groups sit side by side, and their style.
|
|
17172
|
+
const leaves = [...foot.querySelectorAll("p, li, a, span, small, div, h2, h3, h4")].filter((e) => visible(e) && ownText(e) && ![...e.children].some((k) => ownText(k) && /^(P|DIV|UL|LI|H2|H3|H4)$/.test(k.tagName)));
|
|
17173
|
+
const lines = leaves.map((e) => ({ e, r: rect(e) }));
|
|
17174
|
+
if (lines.length) {
|
|
17175
|
+
const top = Math.min(...lines.map((l) => l.r.top));
|
|
17176
|
+
const row = lines.filter((l) => l.r.top <= top + 24);
|
|
17177
|
+
const groups = [];
|
|
17178
|
+
for (const l of row.sort((a, b) => a.r.left - b.r.left)) {
|
|
17179
|
+
const g = groups[groups.length - 1];
|
|
17180
|
+
if (g && l.r.left - g.right < 48) g.right = Math.max(g.right, l.r.right);
|
|
17181
|
+
else groups.push({ left: l.r.left, right: l.r.right });
|
|
17182
|
+
}
|
|
17183
|
+
out.footerColumns = groups.length;
|
|
17184
|
+
out.footerText = textOf(row[0].e);
|
|
17185
|
+
}
|
|
17186
|
+
}
|
|
17187
|
+
return out;
|
|
17188
|
+
})(),
|
|
15813
17189
|
...(sidebar ? { sidebar } : {}),
|
|
15814
17190
|
};
|
|
15815
17191
|
})()`;
|
|
@@ -19510,15 +20886,15 @@ function truncate(v, l = 50, o = "...") {
|
|
|
19510
20886
|
return v;
|
|
19511
20887
|
return str5.substring(0, l - o.length) + o;
|
|
19512
20888
|
}
|
|
19513
|
-
function truncatewords(v,
|
|
20889
|
+
function truncatewords(v, words4 = 15, o = "...") {
|
|
19514
20890
|
const str5 = stringify(v);
|
|
19515
20891
|
o = stringify(o);
|
|
19516
20892
|
this.context.memoryLimit.use(str5.length + o.length);
|
|
19517
20893
|
const arr = str5.split(/\s+/);
|
|
19518
|
-
if (
|
|
19519
|
-
|
|
19520
|
-
let ret = arr.slice(0,
|
|
19521
|
-
if (arr.length >=
|
|
20894
|
+
if (words4 <= 0)
|
|
20895
|
+
words4 = 1;
|
|
20896
|
+
let ret = arr.slice(0, words4).join(" ");
|
|
20897
|
+
if (arr.length >= words4)
|
|
19522
20898
|
ret += o;
|
|
19523
20899
|
return ret;
|
|
19524
20900
|
}
|
|
@@ -28355,28 +29731,28 @@ function gfmFootnoteHtml(options) {
|
|
|
28355
29731
|
},
|
|
28356
29732
|
gfmFootnoteCallString(token) {
|
|
28357
29733
|
let calls = this.getData("gfmFootnoteCallOrder");
|
|
28358
|
-
let
|
|
29734
|
+
let counts2 = this.getData("gfmFootnoteCallCounts");
|
|
28359
29735
|
const id = normalizeIdentifier(this.sliceSerialize(token));
|
|
28360
29736
|
let counter;
|
|
28361
29737
|
this.resume();
|
|
28362
29738
|
if (!calls) this.setData("gfmFootnoteCallOrder", calls = []);
|
|
28363
|
-
if (!
|
|
29739
|
+
if (!counts2) this.setData("gfmFootnoteCallCounts", counts2 = {});
|
|
28364
29740
|
const index = calls.indexOf(id);
|
|
28365
29741
|
const safeId = sanitizeUri(id.toLowerCase());
|
|
28366
29742
|
if (index === -1) {
|
|
28367
29743
|
calls.push(id);
|
|
28368
|
-
|
|
29744
|
+
counts2[id] = 1;
|
|
28369
29745
|
counter = calls.length;
|
|
28370
29746
|
} else {
|
|
28371
|
-
|
|
29747
|
+
counts2[id]++;
|
|
28372
29748
|
counter = index + 1;
|
|
28373
29749
|
}
|
|
28374
|
-
const reuseCounter =
|
|
29750
|
+
const reuseCounter = counts2[id];
|
|
28375
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>");
|
|
28376
29752
|
},
|
|
28377
29753
|
null() {
|
|
28378
29754
|
const calls = this.getData("gfmFootnoteCallOrder") || [];
|
|
28379
|
-
const
|
|
29755
|
+
const counts2 = this.getData("gfmFootnoteCallCounts") || {};
|
|
28380
29756
|
const definitions = this.getData("gfmFootnoteDefinitions") || {};
|
|
28381
29757
|
let index = -1;
|
|
28382
29758
|
if (calls.length > 0) {
|
|
@@ -28392,7 +29768,7 @@ function gfmFootnoteHtml(options) {
|
|
|
28392
29768
|
const safeId = sanitizeUri(id.toLowerCase());
|
|
28393
29769
|
let referenceIndex = 0;
|
|
28394
29770
|
const references = [];
|
|
28395
|
-
while (++referenceIndex <=
|
|
29771
|
+
while (++referenceIndex <= counts2[id]) {
|
|
28396
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>");
|
|
28397
29773
|
}
|
|
28398
29774
|
const reference = references.join(" ");
|
|
@@ -30809,7 +32185,7 @@ var typeOf = (o) => {
|
|
|
30809
32185
|
for (const k of TYPE_KEYS) if (typeof o[k] === "string") return o[k];
|
|
30810
32186
|
return void 0;
|
|
30811
32187
|
};
|
|
30812
|
-
function
|
|
32188
|
+
function sectionsOf3(o) {
|
|
30813
32189
|
if (!isObj3(o)) return void 0;
|
|
30814
32190
|
for (const k of SECTION_KEYS) {
|
|
30815
32191
|
const v = o[k];
|
|
@@ -30840,7 +32216,7 @@ async function findStructuredPages(source, files) {
|
|
|
30840
32216
|
data = p.data;
|
|
30841
32217
|
body = p.body;
|
|
30842
32218
|
}
|
|
30843
|
-
if (
|
|
32219
|
+
if (sectionsOf3(data)) out.push({ path: f, data, ...body ? { body } : {} });
|
|
30844
32220
|
}
|
|
30845
32221
|
return out;
|
|
30846
32222
|
}
|
|
@@ -30956,6 +32332,7 @@ function formFrom(v, ctx) {
|
|
|
30956
32332
|
if (signupOnly)
|
|
30957
32333
|
return {
|
|
30958
32334
|
kind: "newsletter",
|
|
32335
|
+
...fields.some((f) => f.type !== "email") ? { askName: true } : {},
|
|
30959
32336
|
...button ? { buttonLabel: plain2(button) } : {},
|
|
30960
32337
|
...str3(v.email_placeholder) ? { placeholder: str3(v.email_placeholder) } : {},
|
|
30961
32338
|
...filled(v.success_message) ? { success: filled(v.success_message) } : {}
|
|
@@ -31407,7 +32784,7 @@ ${siteBg ? `body{background-color:${siteBg};}` : ""}`
|
|
|
31407
32784
|
placeholders,
|
|
31408
32785
|
fg: fgFor
|
|
31409
32786
|
};
|
|
31410
|
-
const raw2 =
|
|
32787
|
+
const raw2 = sectionsOf3(file.data) ?? [];
|
|
31411
32788
|
const sections = [];
|
|
31412
32789
|
for (const s2 of raw2) {
|
|
31413
32790
|
typesSeen.set(typeOf(s2) ?? "?", (typesSeen.get(typeOf(s2) ?? "?") ?? 0) + 1);
|
|
@@ -40919,13 +42296,14 @@ function fontCredit(a, b, o, i) {
|
|
|
40919
42296
|
const d = metricDistance(ma, mb);
|
|
40920
42297
|
return d <= 0.04 ? 0.85 : d <= 0.07 ? 0.7 : d <= 0.11 ? 0.5 : d <= 0.16 ? 0.25 : 0;
|
|
40921
42298
|
}
|
|
42299
|
+
var SCORER = "v3.1-corrected";
|
|
40922
42300
|
var norm3 = (s2) => (s2 ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
40923
|
-
var
|
|
42301
|
+
var words3 = (s2) => new Set(
|
|
40924
42302
|
s2.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2)
|
|
40925
42303
|
);
|
|
40926
42304
|
var jaccard = (a, b) => {
|
|
40927
|
-
const A =
|
|
40928
|
-
const B =
|
|
42305
|
+
const A = words3(a);
|
|
42306
|
+
const B = words3(b);
|
|
40929
42307
|
if (!A.size || !B.size) return 0;
|
|
40930
42308
|
let n = 0;
|
|
40931
42309
|
for (const w of A) if (B.has(w)) n++;
|
|
@@ -40966,7 +42344,12 @@ var ROLE_OF = {
|
|
|
40966
42344
|
nav: "label",
|
|
40967
42345
|
wordmark: "label"
|
|
40968
42346
|
};
|
|
40969
|
-
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") {
|
|
40970
42353
|
const out = [];
|
|
40971
42354
|
const mobile = o.viewport < 700;
|
|
40972
42355
|
const sizeKey = mobile ? "sizeMobile" : "size";
|
|
@@ -40982,8 +42365,8 @@ function typeChecks(o, i, tol) {
|
|
|
40982
42365
|
"nav",
|
|
40983
42366
|
"wordmark"
|
|
40984
42367
|
]) {
|
|
40985
|
-
const a = o
|
|
40986
|
-
const b = i
|
|
42368
|
+
const a = presetOf(o, k, mode);
|
|
42369
|
+
const b = presetOf(i, k, mode);
|
|
40987
42370
|
if (!a) continue;
|
|
40988
42371
|
if (k === "nav" && mobile) continue;
|
|
40989
42372
|
if ((k === "h1" || k === "h2" || k === "h3") && a.case === "uppercase" && a.size < 16) continue;
|
|
@@ -41128,12 +42511,12 @@ function matchSections(o, i) {
|
|
|
41128
42511
|
});
|
|
41129
42512
|
o.forEach((s2, k) => {
|
|
41130
42513
|
if (out[k] !== void 0) return;
|
|
41131
|
-
const A =
|
|
42514
|
+
const A = words3(`${s2.heading?.text ?? ""} ${s2.words ?? s2.text}`);
|
|
41132
42515
|
if (A.size < 4) return;
|
|
41133
42516
|
let best = -1;
|
|
41134
42517
|
let score = 0.6;
|
|
41135
42518
|
i.forEach((t, n) => {
|
|
41136
|
-
const B =
|
|
42519
|
+
const B = words3(`${t.heading?.text ?? ""} ${t.words ?? t.text}`);
|
|
41137
42520
|
let hit = 0;
|
|
41138
42521
|
for (const w of A) if (B.has(w)) hit++;
|
|
41139
42522
|
if (hit / A.size > score) {
|
|
@@ -41161,7 +42544,10 @@ function matchSections(o, i) {
|
|
|
41161
42544
|
});
|
|
41162
42545
|
return out;
|
|
41163
42546
|
}
|
|
41164
|
-
|
|
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) };
|
|
41165
42551
|
const out = [];
|
|
41166
42552
|
const add = (c) => out.push({ area: "section", ...c });
|
|
41167
42553
|
if (a.background && b.background && !a.backgroundImage) {
|
|
@@ -41298,56 +42684,159 @@ function withVisual(p, visual) {
|
|
|
41298
42684
|
const base = p.exact ?? structure;
|
|
41299
42685
|
const v = Math.max(0, Math.min(1, visual));
|
|
41300
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;
|
|
41301
42688
|
return {
|
|
41302
42689
|
...p,
|
|
41303
42690
|
structure,
|
|
41304
42691
|
visual: Math.round(v * 1e3) / 1e3,
|
|
41305
42692
|
exact,
|
|
41306
|
-
score: Math.round(exact)
|
|
41307
|
-
|
|
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;
|
|
41308
42718
|
}
|
|
41309
|
-
function
|
|
41310
|
-
const
|
|
41311
|
-
|
|
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
|
+
);
|
|
41312
42730
|
const match = matchSections(original.sections, imported.sections);
|
|
42731
|
+
const phone = original.viewport < 700;
|
|
41313
42732
|
const sections = original.sections.map((s2, k) => {
|
|
41314
42733
|
const j = match[k];
|
|
41315
42734
|
const other = j === void 0 ? void 0 : imported.sections[j];
|
|
41316
|
-
|
|
41317
|
-
|
|
41318
|
-
|
|
41319
|
-
|
|
41320
|
-
|
|
41321
|
-
|
|
41322
|
-
|
|
41323
|
-
|
|
41324
|
-
|
|
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
|
+
);
|
|
41325
42746
|
}
|
|
41326
|
-
|
|
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;
|
|
41327
42794
|
return {
|
|
41328
42795
|
index: k,
|
|
41329
42796
|
...s2.heading ? { heading: s2.heading.text } : {},
|
|
41330
|
-
...
|
|
42797
|
+
...at !== void 0 ? { importedIndex: at } : {},
|
|
41331
42798
|
checks: c,
|
|
41332
42799
|
score: scoreOf(c)
|
|
41333
42800
|
};
|
|
41334
42801
|
});
|
|
41335
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");
|
|
41336
42810
|
return {
|
|
41337
42811
|
path: path2,
|
|
41338
42812
|
original: original.url,
|
|
41339
42813
|
imported: imported.url,
|
|
41340
42814
|
viewport: original.viewport,
|
|
41341
|
-
score:
|
|
41342
|
-
structure:
|
|
41343
|
-
exact:
|
|
41344
|
-
checks,
|
|
41345
|
-
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 } : {}
|
|
41346
42827
|
};
|
|
41347
42828
|
}
|
|
41348
42829
|
function fidelityReport(pages) {
|
|
41349
|
-
const
|
|
41350
|
-
|
|
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
|
+
};
|
|
41351
42840
|
}
|
|
41352
42841
|
var cell = (s2) => s2.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
41353
42842
|
function fidelityMarkdown(report, opts = {}) {
|
|
@@ -41355,7 +42844,11 @@ function fidelityMarkdown(report, opts = {}) {
|
|
|
41355
42844
|
"# Visual fidelity",
|
|
41356
42845
|
"",
|
|
41357
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.`,
|
|
41358
|
-
""
|
|
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
|
+
] : []
|
|
41359
42852
|
];
|
|
41360
42853
|
if (opts.tuning) {
|
|
41361
42854
|
lines.push(
|
|
@@ -41370,7 +42863,12 @@ function fidelityMarkdown(report, opts = {}) {
|
|
|
41370
42863
|
);
|
|
41371
42864
|
}
|
|
41372
42865
|
for (const p of report.pages) {
|
|
41373
|
-
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}`), "");
|
|
41374
42872
|
if (p.visual !== void 0)
|
|
41375
42873
|
lines.push(
|
|
41376
42874
|
`Checks ${p.structure ?? p.score}/100 \xB7 looks alike ${Math.round(p.visual * 100)}/100 (screenshots compared).`,
|
|
@@ -41657,9 +43155,9 @@ function htmlToRichText(html, rewrite2 = (h) => h) {
|
|
|
41657
43155
|
)
|
|
41658
43156
|
});
|
|
41659
43157
|
}
|
|
41660
|
-
const
|
|
41661
|
-
for (const a of ctx.aligns)
|
|
41662
|
-
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];
|
|
41663
43161
|
return {
|
|
41664
43162
|
doc,
|
|
41665
43163
|
...top === "center" ? { align: "center" } : top === "right" ? { align: "end" } : {}
|
|
@@ -42058,13 +43556,19 @@ async function mapBlock(ctx, sq, opts = {}) {
|
|
|
42058
43556
|
const href = sq.href ?? sq.card?.button?.href;
|
|
42059
43557
|
const link2 = href ? await linkFor(ctx, href) : void 0;
|
|
42060
43558
|
return one(
|
|
42061
|
-
makeBlock(
|
|
42062
|
-
|
|
42063
|
-
|
|
42064
|
-
|
|
42065
|
-
|
|
42066
|
-
|
|
42067
|
-
|
|
43559
|
+
makeBlock(
|
|
43560
|
+
ctx,
|
|
43561
|
+
"image",
|
|
43562
|
+
{
|
|
43563
|
+
image: image2,
|
|
43564
|
+
aspect: "original",
|
|
43565
|
+
lightbox: false,
|
|
43566
|
+
...caption ? { caption: clip2(caption, 200) } : {},
|
|
43567
|
+
...link2 ? { link: link2 } : {},
|
|
43568
|
+
...sq.natural ? { size: "original" } : {}
|
|
43569
|
+
},
|
|
43570
|
+
sq.natural ? { style: { align: { d: "start" } } } : {}
|
|
43571
|
+
)
|
|
42068
43572
|
);
|
|
42069
43573
|
}
|
|
42070
43574
|
case "gallery": {
|
|
@@ -42091,7 +43595,8 @@ async function mapBlock(ctx, sq, opts = {}) {
|
|
|
42091
43595
|
"gallery",
|
|
42092
43596
|
{
|
|
42093
43597
|
images,
|
|
42094
|
-
aspect: sq.layout === "grid" ? "1:1" : "original",
|
|
43598
|
+
aspect: sq.layout === "grid" || sq.fit === "contain" ? "1:1" : "original",
|
|
43599
|
+
...sq.fit === "contain" ? { fit: "contain" } : {},
|
|
42095
43600
|
columns: images.length >= 4 && sq.layout === "grid" ? "4" : "3",
|
|
42096
43601
|
lightbox: true,
|
|
42097
43602
|
showCaptions: images.some((i) => i.caption)
|
|
@@ -42121,7 +43626,7 @@ async function mapBlock(ctx, sq, opts = {}) {
|
|
|
42121
43626
|
ctx.notices.push({
|
|
42122
43627
|
kind: "form",
|
|
42123
43628
|
where: ctx.where,
|
|
42124
|
-
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."
|
|
42125
43630
|
});
|
|
42126
43631
|
return one(
|
|
42127
43632
|
makeBlock(
|
|
@@ -42130,6 +43635,7 @@ async function mapBlock(ctx, sq, opts = {}) {
|
|
|
42130
43635
|
{
|
|
42131
43636
|
...sq.heading ? { heading: clip2(sq.heading, 80) } : {},
|
|
42132
43637
|
...sq.description ? { description: clip2(sq.description, 240) } : {},
|
|
43638
|
+
...sq.askName ? { askName: true } : {},
|
|
42133
43639
|
...sq.buttonLabel ? { buttonLabel: clip2(sq.buttonLabel, 30) } : {},
|
|
42134
43640
|
...sq.placeholder ? { placeholder: clip2(sq.placeholder, 60) } : {},
|
|
42135
43641
|
...sq.success ? { successMessage: clip2(sq.success, 200) } : {}
|
|
@@ -42298,7 +43804,12 @@ async function mapBlock(ctx, sq, opts = {}) {
|
|
|
42298
43804
|
makeBlock(
|
|
42299
43805
|
ctx,
|
|
42300
43806
|
"product",
|
|
42301
|
-
{
|
|
43807
|
+
{
|
|
43808
|
+
product: { collection: item2.collection, items: [item2.slug] },
|
|
43809
|
+
...sq.show?.excerpt === false ? { showDescription: false } : {},
|
|
43810
|
+
...sq.show?.price === false ? { showPrice: false } : {},
|
|
43811
|
+
...sq.show?.buy === false ? { showBuy: false, imageAspect: "1:1" } : {}
|
|
43812
|
+
},
|
|
42302
43813
|
{ variant: "card" }
|
|
42303
43814
|
)
|
|
42304
43815
|
);
|
|
@@ -42543,7 +44054,8 @@ function stackSection(blocks2, spans, style, width, align) {
|
|
|
42543
44054
|
layout: {
|
|
42544
44055
|
mode: "stack",
|
|
42545
44056
|
columns: cols > 1 ? { d: spans, m: [12] } : { d: [12] },
|
|
42546
|
-
|
|
44057
|
+
// Words beside a picture sit centred on it; columns of words start at the top together.
|
|
44058
|
+
...cols > 1 && blocks2.some(({ block: block4 }) => ["image", "gallery", "video", "map", "embed"].includes(block4.type)) ? { align: "center" } : {},
|
|
42547
44059
|
...width && width !== "content" ? { width } : {}
|
|
42548
44060
|
},
|
|
42549
44061
|
style,
|
|
@@ -42553,6 +44065,75 @@ function stackSection(blocks2, spans, style, width, align) {
|
|
|
42553
44065
|
if (isStarter) section.preset = starterRef({ columns: spans, ...align ? { align } : {} });
|
|
42554
44066
|
return section;
|
|
42555
44067
|
}
|
|
44068
|
+
var headingLevelOf = (b) => b.type === "heading" ? Number(String(b.fields.level ?? "h2").slice(1)) || 2 : void 0;
|
|
44069
|
+
var textOfNodes = (nodes) => JSON.stringify(nodes ?? "").length;
|
|
44070
|
+
function splitAtHeadings(blocks2) {
|
|
44071
|
+
const count2 = (level2) => {
|
|
44072
|
+
let n = 0;
|
|
44073
|
+
for (const { block: block4 } of blocks2) {
|
|
44074
|
+
if (headingLevelOf(block4) === level2) n++;
|
|
44075
|
+
if (block4.type === "text") {
|
|
44076
|
+
const doc = block4.fields.content;
|
|
44077
|
+
for (const node2 of doc?.content ?? [])
|
|
44078
|
+
if (node2.type === "heading" && node2.attrs?.level === level2) n++;
|
|
44079
|
+
}
|
|
44080
|
+
}
|
|
44081
|
+
return n;
|
|
44082
|
+
};
|
|
44083
|
+
const size2 = blocks2.reduce(
|
|
44084
|
+
(n, { block: block4 }) => n + (block4.type === "text" ? textOfNodes(block4.fields.content) : textOfNodes(block4.fields)),
|
|
44085
|
+
0
|
|
44086
|
+
);
|
|
44087
|
+
const media = blocks2.filter(
|
|
44088
|
+
({ block: block4 }) => block4.type === "image" && block4.fields.size === "original"
|
|
44089
|
+
).length;
|
|
44090
|
+
const rules = blocks2.filter(({ block: block4 }) => block4.type === "line").length;
|
|
44091
|
+
if (size2 < 2400 && media < 2 && rules < 1) return [blocks2];
|
|
44092
|
+
const level = count2(2) >= 2 ? 2 : count2(3) >= 3 ? 3 : void 0;
|
|
44093
|
+
if (!level) return [blocks2];
|
|
44094
|
+
const out = [[]];
|
|
44095
|
+
const cur = () => out[out.length - 1];
|
|
44096
|
+
const start = () => {
|
|
44097
|
+
if (!cur().length) return;
|
|
44098
|
+
const last2 = cur().at(-1);
|
|
44099
|
+
const carry = last2.block.type === "image" && last2.block.fields.size === "original";
|
|
44100
|
+
if (!carry) out.push([]);
|
|
44101
|
+
else if (cur().length > 1) {
|
|
44102
|
+
cur().pop();
|
|
44103
|
+
out.push([last2]);
|
|
44104
|
+
}
|
|
44105
|
+
};
|
|
44106
|
+
for (const p of blocks2) {
|
|
44107
|
+
if (headingLevelOf(p.block) === level) {
|
|
44108
|
+
start();
|
|
44109
|
+
cur().push(p);
|
|
44110
|
+
continue;
|
|
44111
|
+
}
|
|
44112
|
+
if (p.block.type !== "text") {
|
|
44113
|
+
cur().push(p);
|
|
44114
|
+
continue;
|
|
44115
|
+
}
|
|
44116
|
+
const doc = p.block.fields.content;
|
|
44117
|
+
const parts = [[]];
|
|
44118
|
+
for (const node2 of doc.content ?? []) {
|
|
44119
|
+
if (node2.type === "heading" && node2.attrs?.level === level && parts.at(-1).length)
|
|
44120
|
+
parts.push([]);
|
|
44121
|
+
parts.at(-1).push(node2);
|
|
44122
|
+
}
|
|
44123
|
+
parts.forEach((nodes, i) => {
|
|
44124
|
+
if (i > 0 || nodes[0]?.type === "heading" && nodes[0].attrs?.level === level) start();
|
|
44125
|
+
cur().push({
|
|
44126
|
+
...p,
|
|
44127
|
+
block: {
|
|
44128
|
+
...p.block,
|
|
44129
|
+
...i > 0 ? { id: newId("blk") } : {},
|
|
44130
|
+
fields: { ...p.block.fields, content: { ...doc, content: nodes } }
|
|
44131
|
+
}
|
|
44132
|
+
});
|
|
44133
|
+
});
|
|
44134
|
+
}
|
|
44135
|
+
return out.filter((c) => c.length);
|
|
44136
|
+
}
|
|
42556
44137
|
async function mapBlocks(ctx, blocks2, inGrid = false) {
|
|
42557
44138
|
const out = [];
|
|
42558
44139
|
for (const b of blocks2) out.push(...await mapBlock(ctx, b, { inGrid }));
|
|
@@ -42651,6 +44232,15 @@ async function mapSectionParts(ctx, sq) {
|
|
|
42651
44232
|
}
|
|
42652
44233
|
return [section];
|
|
42653
44234
|
}
|
|
44235
|
+
if (sq.together && sq.layout.rows.length > 1) {
|
|
44236
|
+
const filled2 = sq.layout.rows.map((r) => r.columns.filter((c) => c.blocks.length));
|
|
44237
|
+
const multi = filled2.some((cols) => cols.length > 1);
|
|
44238
|
+
const shapes = new Set(filled2.map((cols) => cols.map((c) => c.span).join("-")));
|
|
44239
|
+
if (multi && shapes.size > 1) {
|
|
44240
|
+
const grid = await rowsAsGrid(ctx, sq.layout.rows, style, width);
|
|
44241
|
+
if (grid) return [grid];
|
|
44242
|
+
}
|
|
44243
|
+
}
|
|
42654
44244
|
const out = [];
|
|
42655
44245
|
let run2;
|
|
42656
44246
|
const flush = () => {
|
|
@@ -42662,20 +44252,22 @@ async function mapSectionParts(ctx, sq) {
|
|
|
42662
44252
|
run2 = void 0;
|
|
42663
44253
|
return;
|
|
42664
44254
|
}
|
|
42665
|
-
|
|
42666
|
-
|
|
42667
|
-
|
|
42668
|
-
|
|
42669
|
-
|
|
42670
|
-
|
|
42671
|
-
|
|
42672
|
-
|
|
42673
|
-
|
|
42674
|
-
|
|
42675
|
-
|
|
42676
|
-
|
|
42677
|
-
|
|
42678
|
-
|
|
44255
|
+
const parts = run2.spans.length === 1 ? splitAtHeadings(run2.blocks) : [run2.blocks];
|
|
44256
|
+
for (const part of parts)
|
|
44257
|
+
out.push(
|
|
44258
|
+
stackSection(
|
|
44259
|
+
part,
|
|
44260
|
+
run2.spans,
|
|
44261
|
+
out.length ? {
|
|
44262
|
+
...style,
|
|
44263
|
+
// A photo stays with the first part; a plain color continues behind every part.
|
|
44264
|
+
background: style.background?.type === "color" ? style.background : void 0,
|
|
44265
|
+
minHeight: void 0
|
|
44266
|
+
} : style,
|
|
44267
|
+
width,
|
|
44268
|
+
align
|
|
44269
|
+
)
|
|
44270
|
+
);
|
|
42679
44271
|
run2 = void 0;
|
|
42680
44272
|
};
|
|
42681
44273
|
for (const row of sq.layout.rows) {
|
|
@@ -42704,6 +44296,11 @@ async function mapSectionParts(ctx, sq) {
|
|
|
42704
44296
|
flush();
|
|
42705
44297
|
if (!out.length && bg) out.push(stackSection([], [12], style, width, align));
|
|
42706
44298
|
if (sq.role && out.length === 1) applyNearestPreset(out[0], sq.role, !sq.theme);
|
|
44299
|
+
for (const s2 of out)
|
|
44300
|
+
if (s2.layout.mode === "stack" && s2.blocks.length === 1 && s2.blocks[0]?.type === "gallery" && s2.blocks[0].fields.fit === "contain" && (!s2.layout.width || s2.layout.width === "wide" || s2.layout.width === "full")) {
|
|
44301
|
+
s2.layout.width = "narrow";
|
|
44302
|
+
delete s2.preset;
|
|
44303
|
+
}
|
|
42707
44304
|
for (const s2 of out)
|
|
42708
44305
|
for (const k of Object.keys(s2.style))
|
|
42709
44306
|
if (s2.style[k] === void 0) delete s2.style[k];
|
|
@@ -42852,6 +44449,57 @@ function applyNearestPreset(section, role, themeUnknown) {
|
|
|
42852
44449
|
if (model.style?.align && !b.style?.align) b.style = { ...b.style, align: model.style.align };
|
|
42853
44450
|
}
|
|
42854
44451
|
}
|
|
44452
|
+
async function rowsAsGrid(ctx, rows, style, width) {
|
|
44453
|
+
const ROW_PX = 24;
|
|
44454
|
+
const contentPx = width === "wide" || width === "full" ? 1440 : width === "narrow" ? 720 : 1280;
|
|
44455
|
+
const blocks2 = [];
|
|
44456
|
+
const items = {};
|
|
44457
|
+
let y = 1;
|
|
44458
|
+
for (const row of rows) {
|
|
44459
|
+
const cols = row.columns.filter((c) => c.blocks.length);
|
|
44460
|
+
if (!cols.length) continue;
|
|
44461
|
+
const total = row.columns.reduce((n, c) => n + c.span, 0) || 12;
|
|
44462
|
+
let x = 1;
|
|
44463
|
+
let rowH = 1;
|
|
44464
|
+
for (const col of row.columns) {
|
|
44465
|
+
const w = Math.max(
|
|
44466
|
+
1,
|
|
44467
|
+
Math.min(DESKTOP_COLS - x + 1, Math.round(col.span / Math.max(12, total) * DESKTOP_COLS))
|
|
44468
|
+
);
|
|
44469
|
+
let cy = y;
|
|
44470
|
+
for (const b of await mapBlocks(ctx, col.blocks, true)) {
|
|
44471
|
+
const img2 = b.type === "image" ? b.fields.image : void 0;
|
|
44472
|
+
const ratio = img2?.width && img2?.height ? img2.width / img2.height : void 0;
|
|
44473
|
+
const h = b.type === "image" ? Math.max(4, Math.round(w / DESKTOP_COLS * contentPx / (ratio ?? 1.5) / ROW_PX)) : 1;
|
|
44474
|
+
blocks2.push(b);
|
|
44475
|
+
items[b.id] = { d: { x, y: cy, w, h } };
|
|
44476
|
+
cy += h;
|
|
44477
|
+
}
|
|
44478
|
+
rowH = Math.max(rowH, cy - y);
|
|
44479
|
+
x = Math.min(DESKTOP_COLS, x + w);
|
|
44480
|
+
}
|
|
44481
|
+
y += rowH;
|
|
44482
|
+
}
|
|
44483
|
+
if (blocks2.length < 2) return void 0;
|
|
44484
|
+
const section = {
|
|
44485
|
+
id: newId("sec"),
|
|
44486
|
+
layout: {
|
|
44487
|
+
mode: "grid",
|
|
44488
|
+
rows: { d: y - 1 },
|
|
44489
|
+
items,
|
|
44490
|
+
...width && width !== "content" ? { width } : {}
|
|
44491
|
+
},
|
|
44492
|
+
style,
|
|
44493
|
+
blocks: blocks2
|
|
44494
|
+
};
|
|
44495
|
+
const mobile = autoMobileLayout(section, { reset: true });
|
|
44496
|
+
if (mobile && section.layout.mode === "grid") {
|
|
44497
|
+
section.layout.items = mobile;
|
|
44498
|
+
const mRows = Math.max(1, ...Object.values(mobile).map((i) => i.m ? i.m.y + i.m.h - 1 : 1));
|
|
44499
|
+
section.layout.rows = { d: y - 1, m: mRows };
|
|
44500
|
+
}
|
|
44501
|
+
return section;
|
|
44502
|
+
}
|
|
42855
44503
|
|
|
42856
44504
|
// ../import/src/map/site.ts
|
|
42857
44505
|
var clip3 = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1).trimEnd()}\u2026` : s2;
|
|
@@ -43151,6 +44799,12 @@ function headerFor(site2, siteDark, brand) {
|
|
|
43151
44799
|
}
|
|
43152
44800
|
return {
|
|
43153
44801
|
variant,
|
|
44802
|
+
...t.logoHeight ? {
|
|
44803
|
+
logoHeight: {
|
|
44804
|
+
d: Math.round(t.logoHeight.d),
|
|
44805
|
+
...t.logoHeight.m ? { m: Math.round(t.logoHeight.m) } : {}
|
|
44806
|
+
}
|
|
44807
|
+
} : {},
|
|
43154
44808
|
sticky: t.headerFixed === true,
|
|
43155
44809
|
transparentOnHero: t.headerTransparent === true,
|
|
43156
44810
|
mobileMenu: "fullscreen",
|
|
@@ -43393,6 +45047,18 @@ async function buildSite(sq, opts) {
|
|
|
43393
45047
|
blocks: text7 ? [text7] : []
|
|
43394
45048
|
};
|
|
43395
45049
|
}
|
|
45050
|
+
if (footer) splitFooterLine(footer, sq.theme.footerColumns, sq.theme.footerLabels === true);
|
|
45051
|
+
const footBg = sq.theme.footerBackground;
|
|
45052
|
+
if (footer && footBg && /^#[0-9a-f]{6}$/i.test(footBg) && !footer.style.background) {
|
|
45053
|
+
const dark = relativeLuminance(footBg) < 0.18;
|
|
45054
|
+
if (dark || relativeLuminance(footBg) < 0.97) {
|
|
45055
|
+
footer.style = {
|
|
45056
|
+
...footer.style,
|
|
45057
|
+
colorTheme: dark ? "dark" : footer.style.colorTheme ?? "light",
|
|
45058
|
+
background: { type: "color", color: footBg }
|
|
45059
|
+
};
|
|
45060
|
+
}
|
|
45061
|
+
}
|
|
43396
45062
|
const logo = sq.logo ? await opts.media.image(sq.logo, "logo") : void 0;
|
|
43397
45063
|
const favicon = sq.favicon && /^https?:\/\//.test(sq.favicon) && !/\.ico(?:[?#]|$)/i.test(sq.favicon) ? await opts.media.image({ url: sq.favicon, alt: "" }, "favicon") : void 0;
|
|
43398
45064
|
const homeSeoImage = sq.pages.find((p) => p.path === "/")?.seo.image;
|
|
@@ -43485,6 +45151,35 @@ async function buildSite(sq, opts) {
|
|
|
43485
45151
|
linkFixes
|
|
43486
45152
|
};
|
|
43487
45153
|
}
|
|
45154
|
+
function splitFooterLine(footer, columns, labels = false) {
|
|
45155
|
+
if (!columns || columns < 2 || columns > 4) return;
|
|
45156
|
+
if (footer.layout.mode !== "stack" || footer.layout.columns.d.length !== 1) return;
|
|
45157
|
+
const texts = footer.blocks.filter((b) => b.type === "text");
|
|
45158
|
+
if (footer.blocks.length !== 1 || texts.length !== 1) return;
|
|
45159
|
+
const block4 = texts[0];
|
|
45160
|
+
const doc = block4.fields.content;
|
|
45161
|
+
const paras = doc.content ?? [];
|
|
45162
|
+
if (paras.length !== columns || paras.some((p) => p.type !== "paragraph")) return;
|
|
45163
|
+
const span = Math.floor(12 / columns);
|
|
45164
|
+
const spans = paras.map((_, i) => i === paras.length - 1 ? 12 - span * (columns - 1) : span);
|
|
45165
|
+
const aligns = paras.map(
|
|
45166
|
+
(_, i) => i === 0 ? "start" : i === paras.length - 1 ? "end" : "center"
|
|
45167
|
+
);
|
|
45168
|
+
footer.blocks = paras.map((p, i) => ({
|
|
45169
|
+
...block4,
|
|
45170
|
+
id: i === 0 ? block4.id : newId("blk"),
|
|
45171
|
+
column: i,
|
|
45172
|
+
fields: {
|
|
45173
|
+
...block4.fields,
|
|
45174
|
+
content: {
|
|
45175
|
+
...doc,
|
|
45176
|
+
content: [labels ? { ...p, attrs: { ...p.attrs ?? {}, preset: "eyebrow" } } : p]
|
|
45177
|
+
}
|
|
45178
|
+
},
|
|
45179
|
+
style: { ...block4.style ?? {}, align: { d: aligns[i], m: "start" } }
|
|
45180
|
+
}));
|
|
45181
|
+
footer.layout = { ...footer.layout, columns: { d: spans, m: [12] }, align: "center" };
|
|
45182
|
+
}
|
|
43488
45183
|
|
|
43489
45184
|
// ../import/src/media.ts
|
|
43490
45185
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -43714,7 +45409,7 @@ The same rules power Studio's built-in Design check. Follow them so pages look d
|
|
|
43714
45409
|
6. **Two typefaces maximum.** Change the pairing in Site Styles rather than per block.
|
|
43715
45410
|
7. **Never invent facts, testimonials, prices or credentials.** Ask the owner.
|
|
43716
45411
|
8. **Alt text describes the image's purpose,** not just its contents; decorative images are marked decorative.
|
|
43717
|
-
9. **
|
|
45412
|
+
9. **Great on phones, every time.** Most visitors use a phone. Before any publish run phone_check (it measures every page as a phone at 360 and 390 px where a browser is available), then fix_for_phones (phone-only fixes; desktop stays as designed), until there are zero phone errors and warnings. The bar: the hero's headline and main button on the first screen, no sideways scrolling or overlapping text, reading text 16px or larger, tap targets 44px or larger, text never squeezed into narrow columns, blocks in reading order, readable text over photos, no photo taller than the screen.
|
|
43718
45413
|
10. **Prefer fewer, stronger sections** over many thin ones.
|
|
43719
45414
|
11. **Custom components for bespoke moments only.** When no preset can express a distinctive section (an editorial hero, a layered collage, a horizontal story), use a custom component (list_components \u2192 create_component or insert_component from the Designer components library). Expose every word, image and link as a field (owners edit fields, never code), style only with theme tokens (var(--os-color-\u2026), var(--os-font-\u2026), var(--os-space-\u2026)), use the 767/768/1199/1200 breakpoints, honour reduced motion, and look at it with preview_component before showing the owner.
|
|
43720
45415
|
|
|
@@ -43733,12 +45428,12 @@ var ITERATION_GUIDANCE = `# Design iteration loop
|
|
|
43733
45428
|
Repeat until the page is right, keeping each pass small:
|
|
43734
45429
|
|
|
43735
45430
|
1. **Inspect** \u2014 get_page (detail "outline") for ids and copy; get_styles for the current look.
|
|
43736
|
-
2. **Check** \u2014 design_check on the page. Fix errors first (apply_fix for autofixable issues), then warnings that matter.
|
|
45431
|
+
2. **Check** \u2014 design_check and phone_check on the page. Fix errors first (apply_fix for autofixable issues, fix_for_phones for phone issues), then warnings that matter.
|
|
43737
45432
|
3. **Edit** \u2014 one intent per pass: replace demo copy (update_block), swap a section for a better preset (insert_section + delete_section), adjust a section's color theme for rhythm (update_section colorTheme), or restyle globally (set_styles / apply_template_look).
|
|
43738
45433
|
4. **Look** \u2014 screenshot the page (or just the section you changed) at desktop and mobile; use render_page when screenshots are unavailable.
|
|
43739
45434
|
5. **Judge** \u2014 hierarchy (one H1, clear headline), one primary action per screen, rhythm of themes, copy within preset ranges, images consistent, nothing cramped on mobile. Then go back to 2.
|
|
43740
45435
|
|
|
43741
|
-
Finish with design_check showing zero errors and get_draft_diff to summarise the work for the owner.
|
|
45436
|
+
Finish with design_check showing zero errors, phone_check showing zero phone errors and warnings, and get_draft_diff to summarise the work for the owner.
|
|
43742
45437
|
`;
|
|
43743
45438
|
|
|
43744
45439
|
// ../mcp-tools/src/images.ts
|
|
@@ -44158,7 +45853,7 @@ function renderPreviewHtml(doc, pageId, opts = {}) {
|
|
|
44158
45853
|
const html = renderPageHtml({
|
|
44159
45854
|
doc,
|
|
44160
45855
|
pageId: target,
|
|
44161
|
-
mode: "preview",
|
|
45856
|
+
mode: opts.production ? "production" : "preview",
|
|
44162
45857
|
basePath: opts.basePath ?? "",
|
|
44163
45858
|
behaviorsUrl: opts.behaviorsUrl,
|
|
44164
45859
|
extraHead: blocksStyleTag()
|
|
@@ -45316,10 +47011,10 @@ var listSectionPresets = defineTool({
|
|
|
45316
47011
|
let list5 = presetCatalog.slice();
|
|
45317
47012
|
if (input.category) list5 = list5.filter((p) => p.category === input.category);
|
|
45318
47013
|
if (input.query) {
|
|
45319
|
-
const
|
|
47014
|
+
const words4 = input.query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
45320
47015
|
list5 = list5.filter((p) => {
|
|
45321
47016
|
const hay = `${p.id} ${p.label} ${p.description} ${p.tags.join(" ")} ${p.ai.whenToUse}`.toLowerCase();
|
|
45322
|
-
return
|
|
47017
|
+
return words4.every((w) => hay.includes(w));
|
|
45323
47018
|
});
|
|
45324
47019
|
}
|
|
45325
47020
|
const full = input.detail === "full";
|
|
@@ -45678,12 +47373,12 @@ var designCheckTool = defineTool({
|
|
|
45678
47373
|
const issues = runChecks(doc, pageId, input.checks ?? CHECKS, ctx.now()).filter(
|
|
45679
47374
|
(i) => RANK[i.severity] <= min
|
|
45680
47375
|
);
|
|
45681
|
-
const
|
|
45682
|
-
for (const i of issues)
|
|
47376
|
+
const counts2 = { error: 0, warning: 0, info: 0 };
|
|
47377
|
+
for (const i of issues) counts2[i.severity]++;
|
|
45683
47378
|
return {
|
|
45684
47379
|
data: {
|
|
45685
47380
|
...pageId ? { page: pageId } : {},
|
|
45686
|
-
counts,
|
|
47381
|
+
counts: counts2,
|
|
45687
47382
|
issues: issues.map((i) => ({
|
|
45688
47383
|
id: i.id,
|
|
45689
47384
|
rule: i.rule,
|
|
@@ -45744,9 +47439,9 @@ var applyFix = defineTool({
|
|
|
45744
47439
|
});
|
|
45745
47440
|
}
|
|
45746
47441
|
const remaining = runChecks(doc, pageId, CHECKS, ctx.now());
|
|
45747
|
-
const
|
|
45748
|
-
for (const i of remaining)
|
|
45749
|
-
return { data: { applied, skipped, remaining:
|
|
47442
|
+
const counts2 = { error: 0, warning: 0, info: 0 };
|
|
47443
|
+
for (const i of remaining) counts2[i.severity]++;
|
|
47444
|
+
return { data: { applied, skipped, remaining: counts2 } };
|
|
45750
47445
|
}
|
|
45751
47446
|
});
|
|
45752
47447
|
var preview = defineTool({
|
|
@@ -46872,6 +48567,143 @@ var pageTools = [
|
|
|
46872
48567
|
listPosts
|
|
46873
48568
|
];
|
|
46874
48569
|
|
|
48570
|
+
// ../mcp-tools/src/tools/phone.ts
|
|
48571
|
+
var PHONE_PAGE_LIMIT = 12;
|
|
48572
|
+
var stripMeta2 = (op2) => {
|
|
48573
|
+
const { id: _i, actor: _a3, ts: _t, ...body } = op2;
|
|
48574
|
+
return body;
|
|
48575
|
+
};
|
|
48576
|
+
async function measure(ctx, siteId, doc, pages, wantRendered) {
|
|
48577
|
+
const now = ctx.now();
|
|
48578
|
+
const statics = phoneCheck(doc, pages.length === 1 ? pages[0]?.id : void 0, {
|
|
48579
|
+
actor: "phone-check",
|
|
48580
|
+
now
|
|
48581
|
+
});
|
|
48582
|
+
const store = ctx.store;
|
|
48583
|
+
if (!wantRendered || !store.capabilities.canMeasurePhone || !store.measurePhone)
|
|
48584
|
+
return {
|
|
48585
|
+
issues: statics,
|
|
48586
|
+
rendered: false,
|
|
48587
|
+
measuredPages: [],
|
|
48588
|
+
...wantRendered ? {
|
|
48589
|
+
note: store.capabilities.publishNote?.includes("git") ? "Only the rules that don't need a browser ran. For the full phone check install Playwright next to the CLI (npm i -D playwright && npx playwright install chromium) and restart the MCP server." : "Only the rules that don't need a browser ran here; screenshot the page on mobile to look it over."
|
|
48590
|
+
} : {}
|
|
48591
|
+
};
|
|
48592
|
+
const measured = pages.slice(0, PHONE_PAGE_LIMIT);
|
|
48593
|
+
const rendered2 = [];
|
|
48594
|
+
let note;
|
|
48595
|
+
for (const page of measured) {
|
|
48596
|
+
const html = renderPreviewHtml(doc, page.id, { production: true });
|
|
48597
|
+
const results = [];
|
|
48598
|
+
try {
|
|
48599
|
+
for (const width of PHONE_WIDTHS)
|
|
48600
|
+
results.push(await store.measurePhone(siteId, { pageId: page.id, width, html }));
|
|
48601
|
+
} catch (e) {
|
|
48602
|
+
note = `The browser check stopped at \u201C${page.title}\u201D: ${e instanceof Error ? e.message : String(e)}`;
|
|
48603
|
+
break;
|
|
48604
|
+
}
|
|
48605
|
+
rendered2.push(...phoneIssuesFromProbe(doc, page.id, results, { actor: "phone-check", now }));
|
|
48606
|
+
}
|
|
48607
|
+
if (!note && pages.length > measured.length)
|
|
48608
|
+
note = `Measured the first ${measured.length} of ${pages.length} pages; run phone_check with page for the others.`;
|
|
48609
|
+
const seen = /* @__PURE__ */ new Set();
|
|
48610
|
+
const deduped = rendered2.filter((i) => {
|
|
48611
|
+
if (i.page !== "footer") return true;
|
|
48612
|
+
const key2 = `${i.rule}|${i.message}`;
|
|
48613
|
+
if (seen.has(key2)) return false;
|
|
48614
|
+
seen.add(key2);
|
|
48615
|
+
return true;
|
|
48616
|
+
});
|
|
48617
|
+
return {
|
|
48618
|
+
issues: sortIssues([...statics, ...deduped]),
|
|
48619
|
+
rendered: true,
|
|
48620
|
+
measuredPages: measured.map((p) => p.id),
|
|
48621
|
+
...note ? { note } : {}
|
|
48622
|
+
};
|
|
48623
|
+
}
|
|
48624
|
+
var pagesFor = (doc, page) => page ? [resolvePage(doc, page).page] : Object.values(doc.pages);
|
|
48625
|
+
var describe2 = (i) => ({
|
|
48626
|
+
id: i.id,
|
|
48627
|
+
rule: i.rule,
|
|
48628
|
+
severity: i.severity,
|
|
48629
|
+
...i.page ? { page: i.page } : {},
|
|
48630
|
+
...i.section ? { section: i.section } : {},
|
|
48631
|
+
...i.block ? { block: i.block } : {},
|
|
48632
|
+
message: i.message,
|
|
48633
|
+
fix: i.fix,
|
|
48634
|
+
autofix: i.autofix?.label ?? null
|
|
48635
|
+
});
|
|
48636
|
+
var counts = (issues) => {
|
|
48637
|
+
const c = { error: 0, warning: 0, info: 0 };
|
|
48638
|
+
for (const i of issues) c[i.severity]++;
|
|
48639
|
+
return c;
|
|
48640
|
+
};
|
|
48641
|
+
var phoneCheckTool = defineTool({
|
|
48642
|
+
name: "phone_check",
|
|
48643
|
+
title: "Phone check",
|
|
48644
|
+
description: "Check how the site works on phones: the rules that need no browser (sections not laid out for phones, columns squeezed side by side, padding that empties the screen, heading words too long for a phone line, tall photo sections, fixed widths in custom CSS) plus, where a browser is available, a real phone layout at 360 and 390 px (sideways scrolling, cut-off or overlapping text, text too small, buttons too small to tap, text over photos, the main button below the first screen). Run it before every publish; fix with fix_for_phones. Aim for zero errors and warnings.",
|
|
48645
|
+
input: external_exports.object({
|
|
48646
|
+
site: siteArg,
|
|
48647
|
+
page: external_exports.string().optional().describe("Page id/slug/title (default: every page)."),
|
|
48648
|
+
rendered: external_exports.boolean().optional().describe("Measure in a real browser when available (default true).")
|
|
48649
|
+
}),
|
|
48650
|
+
annotations: { readOnlyHint: true },
|
|
48651
|
+
async handler(ctx, input) {
|
|
48652
|
+
const siteId = await ctx.siteId(input.site);
|
|
48653
|
+
const doc = await ctx.doc(siteId);
|
|
48654
|
+
const r = await measure(ctx, siteId, doc, pagesFor(doc, input.page), input.rendered !== false);
|
|
48655
|
+
const c = counts(r.issues);
|
|
48656
|
+
return {
|
|
48657
|
+
data: {
|
|
48658
|
+
rendered: r.rendered,
|
|
48659
|
+
...r.note ? { note: r.note } : {},
|
|
48660
|
+
counts: c,
|
|
48661
|
+
issues: r.issues.map(describe2),
|
|
48662
|
+
next: c.error + c.warning === 0 ? "Looks great on phones." : r.issues.some((i) => i.autofix) ? "Run fix_for_phones to fix everything that can be fixed automatically, then phone_check again; handle the rest by editing." : "Edit the content as each fix says, then run phone_check again."
|
|
48663
|
+
}
|
|
48664
|
+
};
|
|
48665
|
+
}
|
|
48666
|
+
});
|
|
48667
|
+
var fixForPhonesTool = defineTool({
|
|
48668
|
+
name: "fix_for_phones",
|
|
48669
|
+
title: "Fix everything for phones",
|
|
48670
|
+
description: "Apply every automatic phone fix in one change (phone-only settings: phone layouts in reading order, words first in heroes, stacked columns, lighter phone padding, shorter photo sections, smaller phone heading sizes, stronger photo overlays). Desktop stays as designed. Re-measures and returns what's left.",
|
|
48671
|
+
input: external_exports.object({
|
|
48672
|
+
site: siteArg,
|
|
48673
|
+
page: external_exports.string().optional().describe("Limit to a page (default: every page).")
|
|
48674
|
+
}),
|
|
48675
|
+
annotations: {},
|
|
48676
|
+
async handler(ctx, input) {
|
|
48677
|
+
const siteId = await ctx.siteId(input.site);
|
|
48678
|
+
let doc = await ctx.doc(siteId);
|
|
48679
|
+
const pages = pagesFor(doc, input.page);
|
|
48680
|
+
const before = await measure(ctx, siteId, doc, pages, true);
|
|
48681
|
+
const r = fixForPhones(doc, {
|
|
48682
|
+
...input.page ? { pageId: pages[0]?.id } : {},
|
|
48683
|
+
actor: "phone-check",
|
|
48684
|
+
now: ctx.now(),
|
|
48685
|
+
rendered: before.issues
|
|
48686
|
+
});
|
|
48687
|
+
if (r.ops.length) {
|
|
48688
|
+
const applied = await ctx.apply(siteId, r.ops.map(stripMeta2));
|
|
48689
|
+
doc = applied.doc;
|
|
48690
|
+
}
|
|
48691
|
+
const after = await measure(ctx, siteId, doc, pagesFor(doc, input.page), true);
|
|
48692
|
+
const labels = /* @__PURE__ */ new Map();
|
|
48693
|
+
for (const f of r.fixed) labels.set(f.label, (labels.get(f.label) ?? 0) + 1);
|
|
48694
|
+
return {
|
|
48695
|
+
data: {
|
|
48696
|
+
fixed: [...labels].map(([label, n]) => n > 1 ? `${label} (${n}\xD7)` : label),
|
|
48697
|
+
rendered: after.rendered,
|
|
48698
|
+
remaining: counts(after.issues),
|
|
48699
|
+
issues: after.issues.filter((i) => i.severity !== "info").map(describe2),
|
|
48700
|
+
next: after.issues.filter((i) => i.severity !== "info").length === 0 ? "Looks great on phones." : "These need an edit (no automatic fix): follow each fix, then run phone_check again."
|
|
48701
|
+
}
|
|
48702
|
+
};
|
|
48703
|
+
}
|
|
48704
|
+
});
|
|
48705
|
+
var phoneTools = [phoneCheckTool, fixForPhonesTool];
|
|
48706
|
+
|
|
46875
48707
|
// ../mcp-tools/src/tools/publish.ts
|
|
46876
48708
|
var getDraftDiff = defineTool({
|
|
46877
48709
|
name: "get_draft_diff",
|
|
@@ -47017,6 +48849,7 @@ var ALL_TOOLS = [
|
|
|
47017
48849
|
...pageTools,
|
|
47018
48850
|
...designSystemTools,
|
|
47019
48851
|
...designTools,
|
|
48852
|
+
...phoneTools,
|
|
47020
48853
|
...componentTools,
|
|
47021
48854
|
...publishTools
|
|
47022
48855
|
];
|
|
@@ -47808,6 +49641,25 @@ function squarespaceRenditions(u, width, height, canResize = true) {
|
|
|
47808
49641
|
];
|
|
47809
49642
|
}
|
|
47810
49643
|
|
|
49644
|
+
// ../import/src/phone.ts
|
|
49645
|
+
function phoneReady(result) {
|
|
49646
|
+
let fixed;
|
|
49647
|
+
try {
|
|
49648
|
+
fixed = fixForPhones(result.doc, { actor: "import", now: 0 });
|
|
49649
|
+
} catch {
|
|
49650
|
+
return result;
|
|
49651
|
+
}
|
|
49652
|
+
if (!fixed.ops.length) return result;
|
|
49653
|
+
const labels = [...new Set(fixed.fixed.map((f) => f.label.toLowerCase()))];
|
|
49654
|
+
const line = `
|
|
49655
|
+
## Phones
|
|
49656
|
+
|
|
49657
|
+
Made ${fixed.fixed.length} phone-only adjustment${fixed.fixed.length === 1 ? "" : "s"} so the site reads well on phones (${labels.join(", ")}). The desktop design is unchanged.
|
|
49658
|
+
`;
|
|
49659
|
+
return { ...result, doc: fixed.doc, report: `${result.report.trimEnd()}
|
|
49660
|
+
${line}` };
|
|
49661
|
+
}
|
|
49662
|
+
|
|
47811
49663
|
// ../import/src/report.ts
|
|
47812
49664
|
var NOTICE_TITLES = {
|
|
47813
49665
|
"unsupported-block": "Blocks that need a look",
|
|
@@ -47943,17 +49795,17 @@ function renderReport(r) {
|
|
|
47943
49795
|
any = true;
|
|
47944
49796
|
L2.push(`### ${NOTICE_TITLES[kind]}`);
|
|
47945
49797
|
L2.push("");
|
|
47946
|
-
const
|
|
49798
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
47947
49799
|
for (const n of list5) {
|
|
47948
|
-
const e =
|
|
49800
|
+
const e = counts2.get(n.message) ?? { n: 0, where: /* @__PURE__ */ new Set() };
|
|
47949
49801
|
e.n++;
|
|
47950
49802
|
e.where.add(n.where);
|
|
47951
|
-
|
|
49803
|
+
counts2.set(n.message, e);
|
|
47952
49804
|
}
|
|
47953
49805
|
let shown = 0;
|
|
47954
|
-
for (const [message, e] of
|
|
49806
|
+
for (const [message, e] of counts2) {
|
|
47955
49807
|
if (++shown > 15) {
|
|
47956
|
-
L2.push(`- \u2026 and ${
|
|
49808
|
+
L2.push(`- \u2026 and ${counts2.size - 15} more like these.`);
|
|
47957
49809
|
break;
|
|
47958
49810
|
}
|
|
47959
49811
|
const where2 = [...e.where];
|
|
@@ -48346,7 +50198,7 @@ async function crawlWordPress(opts) {
|
|
|
48346
50198
|
...it.excerptHtml ? { caption: it.excerptHtml } : {}
|
|
48347
50199
|
});
|
|
48348
50200
|
const entries = [];
|
|
48349
|
-
const
|
|
50201
|
+
const pathOf2 = (link2, fallback2) => (link2 ? sitePath(link2, base) : void 0) ?? fallback2;
|
|
48350
50202
|
if (rest) {
|
|
48351
50203
|
const [pages2, posts] = [
|
|
48352
50204
|
await listAll("wp/v2/pages", "_embed=1&orderby=menu_order&order=asc"),
|
|
@@ -48373,7 +50225,7 @@ async function crawlWordPress(opts) {
|
|
|
48373
50225
|
}
|
|
48374
50226
|
entries.push({
|
|
48375
50227
|
type,
|
|
48376
|
-
path:
|
|
50228
|
+
path: pathOf2(link2, `/${slug}`),
|
|
48377
50229
|
slug,
|
|
48378
50230
|
title: decodeEntities2(rendered(p.title)) || slug || "Untitled",
|
|
48379
50231
|
html: rendered(p.content),
|
|
@@ -48440,7 +50292,7 @@ async function crawlWordPress(opts) {
|
|
|
48440
50292
|
});
|
|
48441
50293
|
}
|
|
48442
50294
|
if (!rest && !wxr && home) {
|
|
48443
|
-
const { crawlGeneric: crawlGeneric2 } = await import("./crawl-
|
|
50295
|
+
const { crawlGeneric: crawlGeneric2 } = await import("./crawl-WQQDFI7W.js");
|
|
48444
50296
|
const site2 = await crawlGeneric2({
|
|
48445
50297
|
url: base,
|
|
48446
50298
|
fetcher: f,
|
|
@@ -48496,6 +50348,9 @@ async function crawlWordPress(opts) {
|
|
|
48496
50348
|
}
|
|
48497
50349
|
const pages = [];
|
|
48498
50350
|
const builders = /* @__PURE__ */ new Set();
|
|
50351
|
+
const showsTitles = /<h1[^>]*class="[^"]*\b(?:entry-title|page-title|wp-block-post-title)\b/.test(
|
|
50352
|
+
homeHtml
|
|
50353
|
+
);
|
|
48499
50354
|
for (const e of pageEntries.sort((a, b) => (a.menuOrder ?? 0) - (b.menuOrder ?? 0))) {
|
|
48500
50355
|
if (e === postsPage) continue;
|
|
48501
50356
|
if (COMMERCE_SLUGS.test(e.slug) && e.path.split("/").filter(Boolean).length === 1) {
|
|
@@ -48513,7 +50368,11 @@ async function crawlWordPress(opts) {
|
|
|
48513
50368
|
path: e.path,
|
|
48514
50369
|
title: e.title,
|
|
48515
50370
|
seo: { ...e.image && !e.seo.image ? { image: e.image } : {}, ...e.seo },
|
|
48516
|
-
sections:
|
|
50371
|
+
sections: withPageTitle(
|
|
50372
|
+
bodySections(e.html, base, postsPath, notices, e.path),
|
|
50373
|
+
e.title,
|
|
50374
|
+
showsTitles
|
|
50375
|
+
),
|
|
48517
50376
|
...nav.length && !navPaths.has(e.path) && e.path !== "/" ? { hidden: true } : {}
|
|
48518
50377
|
});
|
|
48519
50378
|
}
|
|
@@ -48660,6 +50519,32 @@ function wxrNav(wxr, base, entries) {
|
|
|
48660
50519
|
}
|
|
48661
50520
|
return out;
|
|
48662
50521
|
}
|
|
50522
|
+
function withPageTitle(sections, title, shows) {
|
|
50523
|
+
if (!shows || !title.trim()) return sections;
|
|
50524
|
+
const norm7 = (t) => t.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
50525
|
+
const first2 = sections[0];
|
|
50526
|
+
const blocks2 = first2?.layout.mode === "classic" ? first2.layout.rows.flatMap((r) => r.columns.flatMap((c) => c.blocks)) : [];
|
|
50527
|
+
const opening = blocks2[0];
|
|
50528
|
+
if (opening && (opening.kind === "heading" && norm7(opening.text) === norm7(title) || opening.kind === "text" && /^\s*<h1/i.test(opening.html)))
|
|
50529
|
+
return sections;
|
|
50530
|
+
const heading2 = { kind: "heading", text: title, level: 1 };
|
|
50531
|
+
if (first2 && first2.layout.mode === "classic" && !first2.background?.image && !first2.role) {
|
|
50532
|
+
const row = first2.layout.rows[0];
|
|
50533
|
+
if (row && row.columns.length === 1) {
|
|
50534
|
+
row.columns[0]?.blocks.unshift(heading2);
|
|
50535
|
+
return sections;
|
|
50536
|
+
}
|
|
50537
|
+
first2.layout.rows.unshift({ columns: [{ span: 12, blocks: [heading2] }] });
|
|
50538
|
+
return sections;
|
|
50539
|
+
}
|
|
50540
|
+
return [
|
|
50541
|
+
{
|
|
50542
|
+
theme: "",
|
|
50543
|
+
layout: { mode: "classic", rows: [{ columns: [{ span: 12, blocks: [heading2] }] }] }
|
|
50544
|
+
},
|
|
50545
|
+
...sections
|
|
50546
|
+
];
|
|
50547
|
+
}
|
|
48663
50548
|
|
|
48664
50549
|
// ../import/src/fidelity/component-gen.ts
|
|
48665
50550
|
var norm5 = (s2) => (s2 ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
@@ -48667,7 +50552,7 @@ var esc3 = (s2) => s2.replace(/[<>&"]/g, "");
|
|
|
48667
50552
|
var round5 = (n, d = 2) => Math.round(n * 10 ** d) / 10 ** d;
|
|
48668
50553
|
function canGenerate(section) {
|
|
48669
50554
|
const b = section.blueprint;
|
|
48670
|
-
return !!b && b.length >= 2 && b.length <=
|
|
50555
|
+
return !!b && b.filter((i) => i.kind !== "rule" && i.kind !== "panel").length >= 2 && b.length <= 36 && section.height <= 2400;
|
|
48671
50556
|
}
|
|
48672
50557
|
function componentFromBlueprint(input) {
|
|
48673
50558
|
const items = (input.section.blueprint ?? []).filter(
|
|
@@ -48685,10 +50570,13 @@ function componentFromBlueprint(input) {
|
|
|
48685
50570
|
const css = [
|
|
48686
50571
|
":scope { width: min(100%, " + Math.round(width) + "px); margin-inline: auto; }",
|
|
48687
50572
|
".gx { display: grid; grid-template-columns: repeat(24, minmax(0, 1fr)); grid-auto-rows: minmax(8px, auto); column-gap: 0; }",
|
|
48688
|
-
".gx__i { margin: 0; min-width: 0; overflow-wrap: break-word; }",
|
|
50573
|
+
".gx__i { position: relative; z-index: 1; margin: 0; min-width: 0; overflow-wrap: break-word; }",
|
|
50574
|
+
".gx__i p { margin: 0; }",
|
|
50575
|
+
".gx__p { z-index: 0; }",
|
|
48689
50576
|
".gx__img picture, .gx__img img { display: block; width: 100%; height: 100%; object-fit: cover; }"
|
|
48690
50577
|
];
|
|
48691
50578
|
const phone = [];
|
|
50579
|
+
const named = [];
|
|
48692
50580
|
let headingSeen = false;
|
|
48693
50581
|
const role = (family) => {
|
|
48694
50582
|
if (norm5(family) && norm5(family) === norm5(input.roles.label)) return "label";
|
|
@@ -48723,6 +50611,10 @@ function componentFromBlueprint(input) {
|
|
|
48723
50611
|
]
|
|
48724
50612
|
].join("; ");
|
|
48725
50613
|
};
|
|
50614
|
+
const tokenColor = (hex, fallback2) => {
|
|
50615
|
+
const v = colorVar(hex);
|
|
50616
|
+
return v === "inherit" ? fallback2 : v;
|
|
50617
|
+
};
|
|
48726
50618
|
items.forEach((it, k) => {
|
|
48727
50619
|
const n = k + 1;
|
|
48728
50620
|
const cls = `gx__i gx__i${n}`;
|
|
@@ -48731,10 +50623,26 @@ function componentFromBlueprint(input) {
|
|
|
48731
50623
|
const row = Math.floor((it.box.y - top) / 8) + 1;
|
|
48732
50624
|
const rows = Math.max(1, Math.round(it.box.h / 8));
|
|
48733
50625
|
const place = `grid-column: ${col} / span ${span}; grid-row: ${row} / span ${rows}`;
|
|
50626
|
+
if (it.kind === "rule") {
|
|
50627
|
+
parts.push(`<hr class="gx__p gx__r${n}">`);
|
|
50628
|
+
css.push(
|
|
50629
|
+
`.gx__r${n} { ${place.replace(/span \d+$/, "span 1")}; align-self: start; width: 100%; height: 0; margin: 0; border: 0; border-top: ${Math.max(1, Math.min(8, it.box.h))}px solid ${tokenColor(it.color, "var(--os-color-border)")}; }`
|
|
50630
|
+
);
|
|
50631
|
+
return;
|
|
50632
|
+
}
|
|
50633
|
+
if (it.kind === "panel") {
|
|
50634
|
+
parts.push(`<div class="gx__p gx__p${n}" aria-hidden="true"></div>`);
|
|
50635
|
+
css.push(
|
|
50636
|
+
`.gx__p${n} { ${place}; ${it.color ? `background: ${tokenColor(it.color, "var(--os-color-surface)")};` : ""} ${it.border ? `border: ${Math.max(1, Math.min(8, it.border.width))}px solid ${tokenColor(it.border.color, "var(--os-color-border)")};` : ""} border-radius: ${Math.min(48, Math.round(it.radius ?? 0))}px; }`
|
|
50637
|
+
);
|
|
50638
|
+
phone.push(`.gx__p${n} { display: none; }`);
|
|
50639
|
+
return;
|
|
50640
|
+
}
|
|
48734
50641
|
if (it.kind === "image") {
|
|
48735
50642
|
const key3 = `image${n}`;
|
|
48736
50643
|
fields[key3] = { kind: "image", label: `Picture ${n}` };
|
|
48737
50644
|
values[key3] = { src: it.src, alt: (it.alt ?? "").slice(0, 300) };
|
|
50645
|
+
named.push({ role: "picture", text: it.alt ?? "", keys: [key3] });
|
|
48738
50646
|
parts.push(
|
|
48739
50647
|
`<div class="${cls} gx__img">{{image ${key3} sizes="(max-width: 767px) 100vw, ${Math.max(10, Math.round(it.box.w / 1440 * 100))}vw"}}</div>`
|
|
48740
50648
|
);
|
|
@@ -48753,6 +50661,7 @@ function componentFromBlueprint(input) {
|
|
|
48753
50661
|
};
|
|
48754
50662
|
values[label] = (it.text ?? "").slice(0, 60);
|
|
48755
50663
|
if (!target) {
|
|
50664
|
+
named.push({ role: it.kind, text: it.text ?? "", keys: [label] });
|
|
48756
50665
|
parts.push(`<p class="${cls}">{{${label}}}</p>`);
|
|
48757
50666
|
} else {
|
|
48758
50667
|
const lk = `link${n}`;
|
|
@@ -48761,6 +50670,7 @@ function componentFromBlueprint(input) {
|
|
|
48761
50670
|
label: `${it.kind === "button" ? "Button" : "Link"} ${n} goes to`
|
|
48762
50671
|
};
|
|
48763
50672
|
values[lk] = target;
|
|
50673
|
+
named.push({ role: it.kind, text: it.text ?? "", keys: [label, lk] });
|
|
48764
50674
|
parts.push(
|
|
48765
50675
|
it.kind === "button" ? `<div class="${cls}">{{button ${lk} label=${label} variant="primary"}}</div>` : `<p class="${cls}">{{#link ${lk}}}{{${label}}}{{/link}}</p>`
|
|
48766
50676
|
);
|
|
@@ -48772,6 +50682,22 @@ function componentFromBlueprint(input) {
|
|
|
48772
50682
|
return;
|
|
48773
50683
|
}
|
|
48774
50684
|
const key2 = `text${n}`;
|
|
50685
|
+
const doc = it.kind === "text" ? richDoc(it.runs, input.origin) : void 0;
|
|
50686
|
+
if (doc) {
|
|
50687
|
+
fields[key2] = {
|
|
50688
|
+
kind: "richText",
|
|
50689
|
+
label: `Text ${n}`,
|
|
50690
|
+
marks: ["bold", "italic", "link"],
|
|
50691
|
+
blocks: ["paragraph"],
|
|
50692
|
+
maxLength: 3e3
|
|
50693
|
+
};
|
|
50694
|
+
values[key2] = doc;
|
|
50695
|
+
named.push({ role: textRole(it), text: it.text ?? "", keys: [key2] });
|
|
50696
|
+
parts.push(`<div class="${cls}">{{rich ${key2}}}</div>`);
|
|
50697
|
+
css.push(`.gx__i${n} { ${place}; ${typeCss(it.style, false)}; }`);
|
|
50698
|
+
phone.push(`.gx__i${n} { ${typeCss(it.style, true)}; }`);
|
|
50699
|
+
return;
|
|
50700
|
+
}
|
|
48775
50701
|
const text7 = (it.text ?? "").slice(0, it.kind === "heading" ? 300 : 1500);
|
|
48776
50702
|
fields[key2] = {
|
|
48777
50703
|
kind: "text",
|
|
@@ -48780,6 +50706,7 @@ function componentFromBlueprint(input) {
|
|
|
48780
50706
|
...it.kind === "text" ? { multiline: true } : {}
|
|
48781
50707
|
};
|
|
48782
50708
|
values[key2] = text7;
|
|
50709
|
+
named.push({ role: it.kind === "heading" ? "heading" : textRole(it), text: text7, keys: [key2] });
|
|
48783
50710
|
const tag = it.kind === "heading" ? headingSeen ? "h3" : "h2" : "p";
|
|
48784
50711
|
if (it.kind === "heading") headingSeen = true;
|
|
48785
50712
|
parts.push(`<${tag} class="${cls}">{{${key2}}}</${tag}>`);
|
|
@@ -48787,6 +50714,14 @@ function componentFromBlueprint(input) {
|
|
|
48787
50714
|
phone.push(`.gx__i${n} { ${typeCss(it.style, true)}; }`);
|
|
48788
50715
|
});
|
|
48789
50716
|
if (!parts.length) return void 0;
|
|
50717
|
+
for (const [i, label] of partLabels(named).entries()) {
|
|
50718
|
+
const part = named[i];
|
|
50719
|
+
const [first2, second] = part.keys;
|
|
50720
|
+
const f = fields[first2];
|
|
50721
|
+
if (f) fields[first2] = { ...f, label };
|
|
50722
|
+
const l = second ? fields[second] : void 0;
|
|
50723
|
+
if (second && l) fields[second] = { ...l, label: `${label} goes to` };
|
|
50724
|
+
}
|
|
48790
50725
|
css.push(
|
|
48791
50726
|
`@media (max-width: 767px) {
|
|
48792
50727
|
.gx { display: flex; flex-direction: column; gap: var(--os-space-sm); }
|
|
@@ -48809,15 +50744,109 @@ ${parts.map((p) => ` ${p}`).join("\n")}
|
|
|
48809
50744
|
};
|
|
48810
50745
|
return { def, fields: values };
|
|
48811
50746
|
}
|
|
50747
|
+
function textRole(it) {
|
|
50748
|
+
const words4 = (it.text ?? "").trim().split(/\s+/).filter(Boolean).length;
|
|
50749
|
+
const caps = it.style?.case === "uppercase" || /^[^a-z]*[A-Z][^a-z]*$/.test(it.text ?? "");
|
|
50750
|
+
return words4 <= 5 && (caps || (it.style?.size ?? 16) <= 14) ? "small" : "paragraph";
|
|
50751
|
+
}
|
|
50752
|
+
var ROLE_LABEL = {
|
|
50753
|
+
heading: "Heading",
|
|
50754
|
+
small: "Small label",
|
|
50755
|
+
paragraph: "Paragraph",
|
|
50756
|
+
picture: "Picture",
|
|
50757
|
+
button: "Button",
|
|
50758
|
+
link: "Link"
|
|
50759
|
+
};
|
|
50760
|
+
function snippet(text7) {
|
|
50761
|
+
const words4 = text7.replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
|
|
50762
|
+
let out = "";
|
|
50763
|
+
for (const w of words4) {
|
|
50764
|
+
if (`${out} ${w}`.trim().length > 24) break;
|
|
50765
|
+
out = `${out} ${w}`.trim();
|
|
50766
|
+
}
|
|
50767
|
+
if (!out && words4[0]) out = words4[0].slice(0, 22);
|
|
50768
|
+
if (!out) return "";
|
|
50769
|
+
const cased = /[a-z]/.test(out) ? out : out.charAt(0) + out.slice(1).toLowerCase();
|
|
50770
|
+
return `${cased}${out.length < text7.trim().length ? "\u2026" : ""}`;
|
|
50771
|
+
}
|
|
50772
|
+
function partLabels(parts) {
|
|
50773
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
50774
|
+
for (const p of parts) counts2.set(p.role, (counts2.get(p.role) ?? 0) + 1);
|
|
50775
|
+
const used = /* @__PURE__ */ new Set();
|
|
50776
|
+
const seen = /* @__PURE__ */ new Map();
|
|
50777
|
+
return parts.map((p) => {
|
|
50778
|
+
const nth = (seen.get(p.role) ?? 0) + 1;
|
|
50779
|
+
seen.set(p.role, nth);
|
|
50780
|
+
const base = ROLE_LABEL[p.role];
|
|
50781
|
+
let label;
|
|
50782
|
+
if ((counts2.get(p.role) ?? 0) === 1) label = base;
|
|
50783
|
+
else if (p.role === "button" && nth === 1) label = "Main button";
|
|
50784
|
+
else {
|
|
50785
|
+
const words4 = p.role === "picture" ? "" : snippet(p.text);
|
|
50786
|
+
label = words4 ? `${base} \u201C${words4}\u201D` : `${base} ${nth}`;
|
|
50787
|
+
}
|
|
50788
|
+
let unique = label;
|
|
50789
|
+
for (let k = 2; used.has(unique); k++) unique = `${label} ${k}`;
|
|
50790
|
+
used.add(unique);
|
|
50791
|
+
return unique.slice(0, 60);
|
|
50792
|
+
});
|
|
50793
|
+
}
|
|
50794
|
+
function richDoc(runs, origin) {
|
|
50795
|
+
if (!runs?.length) return void 0;
|
|
50796
|
+
const content3 = [];
|
|
50797
|
+
for (const r of runs) {
|
|
50798
|
+
if (r.t === "\n") {
|
|
50799
|
+
content3.push({ type: "hardBreak" });
|
|
50800
|
+
continue;
|
|
50801
|
+
}
|
|
50802
|
+
const text7 = r.t;
|
|
50803
|
+
if (!text7) continue;
|
|
50804
|
+
const marks = [];
|
|
50805
|
+
if (r.b) marks.push({ type: "bold" });
|
|
50806
|
+
if (r.i) marks.push({ type: "italic" });
|
|
50807
|
+
const href = r.href && safeHref(r.href, origin);
|
|
50808
|
+
if (href) marks.push({ type: "link", attrs: { href } });
|
|
50809
|
+
const last2 = content3[content3.length - 1];
|
|
50810
|
+
if (last2?.type === "text" && JSON.stringify(last2.marks ?? []) === JSON.stringify(marks)) {
|
|
50811
|
+
last2.text = `${last2.text}${text7}`;
|
|
50812
|
+
continue;
|
|
50813
|
+
}
|
|
50814
|
+
content3.push({ type: "text", text: text7, ...marks.length ? { marks } : {} });
|
|
50815
|
+
}
|
|
50816
|
+
const first2 = content3[0];
|
|
50817
|
+
if (first2?.type === "text" && first2.text) first2.text = first2.text.replace(/^\s+/, "");
|
|
50818
|
+
const end = content3[content3.length - 1];
|
|
50819
|
+
if (end?.type === "text" && end.text) end.text = end.text.replace(/\s+$/, "");
|
|
50820
|
+
const kept = content3.filter((c) => c.type !== "text" || c.text);
|
|
50821
|
+
if (!kept.length) return void 0;
|
|
50822
|
+
return { type: "doc", content: [{ type: "paragraph", content: kept }] };
|
|
50823
|
+
}
|
|
50824
|
+
function safeHref(href, origin) {
|
|
50825
|
+
if (/^(?:mailto:|tel:)/i.test(href)) return href;
|
|
50826
|
+
try {
|
|
50827
|
+
const u = new URL(href);
|
|
50828
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") return void 0;
|
|
50829
|
+
if (origin && u.origin === origin) return `${u.pathname}${u.hash}` || "/";
|
|
50830
|
+
return u.toString();
|
|
50831
|
+
} catch {
|
|
50832
|
+
return void 0;
|
|
50833
|
+
}
|
|
50834
|
+
}
|
|
48812
50835
|
|
|
48813
50836
|
// ../import/src/fidelity/tune.ts
|
|
48814
50837
|
var TUNE_GROUPS = [
|
|
48815
50838
|
"type",
|
|
48816
50839
|
"fonts",
|
|
48817
50840
|
"rhythm",
|
|
50841
|
+
"rhythm-phone",
|
|
50842
|
+
"widths",
|
|
50843
|
+
"chapters",
|
|
48818
50844
|
"colors",
|
|
50845
|
+
"page-color",
|
|
48819
50846
|
"buttons",
|
|
48820
50847
|
"layout",
|
|
50848
|
+
"headings",
|
|
50849
|
+
"heights",
|
|
48821
50850
|
"backgrounds",
|
|
48822
50851
|
"crops",
|
|
48823
50852
|
"phone-images",
|
|
@@ -48866,21 +50895,29 @@ var pageOf = (doc, path2) => {
|
|
|
48866
50895
|
const slug = path2.replace(/^\/+|\/+$/g, "");
|
|
48867
50896
|
return Object.values(doc.pages).find((p) => p.slug === slug);
|
|
48868
50897
|
};
|
|
48869
|
-
function tuneRound(input, measured, groups = TUNE_GROUPS) {
|
|
50898
|
+
function tuneRound(input, measured, groups = TUNE_GROUPS, opts = {}) {
|
|
48870
50899
|
const doc = structuredClone(input);
|
|
48871
50900
|
const on = (g) => groups.includes(g);
|
|
48872
50901
|
const changes = [];
|
|
48873
50902
|
const choices = doc.styles.choices;
|
|
48874
50903
|
const typography = { ...choices.typography ?? {} };
|
|
48875
50904
|
const scale = { ...typography.scale ?? {} };
|
|
50905
|
+
const forPreset = (preset, list5) => {
|
|
50906
|
+
if (preset !== "button") return list5;
|
|
50907
|
+
const content3 = list5.filter((m) => !m.original.buttons?.chrome);
|
|
50908
|
+
return content3.length ? content3 : list5;
|
|
50909
|
+
};
|
|
48876
50910
|
for (const preset of on("type") ? new Set(Object.values(PRESET_OF)) : []) {
|
|
48877
50911
|
const label = Object.entries(PRESET_OF).find(([, p]) => p === preset)?.[0];
|
|
48878
50912
|
const step = { ...scale[preset] ?? {} };
|
|
48879
50913
|
let touched = false;
|
|
48880
50914
|
for (const phone2 of [false, true]) {
|
|
48881
50915
|
if (phone2 && preset === "nav") continue;
|
|
48882
|
-
const list5 =
|
|
48883
|
-
|
|
50916
|
+
const list5 = forPreset(
|
|
50917
|
+
preset,
|
|
50918
|
+
measured.filter(
|
|
50919
|
+
(m) => m.viewport < 700 === phone2 && m.original.presets[preset] && m.imported.presets[preset]
|
|
50920
|
+
)
|
|
48884
50921
|
);
|
|
48885
50922
|
if (!list5.length) continue;
|
|
48886
50923
|
const sizes = list5.map((m) => m.original.presets[preset]?.size);
|
|
@@ -48897,14 +50934,21 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
|
|
|
48897
50934
|
touched = true;
|
|
48898
50935
|
}
|
|
48899
50936
|
if (["eyebrow", "button", "nav", "wordmark"].includes(preset)) {
|
|
48900
|
-
const
|
|
48901
|
-
|
|
50937
|
+
const all = forPreset(
|
|
50938
|
+
preset,
|
|
50939
|
+
measured.filter((m) => m.original.presets[preset] && m.imported.presets[preset])
|
|
48902
50940
|
);
|
|
48903
|
-
const
|
|
48904
|
-
|
|
48905
|
-
);
|
|
48906
|
-
|
|
48907
|
-
|
|
50941
|
+
const list5 = all.filter((m) => m.viewport >= 700);
|
|
50942
|
+
const fails = (m) => m.comparison.checks.some((c) => c.what === `${label} letter spacing and case` && !c.ok);
|
|
50943
|
+
const failing = list5.filter(fails);
|
|
50944
|
+
const phones = all.filter((m) => m.viewport < 700);
|
|
50945
|
+
const phoneFailing = phones.filter(fails);
|
|
50946
|
+
if (list5.length && (failing.length * 2 >= list5.length || phones.length && phoneFailing.length * 2 >= phones.length)) {
|
|
50947
|
+
const lsDesk = median(list5.map((m) => m.original.presets[preset]?.letterSpacing));
|
|
50948
|
+
const lsPhone = median(
|
|
50949
|
+
phones.map((m) => m.original.presets[preset]?.letterSpacing)
|
|
50950
|
+
);
|
|
50951
|
+
const ls = lsDesk !== void 0 && lsPhone !== void 0 && Math.abs(lsDesk - lsPhone) <= 0.075 ? (lsDesk + lsPhone) / 2 : lsDesk;
|
|
48908
50952
|
const cases = list5.map((m) => m.original.presets[preset]?.case);
|
|
48909
50953
|
if (ls !== void 0 && Math.abs((step.letterSpacing ?? -1) - ls) > 0.01) {
|
|
48910
50954
|
step.letterSpacing = round6(clamp2(ls, -0.2, 1), 3);
|
|
@@ -48942,7 +50986,7 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
|
|
|
48942
50986
|
for (const preset of ["h2", "h3"]) {
|
|
48943
50987
|
const fams = origs.map((p) => p[preset]?.family).filter((f2) => !!f2);
|
|
48944
50988
|
const f = fams[0];
|
|
48945
|
-
if (!f ||
|
|
50989
|
+
if (!f || !fams.every((x) => norm6(x) === norm6(f))) continue;
|
|
48946
50990
|
const role = norm6(f) === norm6(heading2) ? void 0 : norm6(f) === norm6(label) ? "label" : norm6(f) === norm6(body) ? "body" : void 0;
|
|
48947
50991
|
if (!role || scale[preset]?.font === role) continue;
|
|
48948
50992
|
const sample2 = origs.map((p) => p[preset]).find(Boolean);
|
|
@@ -48965,6 +51009,29 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
|
|
|
48965
51009
|
changes.push(`${role} font: ${match.family} for ${c.original}`);
|
|
48966
51010
|
}
|
|
48967
51011
|
}
|
|
51012
|
+
if (on("fonts")) {
|
|
51013
|
+
const roleFamily = (r) => norm6(
|
|
51014
|
+
typography[r]?.family ?? (r === "label" ? typography.body?.family ?? void 0 : void 0)
|
|
51015
|
+
);
|
|
51016
|
+
for (const preset of ["eyebrow", "button", "nav", "wordmark"]) {
|
|
51017
|
+
const fams = forPreset(
|
|
51018
|
+
preset,
|
|
51019
|
+
measured.filter((m) => m.viewport >= 700 && m.original.presets[preset])
|
|
51020
|
+
).map((m) => m.original.presets[preset]?.family).filter((f2) => !!f2);
|
|
51021
|
+
const f = fams[0];
|
|
51022
|
+
if (!f || !fams.every((x) => norm6(x) === norm6(f))) continue;
|
|
51023
|
+
const want = ["label", "heading", "body"].find(
|
|
51024
|
+
(r) => roleFamily(r) && roleFamily(r) === norm6(f)
|
|
51025
|
+
);
|
|
51026
|
+
if (!want) continue;
|
|
51027
|
+
const cur = scale[preset]?.font ?? "label";
|
|
51028
|
+
const curFamily = roleFamily(cur);
|
|
51029
|
+
if (cur === want || curFamily === norm6(f)) continue;
|
|
51030
|
+
scale[preset] = { ...scale[preset] ?? {}, font: want };
|
|
51031
|
+
changes.push(`${preset}: in the ${want} font (${f}), as on the original`);
|
|
51032
|
+
}
|
|
51033
|
+
if (Object.keys(scale).length) typography.scale = scale;
|
|
51034
|
+
}
|
|
48968
51035
|
choices.typography = typography;
|
|
48969
51036
|
const layout = { ...choices.layout ?? {} };
|
|
48970
51037
|
const desk = measured.filter((m) => m.viewport >= 700);
|
|
@@ -48978,13 +51045,89 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
|
|
|
48978
51045
|
layout.contentWidth = Math.round(cw / 8) * 8;
|
|
48979
51046
|
changes.push(`content width ${layout.contentWidth}px`);
|
|
48980
51047
|
}
|
|
51048
|
+
if (on("widths")) {
|
|
51049
|
+
const content3 = layout.contentWidth;
|
|
51050
|
+
for (const m of desk) {
|
|
51051
|
+
const check = m.comparison.checks.find((c) => c.what === "Content width" && !c.ok);
|
|
51052
|
+
const want = m.original.layout.contentWidth;
|
|
51053
|
+
const have = m.imported.layout.contentWidth;
|
|
51054
|
+
const page = pageOf(doc, m.path);
|
|
51055
|
+
if (!check || !want || !have || !page || want >= have) continue;
|
|
51056
|
+
let width;
|
|
51057
|
+
if (content3 && Math.abs(content3 - want) / want <= 0.06) width = "content";
|
|
51058
|
+
else if (want >= 360 && want <= 1600) {
|
|
51059
|
+
layout.narrowWidth ??= Math.round(want / 8) * 8;
|
|
51060
|
+
if (Math.abs(layout.narrowWidth - want) / want <= 0.06) width = "narrow";
|
|
51061
|
+
}
|
|
51062
|
+
if (!width) continue;
|
|
51063
|
+
let n = 0;
|
|
51064
|
+
for (const section of page.sections) {
|
|
51065
|
+
if (section.layout.mode !== "stack" || section.layout.width === "full") continue;
|
|
51066
|
+
if (section.style.background?.type === "image" || section.style.background?.type === "video")
|
|
51067
|
+
continue;
|
|
51068
|
+
const cur = section.layout.width ?? "content";
|
|
51069
|
+
if (cur === width) continue;
|
|
51070
|
+
if (width === "content") delete section.layout.width;
|
|
51071
|
+
else section.layout.width = width;
|
|
51072
|
+
delete section.preset;
|
|
51073
|
+
n++;
|
|
51074
|
+
}
|
|
51075
|
+
if (n) changes.push(`${m.path}: ${n} section(s) at the ${width} width, as on the original`);
|
|
51076
|
+
}
|
|
51077
|
+
}
|
|
51078
|
+
if (on("chapters")) {
|
|
51079
|
+
const bag = (t) => new Set(
|
|
51080
|
+
t.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 3)
|
|
51081
|
+
);
|
|
51082
|
+
for (const m of desk) {
|
|
51083
|
+
const page = pageOf(doc, m.path);
|
|
51084
|
+
if (!page || m.imported.sections.length !== page.sections.length) continue;
|
|
51085
|
+
const keep = new Set(page.sections.map((_, j) => j));
|
|
51086
|
+
for (const o of m.original.sections) {
|
|
51087
|
+
const mine = bag(`${o.heading?.text ?? ""} ${o.words ?? o.text}`);
|
|
51088
|
+
if (mine.size < 20) continue;
|
|
51089
|
+
const inside = m.imported.sections.map((x, j) => {
|
|
51090
|
+
const theirs = bag(`${x.heading?.text ?? ""} ${x.words ?? x.text}`);
|
|
51091
|
+
let n = 0;
|
|
51092
|
+
for (const w of theirs) if (mine.has(w)) n++;
|
|
51093
|
+
return theirs.size >= 3 && n / theirs.size >= 0.85 ? j : -1;
|
|
51094
|
+
}).filter((j) => j >= 0);
|
|
51095
|
+
let run2 = [];
|
|
51096
|
+
const join21 = () => {
|
|
51097
|
+
if (run2.length >= 2) {
|
|
51098
|
+
const [first2, ...rest] = run2.map((j) => page.sections[j]);
|
|
51099
|
+
for (const r of rest) {
|
|
51100
|
+
first2?.blocks.push(...r.blocks);
|
|
51101
|
+
keep.delete(page.sections.indexOf(r));
|
|
51102
|
+
}
|
|
51103
|
+
changes.push(`${m.path}: ${run2.length} sections joined into one, as on the original`);
|
|
51104
|
+
}
|
|
51105
|
+
run2 = [];
|
|
51106
|
+
};
|
|
51107
|
+
for (const j of inside) {
|
|
51108
|
+
const a = page.sections[j];
|
|
51109
|
+
const prev = run2.length ? page.sections[run2[run2.length - 1]] : void 0;
|
|
51110
|
+
const plain3 = a.layout.mode === "stack" && a.layout.columns.d.length === 1 && !a.anchor && a.style.background?.type !== "image" && a.style.background?.type !== "video";
|
|
51111
|
+
const same = prev && j === run2[run2.length - 1] + 1 && (prev.style.colorTheme ?? "light") === (a.style.colorTheme ?? "light") && prev.layout.mode === "stack" && (prev.layout.width ?? "content") === (a.layout.mode === "stack" ? a.layout.width ?? "content" : "");
|
|
51112
|
+
if (plain3 && (!run2.length || same)) run2.push(j);
|
|
51113
|
+
else {
|
|
51114
|
+
join21();
|
|
51115
|
+
if (plain3) run2.push(j);
|
|
51116
|
+
}
|
|
51117
|
+
}
|
|
51118
|
+
join21();
|
|
51119
|
+
}
|
|
51120
|
+
if (keep.size !== page.sections.length)
|
|
51121
|
+
page.sections = page.sections.filter((_, j) => keep.has(j));
|
|
51122
|
+
}
|
|
51123
|
+
}
|
|
48981
51124
|
const pad3 = (list5) => median(
|
|
48982
51125
|
list5.filter((m) => m.comparison.checks.some((c) => c.what === "Section padding" && !c.ok)).map(
|
|
48983
51126
|
(m) => m.original.layout.sectionPadding && m.imported.layout.sectionPadding ? m.original.layout.sectionPadding / m.imported.layout.sectionPadding : 0
|
|
48984
51127
|
).filter((n) => n > 0)
|
|
48985
51128
|
);
|
|
48986
51129
|
const pd = on("rhythm") ? pad3(desk) : void 0;
|
|
48987
|
-
const pm = on("rhythm") ? pad3(phone) : void 0;
|
|
51130
|
+
const pm = on("rhythm-phone") ? pad3(phone) : void 0;
|
|
48988
51131
|
if (pd) {
|
|
48989
51132
|
const base = layout.sectionPadding ?? median(desk.map((m) => m.imported.layout.sectionPadding ?? 0).filter(Boolean));
|
|
48990
51133
|
if (base) {
|
|
@@ -49010,7 +51153,7 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
|
|
|
49010
51153
|
}
|
|
49011
51154
|
}
|
|
49012
51155
|
const inks = /* @__PURE__ */ new Map();
|
|
49013
|
-
for (const m of on("colors") ? desk : []) {
|
|
51156
|
+
for (const m of on("colors") || on("page-color") ? desk : []) {
|
|
49014
51157
|
const page = pageOf(doc, m.path);
|
|
49015
51158
|
if (!page || m.imported.sections.length !== page.sections.length) continue;
|
|
49016
51159
|
for (const s2 of m.comparison.sections) {
|
|
@@ -49020,21 +51163,38 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
|
|
|
49020
51163
|
const section = page.sections[s2.importedIndex];
|
|
49021
51164
|
if (!o || !i || !section || o.backgroundImage) continue;
|
|
49022
51165
|
const theme = section.style.colorTheme ?? "light";
|
|
49023
|
-
const e = inks.get(theme) ?? { text: [], heading: [] };
|
|
49024
|
-
if (o.
|
|
49025
|
-
e.
|
|
49026
|
-
if (o.
|
|
49027
|
-
|
|
51166
|
+
const e = inks.get(theme) ?? { text: [], heading: [], background: [] };
|
|
51167
|
+
if (o.background && i.background && !section.style.background)
|
|
51168
|
+
e.background.push({ o: o.background, i: i.background });
|
|
51169
|
+
if (o.textColor && i.textColor) e.text.push({ o: o.textColor, i: i.textColor });
|
|
51170
|
+
if (o.heading?.color && i.heading?.color)
|
|
51171
|
+
e.heading.push({ o: o.heading.color, i: i.heading.color });
|
|
49028
51172
|
inks.set(theme, e);
|
|
49029
51173
|
}
|
|
49030
51174
|
}
|
|
49031
51175
|
for (const [theme, e] of inks)
|
|
49032
|
-
for (const role of [
|
|
51176
|
+
for (const role of [
|
|
51177
|
+
...on("colors") ? ["text", "heading"] : [],
|
|
51178
|
+
...on("page-color") ? ["background"] : []
|
|
51179
|
+
]) {
|
|
49033
51180
|
const list5 = e[role];
|
|
49034
|
-
const
|
|
49035
|
-
|
|
49036
|
-
|
|
49037
|
-
|
|
51181
|
+
const tol = role === "background" ? 16 : 28;
|
|
51182
|
+
const passes = (c) => list5.filter((p) => colorDistance(p.o, c(p)) < tol).length;
|
|
51183
|
+
const now = passes((p) => p.i);
|
|
51184
|
+
let best;
|
|
51185
|
+
let bestN = now;
|
|
51186
|
+
for (const cand of new Set(list5.map((p) => p.o))) {
|
|
51187
|
+
const n = passes(() => cand);
|
|
51188
|
+
if (n > bestN) {
|
|
51189
|
+
best = cand;
|
|
51190
|
+
bestN = n;
|
|
51191
|
+
}
|
|
51192
|
+
}
|
|
51193
|
+
if (!best || list5.length < 2) continue;
|
|
51194
|
+
if (!setRole(doc, theme, role, best)) continue;
|
|
51195
|
+
changes.push(
|
|
51196
|
+
`${role} on ${theme} sections ${best}, as on the original (${bestN} of ${list5.length} sections)`
|
|
51197
|
+
);
|
|
49038
51198
|
}
|
|
49039
51199
|
const fills = /* @__PURE__ */ new Map();
|
|
49040
51200
|
for (const m of on("buttons") ? desk : []) {
|
|
@@ -49063,6 +51223,135 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
|
|
|
49063
51223
|
} catch {
|
|
49064
51224
|
themes = void 0;
|
|
49065
51225
|
}
|
|
51226
|
+
if (on("heights")) {
|
|
51227
|
+
const scaleOf = SPACE_SCALES[choices.density] ?? SPACE_SCALES.comfortable;
|
|
51228
|
+
const tokens = Object.entries(scaleOf).sort((a, b) => a[1] - b[1]);
|
|
51229
|
+
const fit = (o, i) => {
|
|
51230
|
+
if (i.contentTop === void 0 || i.contentBottom === void 0) return void 0;
|
|
51231
|
+
const content3 = i.contentBottom - i.contentTop;
|
|
51232
|
+
const room = (o.height - content3) / 2;
|
|
51233
|
+
const token = [...tokens].reverse().find(([, px3]) => px3 <= room)?.[0] ?? "none";
|
|
51234
|
+
return { token, height: Math.round(o.height) };
|
|
51235
|
+
};
|
|
51236
|
+
for (const m of desk) {
|
|
51237
|
+
const page = pageOf(doc, m.path);
|
|
51238
|
+
if (!page || m.imported.sections.length !== page.sections.length) continue;
|
|
51239
|
+
const mp = phone.find((x) => x.path === m.path);
|
|
51240
|
+
for (const s2 of m.comparison.sections) {
|
|
51241
|
+
if (s2.importedIndex === void 0) continue;
|
|
51242
|
+
const h = s2.checks.find((c) => c.what === "Height");
|
|
51243
|
+
const o = m.original.sections[s2.index];
|
|
51244
|
+
const i = m.imported.sections[s2.importedIndex];
|
|
51245
|
+
const section = page.sections[s2.importedIndex];
|
|
51246
|
+
if (!h || h.ok || !o?.backgroundImage || !i || !section) continue;
|
|
51247
|
+
if (section.style.background?.type !== "image" && section.style.background?.type !== "video")
|
|
51248
|
+
continue;
|
|
51249
|
+
const d = fit(o, i);
|
|
51250
|
+
if (!d) continue;
|
|
51251
|
+
const ps = mp?.comparison.sections.find((x) => x.importedIndex === s2.importedIndex);
|
|
51252
|
+
const po = ps ? mp?.original.sections[ps.index] : void 0;
|
|
51253
|
+
const pi = mp?.imported.sections[s2.importedIndex];
|
|
51254
|
+
const p = po?.backgroundImage && pi ? fit(po, pi) : void 0;
|
|
51255
|
+
section.style.padding = { d: d.token, ...p ? { m: p.token } : {} };
|
|
51256
|
+
section.style.minHeight = {
|
|
51257
|
+
d: `${clamp2(d.height, 10, 9999)}px`,
|
|
51258
|
+
...p ? { m: `${clamp2(p.height, 10, 9999)}px` } : {}
|
|
51259
|
+
};
|
|
51260
|
+
changes.push(`${m.path}: section ${s2.index + 1} ${d.height}px tall, as on the original`);
|
|
51261
|
+
}
|
|
51262
|
+
}
|
|
51263
|
+
}
|
|
51264
|
+
if (on("headings")) {
|
|
51265
|
+
const presetSizes = (list5) => {
|
|
51266
|
+
const out = /* @__PURE__ */ new Map();
|
|
51267
|
+
for (const k of ["display", "h1", "h2", "h3", "h4", "lead", "eyebrow"]) {
|
|
51268
|
+
const xs = list5.map((m) => m.imported.presets[k]).filter((t) => !!t);
|
|
51269
|
+
const size2 = median(xs.map((t) => t.size));
|
|
51270
|
+
if (size2 && xs[0]) out.set(k, { size: size2, family: xs[0].family });
|
|
51271
|
+
}
|
|
51272
|
+
return out;
|
|
51273
|
+
};
|
|
51274
|
+
const deskSizes = presetSizes(desk);
|
|
51275
|
+
const phoneSizes = presetSizes(phone);
|
|
51276
|
+
const fits = (a, b) => Math.abs(a - b) <= Math.max(1, a * 0.1);
|
|
51277
|
+
for (const m of desk) {
|
|
51278
|
+
const page = pageOf(doc, m.path);
|
|
51279
|
+
if (!page || m.imported.sections.length !== page.sections.length) continue;
|
|
51280
|
+
const mp = phone.find((x) => x.path === m.path);
|
|
51281
|
+
for (const s2 of m.comparison.sections) {
|
|
51282
|
+
if (s2.importedIndex === void 0) continue;
|
|
51283
|
+
const hc = s2.checks.find((c) => c.what === "Heading");
|
|
51284
|
+
if (!hc || hc.ok) continue;
|
|
51285
|
+
const o = m.original.sections[s2.index]?.heading;
|
|
51286
|
+
const i = m.imported.sections[s2.importedIndex]?.heading;
|
|
51287
|
+
const section = page.sections[s2.importedIndex];
|
|
51288
|
+
if (!o || !i || !section || fits(o.size, i.size)) continue;
|
|
51289
|
+
const po = mp?.original.sections.find((x) => norm6(x.heading?.text) === norm6(o.text))?.heading?.size;
|
|
51290
|
+
const label = o.size < 18 && (o.case === "uppercase" || o.letterSpacing >= 0.05);
|
|
51291
|
+
const pick = [...deskSizes].filter(([k, v]) => {
|
|
51292
|
+
if (!fits(o.size, v.size)) return false;
|
|
51293
|
+
if (k === "eyebrow" !== label) return false;
|
|
51294
|
+
const ph = phoneSizes.get(k);
|
|
51295
|
+
return po === void 0 || !ph || fits(po, ph.size);
|
|
51296
|
+
}).sort(([, a], [, b]) => Math.abs(a.size - o.size) - Math.abs(b.size - o.size))[0]?.[0];
|
|
51297
|
+
if (!pick) continue;
|
|
51298
|
+
const block4 = section.blocks.find(
|
|
51299
|
+
(b) => b.type === "heading" && norm6(String(b.fields.text ?? "")) === norm6(i.text)
|
|
51300
|
+
);
|
|
51301
|
+
const holder = block4 ? void 0 : section.blocks.find(
|
|
51302
|
+
(b) => b.type === "card" && norm6(String(b.fields.heading ?? "")) === norm6(i.text) || b.type === "columns" && Array.isArray(b.fields.items) && b.fields.items.some(
|
|
51303
|
+
(x) => norm6(x.heading) === norm6(i.text)
|
|
51304
|
+
)
|
|
51305
|
+
);
|
|
51306
|
+
if (holder && pick !== "lead") {
|
|
51307
|
+
const style = holder.style ?? {};
|
|
51308
|
+
if (style.typography?.heading === pick) continue;
|
|
51309
|
+
holder.style = {
|
|
51310
|
+
...style,
|
|
51311
|
+
typography: { ...style.typography ?? {}, heading: pick }
|
|
51312
|
+
};
|
|
51313
|
+
changes.push(
|
|
51314
|
+
`${m.path}: "${o.text.slice(0, 40)}" set at the ${pick} size, as on the original`
|
|
51315
|
+
);
|
|
51316
|
+
continue;
|
|
51317
|
+
}
|
|
51318
|
+
if (!block4) {
|
|
51319
|
+
let done = false;
|
|
51320
|
+
for (const b of section.blocks) {
|
|
51321
|
+
if (b.type !== "text" || done) continue;
|
|
51322
|
+
const doc2 = b.fields.content;
|
|
51323
|
+
for (const node2 of doc2?.content ?? []) {
|
|
51324
|
+
const words4 = (node2.content ?? []).map((x) => x.text ?? "").join("");
|
|
51325
|
+
if (node2.type !== "heading" || norm6(words4) !== norm6(i.text)) continue;
|
|
51326
|
+
if (pick === "lead" || node2.attrs?.preset === pick) break;
|
|
51327
|
+
node2.attrs = { ...node2.attrs ?? {}, preset: pick };
|
|
51328
|
+
done = true;
|
|
51329
|
+
break;
|
|
51330
|
+
}
|
|
51331
|
+
}
|
|
51332
|
+
if (done)
|
|
51333
|
+
changes.push(
|
|
51334
|
+
`${m.path}: "${o.text.slice(0, 40)}" set at the ${pick} size, as on the original`
|
|
51335
|
+
);
|
|
51336
|
+
continue;
|
|
51337
|
+
}
|
|
51338
|
+
if (["display", "h1", "h2", "h3", "h4"].includes(pick)) {
|
|
51339
|
+
if (block4.fields.size === pick) continue;
|
|
51340
|
+
block4.fields.size = pick;
|
|
51341
|
+
} else {
|
|
51342
|
+
const style = block4.style ?? {};
|
|
51343
|
+
if (style.typography?.heading === pick) continue;
|
|
51344
|
+
block4.style = {
|
|
51345
|
+
...style,
|
|
51346
|
+
typography: { ...style.typography ?? {}, heading: pick }
|
|
51347
|
+
};
|
|
51348
|
+
}
|
|
51349
|
+
changes.push(
|
|
51350
|
+
`${m.path}: "${o.text.slice(0, 40)}" set at the ${pick} size, as on the original`
|
|
51351
|
+
);
|
|
51352
|
+
}
|
|
51353
|
+
}
|
|
51354
|
+
}
|
|
49066
51355
|
for (const m of on("layout") || on("backgrounds") || on("crops") ? desk : []) {
|
|
49067
51356
|
const page = pageOf(doc, m.path);
|
|
49068
51357
|
if (!page) continue;
|
|
@@ -49156,23 +51445,42 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
|
|
|
49156
51445
|
if (u.protocol === "mailto:") return { type: "email", email: u.pathname };
|
|
49157
51446
|
return u.protocol === "https:" ? { type: "url", url: u.toString() } : void 0;
|
|
49158
51447
|
};
|
|
49159
|
-
for (const m of desk) {
|
|
51448
|
+
for (const m of desk.filter((x) => !opts.paths || opts.paths.includes(x.path))) {
|
|
49160
51449
|
const page = pageOf(doc, m.path);
|
|
49161
51450
|
if (!page || m.imported.sections.length !== page.sections.length) continue;
|
|
49162
51451
|
const used = /* @__PURE__ */ new Set();
|
|
49163
51452
|
for (const s2 of m.comparison.sections) {
|
|
49164
|
-
if (s2.importedIndex === void 0 || s2.score >= COMPONENT_THRESHOLD)
|
|
51453
|
+
if (s2.importedIndex === void 0 || s2.score >= (opts.threshold ?? COMPONENT_THRESHOLD))
|
|
51454
|
+
continue;
|
|
49165
51455
|
if (used.has(s2.importedIndex)) continue;
|
|
49166
51456
|
const o = m.original.sections[s2.index];
|
|
49167
51457
|
const old = page.sections[s2.importedIndex];
|
|
49168
51458
|
if (!o || !old || !canGenerate(o)) continue;
|
|
49169
51459
|
if (m.comparison.sections.filter((x) => x.importedIndex === s2.importedIndex).length > 1)
|
|
49170
51460
|
continue;
|
|
51461
|
+
const bag = (t) => new Set(
|
|
51462
|
+
t.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 3)
|
|
51463
|
+
);
|
|
51464
|
+
const mine = bag(`${o.heading?.text ?? ""} ${o.words ?? o.text}`);
|
|
51465
|
+
const shared = m.imported.sections.some((x, j) => {
|
|
51466
|
+
if (j === s2.importedIndex) return false;
|
|
51467
|
+
const theirs = bag(x.words ?? x.text);
|
|
51468
|
+
if (theirs.size < 4) return false;
|
|
51469
|
+
let n = 0;
|
|
51470
|
+
for (const w of theirs) if (mine.has(w)) n++;
|
|
51471
|
+
return n / theirs.size >= 0.5;
|
|
51472
|
+
});
|
|
51473
|
+
if (shared) continue;
|
|
49171
51474
|
const name = `imported-${(page.slug || "home").replace(/[^a-z0-9]+/g, "-")}-${s2.index + 1}`.replace(/-+/g, "-").slice(0, 40).replace(/-$/, "");
|
|
49172
51475
|
const theme = themes?.[old.style.colorTheme ?? "light"];
|
|
49173
51476
|
const gen = componentFromBlueprint({
|
|
49174
51477
|
name,
|
|
49175
|
-
label:
|
|
51478
|
+
label: sectionLabel(
|
|
51479
|
+
s2.heading ?? o.heading?.text,
|
|
51480
|
+
o.blueprint,
|
|
51481
|
+
page.title || (page.slug ? page.slug : "Home"),
|
|
51482
|
+
s2.index + 1
|
|
51483
|
+
),
|
|
49176
51484
|
section: o,
|
|
49177
51485
|
roles,
|
|
49178
51486
|
...theme ? {
|
|
@@ -49181,10 +51489,14 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
|
|
|
49181
51489
|
heading: theme.heading,
|
|
49182
51490
|
"text-muted": theme["text-muted"],
|
|
49183
51491
|
accent: theme.accent,
|
|
49184
|
-
link: theme.link
|
|
51492
|
+
link: theme.link,
|
|
51493
|
+
surface: theme.surface,
|
|
51494
|
+
border: theme.border,
|
|
51495
|
+
background: theme.background
|
|
49185
51496
|
}
|
|
49186
51497
|
} : {},
|
|
49187
|
-
link: linkFor2(m.original.url)
|
|
51498
|
+
link: linkFor2(m.original.url),
|
|
51499
|
+
origin: new URL(m.original.url).origin
|
|
49188
51500
|
});
|
|
49189
51501
|
if (!gen) continue;
|
|
49190
51502
|
const check = checkComponent(gen.def);
|
|
@@ -49229,6 +51541,14 @@ function setRole(doc, theme, role, value) {
|
|
|
49229
51541
|
doc.styles.overrides = overrides;
|
|
49230
51542
|
return true;
|
|
49231
51543
|
}
|
|
51544
|
+
function sectionLabel(heading2, blueprint, pageTitle, n) {
|
|
51545
|
+
const first2 = heading2?.trim() || blueprint?.find((i) => i.kind === "heading" && i.text?.trim())?.text || blueprint?.find((i) => i.kind === "text" && i.text?.trim())?.text;
|
|
51546
|
+
const words4 = (first2 ?? "").replace(/\s+/g, " ").trim();
|
|
51547
|
+
if (!words4) return `${pageTitle} section ${n}`.slice(0, 60);
|
|
51548
|
+
const short = words4.length > 40 ? `${words4.slice(0, 40).replace(/\s+\S*$/, "")}\u2026` : words4;
|
|
51549
|
+
const cased = /[a-z]/.test(short) ? short : short.charAt(0) + short.slice(1).toLowerCase();
|
|
51550
|
+
return cased.slice(0, 60);
|
|
51551
|
+
}
|
|
49232
51552
|
|
|
49233
51553
|
// ../import/src/index.ts
|
|
49234
51554
|
var REPORT_FILE = "IMPORT-REPORT.md";
|
|
@@ -49438,6 +51758,9 @@ async function detectSite(url, opts = {}) {
|
|
|
49438
51758
|
return { ...detectPlatform(r.text, { "content-type": r.contentType }), url: r.url || base };
|
|
49439
51759
|
}
|
|
49440
51760
|
async function importSite(opts) {
|
|
51761
|
+
return phoneReady(await importAnySite(opts));
|
|
51762
|
+
}
|
|
51763
|
+
async function importAnySite(opts) {
|
|
49441
51764
|
let detection;
|
|
49442
51765
|
let platform;
|
|
49443
51766
|
if (opts.repoDir) return importGitHub(opts);
|
|
@@ -49500,7 +51823,7 @@ async function renderAgentsMd(root, siteTitle) {
|
|
|
49500
51823
|
} catch {
|
|
49501
51824
|
}
|
|
49502
51825
|
}
|
|
49503
|
-
const { generateAgentsMd: generateAgentsMd2 } = await import("./agents-md-
|
|
51826
|
+
const { generateAgentsMd: generateAgentsMd2 } = await import("./agents-md-OWDSHBGF.js");
|
|
49504
51827
|
return generateAgentsMd2(title ? { siteTitle: title } : {});
|
|
49505
51828
|
}
|
|
49506
51829
|
async function writeAgentsMd(root, siteTitle) {
|
|
@@ -49779,7 +52102,7 @@ function compareFidelityTool(opts) {
|
|
|
49779
52102
|
[],
|
|
49780
52103
|
"Install Playwright next to the CLI (npm i -D playwright && npx playwright install chromium) and restart the MCP server."
|
|
49781
52104
|
);
|
|
49782
|
-
const widths = input.device === "desktop" ? [DESKTOP] : input.device === "mobile" ? [
|
|
52105
|
+
const widths = input.device === "desktop" ? [DESKTOP] : input.device === "mobile" ? [PHONE2] : [DESKTOP, PHONE2];
|
|
49783
52106
|
const run2 = opts.compare ?? runCompare;
|
|
49784
52107
|
const r = await run2(u.toString(), opts.siteRoot, {
|
|
49785
52108
|
...input.pages?.length ? { pages: input.pages } : {},
|
|
@@ -49982,8 +52305,8 @@ var DEFAULTS = {
|
|
|
49982
52305
|
products: []
|
|
49983
52306
|
};
|
|
49984
52307
|
var FILE = {
|
|
49985
|
-
rss: { file: "rss.xml", label: "RSS", mime: "application/rss+xml" },
|
|
49986
|
-
atom: { file: "atom.xml", label: "Atom", mime: "application/atom+xml" },
|
|
52308
|
+
rss: { file: "rss.xml", label: "RSS feed", mime: "application/rss+xml" },
|
|
52309
|
+
atom: { file: "atom.xml", label: "Atom feed", mime: "application/atom+xml" },
|
|
49987
52310
|
ical: { file: "calendar.ics", label: "Add to calendar (iCal)", mime: "text/calendar" },
|
|
49988
52311
|
podcast: { file: "podcast.xml", label: "Podcast feed", mime: "application/rss+xml" }
|
|
49989
52312
|
};
|
|
@@ -50798,7 +53121,11 @@ async function startPreviewServer(root, load, opts = {}) {
|
|
|
50798
53121
|
page = Object.values(view.pages).find((p) => p.slug === slug);
|
|
50799
53122
|
}
|
|
50800
53123
|
if (page) {
|
|
50801
|
-
const html = renderPreviewHtml(view, page.id, {
|
|
53124
|
+
const html = renderPreviewHtml(view, page.id, {
|
|
53125
|
+
behaviorsUrl: "/_os/behaviors",
|
|
53126
|
+
// `?os_mode=production`: exactly as published (the Phone check measures this).
|
|
53127
|
+
production: url.searchParams.get("os_mode") === "production"
|
|
53128
|
+
});
|
|
50802
53129
|
res.writeHead(200, {
|
|
50803
53130
|
"content-type": "text/html; charset=utf-8",
|
|
50804
53131
|
"cache-control": "no-store"
|
|
@@ -50862,6 +53189,7 @@ var FileSiteStore = class _FileSiteStore {
|
|
|
50862
53189
|
canPublish: store.hasGit,
|
|
50863
53190
|
canRollback: store.hasGit,
|
|
50864
53191
|
canScreenshot: Boolean(options.screenshotter),
|
|
53192
|
+
canMeasurePhone: Boolean(options.screenshotter),
|
|
50865
53193
|
publishNote: store.hasGit ? "Local site in git: publishing commits content/ and media/ on the current branch; push it to deploy." : "Local site without git: edits are saved to the files; there is no publish step."
|
|
50866
53194
|
};
|
|
50867
53195
|
return store;
|
|
@@ -51170,6 +53498,16 @@ Unpublished work saved as ${tag}`,
|
|
|
51170
53498
|
const url = await this.previewUrl(siteId, path2);
|
|
51171
53499
|
return this.options.screenshotter.capture(url, request);
|
|
51172
53500
|
}
|
|
53501
|
+
async measurePhone(siteId, request) {
|
|
53502
|
+
this.check(siteId);
|
|
53503
|
+
if (!this.options.screenshotter)
|
|
53504
|
+
throw new ToolError("The browser phone check is not available.", "unsupported");
|
|
53505
|
+
const doc = await loadSiteDocument(this.root);
|
|
53506
|
+
const page = doc.pages[request.pageId];
|
|
53507
|
+
const path2 = page?.slug ? `/${page.slug}/` : "/";
|
|
53508
|
+
const url = await this.previewUrl(siteId, `${path2}?os_mode=production`);
|
|
53509
|
+
return this.options.screenshotter.measure(url, request.width);
|
|
53510
|
+
}
|
|
51173
53511
|
async agentsMd() {
|
|
51174
53512
|
try {
|
|
51175
53513
|
return await readFile9(join11(this.root, "AGENTS.md"), "utf8");
|
|
@@ -51285,6 +53623,32 @@ function createScreenshotter(chromium) {
|
|
|
51285
53623
|
await page.close().catch(() => void 0);
|
|
51286
53624
|
}
|
|
51287
53625
|
},
|
|
53626
|
+
async measure(url, width) {
|
|
53627
|
+
browser ??= launch2();
|
|
53628
|
+
let b;
|
|
53629
|
+
try {
|
|
53630
|
+
b = await browser;
|
|
53631
|
+
} catch (e) {
|
|
53632
|
+
browser = void 0;
|
|
53633
|
+
throw e;
|
|
53634
|
+
}
|
|
53635
|
+
const context = await b.newContext({
|
|
53636
|
+
viewport: { width, height: PHONE_HEIGHT },
|
|
53637
|
+
deviceScaleFactor: 2,
|
|
53638
|
+
isMobile: true,
|
|
53639
|
+
hasTouch: true,
|
|
53640
|
+
reducedMotion: "reduce"
|
|
53641
|
+
});
|
|
53642
|
+
const page = await context.newPage();
|
|
53643
|
+
try {
|
|
53644
|
+
await page.goto(url, { waitUntil: "load", timeout: 3e4 });
|
|
53645
|
+
await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => void 0);
|
|
53646
|
+
await page.evaluate(PHONE_PREPARE_SCRIPT).catch(() => void 0);
|
|
53647
|
+
return await page.evaluate(PHONE_PROBE_SCRIPT);
|
|
53648
|
+
} finally {
|
|
53649
|
+
await context.close().catch(() => void 0);
|
|
53650
|
+
}
|
|
53651
|
+
},
|
|
51288
53652
|
async close() {
|
|
51289
53653
|
if (!browser) return;
|
|
51290
53654
|
const b = await browser.catch(() => void 0);
|
|
@@ -51504,6 +53868,147 @@ function rebrand(value, from, to) {
|
|
|
51504
53868
|
return changed ? out : null;
|
|
51505
53869
|
}
|
|
51506
53870
|
|
|
53871
|
+
// src/phone.ts
|
|
53872
|
+
var pathOf = (p) => p.slug ? `/${p.slug}/` : "/";
|
|
53873
|
+
async function measureSite(root, load, doc, shooter, pages, widths, now) {
|
|
53874
|
+
const server = await startPreviewServer(root, load);
|
|
53875
|
+
const out = /* @__PURE__ */ new Map();
|
|
53876
|
+
try {
|
|
53877
|
+
for (const page of pages) {
|
|
53878
|
+
const results = [];
|
|
53879
|
+
for (const w of widths)
|
|
53880
|
+
results.push(await shooter.measure(`${server.url}${pathOf(page)}?os_mode=production`, w));
|
|
53881
|
+
out.set(page.id, phoneIssuesFromProbe(doc, page.id, results, { now, actor: "phone-check" }));
|
|
53882
|
+
}
|
|
53883
|
+
} finally {
|
|
53884
|
+
await server.close();
|
|
53885
|
+
}
|
|
53886
|
+
return out;
|
|
53887
|
+
}
|
|
53888
|
+
async function runPhoneCheck(root, opts = {}) {
|
|
53889
|
+
const now = opts.now ?? Date.now();
|
|
53890
|
+
const widths = opts.widths ?? PHONE_WIDTHS;
|
|
53891
|
+
let doc = await loadSiteDocument(root);
|
|
53892
|
+
const pick = (d) => Object.values(d.pages).filter(
|
|
53893
|
+
(p) => !opts.pages?.length || opts.pages.includes(p.id) || opts.pages.includes(p.slug)
|
|
53894
|
+
);
|
|
53895
|
+
let shooter = opts.screenshotter;
|
|
53896
|
+
let note;
|
|
53897
|
+
let owned = false;
|
|
53898
|
+
if (!shooter && opts.rendered !== false) {
|
|
53899
|
+
const pw = await loadPlaywright();
|
|
53900
|
+
if ("error" in pw)
|
|
53901
|
+
note = "The browser check needs Playwright: run `npm i -D playwright && npx playwright install chromium` in the site folder. Only the rules that don't need a browser ran.";
|
|
53902
|
+
else {
|
|
53903
|
+
shooter = createScreenshotter(pw.chromium);
|
|
53904
|
+
owned = true;
|
|
53905
|
+
}
|
|
53906
|
+
}
|
|
53907
|
+
let current2 = doc;
|
|
53908
|
+
const load = async () => current2;
|
|
53909
|
+
const measure2 = async () => shooter && opts.rendered !== false ? measureSite(root, load, current2, shooter, pick(current2), widths, now).catch((e) => {
|
|
53910
|
+
note = `The browser check couldn't run: ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`;
|
|
53911
|
+
return /* @__PURE__ */ new Map();
|
|
53912
|
+
}) : /* @__PURE__ */ new Map();
|
|
53913
|
+
try {
|
|
53914
|
+
let rendered2 = await measure2();
|
|
53915
|
+
const fixed = [];
|
|
53916
|
+
const written = [];
|
|
53917
|
+
if (opts.fix) {
|
|
53918
|
+
const r = fixForPhones(doc, {
|
|
53919
|
+
now,
|
|
53920
|
+
actor: "phone-check",
|
|
53921
|
+
rendered: [...rendered2.values()].flat()
|
|
53922
|
+
});
|
|
53923
|
+
if (r.ops.length) {
|
|
53924
|
+
doc = r.doc;
|
|
53925
|
+
current2 = doc;
|
|
53926
|
+
const w = await writeSiteDocument(root, doc);
|
|
53927
|
+
written.push(...w.written);
|
|
53928
|
+
fixed.push(...r.fixed);
|
|
53929
|
+
rendered2 = await measure2();
|
|
53930
|
+
}
|
|
53931
|
+
}
|
|
53932
|
+
const statics = phoneCheck(doc, void 0, { now, actor: "phone-check" });
|
|
53933
|
+
const pages = pick(doc).map((p) => ({
|
|
53934
|
+
page: p.id,
|
|
53935
|
+
title: p.title,
|
|
53936
|
+
path: pathOf(p),
|
|
53937
|
+
issues: sortIssues([
|
|
53938
|
+
...statics.filter((i) => i.page === p.id),
|
|
53939
|
+
...(rendered2.get(p.id) ?? []).filter((i) => i.page === p.id)
|
|
53940
|
+
])
|
|
53941
|
+
}));
|
|
53942
|
+
const elsewhere = [
|
|
53943
|
+
...statics.filter((i) => !i.page || !doc.pages[i.page]),
|
|
53944
|
+
// Footer findings repeat on every page: report each once.
|
|
53945
|
+
...dedupe([...rendered2.values()].flat().filter((i) => i.page === "footer"))
|
|
53946
|
+
];
|
|
53947
|
+
const issues = sortIssues([...elsewhere, ...pages.flatMap((p) => p.issues)]);
|
|
53948
|
+
const counts2 = { error: 0, warning: 0, info: 0 };
|
|
53949
|
+
for (const i of issues) counts2[i.severity]++;
|
|
53950
|
+
return {
|
|
53951
|
+
rendered: !!shooter && opts.rendered !== false && !note,
|
|
53952
|
+
...note ? { note } : {},
|
|
53953
|
+
issues,
|
|
53954
|
+
pages,
|
|
53955
|
+
counts: counts2,
|
|
53956
|
+
fixed,
|
|
53957
|
+
written
|
|
53958
|
+
};
|
|
53959
|
+
} finally {
|
|
53960
|
+
if (owned) await shooter?.close();
|
|
53961
|
+
}
|
|
53962
|
+
}
|
|
53963
|
+
function dedupe(issues) {
|
|
53964
|
+
const seen = /* @__PURE__ */ new Set();
|
|
53965
|
+
return issues.filter((i) => {
|
|
53966
|
+
const key2 = `${i.rule}|${i.message}`;
|
|
53967
|
+
if (seen.has(key2)) return false;
|
|
53968
|
+
seen.add(key2);
|
|
53969
|
+
return true;
|
|
53970
|
+
});
|
|
53971
|
+
}
|
|
53972
|
+
function formatPhoneReport(r, paint2) {
|
|
53973
|
+
const lines = [];
|
|
53974
|
+
lines.push(paint2(1, r.rendered ? "Phone check (measured at 360 and 390 px)" : "Phone check"));
|
|
53975
|
+
if (r.note) lines.push(paint2(33, r.note));
|
|
53976
|
+
if (r.fixed.length) {
|
|
53977
|
+
lines.push(
|
|
53978
|
+
paint2(32, `Fixed for phones: ${r.fixed.length} change${r.fixed.length === 1 ? "" : "s"}.`)
|
|
53979
|
+
);
|
|
53980
|
+
for (const f of uniqueLabels(r.fixed)) lines.push(` \u2713 ${f}`);
|
|
53981
|
+
}
|
|
53982
|
+
const groups = /* @__PURE__ */ new Map();
|
|
53983
|
+
for (const i of r.issues) {
|
|
53984
|
+
const page = r.pages.find((p) => p.page === i.page);
|
|
53985
|
+
const key2 = page ? `${page.title} (${page.path})` : i.page === "footer" ? "Footer" : "Whole site";
|
|
53986
|
+
groups.set(key2, [...groups.get(key2) ?? [], i]);
|
|
53987
|
+
}
|
|
53988
|
+
for (const [where2, list5] of groups) {
|
|
53989
|
+
lines.push("", paint2(1, where2));
|
|
53990
|
+
for (const i of list5) {
|
|
53991
|
+
const sev = i.severity === "error" ? paint2(31, "error ") : i.severity === "warning" ? paint2(33, "warning") : "info ";
|
|
53992
|
+
lines.push(` ${sev} ${i.message}`);
|
|
53993
|
+
lines.push(` ${i.autofix ? `Fix for phones: ${i.autofix.label}.` : i.fix}`);
|
|
53994
|
+
}
|
|
53995
|
+
}
|
|
53996
|
+
const { error: error2, warning, info } = r.counts;
|
|
53997
|
+
lines.push("");
|
|
53998
|
+
lines.push(
|
|
53999
|
+
error2 + warning === 0 ? paint2(
|
|
54000
|
+
32,
|
|
54001
|
+
`Looks great on phones${info ? ` (${info} suggestion${info === 1 ? "" : "s"})` : ""}.`
|
|
54002
|
+
) : `${error2} error${error2 === 1 ? "" : "s"}, ${warning} warning${warning === 1 ? "" : "s"} on phones.${r.issues.some((i) => i.autofix) && !r.fixed.length ? " Run `openspace check --phone --fix` to fix what can be fixed automatically." : ""}`
|
|
54003
|
+
);
|
|
54004
|
+
return lines.join("\n");
|
|
54005
|
+
}
|
|
54006
|
+
function uniqueLabels(fixed) {
|
|
54007
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
54008
|
+
for (const f of fixed) counts2.set(f.label, (counts2.get(f.label) ?? 0) + 1);
|
|
54009
|
+
return [...counts2].map(([l, n]) => n > 1 ? `${l} (${n}\xD7)` : l);
|
|
54010
|
+
}
|
|
54011
|
+
|
|
51507
54012
|
// src/wow-first.ts
|
|
51508
54013
|
var WOW_FIRST_MESSAGE = "New and imported sites always start on free GitHub Pages, so the first version goes live without any extra accounts. Shops, donations, member areas and password pages show a friendly \u201Copening soon\u201D note until you switch them on in Studio \u2192 Finish setting up (it moves the site to Cloudflare for you, with no downtime).";
|
|
51509
54014
|
function assertGitHubPagesStart(target) {
|
|
@@ -51682,6 +54187,7 @@ async function runImportSite(io, url, dirArg, opts = {}) {
|
|
|
51682
54187
|
await writeShell(dir, existing.deploy?.target ?? "github-pages", slug);
|
|
51683
54188
|
await writeImport(dir, result);
|
|
51684
54189
|
await writeAgentsMd(dir, result.doc.site.title);
|
|
54190
|
+
await phoneFixRendered(dir, opts, io);
|
|
51685
54191
|
const check2 = await runCheck(dir);
|
|
51686
54192
|
const r2 = { dir, result, check: check2 };
|
|
51687
54193
|
if (opts.summary) await writeSummary(resolve6(io.cwd, opts.summary), r2);
|
|
@@ -51699,11 +54205,21 @@ async function runImportSite(io, url, dirArg, opts = {}) {
|
|
|
51699
54205
|
await rm6(join15(dir, entry), { recursive: true, force: true });
|
|
51700
54206
|
throw e;
|
|
51701
54207
|
}
|
|
54208
|
+
await phoneFixRendered(dir, opts, io);
|
|
51702
54209
|
const check = await runCheck(dir);
|
|
51703
54210
|
const r = { dir, result, check };
|
|
51704
54211
|
if (opts.summary) await writeSummary(resolve6(io.cwd, opts.summary), r);
|
|
51705
54212
|
return r;
|
|
51706
54213
|
}
|
|
54214
|
+
async function phoneFixRendered(dir, opts, io) {
|
|
54215
|
+
if (!opts.render) return;
|
|
54216
|
+
try {
|
|
54217
|
+
const r = await runPhoneCheck(dir, { fix: true });
|
|
54218
|
+
if (r.fixed.length) io.err(` Fixed ${r.fixed.length} thing(s) for phones.`);
|
|
54219
|
+
} catch (e) {
|
|
54220
|
+
io.err(` Couldn't check the pages on a phone: ${e.message.split("\n")[0]}`);
|
|
54221
|
+
}
|
|
54222
|
+
}
|
|
51707
54223
|
function importCounts(r) {
|
|
51708
54224
|
const { result, check } = r;
|
|
51709
54225
|
return {
|
|
@@ -55970,8 +58486,8 @@ var escapeText3 = (s2) => s2.replace(/&/g, "&").replace(/</g, "<").replac
|
|
|
55970
58486
|
var escapeAttr2 = (s2) => s2.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/\{\{/g, "{{");
|
|
55971
58487
|
var UTILITY = /^(?:display|italic|muted|measure|body|lead|text|small|large|wrap|wrapper|inner|container|row|col|cols|grid|flex|block|inline|content|section|item|items|list|link|links|mt|mb|my|mx|px|py|pt|pb|pl|pr|ml|mr|m|p|d|g|is|has|js|u|sr|visually|astro|w|h|max|min|font|bg|color|center|left|right|hidden|show|active|reveal|fade|animate|anim|in|out|copy|art|wf|w|elementor|wp|x|y|z|lg|md|sm|xl|xs|xxl|space|gap|align|justify|order|relative|absolute|fixed|sticky|overflow|shadow|rounded|border|ring|opacity|transition|duration|ease|delay|transform|scale|rotate|translate|uppercase|lowercase|capitalize|tracking|leading|truncate|break|whitespace|select|cursor|pointer|z)$/;
|
|
55972
58488
|
function camel(s2) {
|
|
55973
|
-
const
|
|
55974
|
-
const out =
|
|
58489
|
+
const words4 = s2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean).map((w) => w.toLowerCase());
|
|
58490
|
+
const out = words4.map((w, i) => i === 0 ? w : w.charAt(0).toUpperCase() + w.slice(1)).join("").replace(/^[^a-z]+/, "");
|
|
55975
58491
|
return out.slice(0, 32);
|
|
55976
58492
|
}
|
|
55977
58493
|
function semanticName(el) {
|
|
@@ -56014,9 +58530,34 @@ function nameFor(el, fallback2 = "text") {
|
|
|
56014
58530
|
if (!el) return fallback2;
|
|
56015
58531
|
return semanticName(el) ?? TAG_NAME[tagOf(el)] ?? fallback2;
|
|
56016
58532
|
}
|
|
56017
|
-
|
|
56018
|
-
|
|
56019
|
-
|
|
58533
|
+
var PLAIN_WORDS = {
|
|
58534
|
+
cta: "button",
|
|
58535
|
+
btn: "button",
|
|
58536
|
+
img: "picture",
|
|
58537
|
+
image: "picture",
|
|
58538
|
+
pic: "picture",
|
|
58539
|
+
photo: "picture",
|
|
58540
|
+
desc: "description",
|
|
58541
|
+
txt: "text",
|
|
58542
|
+
href: "link",
|
|
58543
|
+
url: "link",
|
|
58544
|
+
kicker: "small label",
|
|
58545
|
+
eyebrow: "small label",
|
|
58546
|
+
overline: "small label",
|
|
58547
|
+
lede: "intro",
|
|
58548
|
+
lead: "intro",
|
|
58549
|
+
subhead: "subheading",
|
|
58550
|
+
bg: "background",
|
|
58551
|
+
nav: "menu"
|
|
58552
|
+
};
|
|
58553
|
+
function humanize2(key2, kind) {
|
|
58554
|
+
const words4 = key2.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Za-z])(\d)/g, "$1 $2").replace(/(\d)([A-Za-z])/g, "$1 $2").toLowerCase().split(/[\s_-]+/).filter(Boolean).map((w) => PLAIN_WORDS[w] ?? w);
|
|
58555
|
+
const linky = words4.some((w) => w === "button" || w === "link");
|
|
58556
|
+
if (linky && words4.length > 1 && words4[words4.length - 1] === "label")
|
|
58557
|
+
words4[words4.length - 1] = "text";
|
|
58558
|
+
if (kind === "link" && words4[words4.length - 1] !== "link") words4.push("goes to");
|
|
58559
|
+
const out = words4.join(" ") || "text";
|
|
58560
|
+
return (out.charAt(0).toUpperCase() + out.slice(1)).slice(0, 60);
|
|
56020
58561
|
}
|
|
56021
58562
|
var RESERVED2 = /* @__PURE__ */ new Set([
|
|
56022
58563
|
"rich",
|
|
@@ -56058,7 +58599,7 @@ var FieldScope = class {
|
|
|
56058
58599
|
}
|
|
56059
58600
|
add(base, spec, value) {
|
|
56060
58601
|
const key2 = this.key(base);
|
|
56061
|
-
const label = humanize2(key2);
|
|
58602
|
+
const label = humanize2(key2, spec.kind);
|
|
56062
58603
|
this.fields[key2] = { ...spec, label };
|
|
56063
58604
|
this.values[key2] = value;
|
|
56064
58605
|
return key2;
|
|
@@ -56333,10 +58874,10 @@ var SectionConverter = class {
|
|
|
56333
58874
|
async shapes(els, w) {
|
|
56334
58875
|
const map2 = /* @__PURE__ */ new Map();
|
|
56335
58876
|
const sig = (e) => `${tagOf(e)}.${classes(e).sort().join(".")}`;
|
|
56336
|
-
const
|
|
56337
|
-
for (const e of els)
|
|
58877
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
58878
|
+
for (const e of els) counts2.set(sig(e), (counts2.get(sig(e)) ?? 0) + 1);
|
|
56338
58879
|
for (const e of els) {
|
|
56339
|
-
if ((
|
|
58880
|
+
if ((counts2.get(sig(e)) ?? 0) < 2) continue;
|
|
56340
58881
|
const scope = new FieldScope();
|
|
56341
58882
|
const tpl = await this.gen(e, scope, { ...w, listDepth: w.listDepth + 1 });
|
|
56342
58883
|
const kinds = Object.entries(scope.fields).map(([k, f]) => `${k}:${f.kind}`).join(",");
|
|
@@ -57300,7 +59841,11 @@ function tokensFromPage(css, fontLinks) {
|
|
|
57300
59841
|
const btn = step(/class:(?:btn|button)\b/i);
|
|
57301
59842
|
if (btn) type.button = btn;
|
|
57302
59843
|
if (Object.keys(type).length) tokens.type = type;
|
|
57303
|
-
|
|
59844
|
+
const nColors = Object.keys(tokens.colors ?? {}).length;
|
|
59845
|
+
const nType = Object.keys(tokens.type ?? {}).length;
|
|
59846
|
+
const pl = (k, w) => `${k} ${w}${k === 1 ? "" : "s"}`;
|
|
59847
|
+
const note = `Read your page\u2019s styles: ${pl(nColors, "colour")}${families.length ? `, its fonts (${families.slice(0, 4).join(", ")})` : ""}${nType ? ` and ${pl(nType, "text style")}` : ""}.`;
|
|
59848
|
+
return { tokens, notes: [note], families };
|
|
57304
59849
|
}
|
|
57305
59850
|
function linkFamilies(hrefs) {
|
|
57306
59851
|
const out = [];
|
|
@@ -57346,6 +59891,9 @@ function siteHref(bundle, fromPath, href, pagePaths) {
|
|
|
57346
59891
|
return resolved;
|
|
57347
59892
|
}
|
|
57348
59893
|
async function importHtmlBundle(bundle, opts) {
|
|
59894
|
+
return phoneReady(await importBundle(bundle, opts));
|
|
59895
|
+
}
|
|
59896
|
+
async function importBundle(bundle, opts) {
|
|
57349
59897
|
const progress = opts.onProgress ?? (() => void 0);
|
|
57350
59898
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
57351
59899
|
const notices = [];
|
|
@@ -57506,7 +60054,7 @@ ${await r.text()}`;
|
|
|
57506
60054
|
}
|
|
57507
60055
|
const docTitle = text(find(home.root, (e) => e.tagName === "title"));
|
|
57508
60056
|
const wordmark = brand ? text(brand).replace(/\s+/g, " ").trim() : "";
|
|
57509
|
-
const siteTitle = (
|
|
60057
|
+
const siteTitle = ((wordmark && wordmark.length <= 60 ? titleCase(wordmark) : void 0) ?? opts.title ?? homePage.siteName ?? (docTitle.split(/\s+[|·—–-]\s+/)[0] || "My site")).slice(0, 120);
|
|
57510
60058
|
const logoImg = brand ? find(brand, (e) => e.tagName === "img") : void 0;
|
|
57511
60059
|
const logoRef = logoImg ? resolveRef(bundle, attr(logoImg, "src") ?? "", home.path) : void 0;
|
|
57512
60060
|
const segsByPage = /* @__PURE__ */ new Map();
|
|
@@ -57570,6 +60118,7 @@ ${await r.text()}`;
|
|
|
57570
60118
|
const placeholder = email ? attr(email, "placeholder") : void 0;
|
|
57571
60119
|
blocks2.push({
|
|
57572
60120
|
kind: "newsletter",
|
|
60121
|
+
...signupAsksName(f) ? { askName: true } : {},
|
|
57573
60122
|
...label3 ? { buttonLabel: label3.slice(0, 40) } : {},
|
|
57574
60123
|
...placeholder ? { placeholder } : {}
|
|
57575
60124
|
});
|
|
@@ -57933,8 +60482,8 @@ function componentNameFor(el, pagePath2) {
|
|
|
57933
60482
|
)
|
|
57934
60483
|
);
|
|
57935
60484
|
const heading2 = find(el, (e) => /^h[1-3]$/.test(e.tagName));
|
|
57936
|
-
const
|
|
57937
|
-
const raw2 = (id && id.length > 2 && !/^(?:top|main|content|section\d*)$/i.test(id) ? id : void 0) ?? cls ?? (
|
|
60485
|
+
const words4 = text(heading2).split(/\s+/).slice(0, 3).join(" ");
|
|
60486
|
+
const raw2 = (id && id.length > 2 && !/^(?:top|main|content|section\d*)$/i.test(id) ? id : void 0) ?? cls ?? (words4 || el.tagName);
|
|
57938
60487
|
const slug = raw2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^(\d)/, "s-$1").slice(0, 30).replace(/-+$/, "");
|
|
57939
60488
|
const prefix = pagePath2 === "/" ? "" : `${pagePath2.split("/").filter(Boolean).pop()}-`;
|
|
57940
60489
|
return `${prefix}${slug || "section"}`.replace(/^[^a-z]+/, "s-").slice(0, 36).replace(/-+$/, "");
|
|
@@ -58573,7 +61122,8 @@ async function runMcp(io, dir, opts = {}) {
|
|
|
58573
61122
|
|
|
58574
61123
|
// src/fidelity.ts
|
|
58575
61124
|
var DESKTOP = 1440;
|
|
58576
|
-
var
|
|
61125
|
+
var COMPONENT_RETRY_BELOW = 75;
|
|
61126
|
+
var PHONE2 = 390;
|
|
58577
61127
|
async function launch() {
|
|
58578
61128
|
const pw = await loadPlaywright();
|
|
58579
61129
|
if ("error" in pw) throw new CliError(pw.error);
|
|
@@ -58616,7 +61166,7 @@ async function lookProber() {
|
|
|
58616
61166
|
const out = {};
|
|
58617
61167
|
for (const [key2, width] of [
|
|
58618
61168
|
["desktop", DESKTOP],
|
|
58619
|
-
["mobile",
|
|
61169
|
+
["mobile", PHONE2]
|
|
58620
61170
|
]) {
|
|
58621
61171
|
const page = await open(browser, url, width);
|
|
58622
61172
|
try {
|
|
@@ -58765,7 +61315,7 @@ async function runCompare(original, dir, opts = {}, log = () => void 0) {
|
|
|
58765
61315
|
await mkdir8(out, { recursive: true });
|
|
58766
61316
|
if (!opts.out) await writeFile10(join18(out, ".gitignore"), "*\n");
|
|
58767
61317
|
const pages = (opts.pages?.length ? opts.pages : defaultPages(doc)).map((p) => `/${slugOf2(p)}`);
|
|
58768
|
-
const widths = opts.widths ?? [DESKTOP,
|
|
61318
|
+
const widths = opts.widths ?? [DESKTOP, PHONE2];
|
|
58769
61319
|
const server = await startPreviewServer(root, () => loadSiteDocument(root));
|
|
58770
61320
|
const browser = await launch();
|
|
58771
61321
|
const oldPath = /* @__PURE__ */ new Map();
|
|
@@ -58785,7 +61335,7 @@ async function runCompare(original, dir, opts = {}, log = () => void 0) {
|
|
|
58785
61335
|
await page.close().catch(() => void 0);
|
|
58786
61336
|
}
|
|
58787
61337
|
};
|
|
58788
|
-
const
|
|
61338
|
+
const measure2 = async () => {
|
|
58789
61339
|
const comparisons = [];
|
|
58790
61340
|
const measured = [];
|
|
58791
61341
|
const shots = /* @__PURE__ */ new Map();
|
|
@@ -58830,7 +61380,7 @@ async function runCompare(original, dir, opts = {}, log = () => void 0) {
|
|
|
58830
61380
|
let best;
|
|
58831
61381
|
let tuning;
|
|
58832
61382
|
try {
|
|
58833
|
-
best = await
|
|
61383
|
+
best = await measure2();
|
|
58834
61384
|
const first2 = fidelityReport(best.comparisons);
|
|
58835
61385
|
const rounds = Math.max(0, Math.min(5, opts.tune ?? 0));
|
|
58836
61386
|
if (rounds && best.measured.length) {
|
|
@@ -58840,14 +61390,33 @@ async function runCompare(original, dir, opts = {}, log = () => void 0) {
|
|
|
58840
61390
|
let ran = 0;
|
|
58841
61391
|
let withoutComponents;
|
|
58842
61392
|
let withComponents2;
|
|
58843
|
-
let budget = 2 + rounds *
|
|
61393
|
+
let budget = 2 + rounds * 5;
|
|
61394
|
+
let componentBudget = 2 + rounds * 2;
|
|
58844
61395
|
for (let pass = 0; pass < rounds && budget > 0; pass++) {
|
|
58845
61396
|
let improved = false;
|
|
61397
|
+
const attempts = [];
|
|
58846
61398
|
for (const group of TUNE_GROUPS) {
|
|
58847
|
-
if (
|
|
58848
|
-
|
|
61399
|
+
if (group !== "components") {
|
|
61400
|
+
attempts.push({ group });
|
|
61401
|
+
continue;
|
|
61402
|
+
}
|
|
61403
|
+
const paths = [
|
|
61404
|
+
...new Set(
|
|
61405
|
+
best.measured.filter(
|
|
61406
|
+
(m) => m.viewport >= 700 && m.comparison.sections.some((x) => x.score < COMPONENT_RETRY_BELOW)
|
|
61407
|
+
).map((m) => m.path)
|
|
61408
|
+
)
|
|
61409
|
+
];
|
|
61410
|
+
for (const path2 of paths) attempts.push({ group, paths: [path2] });
|
|
61411
|
+
}
|
|
61412
|
+
for (const { group, paths } of attempts) {
|
|
61413
|
+
if (group === "components" ? componentBudget <= 0 : budget <= 0) continue;
|
|
61414
|
+
const t = tuneRound(bestDoc, best.measured, [group], {
|
|
61415
|
+
...paths ? { paths, threshold: COMPONENT_RETRY_BELOW } : {}
|
|
61416
|
+
});
|
|
58849
61417
|
if (!t.changes.length) continue;
|
|
58850
|
-
|
|
61418
|
+
if (group === "components") componentBudget--;
|
|
61419
|
+
else budget--;
|
|
58851
61420
|
log(
|
|
58852
61421
|
`Tuning ${group}: ${t.changes.slice(0, 4).join("; ")}${t.changes.length > 4 ? "\u2026" : ""}`
|
|
58853
61422
|
);
|
|
@@ -58862,7 +61431,7 @@ async function runCompare(original, dir, opts = {}, log = () => void 0) {
|
|
|
58862
61431
|
await writeSiteDocument(root, bestDoc);
|
|
58863
61432
|
continue;
|
|
58864
61433
|
}
|
|
58865
|
-
const next = await
|
|
61434
|
+
const next = await measure2();
|
|
58866
61435
|
const score = fidelityReport(next.comparisons).exact ?? 0;
|
|
58867
61436
|
const better = score > bestScore + 0.1;
|
|
58868
61437
|
log(
|
|
@@ -58980,10 +61549,17 @@ function fidelitySummary(report, tuning, thumbs) {
|
|
|
58980
61549
|
const before = new Map(
|
|
58981
61550
|
(tuning?.before.pages ?? []).map((p) => [`${p.path}@${p.viewport}`, p.score])
|
|
58982
61551
|
);
|
|
61552
|
+
const strictBefore = new Map(
|
|
61553
|
+
(tuning?.before.pages ?? []).map((p) => [`${p.path}@${p.viewport}`, p.strict?.score])
|
|
61554
|
+
);
|
|
58983
61555
|
return {
|
|
58984
61556
|
version: 1,
|
|
58985
61557
|
score: report.score,
|
|
61558
|
+
// The same pages by the strict v3 scorer, side by side (additive: older readers ignore it).
|
|
61559
|
+
...report.strict ? { strict: report.strict.score } : {},
|
|
61560
|
+
...report.scorer ? { scorer: report.scorer } : {},
|
|
58986
61561
|
...tuning ? { before: tuning.before.score, changes: tuning.changes.slice(0, 40) } : {},
|
|
61562
|
+
...tuning?.before.strict ? { strictBefore: tuning.before.strict.score } : {},
|
|
58987
61563
|
...tuning?.withoutComponents !== void 0 ? {
|
|
58988
61564
|
withoutComponents: tuning.withoutComponents,
|
|
58989
61565
|
...tuning.withComponents !== void 0 ? { withComponents: tuning.withComponents } : {}
|
|
@@ -58992,11 +61568,15 @@ function fidelitySummary(report, tuning, thumbs) {
|
|
|
58992
61568
|
const key2 = `${p.path}@${p.viewport}`;
|
|
58993
61569
|
const b = before.get(key2);
|
|
58994
61570
|
const worst = [...p.checks, ...p.sections.flatMap((s2) => s2.checks)].filter((c) => !c.ok && !c.credit).sort((x, y) => y.weight - x.weight).slice(0, 3).map((c) => c.what);
|
|
61571
|
+
const sb = strictBefore.get(key2);
|
|
58995
61572
|
return {
|
|
58996
61573
|
path: p.path,
|
|
58997
61574
|
device: p.viewport < 700 ? "phone" : "desktop",
|
|
58998
61575
|
score: p.score,
|
|
61576
|
+
...p.strict ? { strict: p.strict.score } : {},
|
|
61577
|
+
...p.corrections?.length ? { corrections: p.corrections.slice(0, 6) } : {},
|
|
58999
61578
|
...b !== void 0 ? { before: b } : {},
|
|
61579
|
+
...sb !== void 0 ? { strictBefore: sb } : {},
|
|
59000
61580
|
...thumbs[key2] ? { image: thumbs[key2] } : {},
|
|
59001
61581
|
...worst.length ? { differs: worst } : {}
|
|
59002
61582
|
};
|
|
@@ -59040,6 +61620,7 @@ async function addFidelityToSummary(file, r) {
|
|
|
59040
61620
|
const summary = JSON.parse(await readFile14(file, "utf8"));
|
|
59041
61621
|
summary.fidelity = {
|
|
59042
61622
|
score: r.report.score,
|
|
61623
|
+
...r.report.strict ? { strict: r.report.strict.score } : {},
|
|
59043
61624
|
...r.tuning ? { before: r.tuning.before.score } : {}
|
|
59044
61625
|
};
|
|
59045
61626
|
await writeFile10(file, `${JSON.stringify(summary, null, 2)}
|
|
@@ -59048,8 +61629,9 @@ async function addFidelityToSummary(file, r) {
|
|
|
59048
61629
|
}
|
|
59049
61630
|
}
|
|
59050
61631
|
function compareSummary(r) {
|
|
61632
|
+
const strict = r.report.strict ? r.tuning?.before.strict ? ` Strict v3 scorer: ${r.tuning.before.strict.score}/100 \u2192 ${r.report.strict.score}/100 (the corrected score reads three things v3 gets wrong; see FIDELITY-REPORT.md).` : ` Strict v3 scorer: ${r.report.strict.score}/100 (the corrected score reads three things v3 gets wrong; see FIDELITY-REPORT.md).` : "";
|
|
59051
61633
|
const lines = [
|
|
59052
|
-
r.tuning ? `Visual match with the original: ${r.tuning.before.score}/100 \u2192 ${r.report.score}/100 after ${r.tuning.rounds} tuning round(s)${r.tuning.withoutComponents !== void 0 ? ` (${r.tuning.withoutComponents}/100 before recreating sections as custom components${r.tuning.withComponents !== void 0 ? "" : "; none kept"})` : ""}
|
|
61634
|
+
r.tuning ? `Visual match with the original: ${r.tuning.before.score}/100 \u2192 ${r.report.score}/100 after ${r.tuning.rounds} tuning round(s)${r.tuning.withoutComponents !== void 0 ? ` (${r.tuning.withoutComponents}/100 before recreating sections as custom components${r.tuning.withComponents !== void 0 ? "" : "; none kept"})` : ""}.${strict}` : `Visual match with the original: ${r.report.score}/100.${strict}`
|
|
59053
61635
|
];
|
|
59054
61636
|
for (const p of r.report.pages) {
|
|
59055
61637
|
const off = [
|
|
@@ -59057,7 +61639,7 @@ function compareSummary(r) {
|
|
|
59057
61639
|
...p.sections.filter((s2) => s2.score < 80).map((s2) => `section ${s2.index + 1}${s2.heading ? ` "${s2.heading.slice(0, 30)}"` : ""}`)
|
|
59058
61640
|
];
|
|
59059
61641
|
lines.push(
|
|
59060
|
-
` ${p.path} ${p.viewport}px: ${p.score}/100${off.length ? ` \u2014 differs: ${off.slice(0, 6).join(", ")}${off.length > 6 ? "\u2026" : ""}` : ""}`
|
|
61642
|
+
` ${p.path} ${p.viewport}px: ${p.score}/100${p.strict && p.strict.score !== p.score ? ` (strict v3 ${p.strict.score})` : ""}${off.length ? ` \u2014 differs: ${off.slice(0, 6).join(", ")}${off.length > 6 ? "\u2026" : ""}` : ""}`
|
|
59061
61643
|
);
|
|
59062
61644
|
}
|
|
59063
61645
|
lines.push(` Report and side-by-side images: ${join18(r.out, "FIDELITY-REPORT.md")}`);
|
|
@@ -59277,14 +61859,23 @@ async function runMigrate(root, opts = {}) {
|
|
|
59277
61859
|
var VERSION = "0.1.0";
|
|
59278
61860
|
function createProgram(io, setExit, deps = {}) {
|
|
59279
61861
|
const program = new Command("openspace").description("Check, preview, migrate, scaffold and serve OpenSpace sites to Claude (MCP).").version(VERSION).configureOutput({ writeOut: (s2) => io.out(s2.trimEnd()), writeErr: (s2) => io.err(s2.trimEnd()) }).exitOverride();
|
|
59280
|
-
program.command("check").argument("[dir]", "site folder (default: the current folder)").description("Validate content: schemas, links, images, size budgets and secrets.").option("--ci", "CI mode: also print GitHub Actions annotations").option("--json", "print the report as JSON").option("--media-warn-mb <n>", "media folder warning threshold in MB", Number).option("--media-fail-mb <n>", "media folder failure threshold in MB", Number).action(
|
|
61862
|
+
program.command("check").argument("[dir]", "site folder (default: the current folder)").description("Validate content: schemas, links, images, size budgets and secrets.").option("--ci", "CI mode: also print GitHub Actions annotations").option("--json", "print the report as JSON").option("--media-warn-mb <n>", "media folder warning threshold in MB", Number).option("--media-fail-mb <n>", "media folder failure threshold in MB", Number).option("--phone", "also run the Phone check in a real browser (Playwright) at 360 and 390 px").option("--fix", "with --phone: fix everything for phones (writes the content files)").action(
|
|
59281
61863
|
async (dir, opts) => {
|
|
61864
|
+
let phone;
|
|
61865
|
+
if (opts.phone || opts.fix) {
|
|
61866
|
+
phone = await runPhoneCheck(resolve10(io.cwd, dir ?? "."), { fix: opts.fix });
|
|
61867
|
+
if (!opts.json) {
|
|
61868
|
+
io.out(formatPhoneReport(phone, (code3, s2) => paint(colorEnabled(io), code3, s2)));
|
|
61869
|
+
io.out("");
|
|
61870
|
+
}
|
|
61871
|
+
if (phone.counts.error) setExit(1);
|
|
61872
|
+
}
|
|
59282
61873
|
const budgets = {};
|
|
59283
61874
|
if (opts.mediaWarnMb !== void 0) budgets.mediaWarnBytes = opts.mediaWarnMb * 1024 * 1024;
|
|
59284
61875
|
if (opts.mediaFailMb !== void 0) budgets.mediaFailBytes = opts.mediaFailMb * 1024 * 1024;
|
|
59285
61876
|
const report = await runCheck(resolve10(io.cwd, dir ?? "."), { budgets });
|
|
59286
61877
|
if (opts.json) {
|
|
59287
|
-
io.out(JSON.stringify(report, null, 2));
|
|
61878
|
+
io.out(JSON.stringify(phone ? { ...report, phone } : report, null, 2));
|
|
59288
61879
|
} else {
|
|
59289
61880
|
if (opts.ci) for (const line of githubAnnotations(report)) io.out(line);
|
|
59290
61881
|
const color = colorEnabled(io);
|
|
@@ -59640,4 +62231,4 @@ export {
|
|
|
59640
62231
|
createProgram,
|
|
59641
62232
|
run
|
|
59642
62233
|
};
|
|
59643
|
-
//# sourceMappingURL=chunk-
|
|
62234
|
+
//# sourceMappingURL=chunk-PBN36VUE.js.map
|