@bettercms-ai/convert 0.20.1 → 0.22.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.
package/dist/index.d.ts CHANGED
@@ -955,7 +955,7 @@ declare function attrsFor(path: string, kind: string, props?: AttrBinding[], sco
955
955
  declare function propsAttribute(props: AttrBinding[]): string;
956
956
 
957
957
  /** Bumped whenever the emitted source changes. `isKnownHelper` upgrades anything older. */
958
- declare const HELPER_VERSION = 9;
958
+ declare const HELPER_VERSION = 11;
959
959
  /** Where each dialect's helper lives. Null = this dialect reads nothing at build time. */
960
960
  declare const HELPER_PATH: Record<Dialect, string | null>;
961
961
  /** The directory the snapshots live in, at the repository root. */
package/dist/index.js CHANGED
@@ -4187,7 +4187,7 @@ async function bindImages(ctx, targets) {
4187
4187
 
4188
4188
  // src/helper.ts
4189
4189
  import { createHash as createHash2 } from "crypto";
4190
- var HELPER_VERSION = 9;
4190
+ var HELPER_VERSION = 11;
4191
4191
  var HEADER = "// @bettercms-ai/convert helper v";
4192
4192
  var HELPER_PATH = Object.fromEntries(
4193
4193
  Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
@@ -4511,6 +4511,144 @@ export function bcmsFields(
4511
4511
  return (own ? undefined : moved(key, rest.slice(key?.length ?? 0))) ?? lane + rest;
4512
4512
  };
4513
4513
  }
4514
+
4515
+ /**
4516
+ * C3/C4 \u2014 a section's \`block.style\` as the inline CSS the BetterCMS SDK draws for it.
4517
+ *
4518
+ * A converted section root carries \`data-bcms-style\` beside \`data-bcms-block\`, which is the
4519
+ * site's promise to the editor that "this root's inline style is \`blockStyleToCss(block.style)\`"
4520
+ * \u2014 and the editor only offers a section's spacing handles where every stamped root makes it.
4521
+ * This is \`blockStyleToCss\` from \`@bettercms-ai/types\`, INLINED for the same reason \`readPath\`
4522
+ * is (zero dependencies); \`helper.test.ts\` runs it against the original over a token table.
4523
+ */
4524
+ const STYLE_SPACE_PX: Record<string, string> = { none: "0", xs: "4px", sm: "8px", md: "16px", lg: "32px", xl: "64px" };
4525
+ const STYLE_FONT_SIZE_REM: Record<string, string> = {
4526
+ sm: "0.875rem",
4527
+ base: "1rem",
4528
+ lg: "1.25rem",
4529
+ xl: "1.5rem",
4530
+ "2xl": "2rem",
4531
+ "3xl": "2.25rem",
4532
+ "4xl": "3rem",
4533
+ "5xl": "4rem",
4534
+ };
4535
+ const STYLE_WEIGHT: Record<string, number> = { normal: 400, medium: 500, semibold: 600, bold: 700 };
4536
+ const STYLE_RADIUS_PX: Record<string, string> = { none: "0", sm: "4px", md: "8px", lg: "16px", full: "9999px" };
4537
+ const STYLE_COLOR_HEX: Record<string, string> = { white: "#ffffff", black: "#111111", muted: "#6b7280", mutedBg: "#f3f4f6", primary: "#111111", accent: "#4f46e5" };
4538
+ const STYLE_SPACE_TOKEN_PX: Record<string, string> = {
4539
+ "section-space-none": "0px",
4540
+ "section-space-small": "32px",
4541
+ "section-space-medium": "64px",
4542
+ "section-space-large": "96px",
4543
+ "section-space-xlarge": "144px",
4544
+ "section-space-nav-overlap": "-72px",
4545
+ "space-none": "0px",
4546
+ "space-s": "8px",
4547
+ "space-m": "16px",
4548
+ "space-l": "32px",
4549
+ "space-xl": "64px",
4550
+ };
4551
+
4552
+ const STYLE_THEME: Record<string, { colorScheme: string; background: string; color: string }> = {
4553
+ light: { colorScheme: "light", background: "#ffffff", color: "#111111" },
4554
+ dark: { colorScheme: "dark", background: "#0b0b0d", color: "#f7f8f8" },
4555
+ };
4556
+ const STYLE_BG: Record<string, string> = {
4557
+ surface: "var(--bcms-bg-surface,#ffffff)",
4558
+ muted: "var(--bcms-bg-muted,#f3f4f6)",
4559
+ accent: "var(--bcms-bg-accent,#4f46e5)",
4560
+ dark: "var(--bcms-bg-dark,#0b0b0d)",
4561
+ };
4562
+ const STYLE_CORNER_PX: Record<string, string> = { "0": "0", s: "4px", m: "8px", l: "16px", xl: "24px", "2xl": "32px" };
4563
+ const STYLE_SHADOW: Record<string, string> = {
4564
+ "0": "none",
4565
+ s: "0 1px 2px rgba(0,0,0,.06)",
4566
+ m: "0 4px 12px rgba(0,0,0,.08)",
4567
+ l: "0 10px 24px rgba(0,0,0,.10)",
4568
+ xl: "0 24px 48px rgba(0,0,0,.14)",
4569
+ };
4570
+ const STYLE_HAIRLINE = "1px solid rgba(0,0,0,.10)";
4571
+
4572
+ /** A map entry by OWN key only, so a token like \`constructor\` never reaches CSS. */
4573
+ function styleOwn<T>(map: Record<string, T>, key: unknown): T | undefined {
4574
+ return typeof key === "string" && Object.prototype.hasOwnProperty.call(map, key) ? map[key] : undefined;
4575
+ }
4576
+
4577
+ /** An http(s) or root-relative url with nothing that could end \`url(\u2026)\`; "" for anything else. */
4578
+ function styleSafeUrl(raw: unknown): string {
4579
+ if (typeof raw !== "string") return "";
4580
+ const url = raw.trim();
4581
+ if (!url || url.length > 2000) return "";
4582
+ if (/["'()\\;]|\\s/.test(url)) return "";
4583
+ if (url.startsWith("//")) return "";
4584
+ if (url.startsWith("/")) return url;
4585
+ return /^https?:\\/\\//i.test(url) ? url : "";
4586
+ }
4587
+
4588
+ function styleClamp(n: number, lo: number, hi: number): number {
4589
+ return Math.min(hi, Math.max(lo, n));
4590
+ }
4591
+
4592
+ function styleSpaceToken(token: unknown): string {
4593
+ const px = typeof token === "string" ? STYLE_SPACE_TOKEN_PX[token] : undefined;
4594
+ return px ? "var(--bcms-" + token + "," + px + ")" : "";
4595
+ }
4596
+
4597
+ /** The declarations as a React-style object (camelCase), or undefined when there are none. */
4598
+ export function bcmsSectionStyle(style: unknown): Record<string, string | number> | undefined {
4599
+ if (!style || typeof style !== "object") return undefined;
4600
+ const s = style as Record<string, string>;
4601
+ const css: Record<string, string | number> = {};
4602
+ if (s.paddingY && STYLE_SPACE_PX[s.paddingY] != null) { css.paddingTop = STYLE_SPACE_PX[s.paddingY]!; css.paddingBottom = STYLE_SPACE_PX[s.paddingY]!; }
4603
+ if (s.paddingX && STYLE_SPACE_PX[s.paddingX] != null) { css.paddingLeft = STYLE_SPACE_PX[s.paddingX]!; css.paddingRight = STYLE_SPACE_PX[s.paddingX]!; }
4604
+ const top = styleSpaceToken(s.spaceTop);
4605
+ // A negative band that pulls content under the nav makes sense at the TOP only (SPACE_BOTTOM_TOKENS).
4606
+ const bottom = /^section-space-(none|small|medium|large|xlarge)$/.test(String(s.spaceBottom)) ? styleSpaceToken(s.spaceBottom) : "";
4607
+ const sides = styleSpaceToken(s.spaceSides);
4608
+ const navOverlap = s.spaceTop === "section-space-nav-overlap";
4609
+ if (top && !navOverlap) css.paddingTop = top;
4610
+ if (bottom) css.paddingBottom = bottom;
4611
+ if (sides) { css.paddingLeft = sides; css.paddingRight = sides; }
4612
+ if (s.marginTop && STYLE_SPACE_PX[s.marginTop] != null) css.marginTop = STYLE_SPACE_PX[s.marginTop]!;
4613
+ if (top && navOverlap) { css.marginTop = top; css.paddingTop = "calc(-1 * " + top + ")"; }
4614
+ if (s.marginBottom && STYLE_SPACE_PX[s.marginBottom] != null) css.marginBottom = STYLE_SPACE_PX[s.marginBottom]!;
4615
+ if (s.background && STYLE_COLOR_HEX[s.background]) css.background = STYLE_COLOR_HEX[s.background]!;
4616
+ if (s.textColor && STYLE_COLOR_HEX[s.textColor]) css.color = STYLE_COLOR_HEX[s.textColor]!;
4617
+ if (s.align === "left" || s.align === "center" || s.align === "right") css.textAlign = s.align;
4618
+ if (s.fontSize && STYLE_FONT_SIZE_REM[s.fontSize]) css.fontSize = STYLE_FONT_SIZE_REM[s.fontSize]!;
4619
+ if (s.weight && STYLE_WEIGHT[s.weight] != null) css.fontWeight = STYLE_WEIGHT[s.weight]!;
4620
+ if (s.radius && STYLE_RADIUS_PX[s.radius] != null) css.borderRadius = STYLE_RADIUS_PX[s.radius]!;
4621
+ const theme = styleOwn(STYLE_THEME, s.theme);
4622
+ if (theme) { css.colorScheme = theme.colorScheme; css.background = theme.background; css.color = theme.color; }
4623
+ if (s.bg === "custom") { if (/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(s.bgCustom ?? "")) css.background = s.bgCustom!; }
4624
+ else if (styleOwn(STYLE_BG, s.bg)) css.background = styleOwn(STYLE_BG, s.bg)!;
4625
+ const bgImage = styleSafeUrl(s.bgImage);
4626
+ if (bgImage) { css.backgroundImage = "url(" + bgImage + ")"; css.backgroundSize = "cover"; css.backgroundPosition = "center"; }
4627
+ const n = style as Record<string, unknown>;
4628
+ if (!top && typeof n.paddingTop === "number") css.paddingTop = styleClamp(n.paddingTop, 0, 240) + "px";
4629
+ if (!bottom && typeof n.paddingBottom === "number") css.paddingBottom = styleClamp(n.paddingBottom, 0, 240) + "px";
4630
+ if (!sides && typeof n.paddingSides === "number") {
4631
+ const px = styleClamp(n.paddingSides, 0, 240) + "px";
4632
+ css.paddingLeft = px; css.paddingRight = px;
4633
+ }
4634
+ if (n.fullHeight === true) { css.minHeight = "100vh"; css.display = "flex"; css.flexDirection = "column"; css.justifyContent = "center"; }
4635
+ if (typeof n.opacity === "number") css.opacity = styleClamp(n.opacity, 0, 100) / 100;
4636
+ if (styleOwn(STYLE_CORNER_PX, s.corner) != null) { css.borderRadius = styleOwn(STYLE_CORNER_PX, s.corner)!; css.overflow = "hidden"; }
4637
+ if (styleOwn(STYLE_SHADOW, s.shadow)) css.boxShadow = styleOwn(STYLE_SHADOW, s.shadow)!;
4638
+ if (n.borderTop === true) css.borderTop = STYLE_HAIRLINE;
4639
+ if (n.borderBottom === true) css.borderBottom = STYLE_HAIRLINE;
4640
+ if ((style as { border?: unknown }).border === true) css.border = "1px solid currentColor";
4641
+ return Object.keys(css).length ? css : undefined;
4642
+ }
4643
+
4644
+ /** The same declarations as an Astro \`style\` string (kebab-case, \`;\`-joined), or undefined. */
4645
+ export function bcmsSectionStyleText(style: unknown): string | undefined {
4646
+ const css = bcmsSectionStyle(style);
4647
+ if (!css) return undefined;
4648
+ return Object.entries(css)
4649
+ .map(([k, v]) => k.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase()) + ":" + v)
4650
+ .join(";");
4651
+ }
4514
4652
  `;
4515
4653
  }
4516
4654
  var HISTORICAL = {
@@ -4522,7 +4660,9 @@ var HISTORICAL = {
4522
4660
  5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
4523
4661
  6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649",
4524
4662
  7: "f22514ef48c50230985e605bea6cf9bf41756384a7dc80603e347baba9b65d93",
4525
- 8: "484e72852d2b496e016a1f2a2b1a77bb993f8a163b290781522d6114b6519bb9"
4663
+ 8: "484e72852d2b496e016a1f2a2b1a77bb993f8a163b290781522d6114b6519bb9",
4664
+ 9: "d6441db2ff4578c3c5d4b7d461f3374b89247ef6ba9e0d535791e840d803e4f3",
4665
+ 10: "ffcb59862972b15a67c5951e9f3b94ce5b8b50345e6aac2bfa5789cfc3136910"
4526
4666
  },
4527
4667
  "src/lib/bcms-content.ts": {
4528
4668
  1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
@@ -4532,7 +4672,9 @@ var HISTORICAL = {
4532
4672
  5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba",
4533
4673
  6: "63f1fa29e47e70c5193b81ee24addb238ba100247124d55bc17713102d587df5",
4534
4674
  7: "ed92fb88de953d9951abbe6a1edae1d048f4050eaf6fb607ac08167f94d8bcc7",
4535
- 8: "1a6f669d76d42effe77f082436e9d21c0efa6fc499e919e3d116bc3a27a7f3d5"
4675
+ 8: "1a6f669d76d42effe77f082436e9d21c0efa6fc499e919e3d116bc3a27a7f3d5",
4676
+ 9: "eee9fb1635ae7d6f8f33676a1f390600ca24a62ee992775252273c73a011c494",
4677
+ 10: "35bb7c0e30f5c41fe68e52259ec4764de6de2734064fa0e1521f3338eef0a09e"
4536
4678
  },
4537
4679
  "composables/bcms-content.ts": {
4538
4680
  1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
@@ -4542,7 +4684,9 @@ var HISTORICAL = {
4542
4684
  5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
4543
4685
  6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649",
4544
4686
  7: "f22514ef48c50230985e605bea6cf9bf41756384a7dc80603e347baba9b65d93",
4545
- 8: "484e72852d2b496e016a1f2a2b1a77bb993f8a163b290781522d6114b6519bb9"
4687
+ 8: "484e72852d2b496e016a1f2a2b1a77bb993f8a163b290781522d6114b6519bb9",
4688
+ 9: "d6441db2ff4578c3c5d4b7d461f3374b89247ef6ba9e0d535791e840d803e4f3",
4689
+ 10: "ffcb59862972b15a67c5951e9f3b94ce5b8b50345e6aac2bfa5789cfc3136910"
4546
4690
  }
4547
4691
  };
4548
4692
  var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
@@ -8320,7 +8464,9 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
8320
8464
  ]);
8321
8465
  const fieldsName = introduced.get("bcmsFields");
8322
8466
  const fieldName = introduced.get("bcmsField");
8323
- const { markup, bound, duplicates } = sectionMarkup(content, resolved.root, fieldName);
8467
+ const styleReader = dialect === "astro" ? "bcmsSectionStyleText" : "bcmsSectionStyle";
8468
+ const styled = rootStyleIsFree(content, resolved.root) && !reserved.has("blockStyle") && !reserved.has(styleReader);
8469
+ const { markup, bound, duplicates } = sectionMarkup(content, resolved.root, fieldName, styled ? styleReader : null);
8324
8470
  if (duplicates.length > 0) return { content: "", free: [], duplicates, unconvertible: [] };
8325
8471
  const groupKey = resolved.section.groupKey;
8326
8472
  const locals = helperLocals(parsed, dialect, fromFile);
@@ -8331,7 +8477,8 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
8331
8477
  .../* @__PURE__ */ new Set([
8332
8478
  ...helperNames.map((h) => alias(h.name, h.local)),
8333
8479
  flat3 ? "bcmsSectionPaths" : "bcmsSection",
8334
- ...bound > 0 ? [alias("bcmsFields", fieldsName)] : []
8480
+ ...bound > 0 ? [alias("bcmsFields", fieldsName)] : [],
8481
+ ...styled ? [styleReader] : []
8335
8482
  ])
8336
8483
  ].sort();
8337
8484
  const helperSpecifier = relativeImport(path, HELPER_PATH[dialect]).replace(/\.tsx?$/, "");
@@ -8380,9 +8527,11 @@ interface Props {
8380
8527
  /** The instance's own copy, when an editor gave it any. Wins over the bound group. */
8381
8528
  overrides?: Record<string, unknown>;
8382
8529
  /** The page snapshot, so one component can serve every page whose group has this shape. */
8383
- page?: unknown;
8530
+ page?: unknown;${styled ? `
8531
+ /** The placement's \`block.style\` \u2014 painted on the root, which says so with \`data-bcms-style\`. */
8532
+ blockStyle?: Record<string, unknown>;` : ""}
8384
8533
  }
8385
- const { blockId, bind, bindPaths, overrides, page } = Astro.props;
8534
+ const { blockId, bind, bindPaths, overrides, page${styled ? ", blockStyle" : ""} } = Astro.props;
8386
8535
  ${carriedDeclarations}${declared}
8387
8536
  ---
8388
8537
  ${markup}
@@ -8394,7 +8543,7 @@ ${styles}
8394
8543
  ${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`, `import type { BcmsSectionProps } from "${relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "")}";`].join("\n")}
8395
8544
  ${carriedDeclarations === "" ? "" : `
8396
8545
  ${carriedDeclarations}`}
8397
- export default function ${resolved.name}({ blockId, bind, bindPaths, overrides, page }: BcmsSectionProps) {
8546
+ export default function ${resolved.name}({ blockId, bind, bindPaths, overrides, page${styled ? ", blockStyle" : ""} }: BcmsSectionProps) {
8398
8547
  ${declared.split("\n").join("\n ")}
8399
8548
  return (
8400
8549
  ${markup}
@@ -8402,13 +8551,21 @@ export default function ${resolved.name}({ blockId, bind, bindPaths, overrides,
8402
8551
  }
8403
8552
  ` };
8404
8553
  }
8405
- function sectionMarkup(content, root, fieldName) {
8554
+ function rootStyleIsFree(content, root) {
8555
+ return root.attrs.every((attr) => {
8556
+ if (attr.name === "style") return false;
8557
+ const open = attr.expressionRange ? content.slice(attr.expressionRange.start, attr.expressionRange.end).trimStart() : "";
8558
+ return !open.startsWith("{...");
8559
+ });
8560
+ }
8561
+ function sectionMarkup(content, root, fieldName, styleReader = null) {
8406
8562
  const slice = new MagicString5(content.slice(root.start, root.end));
8407
8563
  const at = (offset) => offset - root.start;
8564
+ const stamp = styleReader ? `data-bcms-block={blockId} data-bcms-style="" style={${styleReader}(blockStyle)}` : "data-bcms-block={blockId}";
8408
8565
  const stamps = root.attrs.filter((attr) => attr.name === "data-bcms-block");
8409
- if (stamps.length === 0) slice.appendLeft(at(root.attrInsertAt), " data-bcms-block={blockId}");
8566
+ if (stamps.length === 0) slice.appendLeft(at(root.attrInsertAt), ` ${stamp}`);
8410
8567
  else {
8411
- slice.overwrite(at(stamps[0].range.start), at(stamps[0].range.end), "data-bcms-block={blockId}");
8568
+ slice.overwrite(at(stamps[0].range.start), at(stamps[0].range.end), stamp);
8412
8569
  for (const extra of stamps.slice(1)) {
8413
8570
  let from = extra.range.start;
8414
8571
  while (from > root.start && /\s/.test(content[from - 1])) from--;
@@ -8759,6 +8916,8 @@ export interface BcmsSectionProps {
8759
8916
  bindPaths?: Record<string, string>;
8760
8917
  overrides?: Record<string, unknown>;
8761
8918
  page?: unknown;
8919
+ /** The placement's \`block.style\`, which the section root paints (\`data-bcms-style\`). */
8920
+ blockStyle?: Record<string, unknown>;
8762
8921
  }
8763
8922
 
8764
8923
  /** One placement on one page: which component, bound to which page field group. */
@@ -8769,6 +8928,8 @@ export interface BcmsPlacement {
8769
8928
  /** A locale copy's own field path for each prop, when its keys differ from the component's. */
8770
8929
  bindPaths?: Record<string, string>;
8771
8930
  overrides?: Record<string, unknown>;
8931
+ /** The placement's \`block.style\` \u2014 what the editor's section spacing and style controls write. */
8932
+ style?: Record<string, unknown>;
8772
8933
  }
8773
8934
 
8774
8935
  /** The page's own snapshot \u2014 the copy every placement on it reads through \`bind\`. */
@@ -8830,6 +8991,7 @@ export function pageSections(pageSlug: string): BcmsPlacement[] {
8830
8991
  bind: typeof props.bind === "string" ? props.bind : undefined,
8831
8992
  bindPaths: isRecord(props.bindPaths) ? (props.bindPaths as Record<string, string>) : undefined,
8832
8993
  overrides: isRecord(props.overrides) ? props.overrides : undefined,
8994
+ style: isRecord(block.style) ? block.style : undefined,
8833
8995
  });
8834
8996
  }
8835
8997
  return out;
@@ -8896,6 +9058,7 @@ const snapshot = pageSnapshot(page);
8896
9058
  bind={placement.bind}
8897
9059
  bindPaths={placement.bindPaths}
8898
9060
  overrides={placement.overrides}
9061
+ blockStyle={placement.style}
8899
9062
  page={snapshot}
8900
9063
  />
8901
9064
  ) : null;
@@ -8919,6 +9082,7 @@ export default function Sections({ page }: { page: string }) {
8919
9082
  bind={placement.bind}
8920
9083
  bindPaths={placement.bindPaths}
8921
9084
  overrides={placement.overrides}
9085
+ blockStyle={placement.style}
8922
9086
  page={snapshot}
8923
9087
  />
8924
9088
  ) : null;