@omniharmonic/openspace-cli 0.1.13 → 0.1.14

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.
@@ -29,7 +29,7 @@ import {
29
29
  text,
30
30
  themeFromCss,
31
31
  videoUrl
32
- } from "./chunk-EFSGZWAO.js";
32
+ } from "./chunk-N7WFQYFE.js";
33
33
  import {
34
34
  BREAKPOINT_INFO,
35
35
  BUTTON_STYLES,
@@ -147,7 +147,7 @@ import {
147
147
  toHex,
148
148
  toJsonSchema,
149
149
  toMarkdown
150
- } from "./chunk-IXN2CNGL.js";
150
+ } from "./chunk-SY5NDWS2.js";
151
151
  import {
152
152
  __commonJS,
153
153
  __export,
@@ -15069,7 +15069,7 @@ function nearestOpenFont(family, metrics) {
15069
15069
  let best;
15070
15070
  for (const [name, cat, xHeight, capHeight, width] of list5) {
15071
15071
  let d = metricDistance(metrics, { xHeight, capHeight, width });
15072
- if (eq.includes(name)) d -= 0.02 * (eq.length - eq.indexOf(name));
15072
+ if (eq.includes(name)) d -= 4e-3 * (eq.length - eq.indexOf(name));
15073
15073
  if (!best || d < (best.distance ?? Number.POSITIVE_INFINITY))
15074
15074
  best = { family: name, category: cat, via: "metrics", distance: Math.max(0, d) };
15075
15075
  }
@@ -15169,6 +15169,11 @@ async function fitGoogleWeights(font, fetchImpl = fetch) {
15169
15169
  // ../import/src/fidelity/look.ts
15170
15170
  var norm2 = (s2) => (s2 ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
15171
15171
  var GENERIC = /^(?:serif|sans-serif|monospace|system-ui|ui-\w+|-apple-system|blinkmacsystemfont)$/i;
15172
+ var colorGap = (a, b) => {
15173
+ const p = (h) => [1, 3, 5].map((i) => Number.parseInt(h.slice(i, i + 2), 16) || 0);
15174
+ const [x, y] = [p(a), p(b)];
15175
+ return Math.sqrt(x.reduce((n, v, i) => n + (v - (y[i] ?? 0)) ** 2, 0));
15176
+ };
15172
15177
  var round = (n, d = 1) => Math.round(n * 10 ** d) / 10 ** d;
15173
15178
  function stepOf(t, mobile, opts = {}) {
15174
15179
  return {
@@ -15241,7 +15246,12 @@ function lookFromProbe(d, m) {
15241
15246
  scale[p] = { ...scale[p], font: "heading", ...w >= 100 && w <= 900 ? { weight: w } : {} };
15242
15247
  }
15243
15248
  }
15244
- if (scale.h1) scale.display = { ...scale.h1 };
15249
+ if (P.display)
15250
+ scale.display = {
15251
+ ...stepOf(P.display, M.display, { weight: true }),
15252
+ font: roleOf(P.display)
15253
+ };
15254
+ else if (scale.h1) scale.display = { ...scale.h1 };
15245
15255
  if (P.lead) scale.lead = { ...stepOf(P.lead, M.lead), font: roleOf(P.lead) };
15246
15256
  if (P.small) scale.small = stepOf(P.small, M.small);
15247
15257
  if (P.eyebrow)
@@ -15263,6 +15273,17 @@ function lookFromProbe(d, m) {
15263
15273
  const mobileLeft = m?.layout.left;
15264
15274
  if (L2.contentWidth && L2.contentWidth >= 480 && L2.contentWidth <= 2400 && L2.contentWidth < d.viewport - 40)
15265
15275
  layout.contentWidth = Math.round(L2.contentWidth / 8) * 8;
15276
+ const page = d.pageBackground;
15277
+ const panels = d.sections.filter(
15278
+ (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
15279
+ );
15280
+ if (page && d.viewport >= 768 && panels.length && panels.length * 2 >= d.sections.length) {
15281
+ layout.surface = "card";
15282
+ layout.pageBackground = page;
15283
+ const measures = panels.map((s2) => s2.textWidth).filter((w2) => !!w2);
15284
+ const w = measures.sort((a, b) => a - b)[Math.floor(measures.length / 2)];
15285
+ if (w && w >= 480 && w <= 1600) layout.contentWidth = Math.round(w / 8) * 8;
15286
+ }
15266
15287
  if (L2.sectionPadding && L2.sectionPadding >= 16 && L2.sectionPadding <= 320)
15267
15288
  layout.sectionPadding = Math.round(L2.sectionPadding);
15268
15289
  if (layout.sectionPadding && m?.layout.sectionPadding && m.layout.sectionPadding <= 240)
@@ -15304,6 +15325,17 @@ function lookFromProbe(d, m) {
15304
15325
  ...d.eyebrowRule ? { eyebrowRule: true } : {},
15305
15326
  ...d.fonts && Object.keys(d.fonts).length ? { metrics: { ...m?.fonts, ...d.fonts } } : {},
15306
15327
  ...hosts.length ? { fontHosts: hosts } : {},
15328
+ ...d.logoHeight && d.logoHeight >= 16 ? {
15329
+ logoHeight: {
15330
+ d: Math.min(240, d.logoHeight),
15331
+ ...m?.logoHeight && m.logoHeight >= 16 ? { m: Math.min(240, m.logoHeight) } : {}
15332
+ }
15333
+ } : {},
15334
+ ...d.footerBackground ? { footerBackground: d.footerBackground } : {},
15335
+ ...d.footerColumns && d.footerColumns >= 2 && d.footerColumns <= 4 ? {
15336
+ footerColumns: d.footerColumns,
15337
+ ...d.footerText && d.footerText.size <= 14 && (d.footerText.case === "uppercase" || d.footerText.letterSpacing >= 0.08) ? { footerLabels: true } : {}
15338
+ } : {},
15307
15339
  ...d.sidebar ? {
15308
15340
  sidebar: {
15309
15341
  side: d.sidebar.side,
@@ -15331,6 +15363,13 @@ function applyLook(theme, look, source, mode = "override") {
15331
15363
  theme.headerLayout = "sidebar";
15332
15364
  if (look.sidebar.background) theme.headerBackground = look.sidebar.background;
15333
15365
  }
15366
+ if (look.logoHeight && !keep(theme.logoHeight)) theme.logoHeight = look.logoHeight;
15367
+ if (look.footerBackground && !keep(theme.footerBackground))
15368
+ theme.footerBackground = look.footerBackground;
15369
+ if (look.footerColumns && !keep(theme.footerColumns)) {
15370
+ theme.footerColumns = look.footerColumns;
15371
+ if (look.footerLabels) theme.footerLabels = true;
15372
+ }
15334
15373
  if (look.metrics) theme.fontMetrics = { ...look.metrics, ...theme.fontMetrics ?? {} };
15335
15374
  if (look.fontHosts)
15336
15375
  theme.fontHosts = [.../* @__PURE__ */ new Set([...theme.fontHosts ?? [], ...look.fontHosts])];
@@ -15541,7 +15580,15 @@ var PROBE_SCRIPT = String.raw`(async () => {
15541
15580
  const er = e.getBoundingClientRect();
15542
15581
  return visible(e) && er.width >= r.width * 0.9 && er.height >= r.height * 0.7;
15543
15582
  });
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) };
15583
+ 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) };
15584
+ // A raised panel: the section's own box draws a shadow or a border (a theme's content card).
15585
+ // (Its own box, or a panel around it narrower than the page: an article card around a band.)
15586
+ for (let e = sec.els ? sec.els[0] : sec, k = 0; e && e !== document.body && k < 5; e = e.parentElement, k++) {
15587
+ const st = cs(e);
15588
+ const er = e.getBoundingClientRect();
15589
+ if (k > 0 && er.width >= vw * 0.95) break;
15590
+ if ((st.boxShadow && st.boxShadow !== "none") || (k === 0 && !sec.els && parseFloat(st.borderTopWidth) >= 1)) { res.raised = true; break; }
15591
+ }
15545
15592
  if (bgEl || coverImg) res.backgroundImage = true;
15546
15593
  const h = [...sec.querySelectorAll(headSel)].find((e) => visible(e) && ownText(e).length > 1);
15547
15594
  if (h) {
@@ -15600,6 +15647,12 @@ var PROBE_SCRIPT = String.raw`(async () => {
15600
15647
  res.body = best.t;
15601
15648
  if (best.t.color) res.textColor = best.t.color;
15602
15649
  }
15650
+ // How wide the running text is set (the measure), and where it starts.
15651
+ const measures = paras.map((p) => rect(p)).filter((x) => x.width > 120);
15652
+ if (measures.length) {
15653
+ res.textWidth = Math.round(Math.max(...measures.map((x) => x.width)));
15654
+ res.textLeft = Math.round(Math.min(...measures.map((x) => x.left)));
15655
+ }
15603
15656
  // Columns: text items whose vertical spans overlap, at distinct left edges.
15604
15657
  const items = [...sec.querySelectorAll(textSel + ", h2, img")].filter((e) => visible(e) && (e.tagName === "IMG" ? e.getBoundingClientRect().width > 60 : ownText(e).length >= 12)).map(rect);
15605
15658
  for (const a of items) {
@@ -15612,6 +15665,24 @@ var PROBE_SCRIPT = String.raw`(async () => {
15612
15665
  if (!sec.querySelector("form, input, textarea, select, iframe, video, canvas, embed, object")) {
15613
15666
  const parts = [];
15614
15667
  const inPart = (e) => parts.some((p) => p.el.contains(e));
15668
+ // Inline links and emphasis inside a paragraph (kept as rich text when it's recreated).
15669
+ const runsOf = (el) => {
15670
+ const out = [];
15671
+ const walk = (n, m) => {
15672
+ for (const c of n.childNodes) {
15673
+ if (c.nodeType === 3) { const t = (c.textContent || "").replace(/\s+/g, " "); if (t) out.push({ t, ...m }); }
15674
+ else if (c.nodeType === 1 && cs(c).display !== "none") {
15675
+ const mm = { ...m };
15676
+ if (c.tagName === "A" && c.getAttribute("href")) mm.href = c.href;
15677
+ if (/^(STRONG|B)$/.test(c.tagName)) mm.b = 1;
15678
+ if (/^(EM|I)$/.test(c.tagName)) mm.i = 1;
15679
+ if (c.tagName === "BR") out.push({ t: "\n" }); else walk(c, mm);
15680
+ }
15681
+ }
15682
+ };
15683
+ walk(el, {});
15684
+ return out.length <= 80 && out.some((r) => r.href || r.b || r.i) ? out : undefined;
15685
+ };
15615
15686
  const btnLike = (e) => { const st = cs(e); return (toHex(st.backgroundColor) || parseFloat(st.borderTopWidth) >= 1) && parseFloat(st.paddingLeft) >= 8; };
15616
15687
  const els = [...sec.querySelectorAll("h1, h2, h3, h4, h5, h6, p, li, blockquote, img, a, button")].filter((e) => visible(e));
15617
15688
  for (const e of els) {
@@ -15630,10 +15701,34 @@ var PROBE_SCRIPT = String.raw`(async () => {
15630
15701
  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
15702
  } else {
15632
15703
  if (!t || e.querySelector("p, li, h1, h2, h3, h4, h5, h6, img")) continue;
15633
- parts.push({ el: e, item: { kind: "text", text: t.slice(0, 1500), box: at, style: { ...textOf(e), align: cs(e).textAlign } } });
15704
+ const runs = runsOf(e);
15705
+ parts.push({ el: e, item: { kind: "text", text: t.slice(0, 1500), box: at, style: { ...textOf(e), align: cs(e).textAlign }, ...(runs ? { runs } : {}) } });
15706
+ }
15707
+ }
15708
+ // Accent rules (thin bars, <hr>) and panels (boxes with their own fill or border) behind
15709
+ // the words: recreated on theme colors.
15710
+ const secBg = res.background;
15711
+ let decor = 0;
15712
+ for (const e of sec.querySelectorAll("hr, div, span, section, article, aside, figure, li")) {
15713
+ if (decor >= 6 || !visible(e) || parts.some((p) => p.el === e)) continue;
15714
+ const b = rect(e);
15715
+ const st = cs(e);
15716
+ const fill = toHex(st.backgroundColor);
15717
+ const bw = parseFloat(st.borderTopWidth) || 0;
15718
+ const line = e.tagName === "HR" || (b.height <= 4 && b.width >= 16 && (fill || bw >= 1));
15719
+ if (line) {
15720
+ 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) } });
15721
+ decor++;
15722
+ continue;
15723
+ }
15724
+ const own = fill && fill !== secBg;
15725
+ const framed = bw >= 1 && st.borderTopStyle !== "none" && parseFloat(st.borderLeftWidth) >= 1;
15726
+ if ((own || framed) && b.width >= 120 && b.height >= 60 && b.width < r.width * 0.95 && !e.querySelector("img, video, iframe") && ownText(e)) {
15727
+ 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 } });
15728
+ decor++;
15634
15729
  }
15635
15730
  }
15636
- if (parts.length && parts.length <= 30) res.blueprint = parts.map((p) => p.item);
15731
+ if (parts.length && parts.length <= 36) res.blueprint = parts.map((p) => p.item);
15637
15732
  }
15638
15733
  // Where the section's own content starts and ends (for the spacing between sections).
15639
15734
  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);
@@ -15667,6 +15762,20 @@ var PROBE_SCRIPT = String.raw`(async () => {
15667
15762
  if (h2) presets.h2 = h2;
15668
15763
  const h3 = median(styleOfAll("h3"));
15669
15764
  if (h3) presets.h3 = h3;
15765
+ // A display line: the page's largest short text, well above its headings (a banner's title
15766
+ // set in a paragraph). Read when importing; the comparison doesn't check it.
15767
+ {
15768
+ const ref = Math.max(presets.h1 ? presets.h1.size : 0, presets.h2 ? presets.h2.size : 0, 16);
15769
+ let best;
15770
+ for (const e of document.querySelectorAll("p, strong, b, span, h1, h2, h3, div")) {
15771
+ if (!visible(e) || !inMain(e)) continue;
15772
+ const t = ownText(e);
15773
+ 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;
15774
+ const st = textOf(e);
15775
+ if (st.size >= ref * 1.3 && (!best || st.size > best.size)) best = st;
15776
+ }
15777
+ if (best) presets.display = best;
15778
+ }
15670
15779
  const tally = new Map();
15671
15780
  for (const { el, t } of all) {
15672
15781
  const k = t.family + "|" + t.size;
@@ -15717,7 +15826,7 @@ var PROBE_SCRIPT = String.raw`(async () => {
15717
15826
  if (btn) {
15718
15827
  presets.button = textOf(btn);
15719
15828
  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(".") : "") };
15829
+ 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
15830
  }
15722
15831
 
15723
15832
  // --- layout ---------------------------------------------------------------------------------
@@ -15810,6 +15919,35 @@ var PROBE_SCRIPT = String.raw`(async () => {
15810
15919
  height: doc.scrollHeight,
15811
15920
  fonts,
15812
15921
  fontRequests,
15922
+ pageBackground: toHex(cs(document.body).backgroundColor) || toHex(cs(doc).backgroundColor) || "#ffffff",
15923
+ ...(() => {
15924
+ // The header's logo picture (its shown height) and the footer's own color.
15925
+ const out = {};
15926
+ const head = document.querySelector(".os-header, header, [role=banner], #header");
15927
+ const logo = head && [...head.querySelectorAll("img, svg")].find((e) => visible(e) && e.getBoundingClientRect().height >= 16 && e.getBoundingClientRect().width >= 24);
15928
+ if (logo) out.logoHeight = Math.round(logo.getBoundingClientRect().height);
15929
+ const foot = [...document.querySelectorAll(".os-footer, footer, [role=contentinfo], #footer")].filter(visible).pop();
15930
+ if (foot) {
15931
+ const c = toHex(cs(foot).backgroundColor) || [...foot.querySelectorAll("*")].map((e) => toHex(cs(e).backgroundColor)).find(Boolean);
15932
+ if (c) out.footerBackground = c;
15933
+ // Its first line of words: how many groups sit side by side, and their style.
15934
+ 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)));
15935
+ const lines = leaves.map((e) => ({ e, r: rect(e) }));
15936
+ if (lines.length) {
15937
+ const top = Math.min(...lines.map((l) => l.r.top));
15938
+ const row = lines.filter((l) => l.r.top <= top + 24);
15939
+ const groups = [];
15940
+ for (const l of row.sort((a, b) => a.r.left - b.r.left)) {
15941
+ const g = groups[groups.length - 1];
15942
+ if (g && l.r.left - g.right < 48) g.right = Math.max(g.right, l.r.right);
15943
+ else groups.push({ left: l.r.left, right: l.r.right });
15944
+ }
15945
+ out.footerColumns = groups.length;
15946
+ out.footerText = textOf(row[0].e);
15947
+ }
15948
+ }
15949
+ return out;
15950
+ })(),
15813
15951
  ...(sidebar ? { sidebar } : {}),
15814
15952
  };
15815
15953
  })()`;
@@ -42058,13 +42196,19 @@ async function mapBlock(ctx, sq, opts = {}) {
42058
42196
  const href = sq.href ?? sq.card?.button?.href;
42059
42197
  const link2 = href ? await linkFor(ctx, href) : void 0;
42060
42198
  return one(
42061
- makeBlock(ctx, "image", {
42062
- image: image2,
42063
- aspect: "original",
42064
- lightbox: false,
42065
- ...caption ? { caption: clip2(caption, 200) } : {},
42066
- ...link2 ? { link: link2 } : {}
42067
- })
42199
+ makeBlock(
42200
+ ctx,
42201
+ "image",
42202
+ {
42203
+ image: image2,
42204
+ aspect: "original",
42205
+ lightbox: false,
42206
+ ...caption ? { caption: clip2(caption, 200) } : {},
42207
+ ...link2 ? { link: link2 } : {},
42208
+ ...sq.natural ? { size: "original" } : {}
42209
+ },
42210
+ sq.natural ? { style: { align: { d: "start" } } } : {}
42211
+ )
42068
42212
  );
42069
42213
  }
42070
42214
  case "gallery": {
@@ -42091,7 +42235,8 @@ async function mapBlock(ctx, sq, opts = {}) {
42091
42235
  "gallery",
42092
42236
  {
42093
42237
  images,
42094
- aspect: sq.layout === "grid" ? "1:1" : "original",
42238
+ aspect: sq.layout === "grid" || sq.fit === "contain" ? "1:1" : "original",
42239
+ ...sq.fit === "contain" ? { fit: "contain" } : {},
42095
42240
  columns: images.length >= 4 && sq.layout === "grid" ? "4" : "3",
42096
42241
  lightbox: true,
42097
42242
  showCaptions: images.some((i) => i.caption)
@@ -42298,7 +42443,12 @@ async function mapBlock(ctx, sq, opts = {}) {
42298
42443
  makeBlock(
42299
42444
  ctx,
42300
42445
  "product",
42301
- { product: { collection: item2.collection, items: [item2.slug] } },
42446
+ {
42447
+ product: { collection: item2.collection, items: [item2.slug] },
42448
+ ...sq.show?.excerpt === false ? { showDescription: false } : {},
42449
+ ...sq.show?.price === false ? { showPrice: false } : {},
42450
+ ...sq.show?.buy === false ? { showBuy: false, imageAspect: "1:1" } : {}
42451
+ },
42302
42452
  { variant: "card" }
42303
42453
  )
42304
42454
  );
@@ -42543,7 +42693,8 @@ function stackSection(blocks2, spans, style, width, align) {
42543
42693
  layout: {
42544
42694
  mode: "stack",
42545
42695
  columns: cols > 1 ? { d: spans, m: [12] } : { d: [12] },
42546
- ...cols > 1 ? { align: "center" } : {},
42696
+ // Words beside a picture sit centred on it; columns of words start at the top together.
42697
+ ...cols > 1 && blocks2.some(({ block: block4 }) => ["image", "gallery", "video", "map", "embed"].includes(block4.type)) ? { align: "center" } : {},
42547
42698
  ...width && width !== "content" ? { width } : {}
42548
42699
  },
42549
42700
  style,
@@ -42553,6 +42704,75 @@ function stackSection(blocks2, spans, style, width, align) {
42553
42704
  if (isStarter) section.preset = starterRef({ columns: spans, ...align ? { align } : {} });
42554
42705
  return section;
42555
42706
  }
42707
+ var headingLevelOf = (b) => b.type === "heading" ? Number(String(b.fields.level ?? "h2").slice(1)) || 2 : void 0;
42708
+ var textOfNodes = (nodes) => JSON.stringify(nodes ?? "").length;
42709
+ function splitAtHeadings(blocks2) {
42710
+ const count2 = (level2) => {
42711
+ let n = 0;
42712
+ for (const { block: block4 } of blocks2) {
42713
+ if (headingLevelOf(block4) === level2) n++;
42714
+ if (block4.type === "text") {
42715
+ const doc = block4.fields.content;
42716
+ for (const node2 of doc?.content ?? [])
42717
+ if (node2.type === "heading" && node2.attrs?.level === level2) n++;
42718
+ }
42719
+ }
42720
+ return n;
42721
+ };
42722
+ const size2 = blocks2.reduce(
42723
+ (n, { block: block4 }) => n + (block4.type === "text" ? textOfNodes(block4.fields.content) : textOfNodes(block4.fields)),
42724
+ 0
42725
+ );
42726
+ const media = blocks2.filter(
42727
+ ({ block: block4 }) => block4.type === "image" && block4.fields.size === "original"
42728
+ ).length;
42729
+ const rules = blocks2.filter(({ block: block4 }) => block4.type === "line").length;
42730
+ if (size2 < 2400 && media < 2 && rules < 1) return [blocks2];
42731
+ const level = count2(2) >= 2 ? 2 : count2(3) >= 3 ? 3 : void 0;
42732
+ if (!level) return [blocks2];
42733
+ const out = [[]];
42734
+ const cur = () => out[out.length - 1];
42735
+ const start = () => {
42736
+ if (!cur().length) return;
42737
+ const last2 = cur().at(-1);
42738
+ const carry = last2.block.type === "image" && last2.block.fields.size === "original";
42739
+ if (!carry) out.push([]);
42740
+ else if (cur().length > 1) {
42741
+ cur().pop();
42742
+ out.push([last2]);
42743
+ }
42744
+ };
42745
+ for (const p of blocks2) {
42746
+ if (headingLevelOf(p.block) === level) {
42747
+ start();
42748
+ cur().push(p);
42749
+ continue;
42750
+ }
42751
+ if (p.block.type !== "text") {
42752
+ cur().push(p);
42753
+ continue;
42754
+ }
42755
+ const doc = p.block.fields.content;
42756
+ const parts = [[]];
42757
+ for (const node2 of doc.content ?? []) {
42758
+ if (node2.type === "heading" && node2.attrs?.level === level && parts.at(-1).length)
42759
+ parts.push([]);
42760
+ parts.at(-1).push(node2);
42761
+ }
42762
+ parts.forEach((nodes, i) => {
42763
+ if (i > 0 || nodes[0]?.type === "heading" && nodes[0].attrs?.level === level) start();
42764
+ cur().push({
42765
+ ...p,
42766
+ block: {
42767
+ ...p.block,
42768
+ ...i > 0 ? { id: newId("blk") } : {},
42769
+ fields: { ...p.block.fields, content: { ...doc, content: nodes } }
42770
+ }
42771
+ });
42772
+ });
42773
+ }
42774
+ return out.filter((c) => c.length);
42775
+ }
42556
42776
  async function mapBlocks(ctx, blocks2, inGrid = false) {
42557
42777
  const out = [];
42558
42778
  for (const b of blocks2) out.push(...await mapBlock(ctx, b, { inGrid }));
@@ -42651,6 +42871,15 @@ async function mapSectionParts(ctx, sq) {
42651
42871
  }
42652
42872
  return [section];
42653
42873
  }
42874
+ if (sq.together && sq.layout.rows.length > 1) {
42875
+ const filled2 = sq.layout.rows.map((r) => r.columns.filter((c) => c.blocks.length));
42876
+ const multi = filled2.some((cols) => cols.length > 1);
42877
+ const shapes = new Set(filled2.map((cols) => cols.map((c) => c.span).join("-")));
42878
+ if (multi && shapes.size > 1) {
42879
+ const grid = await rowsAsGrid(ctx, sq.layout.rows, style, width);
42880
+ if (grid) return [grid];
42881
+ }
42882
+ }
42654
42883
  const out = [];
42655
42884
  let run2;
42656
42885
  const flush = () => {
@@ -42662,20 +42891,22 @@ async function mapSectionParts(ctx, sq) {
42662
42891
  run2 = void 0;
42663
42892
  return;
42664
42893
  }
42665
- out.push(
42666
- stackSection(
42667
- run2.blocks,
42668
- run2.spans,
42669
- out.length ? {
42670
- ...style,
42671
- // A photo stays with the first part; a plain color continues behind every part.
42672
- background: style.background?.type === "color" ? style.background : void 0,
42673
- minHeight: void 0
42674
- } : style,
42675
- width,
42676
- align
42677
- )
42678
- );
42894
+ const parts = run2.spans.length === 1 ? splitAtHeadings(run2.blocks) : [run2.blocks];
42895
+ for (const part of parts)
42896
+ out.push(
42897
+ stackSection(
42898
+ part,
42899
+ run2.spans,
42900
+ out.length ? {
42901
+ ...style,
42902
+ // A photo stays with the first part; a plain color continues behind every part.
42903
+ background: style.background?.type === "color" ? style.background : void 0,
42904
+ minHeight: void 0
42905
+ } : style,
42906
+ width,
42907
+ align
42908
+ )
42909
+ );
42679
42910
  run2 = void 0;
42680
42911
  };
42681
42912
  for (const row of sq.layout.rows) {
@@ -42704,6 +42935,11 @@ async function mapSectionParts(ctx, sq) {
42704
42935
  flush();
42705
42936
  if (!out.length && bg) out.push(stackSection([], [12], style, width, align));
42706
42937
  if (sq.role && out.length === 1) applyNearestPreset(out[0], sq.role, !sq.theme);
42938
+ for (const s2 of out)
42939
+ 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")) {
42940
+ s2.layout.width = "narrow";
42941
+ delete s2.preset;
42942
+ }
42707
42943
  for (const s2 of out)
42708
42944
  for (const k of Object.keys(s2.style))
42709
42945
  if (s2.style[k] === void 0) delete s2.style[k];
@@ -42852,6 +43088,57 @@ function applyNearestPreset(section, role, themeUnknown) {
42852
43088
  if (model.style?.align && !b.style?.align) b.style = { ...b.style, align: model.style.align };
42853
43089
  }
42854
43090
  }
43091
+ async function rowsAsGrid(ctx, rows, style, width) {
43092
+ const ROW_PX = 24;
43093
+ const contentPx = width === "wide" || width === "full" ? 1440 : width === "narrow" ? 720 : 1280;
43094
+ const blocks2 = [];
43095
+ const items = {};
43096
+ let y = 1;
43097
+ for (const row of rows) {
43098
+ const cols = row.columns.filter((c) => c.blocks.length);
43099
+ if (!cols.length) continue;
43100
+ const total = row.columns.reduce((n, c) => n + c.span, 0) || 12;
43101
+ let x = 1;
43102
+ let rowH = 1;
43103
+ for (const col of row.columns) {
43104
+ const w = Math.max(
43105
+ 1,
43106
+ Math.min(DESKTOP_COLS - x + 1, Math.round(col.span / Math.max(12, total) * DESKTOP_COLS))
43107
+ );
43108
+ let cy = y;
43109
+ for (const b of await mapBlocks(ctx, col.blocks, true)) {
43110
+ const img2 = b.type === "image" ? b.fields.image : void 0;
43111
+ const ratio = img2?.width && img2?.height ? img2.width / img2.height : void 0;
43112
+ const h = b.type === "image" ? Math.max(4, Math.round(w / DESKTOP_COLS * contentPx / (ratio ?? 1.5) / ROW_PX)) : 1;
43113
+ blocks2.push(b);
43114
+ items[b.id] = { d: { x, y: cy, w, h } };
43115
+ cy += h;
43116
+ }
43117
+ rowH = Math.max(rowH, cy - y);
43118
+ x = Math.min(DESKTOP_COLS, x + w);
43119
+ }
43120
+ y += rowH;
43121
+ }
43122
+ if (blocks2.length < 2) return void 0;
43123
+ const section = {
43124
+ id: newId("sec"),
43125
+ layout: {
43126
+ mode: "grid",
43127
+ rows: { d: y - 1 },
43128
+ items,
43129
+ ...width && width !== "content" ? { width } : {}
43130
+ },
43131
+ style,
43132
+ blocks: blocks2
43133
+ };
43134
+ const mobile = autoMobileLayout(section, { reset: true });
43135
+ if (mobile && section.layout.mode === "grid") {
43136
+ section.layout.items = mobile;
43137
+ const mRows = Math.max(1, ...Object.values(mobile).map((i) => i.m ? i.m.y + i.m.h - 1 : 1));
43138
+ section.layout.rows = { d: y - 1, m: mRows };
43139
+ }
43140
+ return section;
43141
+ }
42855
43142
 
42856
43143
  // ../import/src/map/site.ts
42857
43144
  var clip3 = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1).trimEnd()}\u2026` : s2;
@@ -43151,6 +43438,12 @@ function headerFor(site2, siteDark, brand) {
43151
43438
  }
43152
43439
  return {
43153
43440
  variant,
43441
+ ...t.logoHeight ? {
43442
+ logoHeight: {
43443
+ d: Math.round(t.logoHeight.d),
43444
+ ...t.logoHeight.m ? { m: Math.round(t.logoHeight.m) } : {}
43445
+ }
43446
+ } : {},
43154
43447
  sticky: t.headerFixed === true,
43155
43448
  transparentOnHero: t.headerTransparent === true,
43156
43449
  mobileMenu: "fullscreen",
@@ -43393,6 +43686,18 @@ async function buildSite(sq, opts) {
43393
43686
  blocks: text7 ? [text7] : []
43394
43687
  };
43395
43688
  }
43689
+ if (footer) splitFooterLine(footer, sq.theme.footerColumns, sq.theme.footerLabels === true);
43690
+ const footBg = sq.theme.footerBackground;
43691
+ if (footer && footBg && /^#[0-9a-f]{6}$/i.test(footBg) && !footer.style.background) {
43692
+ const dark = relativeLuminance(footBg) < 0.18;
43693
+ if (dark || relativeLuminance(footBg) < 0.97) {
43694
+ footer.style = {
43695
+ ...footer.style,
43696
+ colorTheme: dark ? "dark" : footer.style.colorTheme ?? "light",
43697
+ background: { type: "color", color: footBg }
43698
+ };
43699
+ }
43700
+ }
43396
43701
  const logo = sq.logo ? await opts.media.image(sq.logo, "logo") : void 0;
43397
43702
  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
43703
  const homeSeoImage = sq.pages.find((p) => p.path === "/")?.seo.image;
@@ -43485,6 +43790,35 @@ async function buildSite(sq, opts) {
43485
43790
  linkFixes
43486
43791
  };
43487
43792
  }
43793
+ function splitFooterLine(footer, columns, labels = false) {
43794
+ if (!columns || columns < 2 || columns > 4) return;
43795
+ if (footer.layout.mode !== "stack" || footer.layout.columns.d.length !== 1) return;
43796
+ const texts = footer.blocks.filter((b) => b.type === "text");
43797
+ if (footer.blocks.length !== 1 || texts.length !== 1) return;
43798
+ const block4 = texts[0];
43799
+ const doc = block4.fields.content;
43800
+ const paras = doc.content ?? [];
43801
+ if (paras.length !== columns || paras.some((p) => p.type !== "paragraph")) return;
43802
+ const span = Math.floor(12 / columns);
43803
+ const spans = paras.map((_, i) => i === paras.length - 1 ? 12 - span * (columns - 1) : span);
43804
+ const aligns = paras.map(
43805
+ (_, i) => i === 0 ? "start" : i === paras.length - 1 ? "end" : "center"
43806
+ );
43807
+ footer.blocks = paras.map((p, i) => ({
43808
+ ...block4,
43809
+ id: i === 0 ? block4.id : newId("blk"),
43810
+ column: i,
43811
+ fields: {
43812
+ ...block4.fields,
43813
+ content: {
43814
+ ...doc,
43815
+ content: [labels ? { ...p, attrs: { ...p.attrs ?? {}, preset: "eyebrow" } } : p]
43816
+ }
43817
+ },
43818
+ style: { ...block4.style ?? {}, align: { d: aligns[i], m: "start" } }
43819
+ }));
43820
+ footer.layout = { ...footer.layout, columns: { d: spans, m: [12] }, align: "center" };
43821
+ }
43488
43822
 
43489
43823
  // ../import/src/media.ts
43490
43824
  import { createHash as createHash2 } from "crypto";
@@ -48440,7 +48774,7 @@ async function crawlWordPress(opts) {
48440
48774
  });
48441
48775
  }
48442
48776
  if (!rest && !wxr && home) {
48443
- const { crawlGeneric: crawlGeneric2 } = await import("./crawl-TAADG5BR.js");
48777
+ const { crawlGeneric: crawlGeneric2 } = await import("./crawl-YPDLNAN7.js");
48444
48778
  const site2 = await crawlGeneric2({
48445
48779
  url: base,
48446
48780
  fetcher: f,
@@ -48496,6 +48830,9 @@ async function crawlWordPress(opts) {
48496
48830
  }
48497
48831
  const pages = [];
48498
48832
  const builders = /* @__PURE__ */ new Set();
48833
+ const showsTitles = /<h1[^>]*class="[^"]*\b(?:entry-title|page-title|wp-block-post-title)\b/.test(
48834
+ homeHtml
48835
+ );
48499
48836
  for (const e of pageEntries.sort((a, b) => (a.menuOrder ?? 0) - (b.menuOrder ?? 0))) {
48500
48837
  if (e === postsPage) continue;
48501
48838
  if (COMMERCE_SLUGS.test(e.slug) && e.path.split("/").filter(Boolean).length === 1) {
@@ -48513,7 +48850,11 @@ async function crawlWordPress(opts) {
48513
48850
  path: e.path,
48514
48851
  title: e.title,
48515
48852
  seo: { ...e.image && !e.seo.image ? { image: e.image } : {}, ...e.seo },
48516
- sections: bodySections(e.html, base, postsPath, notices, e.path),
48853
+ sections: withPageTitle(
48854
+ bodySections(e.html, base, postsPath, notices, e.path),
48855
+ e.title,
48856
+ showsTitles
48857
+ ),
48517
48858
  ...nav.length && !navPaths.has(e.path) && e.path !== "/" ? { hidden: true } : {}
48518
48859
  });
48519
48860
  }
@@ -48660,6 +49001,32 @@ function wxrNav(wxr, base, entries) {
48660
49001
  }
48661
49002
  return out;
48662
49003
  }
49004
+ function withPageTitle(sections, title, shows) {
49005
+ if (!shows || !title.trim()) return sections;
49006
+ const norm7 = (t) => t.toLowerCase().replace(/[^a-z0-9]+/g, "");
49007
+ const first2 = sections[0];
49008
+ const blocks2 = first2?.layout.mode === "classic" ? first2.layout.rows.flatMap((r) => r.columns.flatMap((c) => c.blocks)) : [];
49009
+ const opening = blocks2[0];
49010
+ if (opening && (opening.kind === "heading" && norm7(opening.text) === norm7(title) || opening.kind === "text" && /^\s*<h1/i.test(opening.html)))
49011
+ return sections;
49012
+ const heading2 = { kind: "heading", text: title, level: 1 };
49013
+ if (first2 && first2.layout.mode === "classic" && !first2.background?.image && !first2.role) {
49014
+ const row = first2.layout.rows[0];
49015
+ if (row && row.columns.length === 1) {
49016
+ row.columns[0]?.blocks.unshift(heading2);
49017
+ return sections;
49018
+ }
49019
+ first2.layout.rows.unshift({ columns: [{ span: 12, blocks: [heading2] }] });
49020
+ return sections;
49021
+ }
49022
+ return [
49023
+ {
49024
+ theme: "",
49025
+ layout: { mode: "classic", rows: [{ columns: [{ span: 12, blocks: [heading2] }] }] }
49026
+ },
49027
+ ...sections
49028
+ ];
49029
+ }
48663
49030
 
48664
49031
  // ../import/src/fidelity/component-gen.ts
48665
49032
  var norm5 = (s2) => (s2 ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
@@ -48667,7 +49034,7 @@ var esc3 = (s2) => s2.replace(/[<>&"]/g, "");
48667
49034
  var round5 = (n, d = 2) => Math.round(n * 10 ** d) / 10 ** d;
48668
49035
  function canGenerate(section) {
48669
49036
  const b = section.blueprint;
48670
- return !!b && b.length >= 2 && b.length <= 30 && section.height <= 2400;
49037
+ return !!b && b.filter((i) => i.kind !== "rule" && i.kind !== "panel").length >= 2 && b.length <= 36 && section.height <= 2400;
48671
49038
  }
48672
49039
  function componentFromBlueprint(input) {
48673
49040
  const items = (input.section.blueprint ?? []).filter(
@@ -48685,7 +49052,9 @@ function componentFromBlueprint(input) {
48685
49052
  const css = [
48686
49053
  ":scope { width: min(100%, " + Math.round(width) + "px); margin-inline: auto; }",
48687
49054
  ".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; }",
49055
+ ".gx__i { position: relative; z-index: 1; margin: 0; min-width: 0; overflow-wrap: break-word; }",
49056
+ ".gx__i p { margin: 0; }",
49057
+ ".gx__p { z-index: 0; }",
48689
49058
  ".gx__img picture, .gx__img img { display: block; width: 100%; height: 100%; object-fit: cover; }"
48690
49059
  ];
48691
49060
  const phone = [];
@@ -48723,6 +49092,10 @@ function componentFromBlueprint(input) {
48723
49092
  ]
48724
49093
  ].join("; ");
48725
49094
  };
49095
+ const tokenColor = (hex, fallback2) => {
49096
+ const v = colorVar(hex);
49097
+ return v === "inherit" ? fallback2 : v;
49098
+ };
48726
49099
  items.forEach((it, k) => {
48727
49100
  const n = k + 1;
48728
49101
  const cls = `gx__i gx__i${n}`;
@@ -48731,6 +49104,21 @@ function componentFromBlueprint(input) {
48731
49104
  const row = Math.floor((it.box.y - top) / 8) + 1;
48732
49105
  const rows = Math.max(1, Math.round(it.box.h / 8));
48733
49106
  const place = `grid-column: ${col} / span ${span}; grid-row: ${row} / span ${rows}`;
49107
+ if (it.kind === "rule") {
49108
+ parts.push(`<hr class="gx__p gx__r${n}">`);
49109
+ css.push(
49110
+ `.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)")}; }`
49111
+ );
49112
+ return;
49113
+ }
49114
+ if (it.kind === "panel") {
49115
+ parts.push(`<div class="gx__p gx__p${n}" aria-hidden="true"></div>`);
49116
+ css.push(
49117
+ `.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; }`
49118
+ );
49119
+ phone.push(`.gx__p${n} { display: none; }`);
49120
+ return;
49121
+ }
48734
49122
  if (it.kind === "image") {
48735
49123
  const key3 = `image${n}`;
48736
49124
  fields[key3] = { kind: "image", label: `Picture ${n}` };
@@ -48772,6 +49160,21 @@ function componentFromBlueprint(input) {
48772
49160
  return;
48773
49161
  }
48774
49162
  const key2 = `text${n}`;
49163
+ const doc = it.kind === "text" ? richDoc(it.runs, input.origin) : void 0;
49164
+ if (doc) {
49165
+ fields[key2] = {
49166
+ kind: "richText",
49167
+ label: `Text ${n}`,
49168
+ marks: ["bold", "italic", "link"],
49169
+ blocks: ["paragraph"],
49170
+ maxLength: 3e3
49171
+ };
49172
+ values[key2] = doc;
49173
+ parts.push(`<div class="${cls}">{{rich ${key2}}}</div>`);
49174
+ css.push(`.gx__i${n} { ${place}; ${typeCss(it.style, false)}; }`);
49175
+ phone.push(`.gx__i${n} { ${typeCss(it.style, true)}; }`);
49176
+ return;
49177
+ }
48775
49178
  const text7 = (it.text ?? "").slice(0, it.kind === "heading" ? 300 : 1500);
48776
49179
  fields[key2] = {
48777
49180
  kind: "text",
@@ -48809,15 +49212,62 @@ ${parts.map((p) => ` ${p}`).join("\n")}
48809
49212
  };
48810
49213
  return { def, fields: values };
48811
49214
  }
49215
+ function richDoc(runs, origin) {
49216
+ if (!runs?.length) return void 0;
49217
+ const content3 = [];
49218
+ for (const r of runs) {
49219
+ if (r.t === "\n") {
49220
+ content3.push({ type: "hardBreak" });
49221
+ continue;
49222
+ }
49223
+ const text7 = r.t;
49224
+ if (!text7) continue;
49225
+ const marks = [];
49226
+ if (r.b) marks.push({ type: "bold" });
49227
+ if (r.i) marks.push({ type: "italic" });
49228
+ const href = r.href && safeHref(r.href, origin);
49229
+ if (href) marks.push({ type: "link", attrs: { href } });
49230
+ const last2 = content3[content3.length - 1];
49231
+ if (last2?.type === "text" && JSON.stringify(last2.marks ?? []) === JSON.stringify(marks)) {
49232
+ last2.text = `${last2.text}${text7}`;
49233
+ continue;
49234
+ }
49235
+ content3.push({ type: "text", text: text7, ...marks.length ? { marks } : {} });
49236
+ }
49237
+ const first2 = content3[0];
49238
+ if (first2?.type === "text" && first2.text) first2.text = first2.text.replace(/^\s+/, "");
49239
+ const end = content3[content3.length - 1];
49240
+ if (end?.type === "text" && end.text) end.text = end.text.replace(/\s+$/, "");
49241
+ const kept = content3.filter((c) => c.type !== "text" || c.text);
49242
+ if (!kept.length) return void 0;
49243
+ return { type: "doc", content: [{ type: "paragraph", content: kept }] };
49244
+ }
49245
+ function safeHref(href, origin) {
49246
+ if (/^(?:mailto:|tel:)/i.test(href)) return href;
49247
+ try {
49248
+ const u = new URL(href);
49249
+ if (u.protocol !== "https:" && u.protocol !== "http:") return void 0;
49250
+ if (origin && u.origin === origin) return `${u.pathname}${u.hash}` || "/";
49251
+ return u.toString();
49252
+ } catch {
49253
+ return void 0;
49254
+ }
49255
+ }
48812
49256
 
48813
49257
  // ../import/src/fidelity/tune.ts
48814
49258
  var TUNE_GROUPS = [
48815
49259
  "type",
48816
49260
  "fonts",
48817
49261
  "rhythm",
49262
+ "rhythm-phone",
49263
+ "widths",
49264
+ "chapters",
48818
49265
  "colors",
49266
+ "page-color",
48819
49267
  "buttons",
48820
49268
  "layout",
49269
+ "headings",
49270
+ "heights",
48821
49271
  "backgrounds",
48822
49272
  "crops",
48823
49273
  "phone-images",
@@ -48866,21 +49316,29 @@ var pageOf = (doc, path2) => {
48866
49316
  const slug = path2.replace(/^\/+|\/+$/g, "");
48867
49317
  return Object.values(doc.pages).find((p) => p.slug === slug);
48868
49318
  };
48869
- function tuneRound(input, measured, groups = TUNE_GROUPS) {
49319
+ function tuneRound(input, measured, groups = TUNE_GROUPS, opts = {}) {
48870
49320
  const doc = structuredClone(input);
48871
49321
  const on = (g) => groups.includes(g);
48872
49322
  const changes = [];
48873
49323
  const choices = doc.styles.choices;
48874
49324
  const typography = { ...choices.typography ?? {} };
48875
49325
  const scale = { ...typography.scale ?? {} };
49326
+ const forPreset = (preset, list5) => {
49327
+ if (preset !== "button") return list5;
49328
+ const content3 = list5.filter((m) => !m.original.buttons?.chrome);
49329
+ return content3.length ? content3 : list5;
49330
+ };
48876
49331
  for (const preset of on("type") ? new Set(Object.values(PRESET_OF)) : []) {
48877
49332
  const label = Object.entries(PRESET_OF).find(([, p]) => p === preset)?.[0];
48878
49333
  const step = { ...scale[preset] ?? {} };
48879
49334
  let touched = false;
48880
49335
  for (const phone2 of [false, true]) {
48881
49336
  if (phone2 && preset === "nav") continue;
48882
- const list5 = measured.filter(
48883
- (m) => m.viewport < 700 === phone2 && m.original.presets[preset] && m.imported.presets[preset]
49337
+ const list5 = forPreset(
49338
+ preset,
49339
+ measured.filter(
49340
+ (m) => m.viewport < 700 === phone2 && m.original.presets[preset] && m.imported.presets[preset]
49341
+ )
48884
49342
  );
48885
49343
  if (!list5.length) continue;
48886
49344
  const sizes = list5.map((m) => m.original.presets[preset]?.size);
@@ -48897,14 +49355,21 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
48897
49355
  touched = true;
48898
49356
  }
48899
49357
  if (["eyebrow", "button", "nav", "wordmark"].includes(preset)) {
48900
- const list5 = measured.filter(
48901
- (m) => m.viewport >= 700 && m.original.presets[preset] && m.imported.presets[preset]
48902
- );
48903
- const failing = list5.filter(
48904
- (m) => m.comparison.checks.some((c) => c.what === `${label} letter spacing and case` && !c.ok)
49358
+ const all = forPreset(
49359
+ preset,
49360
+ measured.filter((m) => m.original.presets[preset] && m.imported.presets[preset])
48905
49361
  );
48906
- if (list5.length && failing.length * 2 >= list5.length) {
48907
- const ls = median(list5.map((m) => m.original.presets[preset]?.letterSpacing));
49362
+ const list5 = all.filter((m) => m.viewport >= 700);
49363
+ const fails = (m) => m.comparison.checks.some((c) => c.what === `${label} letter spacing and case` && !c.ok);
49364
+ const failing = list5.filter(fails);
49365
+ const phones = all.filter((m) => m.viewport < 700);
49366
+ const phoneFailing = phones.filter(fails);
49367
+ if (list5.length && (failing.length * 2 >= list5.length || phones.length && phoneFailing.length * 2 >= phones.length)) {
49368
+ const lsDesk = median(list5.map((m) => m.original.presets[preset]?.letterSpacing));
49369
+ const lsPhone = median(
49370
+ phones.map((m) => m.original.presets[preset]?.letterSpacing)
49371
+ );
49372
+ const ls = lsDesk !== void 0 && lsPhone !== void 0 && Math.abs(lsDesk - lsPhone) <= 0.075 ? (lsDesk + lsPhone) / 2 : lsDesk;
48908
49373
  const cases = list5.map((m) => m.original.presets[preset]?.case);
48909
49374
  if (ls !== void 0 && Math.abs((step.letterSpacing ?? -1) - ls) > 0.01) {
48910
49375
  step.letterSpacing = round6(clamp2(ls, -0.2, 1), 3);
@@ -48942,7 +49407,7 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
48942
49407
  for (const preset of ["h2", "h3"]) {
48943
49408
  const fams = origs.map((p) => p[preset]?.family).filter((f2) => !!f2);
48944
49409
  const f = fams[0];
48945
- if (!f || fams.length < 2 || !fams.every((x) => norm6(x) === norm6(f))) continue;
49410
+ if (!f || !fams.every((x) => norm6(x) === norm6(f))) continue;
48946
49411
  const role = norm6(f) === norm6(heading2) ? void 0 : norm6(f) === norm6(label) ? "label" : norm6(f) === norm6(body) ? "body" : void 0;
48947
49412
  if (!role || scale[preset]?.font === role) continue;
48948
49413
  const sample2 = origs.map((p) => p[preset]).find(Boolean);
@@ -48965,6 +49430,29 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
48965
49430
  changes.push(`${role} font: ${match.family} for ${c.original}`);
48966
49431
  }
48967
49432
  }
49433
+ if (on("fonts")) {
49434
+ const roleFamily = (r) => norm6(
49435
+ typography[r]?.family ?? (r === "label" ? typography.body?.family ?? void 0 : void 0)
49436
+ );
49437
+ for (const preset of ["eyebrow", "button", "nav", "wordmark"]) {
49438
+ const fams = forPreset(
49439
+ preset,
49440
+ measured.filter((m) => m.viewport >= 700 && m.original.presets[preset])
49441
+ ).map((m) => m.original.presets[preset]?.family).filter((f2) => !!f2);
49442
+ const f = fams[0];
49443
+ if (!f || !fams.every((x) => norm6(x) === norm6(f))) continue;
49444
+ const want = ["label", "heading", "body"].find(
49445
+ (r) => roleFamily(r) && roleFamily(r) === norm6(f)
49446
+ );
49447
+ if (!want) continue;
49448
+ const cur = scale[preset]?.font ?? "label";
49449
+ const curFamily = roleFamily(cur);
49450
+ if (cur === want || curFamily === norm6(f)) continue;
49451
+ scale[preset] = { ...scale[preset] ?? {}, font: want };
49452
+ changes.push(`${preset}: in the ${want} font (${f}), as on the original`);
49453
+ }
49454
+ if (Object.keys(scale).length) typography.scale = scale;
49455
+ }
48968
49456
  choices.typography = typography;
48969
49457
  const layout = { ...choices.layout ?? {} };
48970
49458
  const desk = measured.filter((m) => m.viewport >= 700);
@@ -48978,13 +49466,89 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
48978
49466
  layout.contentWidth = Math.round(cw / 8) * 8;
48979
49467
  changes.push(`content width ${layout.contentWidth}px`);
48980
49468
  }
49469
+ if (on("widths")) {
49470
+ const content3 = layout.contentWidth;
49471
+ for (const m of desk) {
49472
+ const check = m.comparison.checks.find((c) => c.what === "Content width" && !c.ok);
49473
+ const want = m.original.layout.contentWidth;
49474
+ const have = m.imported.layout.contentWidth;
49475
+ const page = pageOf(doc, m.path);
49476
+ if (!check || !want || !have || !page || want >= have) continue;
49477
+ let width;
49478
+ if (content3 && Math.abs(content3 - want) / want <= 0.06) width = "content";
49479
+ else if (want >= 360 && want <= 1600) {
49480
+ layout.narrowWidth ??= Math.round(want / 8) * 8;
49481
+ if (Math.abs(layout.narrowWidth - want) / want <= 0.06) width = "narrow";
49482
+ }
49483
+ if (!width) continue;
49484
+ let n = 0;
49485
+ for (const section of page.sections) {
49486
+ if (section.layout.mode !== "stack" || section.layout.width === "full") continue;
49487
+ if (section.style.background?.type === "image" || section.style.background?.type === "video")
49488
+ continue;
49489
+ const cur = section.layout.width ?? "content";
49490
+ if (cur === width) continue;
49491
+ if (width === "content") delete section.layout.width;
49492
+ else section.layout.width = width;
49493
+ delete section.preset;
49494
+ n++;
49495
+ }
49496
+ if (n) changes.push(`${m.path}: ${n} section(s) at the ${width} width, as on the original`);
49497
+ }
49498
+ }
49499
+ if (on("chapters")) {
49500
+ const bag = (t) => new Set(
49501
+ t.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 3)
49502
+ );
49503
+ for (const m of desk) {
49504
+ const page = pageOf(doc, m.path);
49505
+ if (!page || m.imported.sections.length !== page.sections.length) continue;
49506
+ const keep = new Set(page.sections.map((_, j) => j));
49507
+ for (const o of m.original.sections) {
49508
+ const mine = bag(`${o.heading?.text ?? ""} ${o.words ?? o.text}`);
49509
+ if (mine.size < 20) continue;
49510
+ const inside = m.imported.sections.map((x, j) => {
49511
+ const theirs = bag(`${x.heading?.text ?? ""} ${x.words ?? x.text}`);
49512
+ let n = 0;
49513
+ for (const w of theirs) if (mine.has(w)) n++;
49514
+ return theirs.size >= 3 && n / theirs.size >= 0.85 ? j : -1;
49515
+ }).filter((j) => j >= 0);
49516
+ let run2 = [];
49517
+ const join21 = () => {
49518
+ if (run2.length >= 2) {
49519
+ const [first2, ...rest] = run2.map((j) => page.sections[j]);
49520
+ for (const r of rest) {
49521
+ first2?.blocks.push(...r.blocks);
49522
+ keep.delete(page.sections.indexOf(r));
49523
+ }
49524
+ changes.push(`${m.path}: ${run2.length} sections joined into one, as on the original`);
49525
+ }
49526
+ run2 = [];
49527
+ };
49528
+ for (const j of inside) {
49529
+ const a = page.sections[j];
49530
+ const prev = run2.length ? page.sections[run2[run2.length - 1]] : void 0;
49531
+ const plain3 = a.layout.mode === "stack" && a.layout.columns.d.length === 1 && !a.anchor && a.style.background?.type !== "image" && a.style.background?.type !== "video";
49532
+ 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" : "");
49533
+ if (plain3 && (!run2.length || same)) run2.push(j);
49534
+ else {
49535
+ join21();
49536
+ if (plain3) run2.push(j);
49537
+ }
49538
+ }
49539
+ join21();
49540
+ }
49541
+ if (keep.size !== page.sections.length)
49542
+ page.sections = page.sections.filter((_, j) => keep.has(j));
49543
+ }
49544
+ }
48981
49545
  const pad3 = (list5) => median(
48982
49546
  list5.filter((m) => m.comparison.checks.some((c) => c.what === "Section padding" && !c.ok)).map(
48983
49547
  (m) => m.original.layout.sectionPadding && m.imported.layout.sectionPadding ? m.original.layout.sectionPadding / m.imported.layout.sectionPadding : 0
48984
49548
  ).filter((n) => n > 0)
48985
49549
  );
48986
49550
  const pd = on("rhythm") ? pad3(desk) : void 0;
48987
- const pm = on("rhythm") ? pad3(phone) : void 0;
49551
+ const pm = on("rhythm-phone") ? pad3(phone) : void 0;
48988
49552
  if (pd) {
48989
49553
  const base = layout.sectionPadding ?? median(desk.map((m) => m.imported.layout.sectionPadding ?? 0).filter(Boolean));
48990
49554
  if (base) {
@@ -49010,7 +49574,7 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
49010
49574
  }
49011
49575
  }
49012
49576
  const inks = /* @__PURE__ */ new Map();
49013
- for (const m of on("colors") ? desk : []) {
49577
+ for (const m of on("colors") || on("page-color") ? desk : []) {
49014
49578
  const page = pageOf(doc, m.path);
49015
49579
  if (!page || m.imported.sections.length !== page.sections.length) continue;
49016
49580
  for (const s2 of m.comparison.sections) {
@@ -49020,21 +49584,38 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
49020
49584
  const section = page.sections[s2.importedIndex];
49021
49585
  if (!o || !i || !section || o.backgroundImage) continue;
49022
49586
  const theme = section.style.colorTheme ?? "light";
49023
- const e = inks.get(theme) ?? { text: [], heading: [] };
49024
- if (o.textColor && i.textColor && colorDistance(o.textColor, i.textColor) >= 16)
49025
- e.text.push(o.textColor);
49026
- if (o.heading?.color && i.heading?.color && colorDistance(o.heading.color, i.heading.color) >= 16)
49027
- e.heading.push(o.heading.color);
49587
+ const e = inks.get(theme) ?? { text: [], heading: [], background: [] };
49588
+ if (o.background && i.background && !section.style.background)
49589
+ e.background.push({ o: o.background, i: i.background });
49590
+ if (o.textColor && i.textColor) e.text.push({ o: o.textColor, i: i.textColor });
49591
+ if (o.heading?.color && i.heading?.color)
49592
+ e.heading.push({ o: o.heading.color, i: i.heading.color });
49028
49593
  inks.set(theme, e);
49029
49594
  }
49030
49595
  }
49031
49596
  for (const [theme, e] of inks)
49032
- for (const role of ["text", "heading"]) {
49597
+ for (const role of [
49598
+ ...on("colors") ? ["text", "heading"] : [],
49599
+ ...on("page-color") ? ["background"] : []
49600
+ ]) {
49033
49601
  const list5 = e[role];
49034
- const first2 = list5[0];
49035
- if (!first2 || list5.length < 2 || !list5.every((c) => colorDistance(c, first2) < 12)) continue;
49036
- setRole(doc, theme, role, first2);
49037
- changes.push(`${role} on ${theme} sections ${first2}, as on the original`);
49602
+ const tol = role === "background" ? 16 : 28;
49603
+ const passes = (c) => list5.filter((p) => colorDistance(p.o, c(p)) < tol).length;
49604
+ const now = passes((p) => p.i);
49605
+ let best;
49606
+ let bestN = now;
49607
+ for (const cand of new Set(list5.map((p) => p.o))) {
49608
+ const n = passes(() => cand);
49609
+ if (n > bestN) {
49610
+ best = cand;
49611
+ bestN = n;
49612
+ }
49613
+ }
49614
+ if (!best || list5.length < 2) continue;
49615
+ if (!setRole(doc, theme, role, best)) continue;
49616
+ changes.push(
49617
+ `${role} on ${theme} sections ${best}, as on the original (${bestN} of ${list5.length} sections)`
49618
+ );
49038
49619
  }
49039
49620
  const fills = /* @__PURE__ */ new Map();
49040
49621
  for (const m of on("buttons") ? desk : []) {
@@ -49063,6 +49644,135 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
49063
49644
  } catch {
49064
49645
  themes = void 0;
49065
49646
  }
49647
+ if (on("heights")) {
49648
+ const scaleOf = SPACE_SCALES[choices.density] ?? SPACE_SCALES.comfortable;
49649
+ const tokens = Object.entries(scaleOf).sort((a, b) => a[1] - b[1]);
49650
+ const fit = (o, i) => {
49651
+ if (i.contentTop === void 0 || i.contentBottom === void 0) return void 0;
49652
+ const content3 = i.contentBottom - i.contentTop;
49653
+ const room = (o.height - content3) / 2;
49654
+ const token = [...tokens].reverse().find(([, px3]) => px3 <= room)?.[0] ?? "none";
49655
+ return { token, height: Math.round(o.height) };
49656
+ };
49657
+ for (const m of desk) {
49658
+ const page = pageOf(doc, m.path);
49659
+ if (!page || m.imported.sections.length !== page.sections.length) continue;
49660
+ const mp = phone.find((x) => x.path === m.path);
49661
+ for (const s2 of m.comparison.sections) {
49662
+ if (s2.importedIndex === void 0) continue;
49663
+ const h = s2.checks.find((c) => c.what === "Height");
49664
+ const o = m.original.sections[s2.index];
49665
+ const i = m.imported.sections[s2.importedIndex];
49666
+ const section = page.sections[s2.importedIndex];
49667
+ if (!h || h.ok || !o?.backgroundImage || !i || !section) continue;
49668
+ if (section.style.background?.type !== "image" && section.style.background?.type !== "video")
49669
+ continue;
49670
+ const d = fit(o, i);
49671
+ if (!d) continue;
49672
+ const ps = mp?.comparison.sections.find((x) => x.importedIndex === s2.importedIndex);
49673
+ const po = ps ? mp?.original.sections[ps.index] : void 0;
49674
+ const pi = mp?.imported.sections[s2.importedIndex];
49675
+ const p = po?.backgroundImage && pi ? fit(po, pi) : void 0;
49676
+ section.style.padding = { d: d.token, ...p ? { m: p.token } : {} };
49677
+ section.style.minHeight = {
49678
+ d: `${clamp2(d.height, 10, 9999)}px`,
49679
+ ...p ? { m: `${clamp2(p.height, 10, 9999)}px` } : {}
49680
+ };
49681
+ changes.push(`${m.path}: section ${s2.index + 1} ${d.height}px tall, as on the original`);
49682
+ }
49683
+ }
49684
+ }
49685
+ if (on("headings")) {
49686
+ const presetSizes = (list5) => {
49687
+ const out = /* @__PURE__ */ new Map();
49688
+ for (const k of ["display", "h1", "h2", "h3", "h4", "lead", "eyebrow"]) {
49689
+ const xs = list5.map((m) => m.imported.presets[k]).filter((t) => !!t);
49690
+ const size2 = median(xs.map((t) => t.size));
49691
+ if (size2 && xs[0]) out.set(k, { size: size2, family: xs[0].family });
49692
+ }
49693
+ return out;
49694
+ };
49695
+ const deskSizes = presetSizes(desk);
49696
+ const phoneSizes = presetSizes(phone);
49697
+ const fits = (a, b) => Math.abs(a - b) <= Math.max(1, a * 0.1);
49698
+ for (const m of desk) {
49699
+ const page = pageOf(doc, m.path);
49700
+ if (!page || m.imported.sections.length !== page.sections.length) continue;
49701
+ const mp = phone.find((x) => x.path === m.path);
49702
+ for (const s2 of m.comparison.sections) {
49703
+ if (s2.importedIndex === void 0) continue;
49704
+ const hc = s2.checks.find((c) => c.what === "Heading");
49705
+ if (!hc || hc.ok) continue;
49706
+ const o = m.original.sections[s2.index]?.heading;
49707
+ const i = m.imported.sections[s2.importedIndex]?.heading;
49708
+ const section = page.sections[s2.importedIndex];
49709
+ if (!o || !i || !section || fits(o.size, i.size)) continue;
49710
+ const po = mp?.original.sections.find((x) => norm6(x.heading?.text) === norm6(o.text))?.heading?.size;
49711
+ const label = o.size < 18 && (o.case === "uppercase" || o.letterSpacing >= 0.05);
49712
+ const pick = [...deskSizes].filter(([k, v]) => {
49713
+ if (!fits(o.size, v.size)) return false;
49714
+ if (k === "eyebrow" !== label) return false;
49715
+ const ph = phoneSizes.get(k);
49716
+ return po === void 0 || !ph || fits(po, ph.size);
49717
+ }).sort(([, a], [, b]) => Math.abs(a.size - o.size) - Math.abs(b.size - o.size))[0]?.[0];
49718
+ if (!pick) continue;
49719
+ const block4 = section.blocks.find(
49720
+ (b) => b.type === "heading" && norm6(String(b.fields.text ?? "")) === norm6(i.text)
49721
+ );
49722
+ const holder = block4 ? void 0 : section.blocks.find(
49723
+ (b) => b.type === "card" && norm6(String(b.fields.heading ?? "")) === norm6(i.text) || b.type === "columns" && Array.isArray(b.fields.items) && b.fields.items.some(
49724
+ (x) => norm6(x.heading) === norm6(i.text)
49725
+ )
49726
+ );
49727
+ if (holder && pick !== "lead") {
49728
+ const style = holder.style ?? {};
49729
+ if (style.typography?.heading === pick) continue;
49730
+ holder.style = {
49731
+ ...style,
49732
+ typography: { ...style.typography ?? {}, heading: pick }
49733
+ };
49734
+ changes.push(
49735
+ `${m.path}: "${o.text.slice(0, 40)}" set at the ${pick} size, as on the original`
49736
+ );
49737
+ continue;
49738
+ }
49739
+ if (!block4) {
49740
+ let done = false;
49741
+ for (const b of section.blocks) {
49742
+ if (b.type !== "text" || done) continue;
49743
+ const doc2 = b.fields.content;
49744
+ for (const node2 of doc2?.content ?? []) {
49745
+ const words3 = (node2.content ?? []).map((x) => x.text ?? "").join("");
49746
+ if (node2.type !== "heading" || norm6(words3) !== norm6(i.text)) continue;
49747
+ if (pick === "lead" || node2.attrs?.preset === pick) break;
49748
+ node2.attrs = { ...node2.attrs ?? {}, preset: pick };
49749
+ done = true;
49750
+ break;
49751
+ }
49752
+ }
49753
+ if (done)
49754
+ changes.push(
49755
+ `${m.path}: "${o.text.slice(0, 40)}" set at the ${pick} size, as on the original`
49756
+ );
49757
+ continue;
49758
+ }
49759
+ if (["display", "h1", "h2", "h3", "h4"].includes(pick)) {
49760
+ if (block4.fields.size === pick) continue;
49761
+ block4.fields.size = pick;
49762
+ } else {
49763
+ const style = block4.style ?? {};
49764
+ if (style.typography?.heading === pick) continue;
49765
+ block4.style = {
49766
+ ...style,
49767
+ typography: { ...style.typography ?? {}, heading: pick }
49768
+ };
49769
+ }
49770
+ changes.push(
49771
+ `${m.path}: "${o.text.slice(0, 40)}" set at the ${pick} size, as on the original`
49772
+ );
49773
+ }
49774
+ }
49775
+ }
49066
49776
  for (const m of on("layout") || on("backgrounds") || on("crops") ? desk : []) {
49067
49777
  const page = pageOf(doc, m.path);
49068
49778
  if (!page) continue;
@@ -49156,18 +49866,32 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
49156
49866
  if (u.protocol === "mailto:") return { type: "email", email: u.pathname };
49157
49867
  return u.protocol === "https:" ? { type: "url", url: u.toString() } : void 0;
49158
49868
  };
49159
- for (const m of desk) {
49869
+ for (const m of desk.filter((x) => !opts.paths || opts.paths.includes(x.path))) {
49160
49870
  const page = pageOf(doc, m.path);
49161
49871
  if (!page || m.imported.sections.length !== page.sections.length) continue;
49162
49872
  const used = /* @__PURE__ */ new Set();
49163
49873
  for (const s2 of m.comparison.sections) {
49164
- if (s2.importedIndex === void 0 || s2.score >= COMPONENT_THRESHOLD) continue;
49874
+ if (s2.importedIndex === void 0 || s2.score >= (opts.threshold ?? COMPONENT_THRESHOLD))
49875
+ continue;
49165
49876
  if (used.has(s2.importedIndex)) continue;
49166
49877
  const o = m.original.sections[s2.index];
49167
49878
  const old = page.sections[s2.importedIndex];
49168
49879
  if (!o || !old || !canGenerate(o)) continue;
49169
49880
  if (m.comparison.sections.filter((x) => x.importedIndex === s2.importedIndex).length > 1)
49170
49881
  continue;
49882
+ const bag = (t) => new Set(
49883
+ t.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 3)
49884
+ );
49885
+ const mine = bag(`${o.heading?.text ?? ""} ${o.words ?? o.text}`);
49886
+ const shared = m.imported.sections.some((x, j) => {
49887
+ if (j === s2.importedIndex) return false;
49888
+ const theirs = bag(x.words ?? x.text);
49889
+ if (theirs.size < 4) return false;
49890
+ let n = 0;
49891
+ for (const w of theirs) if (mine.has(w)) n++;
49892
+ return n / theirs.size >= 0.5;
49893
+ });
49894
+ if (shared) continue;
49171
49895
  const name = `imported-${(page.slug || "home").replace(/[^a-z0-9]+/g, "-")}-${s2.index + 1}`.replace(/-+/g, "-").slice(0, 40).replace(/-$/, "");
49172
49896
  const theme = themes?.[old.style.colorTheme ?? "light"];
49173
49897
  const gen = componentFromBlueprint({
@@ -49181,10 +49905,14 @@ function tuneRound(input, measured, groups = TUNE_GROUPS) {
49181
49905
  heading: theme.heading,
49182
49906
  "text-muted": theme["text-muted"],
49183
49907
  accent: theme.accent,
49184
- link: theme.link
49908
+ link: theme.link,
49909
+ surface: theme.surface,
49910
+ border: theme.border,
49911
+ background: theme.background
49185
49912
  }
49186
49913
  } : {},
49187
- link: linkFor2(m.original.url)
49914
+ link: linkFor2(m.original.url),
49915
+ origin: new URL(m.original.url).origin
49188
49916
  });
49189
49917
  if (!gen) continue;
49190
49918
  const check = checkComponent(gen.def);
@@ -49500,7 +50228,7 @@ async function renderAgentsMd(root, siteTitle) {
49500
50228
  } catch {
49501
50229
  }
49502
50230
  }
49503
- const { generateAgentsMd: generateAgentsMd2 } = await import("./agents-md-QBMFXTIQ.js");
50231
+ const { generateAgentsMd: generateAgentsMd2 } = await import("./agents-md-63S3T7CX.js");
49504
50232
  return generateAgentsMd2(title ? { siteTitle: title } : {});
49505
50233
  }
49506
50234
  async function writeAgentsMd(root, siteTitle) {
@@ -58573,6 +59301,7 @@ async function runMcp(io, dir, opts = {}) {
58573
59301
 
58574
59302
  // src/fidelity.ts
58575
59303
  var DESKTOP = 1440;
59304
+ var COMPONENT_RETRY_BELOW = 75;
58576
59305
  var PHONE = 390;
58577
59306
  async function launch() {
58578
59307
  const pw = await loadPlaywright();
@@ -58840,14 +59569,33 @@ async function runCompare(original, dir, opts = {}, log = () => void 0) {
58840
59569
  let ran = 0;
58841
59570
  let withoutComponents;
58842
59571
  let withComponents2;
58843
- let budget = 2 + rounds * 3;
59572
+ let budget = 2 + rounds * 5;
59573
+ let componentBudget = 2 + rounds * 2;
58844
59574
  for (let pass = 0; pass < rounds && budget > 0; pass++) {
58845
59575
  let improved = false;
59576
+ const attempts = [];
58846
59577
  for (const group of TUNE_GROUPS) {
58847
- if (budget <= 0) break;
58848
- const t = tuneRound(bestDoc, best.measured, [group]);
59578
+ if (group !== "components") {
59579
+ attempts.push({ group });
59580
+ continue;
59581
+ }
59582
+ const paths = [
59583
+ ...new Set(
59584
+ best.measured.filter(
59585
+ (m) => m.viewport >= 700 && m.comparison.sections.some((x) => x.score < COMPONENT_RETRY_BELOW)
59586
+ ).map((m) => m.path)
59587
+ )
59588
+ ];
59589
+ for (const path2 of paths) attempts.push({ group, paths: [path2] });
59590
+ }
59591
+ for (const { group, paths } of attempts) {
59592
+ if (group === "components" ? componentBudget <= 0 : budget <= 0) continue;
59593
+ const t = tuneRound(bestDoc, best.measured, [group], {
59594
+ ...paths ? { paths, threshold: COMPONENT_RETRY_BELOW } : {}
59595
+ });
58849
59596
  if (!t.changes.length) continue;
58850
- budget--;
59597
+ if (group === "components") componentBudget--;
59598
+ else budget--;
58851
59599
  log(
58852
59600
  `Tuning ${group}: ${t.changes.slice(0, 4).join("; ")}${t.changes.length > 4 ? "\u2026" : ""}`
58853
59601
  );
@@ -59640,4 +60388,4 @@ export {
59640
60388
  createProgram,
59641
60389
  run
59642
60390
  };
59643
- //# sourceMappingURL=chunk-B5C7S4S5.js.map
60391
+ //# sourceMappingURL=chunk-TDLUJHWB.js.map