@reddoorla/maintenance 0.72.0 → 0.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/airtable-6L3JD7FY.js +9 -0
  2. package/dist/{announce-HJCGEVJA.js → announce-V6EYPPUR.js} +4 -4
  3. package/dist/{blux-FKZYMKLF.js → blux-C24OVDDW.js} +1044 -69
  4. package/dist/blux-C24OVDDW.js.map +1 -0
  5. package/dist/{bump-deps-GZEJAUKI.js → bump-deps-OQR3IXLN.js} +2 -2
  6. package/dist/chunk-2OP4JKIQ.js +19 -0
  7. package/dist/chunk-2OP4JKIQ.js.map +1 -0
  8. package/dist/{chunk-BJCA7O4A.js → chunk-3PU2QWOJ.js} +2 -2
  9. package/dist/{chunk-GI4M2AMO.js → chunk-4BHNP6Y2.js} +2 -2
  10. package/dist/{chunk-KRHPS3R4.js → chunk-6OYICQSJ.js} +47 -3
  11. package/dist/chunk-6OYICQSJ.js.map +1 -0
  12. package/dist/{chunk-BWKK2ZXS.js → chunk-GXVX4HUH.js} +32 -2
  13. package/dist/chunk-GXVX4HUH.js.map +1 -0
  14. package/dist/{chunk-KLB2OLPW.js → chunk-HBFMTM2E.js} +3 -3
  15. package/dist/{chunk-SQJ53Z7R.js → chunk-I55YWV6T.js} +3 -3
  16. package/dist/{chunk-7P7YETHG.js → chunk-IDBMYDVH.js} +2 -2
  17. package/dist/{chunk-JGLAWXUC.js → chunk-JR67FZVC.js} +7 -3
  18. package/dist/chunk-JR67FZVC.js.map +1 -0
  19. package/dist/{chunk-APTAITXZ.js → chunk-KKL3RYFQ.js} +2 -2
  20. package/dist/{chunk-NRISG4XD.js → chunk-NUIBQPB3.js} +42 -11
  21. package/dist/chunk-NUIBQPB3.js.map +1 -0
  22. package/dist/{chunk-BHE5CUWI.js → chunk-PERP7LSS.js} +269 -18
  23. package/dist/chunk-PERP7LSS.js.map +1 -0
  24. package/dist/chunk-RHPDQW6D.js +29 -0
  25. package/dist/chunk-RHPDQW6D.js.map +1 -0
  26. package/dist/{chunk-HR5SONJC.js → chunk-SK44IFZN.js} +2 -2
  27. package/dist/cli/bin.js +31 -19
  28. package/dist/cli/bin.js.map +1 -1
  29. package/dist/cli/commands/audit.js +6 -6
  30. package/dist/{convert-to-pnpm-SVFDLXZS.js → convert-to-pnpm-OVMASATZ.js} +2 -2
  31. package/dist/{digest-VX7Q4MNF.js → digest-LEMZS5ZK.js} +99 -7
  32. package/dist/digest-LEMZS5ZK.js.map +1 -0
  33. package/dist/{ensure-site-O7G5ZJS7.js → ensure-site-3SOAWTZF.js} +2 -2
  34. package/dist/forms/index.js +1 -1
  35. package/dist/{github-signals-QVW2LB7N.js → github-signals-TF3VSNIN.js} +4 -4
  36. package/dist/{health-endpoint-IYR5VBUT.js → health-endpoint-JZGYCMTV.js} +2 -2
  37. package/dist/index.d.ts +104 -19
  38. package/dist/index.js +122 -30
  39. package/dist/index.js.map +1 -1
  40. package/dist/{init-CU6PVZRG.js → init-7PTP4O2K.js} +4 -4
  41. package/dist/{launch-JWXCTJRG.js → launch-W33FWWOZ.js} +6 -6
  42. package/dist/{lighthouse-airtable-J5TLASJC.js → lighthouse-airtable-ZXNMDEPE.js} +3 -3
  43. package/dist/{onboard-6UQE5QIO.js → onboard-PX36YDZA.js} +2 -2
  44. package/dist/{orchestrate-ZDH24Z6Y.js → orchestrate-NYRYEKQM.js} +3 -3
  45. package/dist/{preflight-BNTMWTPG.js → preflight-JCQFKB6M.js} +4 -4
  46. package/dist/{renovate-dispatch-C3ALLACX.js → renovate-dispatch-6II2JVOX.js} +29 -27
  47. package/dist/renovate-dispatch-6II2JVOX.js.map +1 -0
  48. package/dist/{report-YLFSGNWR.js → report-POHQEV6F.js} +21 -5
  49. package/dist/report-POHQEV6F.js.map +1 -0
  50. package/dist/screenouts-7P5GOZMV.js +57 -0
  51. package/dist/screenouts-7P5GOZMV.js.map +1 -0
  52. package/dist/{self-updating-64FNRQB5.js → self-updating-2YSVNMPP.js} +2 -2
  53. package/dist/{selftest-7DPNGL6R.js → selftest-OO2WDSDM.js} +5 -5
  54. package/dist/{smoke-suite-TWILWDSF.js → smoke-suite-CSP23NZM.js} +2 -2
  55. package/dist/spam-classifier-VG4ICIWS.js +217 -0
  56. package/dist/spam-classifier-VG4ICIWS.js.map +1 -0
  57. package/dist/submissions-BNRGY3JQ.js +122 -0
  58. package/dist/submissions-BNRGY3JQ.js.map +1 -0
  59. package/dist/submissions-MDKWJYMM.js +281 -0
  60. package/dist/submissions-MDKWJYMM.js.map +1 -0
  61. package/dist/{svelte-codemods-RVDTTXFD.js → svelte-codemods-V54KOBWU.js} +2 -2
  62. package/dist/{sync-configs-BUUD6L5Q.js → sync-configs-UBCJRASZ.js} +2 -2
  63. package/dist/{upgrade-HHKWYJRN.js → upgrade-WR6ICPAG.js} +2 -2
  64. package/dist/{websites-EH2ULELZ.js → websites-U6642XSE.js} +10 -2
  65. package/dist/{write-audits-to-airtable-LZDYTNX5.js → write-audits-to-airtable-22T6D733.js} +4 -4
  66. package/package.json +1 -1
  67. package/dist/airtable-U2GTDTK5.js +0 -9
  68. package/dist/blux-FKZYMKLF.js.map +0 -1
  69. package/dist/chunk-BHE5CUWI.js.map +0 -1
  70. package/dist/chunk-BWKK2ZXS.js.map +0 -1
  71. package/dist/chunk-JGLAWXUC.js.map +0 -1
  72. package/dist/chunk-KRHPS3R4.js.map +0 -1
  73. package/dist/chunk-NRISG4XD.js.map +0 -1
  74. package/dist/chunk-QTHKOIFJ.js +0 -13
  75. package/dist/chunk-QTHKOIFJ.js.map +0 -1
  76. package/dist/digest-VX7Q4MNF.js.map +0 -1
  77. package/dist/renovate-dispatch-C3ALLACX.js.map +0 -1
  78. package/dist/report-YLFSGNWR.js.map +0 -1
  79. /package/dist/{airtable-U2GTDTK5.js.map → airtable-6L3JD7FY.js.map} +0 -0
  80. /package/dist/{announce-HJCGEVJA.js.map → announce-V6EYPPUR.js.map} +0 -0
  81. /package/dist/{bump-deps-GZEJAUKI.js.map → bump-deps-OQR3IXLN.js.map} +0 -0
  82. /package/dist/{chunk-BJCA7O4A.js.map → chunk-3PU2QWOJ.js.map} +0 -0
  83. /package/dist/{chunk-GI4M2AMO.js.map → chunk-4BHNP6Y2.js.map} +0 -0
  84. /package/dist/{chunk-KLB2OLPW.js.map → chunk-HBFMTM2E.js.map} +0 -0
  85. /package/dist/{chunk-SQJ53Z7R.js.map → chunk-I55YWV6T.js.map} +0 -0
  86. /package/dist/{chunk-7P7YETHG.js.map → chunk-IDBMYDVH.js.map} +0 -0
  87. /package/dist/{chunk-APTAITXZ.js.map → chunk-KKL3RYFQ.js.map} +0 -0
  88. /package/dist/{chunk-HR5SONJC.js.map → chunk-SK44IFZN.js.map} +0 -0
  89. /package/dist/{convert-to-pnpm-SVFDLXZS.js.map → convert-to-pnpm-OVMASATZ.js.map} +0 -0
  90. /package/dist/{ensure-site-O7G5ZJS7.js.map → ensure-site-3SOAWTZF.js.map} +0 -0
  91. /package/dist/{github-signals-QVW2LB7N.js.map → github-signals-TF3VSNIN.js.map} +0 -0
  92. /package/dist/{health-endpoint-IYR5VBUT.js.map → health-endpoint-JZGYCMTV.js.map} +0 -0
  93. /package/dist/{init-CU6PVZRG.js.map → init-7PTP4O2K.js.map} +0 -0
  94. /package/dist/{launch-JWXCTJRG.js.map → launch-W33FWWOZ.js.map} +0 -0
  95. /package/dist/{lighthouse-airtable-J5TLASJC.js.map → lighthouse-airtable-ZXNMDEPE.js.map} +0 -0
  96. /package/dist/{onboard-6UQE5QIO.js.map → onboard-PX36YDZA.js.map} +0 -0
  97. /package/dist/{orchestrate-ZDH24Z6Y.js.map → orchestrate-NYRYEKQM.js.map} +0 -0
  98. /package/dist/{preflight-BNTMWTPG.js.map → preflight-JCQFKB6M.js.map} +0 -0
  99. /package/dist/{self-updating-64FNRQB5.js.map → self-updating-2YSVNMPP.js.map} +0 -0
  100. /package/dist/{selftest-7DPNGL6R.js.map → selftest-OO2WDSDM.js.map} +0 -0
  101. /package/dist/{smoke-suite-TWILWDSF.js.map → smoke-suite-CSP23NZM.js.map} +0 -0
  102. /package/dist/{svelte-codemods-RVDTTXFD.js.map → svelte-codemods-V54KOBWU.js.map} +0 -0
  103. /package/dist/{sync-configs-BUUD6L5Q.js.map → sync-configs-UBCJRASZ.js.map} +0 -0
  104. /package/dist/{upgrade-HHKWYJRN.js.map → upgrade-WR6ICPAG.js.map} +0 -0
  105. /package/dist/{websites-EH2ULELZ.js.map → websites-U6642XSE.js.map} +0 -0
  106. /package/dist/{write-audits-to-airtable-LZDYTNX5.js.map → write-audits-to-airtable-22T6D733.js.map} +0 -0
@@ -1,4 +1,5 @@
1
1
  import {
2
+ CDN_HOSTS,
2
3
  collectAssetUrls
3
4
  } from "./chunk-SL24DCGC.js";
4
5
 
@@ -188,12 +189,26 @@ function slugify(s) {
188
189
  }
189
190
  function normalizePages(raw) {
190
191
  const diagnostics = [];
191
- const pages = raw.pages.map((p) => {
192
- const uid = slugify(String(p.title ?? ""));
192
+ const seen = /* @__PURE__ */ new Set();
193
+ const pages = raw.pages.map((p, i) => {
194
+ const urlSlug = String(p.url ?? "").trim();
195
+ const path = i === 0 ? "" : urlSlug ? slugify(urlSlug) : slugify(String(p.title ?? ""));
196
+ let uid = i === 0 ? "home" : path;
197
+ if (seen.has(uid)) {
198
+ const base2 = uid;
199
+ for (let n = 2; seen.has(uid); n++) uid = `${base2}-${n}`;
200
+ diagnostics.push({
201
+ kind: "duplicate-page-uid",
202
+ where: base2,
203
+ message: `page ${i} ("${String(p.title ?? "")}") slugs to "${base2}" already used \u2014 renamed to "${uid}"`
204
+ });
205
+ }
206
+ seen.add(uid);
193
207
  return {
194
208
  uid,
195
209
  title: String(p.title ?? ""),
196
210
  description: String(p.description ?? ""),
211
+ path,
197
212
  sections: (p.items ?? []).map((b) => sectionFromBlock(b, uid, diagnostics))
198
213
  };
199
214
  });
@@ -212,6 +227,7 @@ function normalizeTheme(raw) {
212
227
  const m = entry[innerKey] ?? {};
213
228
  const transform = cleanCssValue(m["text-transform"]);
214
229
  const tracking = cleanCssValue(m["letter-spacing"]);
230
+ const margin = cleanCssValue(m["margin"]);
215
231
  const mobileSize = cleanCssValue(m["__media_mobile_font-size"]);
216
232
  const mobileLineHeight = cleanCssValue(m["__media_mobile_line-height"]);
217
233
  textStyles.push({
@@ -224,10 +240,38 @@ function normalizeTheme(raw) {
224
240
  lineHeight: cleanCssValue(m["line-height"]) || "1.5",
225
241
  ...transform && transform !== "none" ? { transform } : {},
226
242
  ...tracking ? { letterSpacing: tracking } : {},
243
+ ...margin && margin !== "0" ? { margin } : {},
227
244
  ...mobileSize ? { mobileSize } : {},
228
245
  ...mobileLineHeight ? { mobileLineHeight } : {}
229
246
  });
230
247
  }
248
+ const cleanCssMap = (v) => {
249
+ const out = {};
250
+ for (const [k, val] of Object.entries(v ?? {})) {
251
+ if (k === "font-ident") continue;
252
+ const s = cleanCssValue(typeof val === "number" ? String(val) : val);
253
+ if (s) out[k] = s;
254
+ }
255
+ return out;
256
+ };
257
+ const buttonStyles = [];
258
+ for (const v of Object.values(raw.styles.buttons ?? {})) {
259
+ const entry = v ?? {};
260
+ const innerKey = Object.keys(entry).find((k) => /^\.buttons\d+$/.test(k));
261
+ if (!innerKey) continue;
262
+ const css = cleanCssMap(entry[innerKey]);
263
+ if (Object.keys(css).length === 0) continue;
264
+ const hover = cleanCssMap(entry[`${innerKey}:hover`]);
265
+ const active = cleanCssMap(entry[`${innerKey}:active`]);
266
+ buttonStyles.push({
267
+ role: innerKey.slice(1),
268
+ // ".buttons2" -> "buttons2"
269
+ label: str(entry._label),
270
+ css,
271
+ ...Object.keys(hover).length ? { hover } : {},
272
+ ...Object.keys(active).length ? { active } : {}
273
+ });
274
+ }
231
275
  const fonts = raw.settings.fonts ?? {};
232
276
  const roleFont = (r) => textStyles.find((t) => t.role === r)?.fontFamily ?? "";
233
277
  return {
@@ -240,7 +284,8 @@ function normalizeTheme(raw) {
240
284
  parseGoogleFonts(str(fonts.google)),
241
285
  typekitFontLoads(str(fonts.string))
242
286
  ),
243
- textStyles
287
+ textStyles,
288
+ buttonStyles
244
289
  };
245
290
  }
246
291
 
@@ -330,8 +375,17 @@ function assembleIR(input) {
330
375
  alt: String(m.name ?? "")
331
376
  };
332
377
  });
378
+ const faviconId = raw.settings.favicon?.media;
379
+ const favicon = faviconId ? { assetId: faviconId, sourceUrl: urlMap.get(faviconId) ?? null } : void 0;
380
+ if (favicon && !favicon.sourceUrl) {
381
+ diagnostics.push({
382
+ kind: "unresolved-asset",
383
+ where: favicon.assetId,
384
+ message: `no CDN url for favicon ${favicon.assetId}`
385
+ });
386
+ }
333
387
  return {
334
- meta: raw.meta,
388
+ meta: { ...raw.meta, ...favicon ? { favicon } : {} },
335
389
  theme,
336
390
  pages,
337
391
  collections,
@@ -644,6 +698,7 @@ function emitThemeCss(theme) {
644
698
  if (t.fontFamily) lines.push(` --text-${t.role}--font-family: ${t.fontFamily};`);
645
699
  if (t.transform) lines.push(` --text-${t.role}--text-transform: ${t.transform};`);
646
700
  if (t.letterSpacing) lines.push(` --text-${t.role}--letter-spacing: ${t.letterSpacing};`);
701
+ if (t.margin) lines.push(` --text-${t.role}--margin: ${t.margin};`);
647
702
  if (t.mobileSize) lines.push(` --text-${t.role}--mobile-font-size: ${t.mobileSize};`);
648
703
  if (t.mobileLineHeight)
649
704
  lines.push(` --text-${t.role}--mobile-line-height: ${t.mobileLineHeight};`);
@@ -667,12 +722,40 @@ function emitRolesCss(theme) {
667
722
  ` line-height: var(--text-${r}--line-height);`,
668
723
  ` letter-spacing: var(--text-${r}--letter-spacing, normal);`,
669
724
  ` text-transform: var(--text-${r}--text-transform, none);`,
670
- ` margin: 0;`,
725
+ ` margin: var(--text-${r}--margin, 0);`,
671
726
  `}`
672
727
  );
673
728
  }
674
729
  return lines.join("\n") + "\n";
675
730
  }
731
+ function emitButtonsCss(theme) {
732
+ if (!theme.buttonStyles.length) return "";
733
+ const lines = [
734
+ "/* Button skins \u2014 one .buttonsN per Blux button style; raw anchors carry",
735
+ " `ib middle buttonsN` verbatim. Generated by blux emit. */",
736
+ ".ib {",
737
+ " display: inline-block;",
738
+ "}",
739
+ "/* Text links (`ib middle links`) are underlined by the Blux platform CSS;",
740
+ " an inline-block box does not inherit an ancestor's text-decoration, so",
741
+ " the affordance must be declared on the anchor itself. */",
742
+ ".links {",
743
+ " text-decoration: underline;",
744
+ "}"
745
+ ];
746
+ const rule = (selector, css) => {
747
+ lines.push(`${selector} {`);
748
+ for (const [k, v] of Object.entries(css)) lines.push(` ${k}: ${v};`);
749
+ lines.push("}");
750
+ };
751
+ for (const b of theme.buttonStyles) {
752
+ if (b.label) lines.push(`/* ${b.role} \u2014 ${b.label} */`);
753
+ rule(`.${b.role}`, b.css);
754
+ if (b.hover) rule(`.${b.role}:hover`, b.hover);
755
+ if (b.active) rule(`.${b.role}:active`, b.active);
756
+ }
757
+ return lines.join("\n") + "\n";
758
+ }
676
759
 
677
760
  // src/blux/emit/review.ts
678
761
  function buildReviewManifest(ir, opts) {
@@ -769,6 +852,28 @@ function cssProp(style, prop) {
769
852
  const m = new RegExp(`(?:^|;|\\s)${prop}\\s*:\\s*([^;]+)`, "i").exec(style);
770
853
  return m?.[1]?.trim();
771
854
  }
855
+ var MARGIN_UTIL_RE = /\bmargin-(\d+)(r|l|t|b)\b/g;
856
+ var MARGIN_SIDES = { r: "right", l: "left", t: "top", b: "bottom" };
857
+ function utilityStylesFromClass(className) {
858
+ const out = {};
859
+ for (const m of className.matchAll(MARGIN_UTIL_RE)) {
860
+ const [, n, side] = m;
861
+ if (n && side) out[`margin-${MARGIN_SIDES[side]}`] = `${n}%`;
862
+ }
863
+ return out;
864
+ }
865
+ function textLeafStyle(el) {
866
+ const out = utilityStylesFromClass(el.classNames ?? "");
867
+ for (const decl of (el.getAttribute("style") ?? "").split(";")) {
868
+ const colon = decl.indexOf(":");
869
+ if (colon < 0) continue;
870
+ const prop = decl.slice(0, colon).trim().toLowerCase();
871
+ const value = decl.slice(colon + 1).trim();
872
+ if (!value) continue;
873
+ if (prop === "color" || prop === "padding" || prop.startsWith("margin")) out[prop] = value;
874
+ }
875
+ return Object.keys(out).length ? out : null;
876
+ }
772
877
  var CENTERED = /* @__PURE__ */ new Set(["center", "center center", "50% 50%", "50%"]);
773
878
  function readBgSizing(el) {
774
879
  const style = el.getAttribute("style") ?? "";
@@ -860,17 +965,93 @@ function mediaFromElement(el) {
860
965
  var DEFAULT_TOKEN = { cols: 1, raw: "grid-1" };
861
966
  var isElement = (n) => n.tagName !== void 0 && n.tagName !== null;
862
967
  var hasClass = (el, c) => el.classNames.split(/\s+/).includes(c);
968
+ var isFeedTemplate = (el) => {
969
+ const id = el.getAttribute("id") ?? "";
970
+ const style = el.getAttribute("style") ?? "";
971
+ const hidden = /display\s*:\s*none/i.test(style);
972
+ return hidden && (/-template$/.test(id) || /\{\{[a-z_]+\}\}/i.test(el.innerHTML));
973
+ };
863
974
  var isMediaHolder = (el) => hasClass(el, "block-media-holder") || hasClass(el, "camediaload") && !!el.getAttribute("data-media") || el.tagName === "VIDEO";
864
975
  var isLeafElement = (el) => hasClass(el, "block-title") || hasClass(el, "block-body") || hasClass(el, "block-subtitle") || isMediaHolder(el);
865
976
  var isLeafAnchor = (el) => el.tagName === "A" && collectStructuralChildren(el).length === 0;
866
977
  var isStructural = (el) => isLeafElement(el) || el.hasAttribute("data-exec") || // Blux custom-code embed (e.g. map mount)
867
978
  hasClass(el, "cagrid") || isLeafAnchor(el) || parseGridToken(el.classNames) !== null;
868
- function collectStructuralChildren(el) {
979
+ var isCellBoundary = (el) => hasClass(el, "block-grid-container") || hasClass(el, "block-subcontent");
980
+ function inlineBg(el) {
981
+ const c = cssProp(el.getAttribute("style") ?? "", "background-color")?.trim();
982
+ if (!c) return void 0;
983
+ const low = c.toLowerCase().replace(/\s+/g, "");
984
+ if (low === "transparent" || low === "rgba(0,0,0,0)") return void 0;
985
+ return c;
986
+ }
987
+ function inlinePadding(el) {
988
+ const c = cssProp(el.getAttribute("style") ?? "", "padding")?.trim();
989
+ if (!c) return void 0;
990
+ if (/^(0(px|%|em|rem)?\s*)+$/i.test(c)) return void 0;
991
+ return c;
992
+ }
993
+ function inlineMinHeight(el) {
994
+ const c = cssProp(el.getAttribute("style") ?? "", "min-height")?.trim();
995
+ if (!c) return void 0;
996
+ if (/^(0(px|%|em|rem|vh|vw)?|auto)$/i.test(c)) return void 0;
997
+ return c;
998
+ }
999
+ function layerBackground(el) {
1000
+ for (const child of el.childNodes) {
1001
+ if (!isElement(child) || !hasClass(child, "block-background-layer")) continue;
1002
+ const s = child.getAttribute("style") ?? "";
1003
+ const bg = (cssProp(s, "background") ?? cssProp(s, "background-color"))?.trim();
1004
+ if (bg && !/^(none|transparent)$/i.test(bg)) return bg;
1005
+ }
1006
+ return void 0;
1007
+ }
1008
+ function collectStructuralChildren(el, inherited = {}) {
869
1009
  const out = [];
1010
+ const layerBg = inherited.nested === true ? layerBackground(el) : void 0;
1011
+ const base2 = layerBg !== void 0 ? { ...inherited, layerBackground: layerBg } : inherited;
870
1012
  for (const child of el.childNodes) {
871
1013
  if (!isElement(child)) continue;
872
- if (isStructural(child)) out.push(child);
873
- else out.push(...collectStructuralChildren(child));
1014
+ if (hasClass(child, "block-background-layer")) continue;
1015
+ if (isFeedTemplate(child)) continue;
1016
+ const nested = base2.nested === true || isCellBoundary(child);
1017
+ const background = inlineBg(child) ?? base2.background;
1018
+ const padding = (nested ? inlinePadding(child) : void 0) ?? base2.padding;
1019
+ const minHeight = (nested ? inlineMinHeight(child) : void 0) ?? base2.minHeight;
1020
+ const valign = nested && hasClass(child, "valignmiddle") || base2.valign === true;
1021
+ const card = {
1022
+ ...background !== void 0 ? { background } : {},
1023
+ ...padding !== void 0 ? { padding } : {},
1024
+ ...minHeight !== void 0 ? { minHeight } : {},
1025
+ ...base2.layerBackground !== void 0 ? { layerBackground: base2.layerBackground } : {},
1026
+ ...valign ? { valign } : {},
1027
+ ...base2.fill === true ? { fill: true } : {},
1028
+ ...nested ? { nested } : {}
1029
+ };
1030
+ const group = !isStructural(child) && (hasClass(child, "block-subcontent") && parseGridToken(child.classNames) === null || nested && (inlinePadding(child) !== void 0 || inlineMinHeight(child) !== void 0 || layerBackground(child) !== void 0)) && collectStructuralChildren(child).length >= 2;
1031
+ if (isStructural(child)) {
1032
+ const inheritedCard = {
1033
+ ...base2.background !== void 0 ? { background: base2.background } : {},
1034
+ ...base2.padding !== void 0 ? { padding: base2.padding } : {},
1035
+ ...base2.minHeight !== void 0 ? { minHeight: base2.minHeight } : {},
1036
+ ...base2.layerBackground !== void 0 ? { layerBackground: base2.layerBackground } : {},
1037
+ ...base2.valign === true ? { valign: true } : {},
1038
+ ...base2.fill === true ? { fill: true } : {},
1039
+ ...nested ? { nested } : {}
1040
+ };
1041
+ out.push({
1042
+ el: child,
1043
+ ...Object.keys(inheritedCard).length > 0 ? { card: inheritedCard } : {}
1044
+ });
1045
+ } else if (group) {
1046
+ const ownLayer = !isCellBoundary(child) ? layerBackground(child) : void 0;
1047
+ const groupCard = ownLayer !== void 0 ? { ...card, layerBackground: ownLayer } : card;
1048
+ out.push({
1049
+ el: child,
1050
+ ...Object.keys(groupCard).length > 0 ? { card: groupCard } : {}
1051
+ });
1052
+ } else {
1053
+ out.push(...collectStructuralChildren(child, card));
1054
+ }
874
1055
  }
875
1056
  return out;
876
1057
  }
@@ -888,23 +1069,37 @@ function isDisabledWithin(caption, holder) {
888
1069
  }
889
1070
  return false;
890
1071
  }
891
- function parseNode(el) {
1072
+ function parseNode(el, nested = false) {
892
1073
  if (hasClass(el, "block-title") && /^H[1-6]$/.test(el.tagName ?? "")) {
893
1074
  const role = textRoleFromClass(el.classNames);
1075
+ const style = textLeafStyle(el);
894
1076
  return {
895
1077
  kind: "heading",
896
1078
  ...role ? { role } : {},
1079
+ ...style ? { style } : {},
897
1080
  level: headingLevel(el),
898
1081
  html: el.innerHTML
899
1082
  };
900
1083
  }
901
1084
  if (hasClass(el, "block-body")) {
902
1085
  const role = textRoleFromClass(el.classNames);
903
- return { kind: "body", ...role ? { role } : {}, html: el.innerHTML };
1086
+ const style = textLeafStyle(el);
1087
+ return {
1088
+ kind: "body",
1089
+ ...role ? { role } : {},
1090
+ ...style ? { style } : {},
1091
+ html: el.innerHTML
1092
+ };
904
1093
  }
905
1094
  if (hasClass(el, "block-subtitle")) {
906
1095
  const role = textRoleFromClass(el.classNames);
907
- return { kind: "subtitle", ...role ? { role } : {}, text: blockPlainText(el.innerHTML) };
1096
+ const style = textLeafStyle(el);
1097
+ return {
1098
+ kind: "subtitle",
1099
+ ...role ? { role } : {},
1100
+ ...style ? { style } : {},
1101
+ text: blockPlainText(el.innerHTML)
1102
+ };
908
1103
  }
909
1104
  if (isMediaHolder(el)) {
910
1105
  const media = mediaFromElement(el);
@@ -922,14 +1117,52 @@ function parseNode(el) {
922
1117
  if (el.tagName === "A") {
923
1118
  return { kind: "raw", html: el.outerHTML };
924
1119
  }
925
- return parseContainer(el);
926
- }
927
- function parseContainer(el) {
928
- const kids = collectStructuralChildren(el);
929
- const parsed = kids.map((k) => ({ token: parseGridToken(k.classNames), node: parseNode(k) })).filter((p) => !(p.node.kind === "raw" && p.node.html.trim() === ""));
1120
+ return parseContainer(el, nested);
1121
+ }
1122
+ var nodePainted = (style) => "background" in style || "background-color" in style || "min-height" in style;
1123
+ function withCardStyle(node, card) {
1124
+ if (!card || card.background === void 0 && card.padding === void 0 && card.minHeight === void 0 && card.layerBackground === void 0 && !card.valign && !card.fill)
1125
+ return node;
1126
+ if (node.kind === "row" || node.kind === "stack") {
1127
+ const style2 = { ...node.style ?? {} };
1128
+ if (card.background !== void 0) style2["background-color"] = card.background;
1129
+ if (card.layerBackground !== void 0) style2["background"] = card.layerBackground;
1130
+ if (card.padding !== void 0) style2.padding = card.padding;
1131
+ if (card.minHeight !== void 0) style2["min-height"] = card.minHeight;
1132
+ if (card.valign) style2["_valign"] = "middle";
1133
+ if (card.fill && nodePainted(style2)) style2["_fill"] = "column";
1134
+ if (Object.keys(style2).length === 0) return node;
1135
+ return { ...node, style: style2 };
1136
+ }
1137
+ if (node.kind === "media" && card.padding === void 0 && !card.valign) {
1138
+ if (card.minHeight !== void 0 && node.media.minHeight === void 0)
1139
+ return { ...node, media: { ...node.media, minHeight: card.minHeight } };
1140
+ return node;
1141
+ }
1142
+ if (card.padding === void 0 && card.minHeight === void 0 && card.layerBackground === void 0 && !card.valign)
1143
+ return node;
1144
+ const style = {};
1145
+ if (card.padding !== void 0) style.padding = card.padding;
1146
+ if (card.background !== void 0) style["background-color"] = card.background;
1147
+ if (card.layerBackground !== void 0) style["background"] = card.layerBackground;
1148
+ if (card.minHeight !== void 0) style["min-height"] = card.minHeight;
1149
+ if (card.valign) style["_valign"] = "middle";
1150
+ if (card.fill && nodePainted(style)) style["_fill"] = "column";
1151
+ return { kind: "stack", children: [node], style };
1152
+ }
1153
+ function parseContainer(el, nested = false) {
1154
+ const kids = collectStructuralChildren(
1155
+ el,
1156
+ isCellBoundary(el) || hasClass(el, "cagrid") ? { nested: true, ...hasClass(el, "cagridFlexHeight") ? { fill: true } : {} } : nested ? { nested: true } : {}
1157
+ );
1158
+ const parsed = kids.map((k) => ({
1159
+ token: parseGridToken(k.el.classNames),
1160
+ node: withCardStyle(parseNode(k.el, k.card?.nested === true), k.card)
1161
+ })).filter((p) => !(p.node.kind === "raw" && p.node.html.trim() === ""));
930
1162
  const isGrid = hasClass(el, "cagrid");
931
1163
  const tokenCount = parsed.filter((p) => p.token).length;
932
- if ((isGrid || tokenCount >= 2) && parsed.length > 0) {
1164
+ const hasWidthToken = parsed.some((p) => p.token && p.token.cols !== 1 && p.token.cols !== "any");
1165
+ if ((isGrid || tokenCount >= 2 || hasWidthToken) && parsed.length > 0) {
933
1166
  const cells = parsed.map((p) => ({ token: p.token ?? DEFAULT_TOKEN, node: p.node }));
934
1167
  if (hasClass(el, "caslider")) {
935
1168
  const cols = Number(el.getAttribute("data-columns"));
@@ -940,10 +1173,15 @@ function parseContainer(el) {
940
1173
  }
941
1174
  const [only] = parsed;
942
1175
  if (parsed.length === 1 && only) return only.node;
943
- if (parsed.length === 0) return { kind: "raw", html: el.innerHTML };
1176
+ if (parsed.length === 0) return { kind: "raw", html: rawInnerHtml(el) };
944
1177
  return { kind: "stack", children: parsed.map((p) => p.node) };
945
1178
  }
1179
+ function rawInnerHtml(el) {
1180
+ const html = el.innerHTML;
1181
+ return /\{\{[a-z_]+\}\}/i.test(html) ? "" : html;
1182
+ }
946
1183
  var BAND_ID_RE = /^page-block-(\d+)$/;
1184
+ var BLOCK_CLASS_RE = /\bblocks\d+\b/;
947
1185
  function bandBackground(el) {
948
1186
  if (!hasClass(el, "camediaload")) return void 0;
949
1187
  const rawId = el.getAttribute("data-media");
@@ -972,8 +1210,10 @@ function parseGridBands(html) {
972
1210
  const idStr = m[1];
973
1211
  if (idStr === void 0) continue;
974
1212
  const background = bandBackground(child);
1213
+ const blockClass = BLOCK_CLASS_RE.exec(child.classNames)?.[0];
975
1214
  bands.push({
976
1215
  index: Number(idStr),
1216
+ ...blockClass ? { blockClass } : {},
977
1217
  ...background ? { background } : {},
978
1218
  root: parseContainer(child)
979
1219
  });
@@ -1060,7 +1300,11 @@ function isEmptyRaw(node) {
1060
1300
  return text.length === 0;
1061
1301
  }
1062
1302
  function base(band) {
1063
- return { index: band.index, ...band.background ? { background: band.background } : {} };
1303
+ return {
1304
+ index: band.index,
1305
+ ...band.blockClass ? { blockClass: band.blockClass } : {},
1306
+ ...band.background ? { background: band.background } : {}
1307
+ };
1064
1308
  }
1065
1309
  function nodeText(node) {
1066
1310
  switch (node.kind) {
@@ -1077,8 +1321,18 @@ function nodeText(node) {
1077
1321
  return "";
1078
1322
  }
1079
1323
  }
1324
+ function unboxed(node) {
1325
+ let n = node;
1326
+ while (n.kind === "stack" && n.children.length === 1) {
1327
+ const only = n.children[0];
1328
+ if (!only) break;
1329
+ n = only;
1330
+ }
1331
+ return n;
1332
+ }
1080
1333
  function pureCellMedia(cell) {
1081
- return cell.node.kind === "media" ? cell.node.media : null;
1334
+ const n = unboxed(cell.node);
1335
+ return n.kind === "media" ? n.media : null;
1082
1336
  }
1083
1337
  function textRoleMeta(heading, subtitle) {
1084
1338
  return {
@@ -1093,6 +1347,12 @@ function cellRatio(cell) {
1093
1347
  if (t.cols === "any") return 50;
1094
1348
  return Math.round(100 / t.cols);
1095
1349
  }
1350
+ var isTextNode = (n) => n.kind === "heading" || n.kind === "body" || n.kind === "subtitle";
1351
+ var isBlankText = (n) => {
1352
+ if (n.kind === "heading" || n.kind === "body") return blockPlainText(n.html) === "";
1353
+ if (n.kind === "subtitle") return n.text.trim() === "";
1354
+ return false;
1355
+ };
1096
1356
  function carouselSlides(cells) {
1097
1357
  const out = [];
1098
1358
  for (const c of cells) {
@@ -1101,12 +1361,18 @@ function carouselSlides(cells) {
1101
1361
  out.push({ media: n.media });
1102
1362
  continue;
1103
1363
  }
1104
- if (n.kind === "stack" && n.children.length === 2) {
1105
- const [m, h] = n.children;
1106
- if (m?.kind === "media" && h?.kind === "heading") {
1364
+ if (n.kind === "stack" && n.children.length >= 2) {
1365
+ const [m, ...rest] = n.children;
1366
+ const h = rest.find((r) => r.kind === "heading");
1367
+ if (m?.kind === "media" && h?.kind === "heading" && rest.every(isTextNode)) {
1368
+ const sub = rest.find(
1369
+ (r) => r !== h && (r.kind === "body" || r.kind === "subtitle") && !isBlankText(r)
1370
+ );
1371
+ const subcaption = sub?.kind === "body" ? { html: sub.html, ...sub.role ? { role: sub.role } : {} } : sub?.kind === "subtitle" ? { html: sub.text, ...sub.role ? { role: sub.role } : {} } : void 0;
1107
1372
  out.push({
1108
1373
  media: m.media,
1109
- caption: { html: h.html, level: h.level, ...h.role ? { role: h.role } : {} }
1374
+ caption: { html: h.html, level: h.level, ...h.role ? { role: h.role } : {} },
1375
+ ...subcaption ? { subcaption } : {}
1110
1376
  });
1111
1377
  continue;
1112
1378
  }
@@ -1134,12 +1400,18 @@ function rewriteMapMounts(node, isMapMount) {
1134
1400
  token: c.token,
1135
1401
  node: rewriteMapMounts(c.node, isMapMount)
1136
1402
  })),
1137
- // Keep the slider marker dropping it here would silently demote a
1138
- // Carousel to Grid whenever a map config is present.
1139
- ...node.slider ? { slider: node.slider } : {}
1403
+ // Preserve the row's own markers when rebuilding: dropping the slider
1404
+ // would silently demote a Carousel to Grid, and dropping the style would
1405
+ // lose a card background for every band whenever a map config exists.
1406
+ ...node.slider ? { slider: node.slider } : {},
1407
+ ...node.style ? { style: node.style } : {}
1140
1408
  };
1141
1409
  case "stack":
1142
- return { kind: "stack", children: node.children.map((n) => rewriteMapMounts(n, isMapMount)) };
1410
+ return {
1411
+ kind: "stack",
1412
+ children: node.children.map((n) => rewriteMapMounts(n, isMapMount)),
1413
+ ...node.style ? { style: node.style } : {}
1414
+ };
1143
1415
  case "heading":
1144
1416
  case "body":
1145
1417
  case "subtitle":
@@ -1375,10 +1647,272 @@ function extractToggles(html) {
1375
1647
  return groups.length === labels.length && groups.every((g) => g !== void 0) ? groups : [];
1376
1648
  }
1377
1649
 
1650
+ // src/blux/grid/feed-grid.ts
1651
+ var termMatchesTag = (term, tag) => term === tag || term.endsWith("s") && term.slice(0, -1) === tag || tag.endsWith("s") && tag.slice(0, -1) === term;
1652
+ function tagFilter(expr) {
1653
+ const groups = (expr ?? "").split("||").map(
1654
+ (g) => g.split("&&").map((t) => t.trim().toLowerCase()).filter(Boolean)
1655
+ ).filter((g) => g.length > 0);
1656
+ if (!groups.length) return () => true;
1657
+ return (tags) => {
1658
+ const set = tags.map((t) => t.toLowerCase());
1659
+ return groups.some((g) => g.every((term) => set.some((tag) => termMatchesTag(term, tag))));
1660
+ };
1661
+ }
1662
+ function sortRecords(records, sort) {
1663
+ if (sort === "title") {
1664
+ return [...records].sort(
1665
+ (a, b) => String(a["title"] ?? "").localeCompare(String(b["title"] ?? ""))
1666
+ );
1667
+ }
1668
+ if (sort === "fdate" || sort === "date") {
1669
+ return [...records].sort(
1670
+ (a, b) => String(b["date"] ?? "").localeCompare(String(a["date"] ?? ""))
1671
+ );
1672
+ }
1673
+ return records;
1674
+ }
1675
+ function cropRatioOf(v) {
1676
+ if (typeof v !== "string") return void 0;
1677
+ const m = /^\s*(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)\s*$/.exec(v);
1678
+ if (!m) return void 0;
1679
+ const w = Number(m[1]);
1680
+ const h = Number(m[2]);
1681
+ return w > 0 && h > 0 ? `${w}:${h}` : void 0;
1682
+ }
1683
+ var isDisabled = (r) => r["disabled"] === true || r["disable"] === true;
1684
+ var styleDisabled = (cfg) => !!cfg && typeof cfg === "object" && cfg["class"] === "disable";
1685
+ function resolveFeedTiles(bandDef, resolvers) {
1686
+ const sources = Array.isArray(bandDef.sources) ? bandDef.sources.map(String) : [];
1687
+ const source = sources[0];
1688
+ if (!source) return null;
1689
+ const cfg = bandDef.sourceConfig ?? {};
1690
+ const filterExpr = cfg["filters"]?.tag;
1691
+ const match = tagFilter(filterExpr);
1692
+ const sort = cfg["sort"];
1693
+ const bodyOff = styleDisabled(cfg["_body"]);
1694
+ const titleOff = styleDisabled(cfg["_title"]);
1695
+ const ratio = cropRatioOf(cfg["mediaRatio"] ?? cfg["ratio"]);
1696
+ const framed = (m) => m && ratio ? { ...m, cropRatio: ratio, fit: "cover" } : m;
1697
+ if (source === "__media") {
1698
+ const matched = [];
1699
+ for (const [uuid, entry] of resolvers.media) {
1700
+ const type = String(entry["type"] ?? "");
1701
+ if (!type.startsWith("image/")) continue;
1702
+ if (!match(entry["tags"] ?? [])) continue;
1703
+ matched.push({ ...entry, __uuid: uuid });
1704
+ }
1705
+ const tiles2 = sortRecords(matched, sort).map((entry) => {
1706
+ const media = framed(
1707
+ resolvers.mediaFor(
1708
+ String(entry["__uuid"]),
1709
+ entry["type"],
1710
+ entry["name"]
1711
+ )
1712
+ );
1713
+ const tile = {};
1714
+ if (media) tile.media = media;
1715
+ if (!titleOff && entry["name"]) tile.title = escapeHtml(String(entry["name"]));
1716
+ if (!bodyOff && entry["description"]) tile.body = plainToHtml(String(entry["description"]));
1717
+ return tile;
1718
+ }).filter((t) => t.media || t.title || t.body);
1719
+ return tiles2.length ? tiles2 : null;
1720
+ }
1721
+ const records = resolvers.feeds.get(source);
1722
+ if (!records) return null;
1723
+ const enabled = records.filter((r) => !isDisabled(r));
1724
+ const filtered = filterExpr ? enabled.filter((r) => match(r["tags"] ?? [])) : enabled;
1725
+ const ordered = sortRecords(filtered, sort);
1726
+ const tiles = ordered.map((r) => {
1727
+ const tile = {};
1728
+ const m = r["media"];
1729
+ if (m?.media) {
1730
+ const media = framed(resolvers.mediaFor(m.media, m.type));
1731
+ if (media) tile.media = media;
1732
+ }
1733
+ if (!titleOff && r["title"]) tile.title = String(r["title"]);
1734
+ if (!bodyOff && r["body"]) tile.body = String(r["body"]);
1735
+ return tile;
1736
+ });
1737
+ return tiles.length ? tiles : null;
1738
+ }
1739
+ function materializeFeedGrid(opts) {
1740
+ const { heading, tiles, columns, spacing, overlay } = opts;
1741
+ const headingNode = heading ? {
1742
+ kind: "heading",
1743
+ level: heading.level,
1744
+ html: heading.html,
1745
+ ...heading.role ? { role: heading.role } : {}
1746
+ } : null;
1747
+ if (!tiles || !tiles.length) return headingNode;
1748
+ const cols = Math.max(1, Math.round(columns));
1749
+ const cells = tiles.map((t) => {
1750
+ const parts = [];
1751
+ if (t.media) parts.push({ kind: "media", media: t.media });
1752
+ if (t.title) parts.push({ kind: "heading", level: 6, html: t.title, role: "text6" });
1753
+ if (t.body) parts.push({ kind: "body", html: t.body });
1754
+ let node;
1755
+ if (overlay && t.media) {
1756
+ node = {
1757
+ kind: "stack",
1758
+ children: parts,
1759
+ style: {
1760
+ _overlay: overlay.ratio,
1761
+ ...overlay.color ? { _overlayColor: overlay.color } : {},
1762
+ ...overlay.valign ? { _overlayValign: overlay.valign } : {}
1763
+ }
1764
+ };
1765
+ } else {
1766
+ node = parts.length === 1 ? parts[0] : { kind: "stack", children: parts };
1767
+ }
1768
+ return {
1769
+ token: {
1770
+ cols,
1771
+ raw: `grid-${cols}${spacing ? `-s${spacing}` : ""}`,
1772
+ ...spacing ? { spacing } : {}
1773
+ },
1774
+ node
1775
+ };
1776
+ });
1777
+ const row = { kind: "row", cells };
1778
+ return headingNode ? { kind: "stack", children: [headingNode, row] } : row;
1779
+ }
1780
+ function escapeHtml(s) {
1781
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1782
+ }
1783
+ function plainToHtml(s) {
1784
+ const inner = escapeHtml(s.trim()).replace(/\r?\n/g, "<br>");
1785
+ return `<p>${inner}</p>`;
1786
+ }
1787
+ function extFor(mime, name) {
1788
+ const byMime = {
1789
+ "image/jpeg": "jpg",
1790
+ "image/jpg": "jpg",
1791
+ "image/png": "png",
1792
+ "image/svg+xml": "svg",
1793
+ "image/gif": "gif",
1794
+ "image/webp": "webp",
1795
+ "image/avif": "avif",
1796
+ "image/bmp": "bmp",
1797
+ "image/tiff": "tiff"
1798
+ };
1799
+ if (mime && byMime[mime]) return byMime[mime];
1800
+ const m = /\.([a-z0-9]{2,5})$/i.exec(name ?? "");
1801
+ const ext = m?.[1]?.toLowerCase();
1802
+ const IMG = /* @__PURE__ */ new Set(["jpg", "jpeg", "png", "svg", "gif", "webp", "avif", "bmp", "tif", "tiff"]);
1803
+ return ext && IMG.has(ext) ? ext === "jpeg" ? "jpg" : ext : null;
1804
+ }
1805
+ function isFeedBand(item) {
1806
+ return !!item && typeof item === "object" && Array.isArray(item.sources) && item.sources.length > 0;
1807
+ }
1808
+ function feedAssetBase(htmls, siteId) {
1809
+ for (const html of htmls) {
1810
+ const m = /data-base="(https?:\/\/[^"]+?\/)"/i.exec(html);
1811
+ if (m?.[1]) return m[1].replace(/^http:/, "https:");
1812
+ }
1813
+ return `https://${CDN_HOSTS[0]}/${siteId}/`;
1814
+ }
1815
+ function buildFeedResolvers(feeds, mediaLibrary, base2) {
1816
+ const feedMap = /* @__PURE__ */ new Map();
1817
+ for (const [id, f] of Object.entries(feeds)) {
1818
+ if (Array.isArray(f?.items)) feedMap.set(id, f.items);
1819
+ }
1820
+ const mediaMap = new Map(Object.entries(mediaLibrary));
1821
+ return {
1822
+ feeds: feedMap,
1823
+ media: mediaMap,
1824
+ mediaFor: (uuid, type, name) => {
1825
+ const ext = extFor(type, name);
1826
+ if (!ext) return null;
1827
+ return { kind: "image", assetId: uuid, base: base2, ext };
1828
+ }
1829
+ };
1830
+ }
1831
+
1832
+ // src/blux/products.ts
1833
+ var CANONICAL_CATEGORIES = ["Upholstered", "Case", "Exterior", "Metal", "Finishes"];
1834
+ var CATEGORY_ALIASES = {
1835
+ upholstrered: "Upholstered",
1836
+ upholsered: "Upholstered"
1837
+ };
1838
+ var SUBCATEGORY_ALIASES = {
1839
+ banuette: "Banquette",
1840
+ "benches & ottomans": "Ottomans & Benches",
1841
+ miscellaneous: "Misc."
1842
+ };
1843
+ function cleanWs(s) {
1844
+ return String(s ?? "").replace(/\s+/g, " ").trim();
1845
+ }
1846
+ function titleCase(s) {
1847
+ return s.replace(/\b\w/g, (c) => c.toUpperCase());
1848
+ }
1849
+ function normalizeCategory(raw) {
1850
+ const cleaned = cleanWs(raw);
1851
+ if (!cleaned) return "";
1852
+ const lower = cleaned.toLowerCase();
1853
+ if (CATEGORY_ALIASES[lower]) return CATEGORY_ALIASES[lower];
1854
+ const canon = CANONICAL_CATEGORIES.find((c) => c.toLowerCase() === lower);
1855
+ return canon ?? titleCase(lower);
1856
+ }
1857
+ function normalizeSubCategory(raw) {
1858
+ const cleaned = cleanWs(raw);
1859
+ if (!cleaned) return "";
1860
+ const lower = cleaned.toLowerCase();
1861
+ if (SUBCATEGORY_ALIASES[lower]) return SUBCATEGORY_ALIASES[lower];
1862
+ return titleCase(lower);
1863
+ }
1864
+ function productSlug(record) {
1865
+ const url = typeof record.url === "string" ? record.url.trim() : "";
1866
+ if (url) return url.replace(/^\/+products\/+/i, "").replace(/^\/+|\/+$/g, "");
1867
+ return String(record.title ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1868
+ }
1869
+ function mediaUuid(media) {
1870
+ if (media && typeof media === "object") {
1871
+ const m = media.media;
1872
+ if (typeof m === "string" && m) return m;
1873
+ }
1874
+ return void 0;
1875
+ }
1876
+ function imageFor(uuid, resolveImage) {
1877
+ if (!uuid) return void 0;
1878
+ const url = resolveImage(uuid);
1879
+ return url ? { assetId: uuid, url } : void 0;
1880
+ }
1881
+ function materializeProducts(records, resolveImage) {
1882
+ const bySlug = /* @__PURE__ */ new Map();
1883
+ for (const r of records) {
1884
+ const slug = productSlug(r);
1885
+ if (!slug) continue;
1886
+ const gallery = [];
1887
+ if (Array.isArray(r.items)) {
1888
+ for (const it of r.items) {
1889
+ const img2 = imageFor(mediaUuid(it?.media), resolveImage);
1890
+ if (img2) gallery.push(img2);
1891
+ }
1892
+ }
1893
+ const image = imageFor(mediaUuid(r.media), resolveImage);
1894
+ const product = {
1895
+ slug,
1896
+ title: cleanWs(r.title),
1897
+ category: normalizeCategory(r.category),
1898
+ subCategory: normalizeSubCategory(r.sub_category),
1899
+ dimensions: String(r.dimensions ?? ""),
1900
+ tags: Array.isArray(r.tags) ? r.tags.filter((t) => typeof t === "string") : [],
1901
+ disabled: r.disabled === true,
1902
+ gallery,
1903
+ // `exactOptionalPropertyTypes`: omit the key rather than set undefined.
1904
+ ...image ? { image } : {}
1905
+ };
1906
+ const existing = bySlug.get(slug);
1907
+ if (!existing || existing.disabled && !product.disabled) bySlug.set(slug, product);
1908
+ }
1909
+ return [...bySlug.values()];
1910
+ }
1911
+
1378
1912
  // src/blux/emit/block-styles.ts
1379
- function blockStylesByIndex(siteJson) {
1913
+ function blockStylesByIndex(siteJson, pageIndex = 0) {
1380
1914
  const out = /* @__PURE__ */ new Map();
1381
- const blocks = siteJson?.content?.pages?.[0]?.items;
1915
+ const blocks = siteJson?.content?.pages?.[pageIndex]?.items;
1382
1916
  if (!Array.isArray(blocks)) return out;
1383
1917
  blocks.forEach((b, i) => {
1384
1918
  const styles = b?.styles ?? {};
@@ -1391,6 +1925,30 @@ function blockStylesByIndex(siteJson) {
1391
1925
  });
1392
1926
  return out;
1393
1927
  }
1928
+ var CONTAINER_RE = /^\.(blocks\d+)container$/;
1929
+ function blockClassDefaults(siteJson) {
1930
+ const out = /* @__PURE__ */ new Map();
1931
+ const blocks = siteJson?.styles?.blocks;
1932
+ if (!Array.isArray(blocks)) return out;
1933
+ for (const slot of blocks) {
1934
+ if (slot === null || typeof slot !== "object") continue;
1935
+ for (const [selector, css] of Object.entries(slot)) {
1936
+ const blockClass = CONTAINER_RE.exec(selector)?.[1];
1937
+ if (!blockClass || css === null || typeof css !== "object") continue;
1938
+ const rec = css;
1939
+ const padding = cleanCssValue(rec["padding"]);
1940
+ const mobilePadding = cleanCssValue(rec["__media_mobile_padding"]);
1941
+ const maxWidth = cleanCssValue(rec["max-width"]);
1942
+ const defaults = {
1943
+ ...padding ? { padding } : {},
1944
+ ...mobilePadding ? { mobilePadding } : {},
1945
+ ...maxWidth ? { maxWidth } : {}
1946
+ };
1947
+ if (Object.keys(defaults).length) out.set(blockClass, defaults);
1948
+ }
1949
+ }
1950
+ return out;
1951
+ }
1394
1952
 
1395
1953
  // src/blux/emit/grid-slice.ts
1396
1954
  function stripTags(html) {
@@ -1446,9 +2004,10 @@ function sliceSpecToPlanSlice(spec) {
1446
2004
  return {
1447
2005
  slice_type: "carousel",
1448
2006
  variation: "default",
1449
- items: spec.slides.map(
1450
- (s) => s.caption ? { caption: blockPlainText(s.caption.html) } : {}
1451
- ),
2007
+ items: spec.slides.map((s) => ({
2008
+ ...s.caption ? { caption: blockPlainText(s.caption.html) } : {},
2009
+ ...s.subcaption ? { subcaption: blockPlainText(s.subcaption.html) } : {}
2010
+ })),
1452
2011
  primary: { band: spec.index }
1453
2012
  };
1454
2013
  case "MediaFull":
@@ -1526,13 +2085,17 @@ function collectPlanAssets(specs, resolve, diagnostics) {
1526
2085
  }
1527
2086
  function buildGridPlan(specs, ir) {
1528
2087
  const page = ir.pages[0];
1529
- const uid = page?.uid ?? "home";
1530
- const title = page?.title ?? uid;
1531
- const doc = {
2088
+ return buildGridSitePlan(
2089
+ [{ uid: page?.uid ?? "home", title: page?.title ?? page?.uid ?? "home", specs }],
2090
+ ir
2091
+ );
2092
+ }
2093
+ function buildGridSitePlan(pages, ir) {
2094
+ const documents = pages.map((p) => ({
1532
2095
  type: "page",
1533
- uid,
1534
- data: { title: richText(`<h1>${title}</h1>`), slices: specs.map(sliceSpecToPlanSlice) }
1535
- };
2096
+ uid: p.uid,
2097
+ data: { title: richText(`<h1>${p.title}</h1>`), slices: p.specs.map(sliceSpecToPlanSlice) }
2098
+ }));
1536
2099
  const assetById = new Map(ir.assets.map((a) => [a.id, a]));
1537
2100
  const sourceUrlById = new Map(ir.assets.map((a) => [a.id, a.sourceUrl]));
1538
2101
  const resolve = (m) => {
@@ -1541,9 +2104,13 @@ function buildGridPlan(specs, ir) {
1541
2104
  return url ? { id: m.assetId, url, alt: asset?.alt ?? "" } : null;
1542
2105
  };
1543
2106
  const diagnostics = [...ir.diagnostics ?? []];
1544
- const assets = collectPlanAssets(specs, resolve, diagnostics);
2107
+ const assets = collectPlanAssets(
2108
+ pages.flatMap((p) => p.specs),
2109
+ resolve,
2110
+ diagnostics
2111
+ );
1545
2112
  const customTypes = ir.collections.map(buildCustomType);
1546
- return { customTypes, documents: [doc], assets, stylesManifest: [], diagnostics };
2113
+ return { customTypes, documents, assets, stylesManifest: [], diagnostics };
1547
2114
  }
1548
2115
 
1549
2116
  // src/blux/emit/presentation.ts
@@ -1562,7 +2129,7 @@ function renderNode(node, resolve) {
1562
2129
  const rn = renderNode(c.node, resolve);
1563
2130
  if (rn) cells.push({ token: renderToken(c.token), node: rn });
1564
2131
  }
1565
- return { kind: "row", cells };
2132
+ return { kind: "row", cells, ...node.style ? { style: node.style } : {} };
1566
2133
  }
1567
2134
  case "stack": {
1568
2135
  const children = [];
@@ -1570,19 +2137,30 @@ function renderNode(node, resolve) {
1570
2137
  const rn = renderNode(c, resolve);
1571
2138
  if (rn) children.push(rn);
1572
2139
  }
1573
- return { kind: "stack", children };
2140
+ return { kind: "stack", children, ...node.style ? { style: node.style } : {} };
1574
2141
  }
1575
2142
  case "heading":
1576
2143
  return {
1577
2144
  kind: "heading",
1578
2145
  level: node.level,
1579
2146
  html: node.html,
1580
- ...node.role ? { role: node.role } : {}
2147
+ ...node.role ? { role: node.role } : {},
2148
+ ...node.style ? { style: node.style } : {}
1581
2149
  };
1582
2150
  case "body":
1583
- return { kind: "body", html: node.html, ...node.role ? { role: node.role } : {} };
2151
+ return {
2152
+ kind: "body",
2153
+ html: node.html,
2154
+ ...node.role ? { role: node.role } : {},
2155
+ ...node.style ? { style: node.style } : {}
2156
+ };
1584
2157
  case "subtitle":
1585
- return { kind: "subtitle", text: node.text, ...node.role ? { role: node.role } : {} };
2158
+ return {
2159
+ kind: "subtitle",
2160
+ text: node.text,
2161
+ ...node.role ? { role: node.role } : {},
2162
+ ...node.style ? { style: node.style } : {}
2163
+ };
1586
2164
  case "media": {
1587
2165
  const m = resolve(node.media);
1588
2166
  return m ? { kind: "media", media: m } : null;
@@ -1599,12 +2177,45 @@ function hasMapWidget(node) {
1599
2177
  if (node.kind === "stack") return node.children.some(hasMapWidget);
1600
2178
  return false;
1601
2179
  }
2180
+ function markPanelRows(node, toggleCount) {
2181
+ if (node.kind === "row") {
2182
+ return {
2183
+ ...node,
2184
+ cells: node.cells.map((c) => ({ ...c, node: markPanelRows(c.node, toggleCount) }))
2185
+ };
2186
+ }
2187
+ if (node.kind === "stack") {
2188
+ const children = node.children.map((c) => markPanelRows(c, toggleCount));
2189
+ for (let i = 0; i + 1 < children.length; i++) {
2190
+ const cur = children[i];
2191
+ const next = children[i + 1];
2192
+ if (cur?.kind === "widget" && cur.widget.type === "map" && next?.kind === "row" && next.cells.length === toggleCount) {
2193
+ children[i + 1] = { ...next, panels: true };
2194
+ }
2195
+ }
2196
+ return { ...node, children };
2197
+ }
2198
+ return node;
2199
+ }
1602
2200
  function buildPresentation(specs, deps) {
1603
2201
  const bands = {};
1604
2202
  for (const spec of specs) {
1605
2203
  const bp = {};
1606
2204
  const style = deps.styleFor(spec.index);
1607
2205
  if (style) bp.style = style;
2206
+ const defaults = spec.blockClass ? deps.defaultsFor(spec.blockClass) : void 0;
2207
+ if (defaults) {
2208
+ const own = bp.style ?? {};
2209
+ const fill = {};
2210
+ if (own["_contentPadding"] === void 0 && defaults.padding) {
2211
+ fill["_contentPadding"] = defaults.padding;
2212
+ if (defaults.mobilePadding) fill["_contentPaddingMobile"] = defaults.mobilePadding;
2213
+ }
2214
+ if (own["_max-content-width"] === void 0 && defaults.maxWidth) {
2215
+ fill["_max-content-width"] = defaults.maxWidth;
2216
+ }
2217
+ if (Object.keys(fill).length) bp.style = { ...own, ...fill };
2218
+ }
1608
2219
  if (spec.background) {
1609
2220
  const bg = deps.resolveMedia(spec.background);
1610
2221
  if (bg) bp.background = bg;
@@ -1633,14 +2244,15 @@ function buildPresentation(specs, deps) {
1633
2244
  for (const s of spec.slides) {
1634
2245
  const media = deps.resolveMedia(s.media);
1635
2246
  if (!media) break;
1636
- const slide = {
1637
- media
1638
- };
2247
+ const slide = { media };
1639
2248
  if (s.caption) {
1640
2249
  const caption = { level: s.caption.level };
1641
2250
  if (s.caption.role !== void 0) caption.role = s.caption.role;
1642
2251
  slide.caption = caption;
1643
2252
  }
2253
+ if (s.subcaption) {
2254
+ slide.subcaption = s.subcaption.role !== void 0 ? { role: s.subcaption.role } : {};
2255
+ }
1644
2256
  slides.push(slide);
1645
2257
  }
1646
2258
  if (slides.length > 0) {
@@ -1665,8 +2277,9 @@ function buildPresentation(specs, deps) {
1665
2277
  break;
1666
2278
  case "Grid": {
1667
2279
  const tree = renderNode(spec.root, deps.resolveMedia);
1668
- if (tree) bp.tree = tree;
1669
- if (deps.map && hasMapWidget(spec.root)) bp.map = deps.map;
2280
+ const co = deps.map && hasMapWidget(spec.root) ? deps.map : null;
2281
+ if (co) bp.map = co;
2282
+ if (tree) bp.tree = co ? markPanelRows(tree, co.toggles.length) : tree;
1670
2283
  break;
1671
2284
  }
1672
2285
  }
@@ -1699,6 +2312,7 @@ function convertExport({
1699
2312
  const assetsById = new Map(ir.assets.map((a) => [a.id, a]));
1700
2313
  const sourceUrlById = new Map(ir.assets.map((a) => [a.id, a.sourceUrl]));
1701
2314
  const styles = blockStylesByIndex(siteJson);
2315
+ const defaults = blockClassDefaults(siteJson);
1702
2316
  const deps = {
1703
2317
  resolveMedia: (m) => {
1704
2318
  const url = mediaUrl(m, sourceUrlById);
@@ -1713,17 +2327,284 @@ function convertExport({
1713
2327
  ...m.fit ? { fit: m.fit } : {},
1714
2328
  ...m.position ? { position: m.position } : {},
1715
2329
  ...m.minHeight ? { minHeight: m.minHeight } : {},
2330
+ ...m.cropRatio ? { cropRatio: m.cropRatio } : {},
1716
2331
  ...m.playback ? { playback: m.playback } : {}
1717
2332
  };
1718
2333
  return rm;
1719
2334
  },
1720
2335
  styleFor: (i) => styles.get(i),
2336
+ defaultsFor: (blockClass) => defaults.get(blockClass),
1721
2337
  map: mapConfig ? mapRenderFromConfig(mapConfig) : null
1722
2338
  };
1723
2339
  const plan = buildGridPlan(specs, ir);
1724
2340
  const presentation = buildPresentation(specs, deps);
1725
2341
  return { bands, specs, ir, mapConfig, plan, presentation };
1726
2342
  }
2343
+ function leadingHeadings(root) {
2344
+ if (root.kind === "heading" || root.kind === "subtitle") return [root];
2345
+ if (root.kind === "stack") {
2346
+ const out = [];
2347
+ for (const c of root.children) {
2348
+ if (c.kind === "heading" || c.kind === "subtitle") out.push(c);
2349
+ else break;
2350
+ }
2351
+ return out;
2352
+ }
2353
+ return [];
2354
+ }
2355
+ function feedColumns(item) {
2356
+ const n = Number(item.columns);
2357
+ return Number.isFinite(n) && n > 0 ? n : 3;
2358
+ }
2359
+ function tileOverlay(item) {
2360
+ const cfg = item?.sourceConfig;
2361
+ if (!cfg || cfg["overlay"] !== true) return null;
2362
+ const ratio = cropRatioOf(cfg["mediaRatio"] ?? cfg["ratio"]);
2363
+ if (!ratio) return null;
2364
+ const color = typeof cfg["overlayColor"] === "string" ? cfg["overlayColor"] : void 0;
2365
+ const valign = typeof cfg["contentvalign"] === "string" ? cfg["contentvalign"] : void 0;
2366
+ return { ratio, ...color ? { color } : {}, ...valign ? { valign } : {} };
2367
+ }
2368
+ function isEmptyish(root) {
2369
+ switch (root.kind) {
2370
+ case "heading":
2371
+ case "subtitle":
2372
+ return true;
2373
+ case "raw":
2374
+ return root.html.trim() === "";
2375
+ case "media":
2376
+ case "widget":
2377
+ return false;
2378
+ case "row":
2379
+ return root.cells.length === 0;
2380
+ case "stack":
2381
+ return root.children.every(isEmptyish);
2382
+ default:
2383
+ return false;
2384
+ }
2385
+ }
2386
+ function materializeFeedBands(bands, pageItems, resolvers, diagnostics) {
2387
+ if (!Array.isArray(pageItems)) return;
2388
+ for (const band of bands) {
2389
+ const item = pageItems[band.index];
2390
+ if (!isFeedBand(item)) continue;
2391
+ if (!isEmptyish(band.root)) {
2392
+ diagnostics.push({
2393
+ kind: "empty-feed-grid",
2394
+ where: String(band.index),
2395
+ message: `band ${band.index} has parsed content but site.json item is a feed source \u2014 positional join misaligned, left as parsed`
2396
+ });
2397
+ continue;
2398
+ }
2399
+ const tiles = resolveFeedTiles(item, resolvers);
2400
+ if (!tiles) {
2401
+ diagnostics.push({
2402
+ kind: "empty-feed-grid",
2403
+ where: String(band.index),
2404
+ message: `feed band ${band.index} (source ${String(item.sources[0])}) resolved to no tiles`
2405
+ });
2406
+ continue;
2407
+ }
2408
+ const spacing = parseInt(String(item.spacing ?? ""), 10);
2409
+ const row = materializeFeedGrid({
2410
+ tiles,
2411
+ columns: feedColumns(item),
2412
+ ...Number.isFinite(spacing) && spacing > 0 ? { spacing } : {},
2413
+ ...tileOverlay(item) ? { overlay: tileOverlay(item) } : {}
2414
+ });
2415
+ if (!row) continue;
2416
+ const headings = leadingHeadings(band.root);
2417
+ band.root = headings.length ? { kind: "stack", children: [...headings, row] } : row;
2418
+ }
2419
+ }
2420
+ function sitePages(siteJson) {
2421
+ const { pages } = normalizePages(parseBluxSite(siteJson));
2422
+ return pages.map((p) => ({ uid: p.uid, path: p.path, title: p.title }));
2423
+ }
2424
+ function convertSite({
2425
+ siteJson,
2426
+ htmlByUid
2427
+ }) {
2428
+ const ir = assembleIR({ siteJson, htmls: [...htmlByUid.values()] });
2429
+ const assetsById = new Map(ir.assets.map((a) => [a.id, a]));
2430
+ const sourceUrlById = new Map(ir.assets.map((a) => [a.id, a.sourceUrl]));
2431
+ const defaults = blockClassDefaults(siteJson);
2432
+ const raw = parseBluxSite(siteJson);
2433
+ const assetBase = feedAssetBase([...htmlByUid.values()], ir.meta.bluxSiteId);
2434
+ const feedResolvers = buildFeedResolvers(raw.feeds, raw.media, assetBase);
2435
+ const pageItemsByIndex = siteJson?.content?.pages;
2436
+ const pages = [];
2437
+ const presentation = { pages: {} };
2438
+ ir.pages.forEach((page, pageIndex) => {
2439
+ const html = htmlByUid.get(page.uid);
2440
+ if (html === void 0) {
2441
+ ir.diagnostics.push({
2442
+ kind: "missing-page-html",
2443
+ where: page.uid,
2444
+ message: `no rendered html for page "${page.uid}" (${page.path || "/"}) \u2014 page skipped`
2445
+ });
2446
+ return;
2447
+ }
2448
+ const bands = parseGridBands(html);
2449
+ materializeFeedBands(
2450
+ bands,
2451
+ pageItemsByIndex?.[pageIndex]?.items,
2452
+ feedResolvers,
2453
+ ir.diagnostics
2454
+ );
2455
+ const mapConfig = extractMapConfig(html);
2456
+ const specs = classifyBands(bands, mapConfig ? { isMapMount: makeIsMapMount(mapConfig) } : {});
2457
+ const styles = blockStylesByIndex(siteJson, pageIndex);
2458
+ const deps = {
2459
+ resolveMedia: (m) => {
2460
+ const url = mediaUrl(m, sourceUrlById);
2461
+ if (!url) return null;
2462
+ const alt = assetsById.get(m.assetId)?.alt;
2463
+ const rm = {
2464
+ kind: m.kind,
2465
+ url,
2466
+ ...alt ? { alt } : {},
2467
+ ...m.width !== void 0 ? { width: m.width } : {},
2468
+ ...m.aspect !== void 0 ? { aspect: m.aspect } : {},
2469
+ ...m.fit ? { fit: m.fit } : {},
2470
+ ...m.position ? { position: m.position } : {},
2471
+ ...m.minHeight ? { minHeight: m.minHeight } : {},
2472
+ ...m.cropRatio ? { cropRatio: m.cropRatio } : {},
2473
+ ...m.playback ? { playback: m.playback } : {}
2474
+ };
2475
+ return rm;
2476
+ },
2477
+ styleFor: (i) => styles.get(i),
2478
+ defaultsFor: (blockClass) => defaults.get(blockClass),
2479
+ map: mapConfig ? mapRenderFromConfig(mapConfig) : null
2480
+ };
2481
+ presentation.pages[page.uid] = buildPresentation(specs, deps);
2482
+ pages.push({ uid: page.uid, title: page.title, path: page.path, bands, specs, mapConfig });
2483
+ });
2484
+ const plan = buildGridSitePlan(
2485
+ pages.map((p) => ({ uid: p.uid, title: p.title, specs: p.specs })),
2486
+ ir
2487
+ );
2488
+ return { pages, ir, plan, presentation };
2489
+ }
2490
+
2491
+ // src/blux/emit/site-config.ts
2492
+ function parseNavItems(items) {
2493
+ if (!Array.isArray(items)) return [];
2494
+ const out = [];
2495
+ for (const raw of items) {
2496
+ const label = typeof raw?.title === "string" ? raw.title.trim() : "";
2497
+ if (!label) continue;
2498
+ const href = typeof raw?.link === "string" ? raw.link : "";
2499
+ const children = parseNavItems(raw?.items);
2500
+ out.push({ label, href, ...children.length ? { children } : {} });
2501
+ }
2502
+ return out;
2503
+ }
2504
+ var SOCIAL_DOMAINS = {
2505
+ facebook: ["facebook.com", "fb.com"],
2506
+ twitter: ["twitter.com", "x.com"],
2507
+ instagram: ["instagram.com"],
2508
+ pinterest: ["pinterest.com"],
2509
+ linkedin: ["linkedin.com"],
2510
+ "linkedin-company": ["linkedin.com"],
2511
+ youtube: ["youtube.com", "youtu.be"],
2512
+ reddit: ["reddit.com"]
2513
+ };
2514
+ function hostMatches(host, domain) {
2515
+ return host === domain || host.endsWith("." + domain);
2516
+ }
2517
+ function hostOf(url) {
2518
+ try {
2519
+ return new URL(url).hostname.toLowerCase();
2520
+ } catch {
2521
+ return "";
2522
+ }
2523
+ }
2524
+ function footerRegion(html) {
2525
+ const lower = html.toLowerCase();
2526
+ const start = lower.lastIndexOf("<footer");
2527
+ if (start === -1) return html;
2528
+ const end = lower.indexOf("</footer>", start);
2529
+ return end === -1 ? html.slice(start) : html.slice(start, end + "</footer>".length);
2530
+ }
2531
+ function socialHrefResolverFromHtml(htmls) {
2532
+ const hrefs = [];
2533
+ const re = /href\s*=\s*["']([^"']+)["']/gi;
2534
+ for (const html of htmls) {
2535
+ if (typeof html !== "string") continue;
2536
+ const region = footerRegion(html);
2537
+ let m;
2538
+ while (m = re.exec(region)) {
2539
+ const url = m[1];
2540
+ if (url && /^https?:\/\//i.test(url)) hrefs.push(url);
2541
+ }
2542
+ }
2543
+ return (network) => {
2544
+ const domains = SOCIAL_DOMAINS[network];
2545
+ if (!domains) return void 0;
2546
+ return hrefs.find((u) => {
2547
+ const host = hostOf(u);
2548
+ return host && domains.some((d) => hostMatches(host, d));
2549
+ });
2550
+ };
2551
+ }
2552
+ function parseSocials(raw, resolveSocialHref) {
2553
+ const out = [];
2554
+ const items = Array.isArray(raw) ? raw : [];
2555
+ for (const it of items) {
2556
+ const media = it?.media;
2557
+ if (!media || media.type !== "social") continue;
2558
+ const networks = media.networks;
2559
+ if (!networks || typeof networks !== "object") continue;
2560
+ const urls = media.urls ?? {};
2561
+ for (const [network, on] of Object.entries(networks)) {
2562
+ if (on !== true) continue;
2563
+ const fromExport = typeof urls[network] === "string" ? urls[network] : void 0;
2564
+ const url = fromExport ?? resolveSocialHref?.(network);
2565
+ out.push({ network, ...url ? { href: url } : {} });
2566
+ }
2567
+ }
2568
+ return out;
2569
+ }
2570
+ function collectTitles(node, acc) {
2571
+ if (Array.isArray(node)) {
2572
+ for (const n of node) collectTitles(n, acc);
2573
+ return acc;
2574
+ }
2575
+ if (node && typeof node === "object") {
2576
+ const rec = node;
2577
+ if (typeof rec.title === "string" && rec.title.trim()) acc.push(rec.title.trim());
2578
+ for (const k of Object.keys(rec)) if (k !== "title") collectTitles(rec[k], acc);
2579
+ }
2580
+ return acc;
2581
+ }
2582
+ var COPYRIGHT_RE = /©|\(c\)|\bcopyright\b|rights reserved/i;
2583
+ function parseFooterText(raw) {
2584
+ return collectTitles(raw, []).find((t) => COPYRIGHT_RE.test(t));
2585
+ }
2586
+ function buildSiteConfig(siteJson, resolveLogo, resolveSocialHref) {
2587
+ const j = siteJson;
2588
+ const navRoot = Array.isArray(j.navigation) ? j.navigation[0] : j.navigation;
2589
+ const footRoot = Array.isArray(j.footer) ? j.footer[0] : j.footer;
2590
+ const items = parseNavItems(navRoot?.items);
2591
+ const logoMedia = navRoot?.logo?.media;
2592
+ const logoUuid = typeof logoMedia?.media === "string" ? logoMedia.media : void 0;
2593
+ const logoUrl = logoUuid ? resolveLogo(logoUuid) : null;
2594
+ const maxWidth = typeof logoMedia?.["max-width"] === "string" ? logoMedia["max-width"] : void 0;
2595
+ const socials = parseSocials(footRoot?.items, resolveSocialHref);
2596
+ const text = parseFooterText(footRoot?.items);
2597
+ return {
2598
+ nav: {
2599
+ ...logoUrl ? { logo: { url: logoUrl, ...maxWidth ? { maxWidth } : {} } } : {},
2600
+ items
2601
+ },
2602
+ footer: {
2603
+ socials,
2604
+ ...text ? { text } : {}
2605
+ }
2606
+ };
2607
+ }
1727
2608
 
1728
2609
  // src/blux/emit/validate-layout.ts
1729
2610
  function tokKey(t) {
@@ -1963,11 +2844,19 @@ function walkBand(bp, map) {
1963
2844
  };
1964
2845
  return out;
1965
2846
  }
1966
- function rewriteManifestUrls(manifest, urlMap) {
2847
+ function rewritePage(p, map) {
1967
2848
  const bands = {};
1968
- for (const [k, bp] of Object.entries(manifest.bands)) bands[k] = walkBand(bp, urlMap);
2849
+ for (const [k, bp] of Object.entries(p.bands)) bands[k] = walkBand(bp, map);
1969
2850
  return { bands };
1970
2851
  }
2852
+ function rewriteManifestUrls(manifest, urlMap) {
2853
+ if ("pages" in manifest && manifest.pages) {
2854
+ const pages = {};
2855
+ for (const [uid, p] of Object.entries(manifest.pages)) pages[uid] = rewritePage(p, urlMap);
2856
+ return { pages };
2857
+ }
2858
+ return rewritePage(manifest, urlMap);
2859
+ }
1971
2860
 
1972
2861
  // src/cli/commands/blux.ts
1973
2862
  async function runBluxCommand(action, dir, opts) {
@@ -2021,9 +2910,10 @@ async function runBluxCommand(action, dir, opts) {
2021
2910
  await writeFile(join(out, "ir.json"), JSON.stringify(ir, null, 2));
2022
2911
  await writeFile(join(out, "migration-plan.json"), JSON.stringify(plan, null, 2));
2023
2912
  const rolesCss = emitRolesCss(ir.theme);
2913
+ const buttonsCss = emitButtonsCss(ir.theme);
2024
2914
  await writeFile(
2025
2915
  join(out, "theme.css"),
2026
- emitThemeCss(ir.theme) + (rolesCss ? "\n" + rolesCss : "")
2916
+ emitThemeCss(ir.theme) + (rolesCss ? "\n" + rolesCss : "") + (buttonsCss ? "\n" + buttonsCss : "")
2027
2917
  );
2028
2918
  await writeFile(join(out, "review-manifest.json"), JSON.stringify(manifest, null, 2));
2029
2919
  await writeFile(
@@ -2068,12 +2958,16 @@ async function runBluxCommand(action, dir, opts) {
2068
2958
  WARNING missing assets: ${r.missingAssets.join(", ")}` : "";
2069
2959
  let manifestNote = "";
2070
2960
  const manifestPath = join(dirname(planPath), "blux-presentation.json");
2961
+ let manifestRaw = null;
2071
2962
  try {
2072
- const manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
2963
+ manifestRaw = await readFile(manifestPath, "utf-8");
2964
+ } catch {
2965
+ }
2966
+ if (manifestRaw !== null) {
2967
+ const manifest = JSON.parse(manifestRaw);
2073
2968
  const rewritten = rewriteManifestUrls(manifest, r.assetUrlByCdn);
2074
2969
  await writeFile(manifestPath, JSON.stringify(rewritten, null, 2) + "\n");
2075
2970
  manifestNote = "\nmanifest media rewritten to Prismic urls";
2076
- } catch {
2077
2971
  }
2078
2972
  return {
2079
2973
  output: `custom types pushed: ${pushed.join(", ") || "none"}
@@ -2163,15 +3057,24 @@ assets: ${r.assetsUploaded} uploaded, ${r.assetsReused} reused | documents: ${r.
2163
3057
  }
2164
3058
  if (action === "convert") {
2165
3059
  if (!dir) return { output: "blux convert needs a Blux export directory.", code: 1 };
2166
- let html;
2167
3060
  let siteJson;
2168
3061
  try {
2169
- html = await readFile(join(dir, "index.html"), "utf-8");
2170
3062
  siteJson = JSON.parse(await readFile(join(dir, "site.json"), "utf-8"));
2171
3063
  } catch (err) {
2172
3064
  return { output: `could not read export in ${dir}: ${err.message}`, code: 1 };
2173
3065
  }
2174
- const { bands, mapConfig, plan, presentation, ir, specs } = convertExport({ html, siteJson });
3066
+ const htmlByUid = /* @__PURE__ */ new Map();
3067
+ for (const p of sitePages(siteJson)) {
3068
+ const file = p.path ? join(dir, p.path, "index.html") : join(dir, "index.html");
3069
+ try {
3070
+ htmlByUid.set(p.uid, await readFile(file, "utf-8"));
3071
+ } catch {
3072
+ }
3073
+ }
3074
+ if (!htmlByUid.size) {
3075
+ return { output: `could not read any page html in ${dir}`, code: 1 };
3076
+ }
3077
+ const { pages, ir, plan, presentation } = convertSite({ siteJson, htmlByUid });
2175
3078
  const outDir = opts.out ?? join(dir, "blux-out");
2176
3079
  await mkdir(outDir, { recursive: true });
2177
3080
  await writeFile(join(outDir, "migration-plan.json"), JSON.stringify(plan, null, 2));
@@ -2179,18 +3082,90 @@ assets: ${r.assetsUploaded} uploaded, ${r.assetsReused} reused | documents: ${r.
2179
3082
  join(outDir, "blux-presentation.json"),
2180
3083
  JSON.stringify(presentation, null, 2) + "\n"
2181
3084
  );
2182
- await writeFile(
2183
- join(outDir, "theme.css"),
2184
- emitThemeCss(ir.theme) + "\n" + emitRolesCss(ir.theme)
3085
+ {
3086
+ const buttonsCss = emitButtonsCss(ir.theme);
3087
+ await writeFile(
3088
+ join(outDir, "theme.css"),
3089
+ emitThemeCss(ir.theme) + "\n" + emitRolesCss(ir.theme) + (buttonsCss ? "\n" + buttonsCss : "")
3090
+ );
3091
+ }
3092
+ const mapConfigs = Object.fromEntries(
3093
+ pages.filter((p) => p.mapConfig).map((p) => [p.uid, p.mapConfig])
2185
3094
  );
2186
- if (mapConfig) {
2187
- await writeFile(join(outDir, "map-config.json"), JSON.stringify(mapConfig, null, 2) + "\n");
3095
+ if (Object.keys(mapConfigs).length) {
3096
+ await writeFile(join(outDir, "map-config.json"), JSON.stringify(mapConfigs, null, 2) + "\n");
2188
3097
  }
2189
- const layout = validateLayout(specs, presentation);
2190
- await writeFile(join(outDir, "layout-report.json"), JSON.stringify(layout, null, 2) + "\n");
2191
- const sliceCount = plan.documents[0]?.data.slices?.length ?? 0;
3098
+ {
3099
+ const sourceUrlById = new Map(ir.assets.map((a) => [a.id, a.sourceUrl]));
3100
+ const base2 = feedAssetBase([...htmlByUid.values()], ir.meta.bluxSiteId);
3101
+ const mediaDict = siteJson.media ?? {};
3102
+ const extByMime = {
3103
+ "image/png": "png",
3104
+ "image/jpeg": "jpg",
3105
+ "image/svg+xml": "svg",
3106
+ "image/gif": "gif",
3107
+ "image/webp": "webp"
3108
+ };
3109
+ const resolveLogo = (uuid) => {
3110
+ const scraped = sourceUrlById.get(uuid);
3111
+ if (scraped) return scraped;
3112
+ const ext = extByMime[String(mediaDict[uuid]?.type ?? "")];
3113
+ return ext ? `${base2}${uuid}.${ext}` : null;
3114
+ };
3115
+ const resolveSocialHref = socialHrefResolverFromHtml([...htmlByUid.values()]);
3116
+ const siteConfig = buildSiteConfig(siteJson, resolveLogo, resolveSocialHref);
3117
+ await writeFile(join(outDir, "site-config.json"), JSON.stringify(siteConfig, null, 2) + "\n");
3118
+ }
3119
+ {
3120
+ const feeds = siteJson.feeds ?? {};
3121
+ const productFeed = Object.values(feeds).find((f) => f?.publish === "products");
3122
+ if (productFeed && Array.isArray(productFeed.items)) {
3123
+ const base2 = feedAssetBase([...htmlByUid.values()], ir.meta.bluxSiteId);
3124
+ const mediaMeta = siteJson.media ?? {};
3125
+ const resolveImage = (uuid) => {
3126
+ const ext = extFor(mediaMeta[uuid]?.type, mediaMeta[uuid]?.name);
3127
+ return ext ? `${base2}${uuid}.${ext}` : null;
3128
+ };
3129
+ const products = materializeProducts(productFeed.items, resolveImage);
3130
+ await writeFile(join(outDir, "products.json"), JSON.stringify(products, null, 2) + "\n");
3131
+ }
3132
+ }
3133
+ let faviconLine = "";
3134
+ if (ir.meta.favicon?.sourceUrl) {
3135
+ const { assetId, sourceUrl } = ir.meta.favicon;
3136
+ try {
3137
+ const res = await (opts.fetchImpl ?? fetch)(sourceUrl);
3138
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
3139
+ await writeFile(join(outDir, "favicon.png"), new Uint8Array(await res.arrayBuffer()));
3140
+ faviconLine = `
3141
+ favicon \u2192 ${join(outDir, "favicon.png")}`;
3142
+ } catch (err) {
3143
+ await writeFile(
3144
+ join(outDir, "favicon.json"),
3145
+ JSON.stringify({ assetId, url: sourceUrl }, null, 2) + "\n"
3146
+ );
3147
+ faviconLine = `
3148
+ favicon fetch failed (${err.message}) \u2014 url preserved in ${join(outDir, "favicon.json")}`;
3149
+ }
3150
+ }
3151
+ const layoutByUid = {};
3152
+ const reportLines = [];
3153
+ for (const p of pages) {
3154
+ const pagePresentation = presentation.pages[p.uid];
3155
+ if (!pagePresentation) continue;
3156
+ const layout = validateLayout(p.specs, pagePresentation);
3157
+ layoutByUid[p.uid] = layout;
3158
+ reportLines.push(`[${p.uid}] ${formatLayoutReport(layout)}`);
3159
+ }
3160
+ await writeFile(
3161
+ join(outDir, "layout-report.json"),
3162
+ JSON.stringify({ pages: layoutByUid }, null, 2) + "\n"
3163
+ );
3164
+ const totalBands = pages.reduce((n, p) => n + p.bands.length, 0);
3165
+ const missing = ir.diagnostics.filter((d) => d.kind === "missing-page-html");
2192
3166
  return {
2193
- output: `Converted ${bands.length} bands \u2192 ${outDir} (${Object.keys(presentation.bands).length} manifest bands, ${sliceCount} slices` + (mapConfig ? ", map config extracted" : "") + ")\n" + formatLayoutReport(layout),
3167
+ output: `Converted ${pages.length} pages / ${totalBands} bands \u2192 ${outDir} (${plan.documents.length} page documents` + (Object.keys(mapConfigs).length ? `, map config on ${Object.keys(mapConfigs).join(", ")}` : "") + ")" + (missing.length ? `
3168
+ skipped (no html): ${missing.map((d) => d.where).join(", ")}` : "") + faviconLine + "\n" + reportLines.join("\n"),
2194
3169
  code: 0
2195
3170
  };
2196
3171
  }
@@ -2202,4 +3177,4 @@ assets: ${r.assetsUploaded} uploaded, ${r.assetsReused} reused | documents: ${r.
2202
3177
  export {
2203
3178
  runBluxCommand
2204
3179
  };
2205
- //# sourceMappingURL=blux-FKZYMKLF.js.map
3180
+ //# sourceMappingURL=blux-C24OVDDW.js.map