@bettercms-ai/ui 0.4.0 → 0.5.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/renderer.js CHANGED
@@ -6,7 +6,9 @@ function Heading({
6
6
  text,
7
7
  level = 2,
8
8
  align = "left",
9
- className = ""
9
+ className = "",
10
+ "data-bcms-field": field,
11
+ "data-bcms-kind": kind
10
12
  }) {
11
13
  const alignClass = {
12
14
  left: "text-left",
@@ -16,13 +18,13 @@ function Heading({
16
18
  const classNames = `font-heading font-bold tracking-tight text-foreground ${alignClass} ${className}`;
17
19
  switch (level) {
18
20
  case 1:
19
- return /* @__PURE__ */ jsx("h1", { className: classNames, children: text });
21
+ return /* @__PURE__ */ jsx("h1", { className: classNames, "data-bcms-field": field, "data-bcms-kind": kind, children: text });
20
22
  case 2:
21
- return /* @__PURE__ */ jsx("h2", { className: classNames, children: text });
23
+ return /* @__PURE__ */ jsx("h2", { className: classNames, "data-bcms-field": field, "data-bcms-kind": kind, children: text });
22
24
  case 3:
23
- return /* @__PURE__ */ jsx("h3", { className: classNames, children: text });
25
+ return /* @__PURE__ */ jsx("h3", { className: classNames, "data-bcms-field": field, "data-bcms-kind": kind, children: text });
24
26
  default:
25
- return /* @__PURE__ */ jsx("h2", { className: classNames, children: text });
27
+ return /* @__PURE__ */ jsx("h2", { className: classNames, "data-bcms-field": field, "data-bcms-kind": kind, children: text });
26
28
  }
27
29
  }
28
30
 
@@ -68,7 +70,14 @@ function PortableText({ doc }) {
68
70
 
69
71
  // src/blocks/Text.tsx
70
72
  import { jsx as jsx3 } from "react/jsx-runtime";
71
- function Text({ html, content, align = "left", className = "" }) {
73
+ function Text({
74
+ html,
75
+ content,
76
+ align = "left",
77
+ className = "",
78
+ "data-bcms-field": field,
79
+ "data-bcms-kind": kind
80
+ }) {
72
81
  const alignClass = {
73
82
  left: "text-left",
74
83
  center: "text-center",
@@ -82,6 +91,8 @@ function Text({ html, content, align = "left", className = "" }) {
82
91
  "div",
83
92
  {
84
93
  className: cls,
94
+ "data-bcms-field": field,
95
+ "data-bcms-kind": kind,
85
96
  dangerouslySetInnerHTML: { __html: html }
86
97
  }
87
98
  );
@@ -103,7 +114,9 @@ function Image({
103
114
  height,
104
115
  caption,
105
116
  className = "",
106
- sizes
117
+ sizes,
118
+ "data-bcms-field": field,
119
+ "data-bcms-kind": kind
107
120
  }) {
108
121
  return /* @__PURE__ */ jsxs("figure", { className: cn("w-full", className), children: [
109
122
  /* @__PURE__ */ jsx4(
@@ -114,6 +127,8 @@ function Image({
114
127
  width,
115
128
  height,
116
129
  sizes,
130
+ "data-bcms-field": field,
131
+ "data-bcms-kind": kind,
117
132
  className: "w-full h-auto rounded-md object-cover",
118
133
  loading: "lazy"
119
134
  }
@@ -129,7 +144,9 @@ function Button({
129
144
  href,
130
145
  variant = "primary",
131
146
  align = "left",
132
- className = ""
147
+ className = "",
148
+ "data-bcms-field": field,
149
+ "data-bcms-kind": kind
133
150
  }) {
134
151
  const baseStyles = "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50";
135
152
  const variantStyles = {
@@ -145,6 +162,8 @@ function Button({
145
162
  "a",
146
163
  {
147
164
  href,
165
+ "data-bcms-field": field,
166
+ "data-bcms-kind": kind,
148
167
  className: cn(baseStyles, variantStyles[variant], alignClass, "px-4 py-2", className),
149
168
  children: text
150
169
  }
@@ -352,7 +371,7 @@ function Tabs({ tabs, className = "", renderBlock }) {
352
371
  }
353
372
 
354
373
  // src/blocks/Placeholders.tsx
355
- import { jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
374
+ import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
356
375
  var box = "rounded border border-dashed border-neutral-300 bg-neutral-50 p-4 text-sm text-neutral-500";
357
376
  function ComponentInstance({
358
377
  componentId,
@@ -369,9 +388,28 @@ function FormPlaceholder({ formId, className = "" }) {
369
388
  /* @__PURE__ */ jsx14("code", { children: formId })
370
389
  ] });
371
390
  }
391
+ function CollectionPlaceholder({
392
+ cardComponentId,
393
+ className = ""
394
+ }) {
395
+ return /* @__PURE__ */ jsxs8("div", { className: cn(box, className), children: [
396
+ "Collection: entries of this page",
397
+ cardComponentId ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
398
+ " as ",
399
+ /* @__PURE__ */ jsx14("code", { children: cardComponentId })
400
+ ] }) : /* @__PURE__ */ jsx14(Fragment2, { children: " (default card)" })
401
+ ] });
402
+ }
372
403
 
373
404
  // src/registry.ts
374
405
  var BLOCK_REGISTRY = {
406
+ // The entries of the page this block sits on. No preview here by design: the list is
407
+ // fetched per request by the site renderer, so the builder has nothing to show.
408
+ collection: {
409
+ component: CollectionPlaceholder,
410
+ label: "Collection",
411
+ icon: "list"
412
+ },
375
413
  heading: {
376
414
  component: Heading,
377
415
  label: "Heading",
@@ -463,7 +501,22 @@ function blockStyleToReactStyle(style) {
463
501
  import "react";
464
502
  import { jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
465
503
  var CONTAINER_TYPES = /* @__PURE__ */ new Set(["columns", "section", "slider", "tabs"]);
466
- function resolveBlock(block, className) {
504
+ function bindingProps(block, annotate) {
505
+ if (!annotate || !block.id) return {};
506
+ switch (block.type) {
507
+ case "heading":
508
+ case "button":
509
+ return { "data-bcms-field": `${block.id}__text`, "data-bcms-kind": "text" };
510
+ case "text":
511
+ case "richtext":
512
+ return { "data-bcms-field": `${block.id}__html`, "data-bcms-kind": "richtext" };
513
+ case "image":
514
+ return { "data-bcms-field": `${block.id}__src`, "data-bcms-kind": "image" };
515
+ default:
516
+ return {};
517
+ }
518
+ }
519
+ function resolveBlock(block, className, annotate = false) {
467
520
  const entry = BLOCK_REGISTRY[block.type];
468
521
  if (!entry) {
469
522
  return /* @__PURE__ */ jsxs9(
@@ -479,13 +532,14 @@ function resolveBlock(block, className) {
479
532
  }
480
533
  const Component = entry.component;
481
534
  const blockStyle = blockStyleToReactStyle(block.style);
482
- const el = /* @__PURE__ */ jsx15(Component, { ...block.props, className });
535
+ const el = /* @__PURE__ */ jsx15(Component, { ...block.props, className, ...bindingProps(block, annotate) });
483
536
  return blockStyle ? /* @__PURE__ */ jsx15("div", { style: blockStyle, children: el }) : el;
484
537
  }
485
538
  function BlockRenderer({
486
539
  block,
487
540
  className = "",
488
- renderBlock
541
+ renderBlock,
542
+ annotate = false
489
543
  }) {
490
544
  const entry = BLOCK_REGISTRY[block.type];
491
545
  if (!entry) {
@@ -501,22 +555,32 @@ function BlockRenderer({
501
555
  );
502
556
  }
503
557
  const Component = entry.component;
504
- const containerProps = CONTAINER_TYPES.has(block.type) ? { renderBlock: renderBlock ?? resolveBlock } : {};
558
+ const containerProps = CONTAINER_TYPES.has(block.type) ? { renderBlock: renderBlock ?? ((child) => resolveBlock(child, void 0, annotate)) } : {};
505
559
  const blockStyle = blockStyleToReactStyle(block.style);
506
- const el = /* @__PURE__ */ jsx15(Component, { ...block.props, className, ...containerProps });
560
+ const el = /* @__PURE__ */ jsx15(
561
+ Component,
562
+ {
563
+ ...block.props,
564
+ className,
565
+ ...containerProps,
566
+ ...bindingProps(block, annotate)
567
+ }
568
+ );
507
569
  return blockStyle ? /* @__PURE__ */ jsx15("div", { style: blockStyle, children: el }) : el;
508
570
  }
509
571
  function BlocksRenderer({
510
572
  blocks,
511
573
  className = "",
512
- renderBlock
574
+ renderBlock,
575
+ annotate = false
513
576
  }) {
514
577
  if (blocks.length === 0) return null;
515
578
  return /* @__PURE__ */ jsx15("div", { className, children: blocks.map((block) => /* @__PURE__ */ jsx15(
516
579
  BlockRenderer,
517
580
  {
518
581
  block,
519
- renderBlock
582
+ renderBlock,
583
+ annotate
520
584
  },
521
585
  block.id
522
586
  )) });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/blocks/Heading.tsx","../src/blocks/portableText.tsx","../src/blocks/Text.tsx","../src/lib/utils.ts","../src/blocks/Image.tsx","../src/blocks/Button.tsx","../src/blocks/Spacer.tsx","../src/blocks/Columns.tsx","../src/blocks/Video.tsx","../src/blocks/Section.tsx","../src/blocks/Navbar.tsx","../src/blocks/Footer.tsx","../src/blocks/Slider.tsx","../src/blocks/Tabs.tsx","../src/blocks/Placeholders.tsx","../src/registry.ts","../src/blockStyle.ts","../src/renderer.tsx"],"sourcesContent":["import type { HeadingProps } from \"@bettercms-ai/types\";\n\ninterface HeadingBlockProps extends HeadingProps {\n className?: string;\n align?: \"left\" | \"center\" | \"right\";\n}\n\nexport function Heading({\n text,\n level = 2,\n align = \"left\",\n className = \"\",\n}: HeadingBlockProps) {\n const alignClass = {\n left: \"text-left\",\n center: \"text-center\",\n right: \"text-right\",\n }[align];\n\n const classNames = `font-heading font-bold tracking-tight text-foreground ${alignClass} ${className}`;\n\n switch (level) {\n case 1:\n return <h1 className={classNames}>{text}</h1>;\n case 2:\n return <h2 className={classNames}>{text}</h2>;\n case 3:\n return <h3 className={classNames}>{text}</h3>;\n default:\n return <h2 className={classNames}>{text}</h2>;\n }\n}\n","import React from \"react\";\nimport type { PortableTextDocument, PortableTextNode } from \"@bettercms-ai/types\";\n\n/**\n * Portable Text → React.\n *\n * React rather than an HTML string, deliberately: React escapes text children by default, so\n * author data cannot become markup here at all. The `dangerouslySetInnerHTML` path that the\n * `html` cache still uses has to be trusted; this one does not.\n *\n * Unknown marks and unknown styles are DROPPED, never passed through. A colour mark carries a\n * token NAME (`color:brand-accent`) which becomes a class the consuming site styles — so it is\n * theme-aware and survives a rebrand, which a baked hex does not.\n */\n\nconst MARK_TAGS: Record<string, keyof React.JSX.IntrinsicElements> = {\n strong: \"strong\",\n em: \"em\",\n underline: \"u\",\n \"strike-through\": \"s\",\n code: \"code\",\n};\n\nconst BLOCK_TAGS: Record<string, keyof React.JSX.IntrinsicElements> = {\n normal: \"p\",\n h1: \"h1\",\n h2: \"h2\",\n h3: \"h3\",\n h4: \"h4\",\n h5: \"h5\",\n h6: \"h6\",\n blockquote: \"blockquote\",\n};\n\nfunction Span({ node, index }: { node: PortableTextNode; index: number }) {\n const marks = node.marks ?? [];\n const colour = marks.find((m) => m.startsWith(\"color:\"))?.slice(\"color:\".length);\n\n // Text is a React child, so it is escaped by construction.\n let out: React.ReactNode = node.text ?? \"\";\n for (const m of marks) {\n const Tag = MARK_TAGS[m];\n if (Tag) out = <Tag>{out}</Tag>;\n }\n if (colour && /^[a-z0-9-]+$/i.test(colour)) {\n out = <span className={`bcms-color-${colour}`}>{out}</span>;\n }\n return <React.Fragment key={node._key ?? index}>{out}</React.Fragment>;\n}\n\nexport function PortableText({ doc }: { doc: PortableTextDocument }) {\n return (\n <>\n {doc.content.map((block, i) => {\n const Tag = BLOCK_TAGS[block.style ?? \"normal\"] ?? \"p\";\n return (\n <Tag key={block._key ?? i}>\n {(block.children ?? []).map((c, j) => (\n <Span key={c._key ?? j} node={c} index={j} />\n ))}\n </Tag>\n );\n })}\n </>\n );\n}\n","import type { PortableTextDocument, TextProps } from \"@bettercms-ai/types\";\nimport { PortableText } from \"./portableText\";\n\ninterface TextBlockProps extends TextProps {\n className?: string;\n align?: \"left\" | \"center\" | \"right\";\n /** Portable Text source. WINS over `html`, which is the derived cache. */\n content?: PortableTextDocument;\n}\n\nexport function Text({ html, content, align = \"left\", className = \"\" }: TextBlockProps) {\n const alignClass = {\n left: \"text-left\",\n center: \"text-center\",\n right: \"text-right\",\n }[align];\n\n const cls = `text-muted-foreground leading-relaxed ${alignClass} ${className}`;\n\n // `content` wins when present — it is the source of truth and renders through React, which\n // escapes by construction. `html` remains the fallback so every block authored before\n // Portable Text existed renders exactly as it always did.\n if (content && content.content.length > 0) {\n return (\n <div className={cls}>\n <PortableText doc={content} />\n </div>\n );\n }\n\n return (\n <div\n className={cls}\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{ __html: html }}\n />\n );\n}\n","import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","import type { ImageProps } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\n\ninterface ImageBlockProps extends ImageProps {\n className?: string;\n sizes?: string;\n}\n\nexport function Image({\n src,\n alt,\n width,\n height,\n caption,\n className = \"\",\n sizes,\n}: ImageBlockProps) {\n return (\n <figure className={cn(\"w-full\", className)}>\n <img\n src={src}\n alt={alt}\n width={width}\n height={height}\n sizes={sizes}\n className=\"w-full h-auto rounded-md object-cover\"\n loading=\"lazy\"\n />\n {caption && (\n <figcaption className=\"mt-2 text-sm text-muted-foreground text-center italic\">\n {caption}\n </figcaption>\n )}\n </figure>\n );\n}\n","import type { ButtonProps } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\n\ninterface ButtonBlockProps extends ButtonProps {\n className?: string;\n align?: \"left\" | \"center\" | \"right\";\n}\n\nexport function Button({\n text,\n href,\n variant = \"primary\",\n align = \"left\",\n className = \"\",\n}: ButtonBlockProps) {\n const baseStyles =\n \"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\";\n\n const variantStyles = {\n primary:\n \"bg-primary text-primary-foreground shadow hover:bg-primary/90\",\n secondary:\n \"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80\",\n };\n\n const alignClass = {\n left: \"justify-start\",\n center: \"justify-center\",\n right: \"justify-end\",\n }[align];\n\n return (\n <a\n href={href}\n className={cn(baseStyles, variantStyles[variant], alignClass, \"px-4 py-2\", className)}\n >\n {text}\n </a>\n );\n}\n","import type { SpacerProps } from \"@bettercms-ai/types\";\n\ninterface SpacerBlockProps extends SpacerProps {\n className?: string;\n}\n\nexport function Spacer({ height = 32, className = \"\" }: SpacerBlockProps) {\n return (\n <div\n className={`w-full border border-dashed border-muted-foreground/20 rounded flex items-center justify-center ${className}`}\n style={{ height: `${height}px` }}\n aria-hidden=\"true\"\n >\n <span className=\"text-xs text-muted-foreground/40 select-none\">\n {height}px\n </span>\n </div>\n );\n}\n","import type { ColumnsProps, ContentBlock } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\nimport React from \"react\";\n\ninterface ColumnsBlockProps extends ColumnsProps {\n className?: string;\n renderBlock?: (block: ContentBlock) => React.ReactNode;\n}\n\nexport function Columns({\n columns,\n gap = 16,\n className = \"\",\n renderBlock,\n}: ColumnsBlockProps) {\n return (\n <div\n className={cn(\"grid w-full\", className)}\n style={{\n gridTemplateColumns: `repeat(${columns.length}, minmax(0, 1fr))`,\n gap: `${gap}px`,\n }}\n >\n {columns.map((columnBlocks, colIndex) => (\n <div key={colIndex} className=\"flex flex-col gap-4 min-w-0\">\n {columnBlocks.map((block) => (\n <span key={block.id}>\n {renderBlock ? renderBlock(block) : null}\n </span>\n ))}\n </div>\n ))}\n </div>\n );\n}\n","import type { VideoProps } from \"@bettercms-ai/types\";\nimport { useState } from \"react\";\nimport { cn } from \"../lib/utils\";\n\ninterface VideoBlockProps extends VideoProps {\n className?: string;\n}\n\nfunction getEmbedUrl(url: string): { embedUrl: string; provider: \"youtube\" | \"vimeo\" | \"unknown\" } {\n // YouTube\n const ytMatch = url.match(\n /(?:youtube\\.com\\/(?:watch\\?v=|embed\\/)|youtu\\.be\\/)([a-zA-Z0-9_-]{11})/\n );\n if (ytMatch) {\n return {\n embedUrl: `https://www.youtube.com/embed/${ytMatch[1]}`,\n provider: \"youtube\",\n };\n }\n\n // Vimeo\n const vimeoMatch = url.match(/vimeo\\.com\\/(\\d+)/);\n if (vimeoMatch) {\n return {\n embedUrl: `https://player.vimeo.com/video/${vimeoMatch[1]}`,\n provider: \"vimeo\",\n };\n }\n\n return { embedUrl: url, provider: \"unknown\" };\n}\n\nexport function Video({\n url,\n poster,\n autoplay = false,\n loop = false,\n className = \"\",\n}: VideoBlockProps) {\n const [isLoaded, setIsLoaded] = useState(false);\n const { embedUrl, provider } = getEmbedUrl(url);\n\n // If not a recognized embed, render a native video tag\n if (provider === \"unknown\") {\n return (\n <video\n src={url}\n poster={poster}\n autoPlay={autoplay}\n loop={loop}\n controls\n className={className}\n playsInline\n />\n );\n }\n\n return (\n <div className={cn(\"w-full overflow-hidden rounded-md bg-black aspect-video\", className)}>\n {!isLoaded && poster && (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={poster}\n alt=\"Video thumbnail\"\n className=\"w-full h-full object-cover absolute inset-0\"\n />\n )}\n <iframe\n src={`${embedUrl}?autoplay=${autoplay ? 1 : 0}&loop=${loop ? 1 : 0}&mute=${autoplay ? 1 : 0}`}\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n loading=\"lazy\"\n onLoad={() => setIsLoaded(true)}\n className=\"w-full h-full\"\n />\n </div>\n );\n}\n","import type { SectionProps, ContentBlock } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\nimport React from \"react\";\n\ninterface SectionBlockProps extends SectionProps {\n className?: string;\n renderBlock?: (block: ContentBlock) => React.ReactNode;\n}\n\nexport function Section({\n children,\n background,\n paddingY,\n maxWidth,\n className = \"\",\n renderBlock,\n}: SectionBlockProps) {\n const inner = (children ?? []).map((block) => (\n <span key={block.id}>{renderBlock ? renderBlock(block) : null}</span>\n ));\n return (\n <section\n className={cn(\"w-full\", className)}\n style={{ background, paddingTop: paddingY, paddingBottom: paddingY }}\n >\n {maxWidth ? <div style={{ maxWidth, marginInline: \"auto\" }}>{inner}</div> : inner}\n </section>\n );\n}\n","import type { NavbarProps } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\n\ninterface NavbarBlockProps extends NavbarProps {\n className?: string;\n}\n\nexport function Navbar({ logo, links, cta, className = \"\" }: NavbarBlockProps) {\n return (\n <nav className={cn(\"flex items-center gap-6 py-4\", className)}>\n {logo?.src ? (\n <a href={logo.href ?? \"/\"} className=\"shrink-0\">\n <img src={logo.src} alt={logo.alt} className=\"h-8 w-auto\" />\n </a>\n ) : null}\n <div className=\"flex flex-1 gap-5\">\n {(links ?? []).map((l, i) => (\n <a key={i} href={l.href} className=\"text-inherit no-underline\">\n {l.label}\n </a>\n ))}\n </div>\n {cta ? (\n <a\n href={cta.href}\n className=\"rounded-lg bg-black px-4 py-2 font-semibold text-white no-underline\"\n >\n {cta.text}\n </a>\n ) : null}\n </nav>\n );\n}\n","import type { FooterProps } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\n\ninterface FooterBlockProps extends FooterProps {\n className?: string;\n}\n\nexport function Footer({ columns, copyright, className = \"\" }: FooterBlockProps) {\n return (\n <footer className={cn(\"mt-12 border-t pt-8\", className)}>\n <div className=\"flex flex-wrap gap-12\">\n {(columns ?? []).map((col, i) => (\n <div key={i} className=\"flex flex-col gap-1.5\">\n {col.heading ? <h4 className=\"mb-2 font-semibold\">{col.heading}</h4> : null}\n {(col.links ?? []).map((l, j) => (\n <a key={j} href={l.href} className=\"text-neutral-500 no-underline\">\n {l.label}\n </a>\n ))}\n </div>\n ))}\n </div>\n {copyright ? <p className=\"mt-6 text-sm text-neutral-500\">{copyright}</p> : null}\n </footer>\n );\n}\n","\"use client\";\n\nimport type { SliderProps, ContentBlock } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\nimport React, { useState } from \"react\";\n\ninterface SliderBlockProps extends SliderProps {\n className?: string;\n renderBlock?: (block: ContentBlock) => React.ReactNode;\n}\n\nexport function Slider({ slides, className = \"\", renderBlock }: SliderBlockProps) {\n const [i, setI] = useState(0);\n const list = slides ?? [];\n const go = (n: number) => setI((n + list.length) % (list.length || 1));\n const btn =\n \"absolute top-1/2 -translate-y-1/2 flex h-9 w-9 items-center justify-center rounded-full border-0 bg-black/50 text-white cursor-pointer\";\n return (\n <div className={cn(\"relative w-full\", className)}>\n {list.map((s, idx) => (\n <div key={s.id} hidden={idx !== i} className=\"flex flex-col gap-4\">\n {(s.children ?? []).map((b) => (\n <span key={b.id}>{renderBlock ? renderBlock(b) : null}</span>\n ))}\n </div>\n ))}\n <button type=\"button\" aria-label=\"Previous\" onClick={() => go(i - 1)} className={cn(btn, \"left-2\")}>\n &#8249;\n </button>\n <button type=\"button\" aria-label=\"Next\" onClick={() => go(i + 1)} className={cn(btn, \"right-2\")}>\n &#8250;\n </button>\n </div>\n );\n}\n","\"use client\";\n\nimport type { TabsProps, ContentBlock } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\nimport React, { useState } from \"react\";\n\ninterface TabsBlockProps extends TabsProps {\n className?: string;\n renderBlock?: (block: ContentBlock) => React.ReactNode;\n}\n\nexport function Tabs({ tabs, className = \"\", renderBlock }: TabsBlockProps) {\n const [active, setActive] = useState(0);\n const list = tabs ?? [];\n return (\n <div className={cn(\"w-full\", className)}>\n <div role=\"tablist\" className=\"mb-4 flex gap-1 border-b\">\n {list.map((t, idx) => (\n <button\n key={t.id}\n role=\"tab\"\n type=\"button\"\n aria-selected={idx === active}\n onClick={() => setActive(idx)}\n className={cn(\n \"cursor-pointer border-b-2 border-transparent bg-transparent px-4 py-2\",\n idx === active && \"border-black font-semibold\",\n )}\n >\n {t.label}\n </button>\n ))}\n </div>\n {list.map((t, idx) => (\n <div key={t.id} role=\"tabpanel\" hidden={idx !== active} className=\"flex flex-col gap-4\">\n {(t.children ?? []).map((b) => (\n <span key={b.id}>{renderBlock ? renderBlock(b) : null}</span>\n ))}\n </div>\n ))}\n </div>\n );\n}\n","import type { ComponentProps as BcmsComponentProps, FormProps } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\n\nconst box =\n \"rounded border border-dashed border-neutral-300 bg-neutral-50 p-4 text-sm text-neutral-500\";\n\n/**\n * Builder-preview placeholder for a `component` instance. Live instance resolution\n * (definition lookup + override merge) happens server-side in the delivery renderer\n * and in the page-builder, which pass a resolver — the standalone library preview\n * just shows what's referenced.\n */\nexport function ComponentInstance({\n componentId,\n className = \"\",\n}: BcmsComponentProps & { className?: string }) {\n return (\n <div className={cn(box, className)}>\n Component instance: <code>{componentId}</code>\n </div>\n );\n}\n\n/** Builder-preview placeholder for a `form` block (rendered live by @bettercms-ai/next). */\nexport function FormPlaceholder({ formId, className = \"\" }: FormProps & { className?: string }) {\n return (\n <div className={cn(box, className)}>\n Form: <code>{formId}</code>\n </div>\n );\n}\n","import type { ContentBlock, BlockType } from \"@bettercms-ai/types\";\nimport { Heading } from \"./blocks/Heading\";\nimport { Text } from \"./blocks/Text\";\nimport { Image } from \"./blocks/Image\";\nimport { Button } from \"./blocks/Button\";\nimport { Spacer } from \"./blocks/Spacer\";\nimport { Columns } from \"./blocks/Columns\";\nimport { Video } from \"./blocks/Video\";\nimport { Section } from \"./blocks/Section\";\nimport { Navbar } from \"./blocks/Navbar\";\nimport { Footer } from \"./blocks/Footer\";\nimport { Slider } from \"./blocks/Slider\";\nimport { Tabs } from \"./blocks/Tabs\";\nimport { ComponentInstance, FormPlaceholder } from \"./blocks/Placeholders\";\n\n/**\n * Block registry entry — maps a block type to its React component\n * and metadata for the block library UI.\n */\nexport interface BlockRegistryEntry {\n component: React.ComponentType<Record<string, unknown>>;\n label: string;\n icon: string;\n}\n\nexport const BLOCK_REGISTRY: Record<BlockType, BlockRegistryEntry> = {\n heading: {\n component: Heading as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Heading\",\n icon: \"type\",\n },\n text: {\n component: Text as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Text\",\n icon: \"align-left\",\n },\n // Same payload, same renderer — deliberately. `richtext` differs from `text` only in how\n // an author EDITS it (a formatting surface vs a plain field); at delivery both are an HTML\n // string, so duplicating the component would be two things to keep in sync for no gain.\n richtext: {\n component: Text as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Rich text\",\n icon: \"pilcrow\",\n },\n image: {\n component: Image as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Image\",\n icon: \"image\",\n },\n button: {\n component: Button as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Button\",\n icon: \"square\",\n },\n spacer: {\n component: Spacer as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Spacer\",\n icon: \"minus\",\n },\n columns: {\n component: Columns as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Columns\",\n icon: \"columns\",\n },\n video: {\n component: Video as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Video\",\n icon: \"play\",\n },\n section: {\n component: Section as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Section\",\n icon: \"layout\",\n },\n navbar: {\n component: Navbar as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Navbar\",\n icon: \"menu\",\n },\n footer: {\n component: Footer as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Footer\",\n icon: \"panel-bottom\",\n },\n slider: {\n component: Slider as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Slider\",\n icon: \"gallery-horizontal\",\n },\n tabs: {\n component: Tabs as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Tabs\",\n icon: \"folder\",\n },\n component: {\n component: ComponentInstance as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Component\",\n icon: \"component\",\n },\n form: {\n component: FormPlaceholder as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Form\",\n icon: \"clipboard-list\",\n },\n} as const;\n\nexport const BLOCK_TYPES = Object.keys(BLOCK_REGISTRY) as BlockType[];\n\nexport type { ContentBlock, BlockType };\n","import type { CSSProperties } from \"react\";\nimport { blockStyleToCss, type BlockStyle } from \"@bettercms-ai/types\";\n\n/**\n * FLO-264 — React-typed wrapper over the shared, framework-agnostic\n * `blockStyleToCss` in @bettercms-ai/types. One mapping, three consumers (this UI\n * renderer, the @bettercms-ai/next adapter, and — in string form — the hosted\n * render-page.ts).\n */\nexport function blockStyleToReactStyle(style: BlockStyle | undefined): CSSProperties | undefined {\n return blockStyleToCss(style) as CSSProperties | undefined;\n}\n","import type { ContentBlock } from \"@bettercms-ai/types\";\nimport { BLOCK_REGISTRY, type BlockRegistryEntry } from \"./registry\";\nimport { blockStyleToReactStyle } from \"./blockStyle\";\nimport React from \"react\";\n\n// Container blocks hold nested blocks and need the `renderBlock` callback threaded\n// so their children render recursively.\nconst CONTAINER_TYPES = new Set([\"columns\", \"section\", \"slider\", \"tabs\"]);\n\ninterface BlockRendererProps {\n block: ContentBlock;\n className?: string;\n renderBlock?: (block: ContentBlock) => React.ReactNode;\n}\n\nfunction resolveBlock(block: ContentBlock, className?: string): React.ReactNode {\n const entry: BlockRegistryEntry | undefined = BLOCK_REGISTRY[block.type];\n\n if (!entry) {\n return (\n <div\n className={`w-full p-4 border border-dashed border-red-400 bg-red-50 text-red-600 text-sm rounded ${className ?? \"\"}`}\n >\n Unknown block type: <code>{block.type}</code>\n </div>\n );\n }\n\n const Component = entry.component;\n const blockStyle = blockStyleToReactStyle(block.style);\n const el = <Component {...block.props} className={className} />;\n return blockStyle ? <div style={blockStyle}>{el}</div> : el;\n}\n\nexport function BlockRenderer({\n block,\n className = \"\",\n renderBlock,\n}: BlockRendererProps) {\n const entry: BlockRegistryEntry | undefined = BLOCK_REGISTRY[block.type];\n\n if (!entry) {\n return (\n <div\n className={`w-full p-4 border border-dashed border-red-400 bg-red-50 text-red-600 text-sm rounded ${className}`}\n >\n Unknown block type: <code>{block.type}</code>\n </div>\n );\n }\n\n // Container blocks (columns/section/slider/tabs) hold nested blocks, so thread the\n // renderBlock callback. Standalone usage (no BlocksRenderer above) falls back to\n // resolveBlock so one level of nesting still renders.\n const Component = entry.component;\n const containerProps = CONTAINER_TYPES.has(block.type)\n ? { renderBlock: renderBlock ?? resolveBlock }\n : {};\n const blockStyle = blockStyleToReactStyle(block.style);\n const el = <Component {...block.props} className={className} {...containerProps} />;\n return blockStyle ? <div style={blockStyle}>{el}</div> : el;\n}\n\ninterface BlocksRendererProps {\n blocks: ContentBlock[];\n className?: string;\n renderBlock?: (block: ContentBlock) => React.ReactNode;\n}\n\nexport function BlocksRenderer({\n blocks,\n className = \"\",\n renderBlock,\n}: BlocksRendererProps) {\n if (blocks.length === 0) return null;\n\n return (\n <div className={className}>\n {blocks.map((block) => (\n <BlockRenderer\n key={block.id}\n block={block}\n renderBlock={renderBlock}\n />\n ))}\n </div>\n );\n}\n"],"mappings":"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","names":["jsx","jsx","jsx","jsx","jsx","jsxs","jsx","jsx","jsxs","jsx","jsx","jsxs","jsx","jsxs","useState","jsx","jsxs","useState","jsx","jsxs","jsx","jsxs","jsx","jsxs"]}
1
+ {"version":3,"sources":["../src/blocks/Heading.tsx","../src/blocks/portableText.tsx","../src/blocks/Text.tsx","../src/lib/utils.ts","../src/blocks/Image.tsx","../src/blocks/Button.tsx","../src/blocks/Spacer.tsx","../src/blocks/Columns.tsx","../src/blocks/Video.tsx","../src/blocks/Section.tsx","../src/blocks/Navbar.tsx","../src/blocks/Footer.tsx","../src/blocks/Slider.tsx","../src/blocks/Tabs.tsx","../src/blocks/Placeholders.tsx","../src/registry.ts","../src/blockStyle.ts","../src/renderer.tsx"],"sourcesContent":["import type { HeadingProps } from \"@bettercms-ai/types\";\nimport type { BcmsBindingProps } from \"../binding\";\n\ninterface HeadingBlockProps extends HeadingProps, BcmsBindingProps {\n className?: string;\n align?: \"left\" | \"center\" | \"right\";\n}\n\nexport function Heading({\n text,\n level = 2,\n align = \"left\",\n className = \"\",\n \"data-bcms-field\": field,\n \"data-bcms-kind\": kind,\n}: HeadingBlockProps) {\n const alignClass = {\n left: \"text-left\",\n center: \"text-center\",\n right: \"text-right\",\n }[align];\n\n const classNames = `font-heading font-bold tracking-tight text-foreground ${alignClass} ${className}`;\n\n switch (level) {\n case 1:\n return <h1 className={classNames} data-bcms-field={field} data-bcms-kind={kind}>{text}</h1>;\n case 2:\n return <h2 className={classNames} data-bcms-field={field} data-bcms-kind={kind}>{text}</h2>;\n case 3:\n return <h3 className={classNames} data-bcms-field={field} data-bcms-kind={kind}>{text}</h3>;\n default:\n return <h2 className={classNames} data-bcms-field={field} data-bcms-kind={kind}>{text}</h2>;\n }\n}\n","import React from \"react\";\nimport type { PortableTextDocument, PortableTextNode } from \"@bettercms-ai/types\";\n\n/**\n * Portable Text → React.\n *\n * React rather than an HTML string, deliberately: React escapes text children by default, so\n * author data cannot become markup here at all. The `dangerouslySetInnerHTML` path that the\n * `html` cache still uses has to be trusted; this one does not.\n *\n * Unknown marks and unknown styles are DROPPED, never passed through. A colour mark carries a\n * token NAME (`color:brand-accent`) which becomes a class the consuming site styles — so it is\n * theme-aware and survives a rebrand, which a baked hex does not.\n */\n\nconst MARK_TAGS: Record<string, keyof React.JSX.IntrinsicElements> = {\n strong: \"strong\",\n em: \"em\",\n underline: \"u\",\n \"strike-through\": \"s\",\n code: \"code\",\n};\n\nconst BLOCK_TAGS: Record<string, keyof React.JSX.IntrinsicElements> = {\n normal: \"p\",\n h1: \"h1\",\n h2: \"h2\",\n h3: \"h3\",\n h4: \"h4\",\n h5: \"h5\",\n h6: \"h6\",\n blockquote: \"blockquote\",\n};\n\nfunction Span({ node, index }: { node: PortableTextNode; index: number }) {\n const marks = node.marks ?? [];\n const colour = marks.find((m) => m.startsWith(\"color:\"))?.slice(\"color:\".length);\n\n // Text is a React child, so it is escaped by construction.\n let out: React.ReactNode = node.text ?? \"\";\n for (const m of marks) {\n const Tag = MARK_TAGS[m];\n if (Tag) out = <Tag>{out}</Tag>;\n }\n if (colour && /^[a-z0-9-]+$/i.test(colour)) {\n out = <span className={`bcms-color-${colour}`}>{out}</span>;\n }\n return <React.Fragment key={node._key ?? index}>{out}</React.Fragment>;\n}\n\nexport function PortableText({ doc }: { doc: PortableTextDocument }) {\n return (\n <>\n {doc.content.map((block, i) => {\n const Tag = BLOCK_TAGS[block.style ?? \"normal\"] ?? \"p\";\n return (\n <Tag key={block._key ?? i}>\n {(block.children ?? []).map((c, j) => (\n <Span key={c._key ?? j} node={c} index={j} />\n ))}\n </Tag>\n );\n })}\n </>\n );\n}\n","import type { PortableTextDocument, TextProps } from \"@bettercms-ai/types\";\nimport { PortableText } from \"./portableText\";\nimport type { BcmsBindingProps } from \"../binding\";\n\ninterface TextBlockProps extends TextProps, BcmsBindingProps {\n className?: string;\n align?: \"left\" | \"center\" | \"right\";\n /** Portable Text source. WINS over `html`, which is the derived cache. */\n content?: PortableTextDocument;\n}\n\nexport function Text({\n html,\n content,\n align = \"left\",\n className = \"\",\n \"data-bcms-field\": field,\n \"data-bcms-kind\": kind,\n}: TextBlockProps) {\n const alignClass = {\n left: \"text-left\",\n center: \"text-center\",\n right: \"text-right\",\n }[align];\n\n const cls = `text-muted-foreground leading-relaxed ${alignClass} ${className}`;\n\n // `content` wins when present — it is the source of truth and renders through React, which\n // escapes by construction. `html` remains the fallback so every block authored before\n // Portable Text existed renders exactly as it always did.\n if (content && content.content.length > 0) {\n return (\n <div className={cls}>\n <PortableText doc={content} />\n </div>\n );\n }\n\n return (\n <div\n className={cls}\n data-bcms-field={field}\n data-bcms-kind={kind}\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{ __html: html }}\n />\n );\n}\n","import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","import type { ImageProps } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\nimport type { BcmsBindingProps } from \"../binding\";\n\ninterface ImageBlockProps extends ImageProps, BcmsBindingProps {\n className?: string;\n sizes?: string;\n}\n\nexport function Image({\n src,\n alt,\n width,\n height,\n caption,\n className = \"\",\n sizes,\n \"data-bcms-field\": field,\n \"data-bcms-kind\": kind,\n}: ImageBlockProps) {\n return (\n <figure className={cn(\"w-full\", className)}>\n <img\n src={src}\n alt={alt}\n width={width}\n height={height}\n sizes={sizes}\n data-bcms-field={field}\n data-bcms-kind={kind}\n className=\"w-full h-auto rounded-md object-cover\"\n loading=\"lazy\"\n />\n {caption && (\n <figcaption className=\"mt-2 text-sm text-muted-foreground text-center italic\">\n {caption}\n </figcaption>\n )}\n </figure>\n );\n}\n","import type { ButtonProps } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\nimport type { BcmsBindingProps } from \"../binding\";\n\ninterface ButtonBlockProps extends ButtonProps, BcmsBindingProps {\n className?: string;\n align?: \"left\" | \"center\" | \"right\";\n}\n\nexport function Button({\n text,\n href,\n variant = \"primary\",\n align = \"left\",\n className = \"\",\n \"data-bcms-field\": field,\n \"data-bcms-kind\": kind,\n}: ButtonBlockProps) {\n const baseStyles =\n \"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\";\n\n const variantStyles = {\n primary:\n \"bg-primary text-primary-foreground shadow hover:bg-primary/90\",\n secondary:\n \"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80\",\n };\n\n const alignClass = {\n left: \"justify-start\",\n center: \"justify-center\",\n right: \"justify-end\",\n }[align];\n\n return (\n <a\n href={href}\n data-bcms-field={field}\n data-bcms-kind={kind}\n className={cn(baseStyles, variantStyles[variant], alignClass, \"px-4 py-2\", className)}\n >\n {text}\n </a>\n );\n}\n","import type { SpacerProps } from \"@bettercms-ai/types\";\n\ninterface SpacerBlockProps extends SpacerProps {\n className?: string;\n}\n\nexport function Spacer({ height = 32, className = \"\" }: SpacerBlockProps) {\n return (\n <div\n className={`w-full border border-dashed border-muted-foreground/20 rounded flex items-center justify-center ${className}`}\n style={{ height: `${height}px` }}\n aria-hidden=\"true\"\n >\n <span className=\"text-xs text-muted-foreground/40 select-none\">\n {height}px\n </span>\n </div>\n );\n}\n","import type { ColumnsProps, ContentBlock } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\nimport React from \"react\";\n\ninterface ColumnsBlockProps extends ColumnsProps {\n className?: string;\n renderBlock?: (block: ContentBlock) => React.ReactNode;\n}\n\nexport function Columns({\n columns,\n gap = 16,\n className = \"\",\n renderBlock,\n}: ColumnsBlockProps) {\n return (\n <div\n className={cn(\"grid w-full\", className)}\n style={{\n gridTemplateColumns: `repeat(${columns.length}, minmax(0, 1fr))`,\n gap: `${gap}px`,\n }}\n >\n {columns.map((columnBlocks, colIndex) => (\n <div key={colIndex} className=\"flex flex-col gap-4 min-w-0\">\n {columnBlocks.map((block) => (\n <span key={block.id}>\n {renderBlock ? renderBlock(block) : null}\n </span>\n ))}\n </div>\n ))}\n </div>\n );\n}\n","import type { VideoProps } from \"@bettercms-ai/types\";\nimport { useState } from \"react\";\nimport { cn } from \"../lib/utils\";\n\ninterface VideoBlockProps extends VideoProps {\n className?: string;\n}\n\nfunction getEmbedUrl(url: string): { embedUrl: string; provider: \"youtube\" | \"vimeo\" | \"unknown\" } {\n // YouTube\n const ytMatch = url.match(\n /(?:youtube\\.com\\/(?:watch\\?v=|embed\\/)|youtu\\.be\\/)([a-zA-Z0-9_-]{11})/\n );\n if (ytMatch) {\n return {\n embedUrl: `https://www.youtube.com/embed/${ytMatch[1]}`,\n provider: \"youtube\",\n };\n }\n\n // Vimeo\n const vimeoMatch = url.match(/vimeo\\.com\\/(\\d+)/);\n if (vimeoMatch) {\n return {\n embedUrl: `https://player.vimeo.com/video/${vimeoMatch[1]}`,\n provider: \"vimeo\",\n };\n }\n\n return { embedUrl: url, provider: \"unknown\" };\n}\n\nexport function Video({\n url,\n poster,\n autoplay = false,\n loop = false,\n className = \"\",\n}: VideoBlockProps) {\n const [isLoaded, setIsLoaded] = useState(false);\n const { embedUrl, provider } = getEmbedUrl(url);\n\n // If not a recognized embed, render a native video tag\n if (provider === \"unknown\") {\n return (\n <video\n src={url}\n poster={poster}\n autoPlay={autoplay}\n loop={loop}\n controls\n className={className}\n playsInline\n />\n );\n }\n\n return (\n <div className={cn(\"w-full overflow-hidden rounded-md bg-black aspect-video\", className)}>\n {!isLoaded && poster && (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={poster}\n alt=\"Video thumbnail\"\n className=\"w-full h-full object-cover absolute inset-0\"\n />\n )}\n <iframe\n src={`${embedUrl}?autoplay=${autoplay ? 1 : 0}&loop=${loop ? 1 : 0}&mute=${autoplay ? 1 : 0}`}\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n loading=\"lazy\"\n onLoad={() => setIsLoaded(true)}\n className=\"w-full h-full\"\n />\n </div>\n );\n}\n","import type { SectionProps, ContentBlock } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\nimport React from \"react\";\n\ninterface SectionBlockProps extends SectionProps {\n className?: string;\n renderBlock?: (block: ContentBlock) => React.ReactNode;\n}\n\nexport function Section({\n children,\n background,\n paddingY,\n maxWidth,\n className = \"\",\n renderBlock,\n}: SectionBlockProps) {\n const inner = (children ?? []).map((block) => (\n <span key={block.id}>{renderBlock ? renderBlock(block) : null}</span>\n ));\n return (\n <section\n className={cn(\"w-full\", className)}\n style={{ background, paddingTop: paddingY, paddingBottom: paddingY }}\n >\n {maxWidth ? <div style={{ maxWidth, marginInline: \"auto\" }}>{inner}</div> : inner}\n </section>\n );\n}\n","import type { NavbarProps } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\n\ninterface NavbarBlockProps extends NavbarProps {\n className?: string;\n}\n\nexport function Navbar({ logo, links, cta, className = \"\" }: NavbarBlockProps) {\n return (\n <nav className={cn(\"flex items-center gap-6 py-4\", className)}>\n {logo?.src ? (\n <a href={logo.href ?? \"/\"} className=\"shrink-0\">\n <img src={logo.src} alt={logo.alt} className=\"h-8 w-auto\" />\n </a>\n ) : null}\n <div className=\"flex flex-1 gap-5\">\n {(links ?? []).map((l, i) => (\n <a key={i} href={l.href} className=\"text-inherit no-underline\">\n {l.label}\n </a>\n ))}\n </div>\n {cta ? (\n <a\n href={cta.href}\n className=\"rounded-lg bg-black px-4 py-2 font-semibold text-white no-underline\"\n >\n {cta.text}\n </a>\n ) : null}\n </nav>\n );\n}\n","import type { FooterProps } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\n\ninterface FooterBlockProps extends FooterProps {\n className?: string;\n}\n\nexport function Footer({ columns, copyright, className = \"\" }: FooterBlockProps) {\n return (\n <footer className={cn(\"mt-12 border-t pt-8\", className)}>\n <div className=\"flex flex-wrap gap-12\">\n {(columns ?? []).map((col, i) => (\n <div key={i} className=\"flex flex-col gap-1.5\">\n {col.heading ? <h4 className=\"mb-2 font-semibold\">{col.heading}</h4> : null}\n {(col.links ?? []).map((l, j) => (\n <a key={j} href={l.href} className=\"text-neutral-500 no-underline\">\n {l.label}\n </a>\n ))}\n </div>\n ))}\n </div>\n {copyright ? <p className=\"mt-6 text-sm text-neutral-500\">{copyright}</p> : null}\n </footer>\n );\n}\n","\"use client\";\n\nimport type { SliderProps, ContentBlock } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\nimport React, { useState } from \"react\";\n\ninterface SliderBlockProps extends SliderProps {\n className?: string;\n renderBlock?: (block: ContentBlock) => React.ReactNode;\n}\n\nexport function Slider({ slides, className = \"\", renderBlock }: SliderBlockProps) {\n const [i, setI] = useState(0);\n const list = slides ?? [];\n const go = (n: number) => setI((n + list.length) % (list.length || 1));\n const btn =\n \"absolute top-1/2 -translate-y-1/2 flex h-9 w-9 items-center justify-center rounded-full border-0 bg-black/50 text-white cursor-pointer\";\n return (\n <div className={cn(\"relative w-full\", className)}>\n {list.map((s, idx) => (\n <div key={s.id} hidden={idx !== i} className=\"flex flex-col gap-4\">\n {(s.children ?? []).map((b) => (\n <span key={b.id}>{renderBlock ? renderBlock(b) : null}</span>\n ))}\n </div>\n ))}\n <button type=\"button\" aria-label=\"Previous\" onClick={() => go(i - 1)} className={cn(btn, \"left-2\")}>\n &#8249;\n </button>\n <button type=\"button\" aria-label=\"Next\" onClick={() => go(i + 1)} className={cn(btn, \"right-2\")}>\n &#8250;\n </button>\n </div>\n );\n}\n","\"use client\";\n\nimport type { TabsProps, ContentBlock } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\nimport React, { useState } from \"react\";\n\ninterface TabsBlockProps extends TabsProps {\n className?: string;\n renderBlock?: (block: ContentBlock) => React.ReactNode;\n}\n\nexport function Tabs({ tabs, className = \"\", renderBlock }: TabsBlockProps) {\n const [active, setActive] = useState(0);\n const list = tabs ?? [];\n return (\n <div className={cn(\"w-full\", className)}>\n <div role=\"tablist\" className=\"mb-4 flex gap-1 border-b\">\n {list.map((t, idx) => (\n <button\n key={t.id}\n role=\"tab\"\n type=\"button\"\n aria-selected={idx === active}\n onClick={() => setActive(idx)}\n className={cn(\n \"cursor-pointer border-b-2 border-transparent bg-transparent px-4 py-2\",\n idx === active && \"border-black font-semibold\",\n )}\n >\n {t.label}\n </button>\n ))}\n </div>\n {list.map((t, idx) => (\n <div key={t.id} role=\"tabpanel\" hidden={idx !== active} className=\"flex flex-col gap-4\">\n {(t.children ?? []).map((b) => (\n <span key={b.id}>{renderBlock ? renderBlock(b) : null}</span>\n ))}\n </div>\n ))}\n </div>\n );\n}\n","import type { ComponentProps as BcmsComponentProps, FormProps, CollectionProps } from \"@bettercms-ai/types\";\nimport { cn } from \"../lib/utils\";\n\nconst box =\n \"rounded border border-dashed border-neutral-300 bg-neutral-50 p-4 text-sm text-neutral-500\";\n\n/**\n * Builder-preview placeholder for a `component` instance. Live instance resolution\n * (definition lookup + override merge) happens server-side in the delivery renderer\n * and in the page-builder, which pass a resolver — the standalone library preview\n * just shows what's referenced.\n */\nexport function ComponentInstance({\n componentId,\n className = \"\",\n}: BcmsComponentProps & { className?: string }) {\n return (\n <div className={cn(box, className)}>\n Component instance: <code>{componentId}</code>\n </div>\n );\n}\n\n/** Builder-preview placeholder for a `form` block (rendered live by @bettercms-ai/next). */\nexport function FormPlaceholder({ formId, className = \"\" }: FormProps & { className?: string }) {\n return (\n <div className={cn(box, className)}>\n Form: <code>{formId}</code>\n </div>\n );\n}\n\n/**\n * Builder-preview placeholder for a `collection` block.\n *\n * The builder canvas has no entries to list — the list is fetched per request by the site\n * renderer, deliberately, so that a published snapshot can never bake in a stale set of\n * posts. A placeholder is therefore the honest render here, not a limitation.\n */\nexport function CollectionPlaceholder({\n cardComponentId,\n className = \"\",\n}: CollectionProps & { className?: string }) {\n return (\n <div className={cn(box, className)}>\n Collection: entries of this page\n {cardComponentId ? <> as <code>{cardComponentId}</code></> : <> (default card)</>}\n </div>\n );\n}\n","import type { ContentBlock, BlockType } from \"@bettercms-ai/types\";\nimport { Heading } from \"./blocks/Heading\";\nimport { Text } from \"./blocks/Text\";\nimport { Image } from \"./blocks/Image\";\nimport { Button } from \"./blocks/Button\";\nimport { Spacer } from \"./blocks/Spacer\";\nimport { Columns } from \"./blocks/Columns\";\nimport { Video } from \"./blocks/Video\";\nimport { Section } from \"./blocks/Section\";\nimport { Navbar } from \"./blocks/Navbar\";\nimport { Footer } from \"./blocks/Footer\";\nimport { Slider } from \"./blocks/Slider\";\nimport { Tabs } from \"./blocks/Tabs\";\nimport { ComponentInstance, FormPlaceholder, CollectionPlaceholder } from \"./blocks/Placeholders\";\n\n/**\n * Block registry entry — maps a block type to its React component\n * and metadata for the block library UI.\n */\nexport interface BlockRegistryEntry {\n component: React.ComponentType<Record<string, unknown>>;\n label: string;\n icon: string;\n}\n\nexport const BLOCK_REGISTRY: Record<BlockType, BlockRegistryEntry> = {\n // The entries of the page this block sits on. No preview here by design: the list is\n // fetched per request by the site renderer, so the builder has nothing to show.\n collection: {\n component: CollectionPlaceholder as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Collection\",\n icon: \"list\",\n },\n heading: {\n component: Heading as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Heading\",\n icon: \"type\",\n },\n text: {\n component: Text as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Text\",\n icon: \"align-left\",\n },\n // Same payload, same renderer — deliberately. `richtext` differs from `text` only in how\n // an author EDITS it (a formatting surface vs a plain field); at delivery both are an HTML\n // string, so duplicating the component would be two things to keep in sync for no gain.\n richtext: {\n component: Text as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Rich text\",\n icon: \"pilcrow\",\n },\n image: {\n component: Image as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Image\",\n icon: \"image\",\n },\n button: {\n component: Button as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Button\",\n icon: \"square\",\n },\n spacer: {\n component: Spacer as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Spacer\",\n icon: \"minus\",\n },\n columns: {\n component: Columns as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Columns\",\n icon: \"columns\",\n },\n video: {\n component: Video as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Video\",\n icon: \"play\",\n },\n section: {\n component: Section as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Section\",\n icon: \"layout\",\n },\n navbar: {\n component: Navbar as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Navbar\",\n icon: \"menu\",\n },\n footer: {\n component: Footer as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Footer\",\n icon: \"panel-bottom\",\n },\n slider: {\n component: Slider as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Slider\",\n icon: \"gallery-horizontal\",\n },\n tabs: {\n component: Tabs as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Tabs\",\n icon: \"folder\",\n },\n component: {\n component: ComponentInstance as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Component\",\n icon: \"component\",\n },\n form: {\n component: FormPlaceholder as unknown as React.ComponentType<Record<string, unknown>>,\n label: \"Form\",\n icon: \"clipboard-list\",\n },\n} as const;\n\nexport const BLOCK_TYPES = Object.keys(BLOCK_REGISTRY) as BlockType[];\n\nexport type { ContentBlock, BlockType };\n","import type { CSSProperties } from \"react\";\nimport { blockStyleToCss, type BlockStyle } from \"@bettercms-ai/types\";\n\n/**\n * FLO-264 — React-typed wrapper over the shared, framework-agnostic\n * `blockStyleToCss` in @bettercms-ai/types. One mapping, three consumers (this UI\n * renderer, the @bettercms-ai/next adapter, and — in string form — the hosted\n * render-page.ts).\n */\nexport function blockStyleToReactStyle(style: BlockStyle | undefined): CSSProperties | undefined {\n return blockStyleToCss(style) as CSSProperties | undefined;\n}\n","import type { ContentBlock } from \"@bettercms-ai/types\";\nimport { BLOCK_REGISTRY, type BlockRegistryEntry } from \"./registry\";\nimport { blockStyleToReactStyle } from \"./blockStyle\";\nimport type { BcmsBindingProps } from \"./binding\";\nimport React from \"react\";\n\n// Container blocks hold nested blocks and need the `renderBlock` callback threaded\n// so their children render recursively.\nconst CONTAINER_TYPES = new Set([\"columns\", \"section\", \"slider\", \"tabs\"]);\n\ninterface BlockRendererProps {\n block: ContentBlock;\n className?: string;\n renderBlock?: (block: ContentBlock) => React.ReactNode;\n /** Emit visual-editor field bindings. Default-off keeps published consumers clean. */\n annotate?: boolean;\n}\n\nfunction bindingProps(block: ContentBlock, annotate: boolean): BcmsBindingProps {\n if (!annotate || !block.id) return {};\n switch (block.type) {\n case \"heading\":\n case \"button\":\n return { \"data-bcms-field\": `${block.id}__text`, \"data-bcms-kind\": \"text\" };\n case \"text\":\n case \"richtext\":\n return { \"data-bcms-field\": `${block.id}__html`, \"data-bcms-kind\": \"richtext\" };\n case \"image\":\n return { \"data-bcms-field\": `${block.id}__src`, \"data-bcms-kind\": \"image\" };\n default:\n return {};\n }\n}\n\nfunction resolveBlock(block: ContentBlock, className?: string, annotate = false): React.ReactNode {\n const entry: BlockRegistryEntry | undefined = BLOCK_REGISTRY[block.type];\n\n if (!entry) {\n return (\n <div\n className={`w-full p-4 border border-dashed border-red-400 bg-red-50 text-red-600 text-sm rounded ${className ?? \"\"}`}\n >\n Unknown block type: <code>{block.type}</code>\n </div>\n );\n }\n\n const Component = entry.component;\n const blockStyle = blockStyleToReactStyle(block.style);\n const el = <Component {...block.props} className={className} {...bindingProps(block, annotate)} />;\n return blockStyle ? <div style={blockStyle}>{el}</div> : el;\n}\n\nexport function BlockRenderer({\n block,\n className = \"\",\n renderBlock,\n annotate = false,\n}: BlockRendererProps) {\n const entry: BlockRegistryEntry | undefined = BLOCK_REGISTRY[block.type];\n\n if (!entry) {\n return (\n <div\n className={`w-full p-4 border border-dashed border-red-400 bg-red-50 text-red-600 text-sm rounded ${className}`}\n >\n Unknown block type: <code>{block.type}</code>\n </div>\n );\n }\n\n // Container blocks (columns/section/slider/tabs) hold nested blocks, so thread the\n // renderBlock callback. Standalone usage (no BlocksRenderer above) falls back to\n // resolveBlock so one level of nesting still renders.\n const Component = entry.component;\n const containerProps = CONTAINER_TYPES.has(block.type)\n ? { renderBlock: renderBlock ?? ((child: ContentBlock) => resolveBlock(child, undefined, annotate)) }\n : {};\n const blockStyle = blockStyleToReactStyle(block.style);\n const el = (\n <Component\n {...block.props}\n className={className}\n {...containerProps}\n {...bindingProps(block, annotate)}\n />\n );\n return blockStyle ? <div style={blockStyle}>{el}</div> : el;\n}\n\ninterface BlocksRendererProps {\n blocks: ContentBlock[];\n className?: string;\n renderBlock?: (block: ContentBlock) => React.ReactNode;\n annotate?: boolean;\n}\n\nexport function BlocksRenderer({\n blocks,\n className = \"\",\n renderBlock,\n annotate = false,\n}: BlocksRendererProps) {\n if (blocks.length === 0) return null;\n\n return (\n <div className={className}>\n {blocks.map((block) => (\n <BlockRenderer\n key={block.id}\n block={block}\n renderBlock={renderBlock}\n annotate={annotate}\n />\n ))}\n </div>\n );\n}\n"],"mappings":"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","names":["jsx","jsx","jsx","jsx","jsx","jsxs","jsx","jsx","jsxs","jsx","jsx","jsxs","jsx","jsxs","useState","jsx","jsxs","useState","jsx","jsxs","Fragment","jsx","jsxs","jsx","jsxs"]}
package/dist/resolve.d.ts CHANGED
@@ -36,5 +36,38 @@ declare function setPath(obj: Record<string, unknown>, path: string, value: unkn
36
36
  * non-array input.
37
37
  */
38
38
  declare function applyOverrides<B extends ResolverBlock>(blockJson: unknown, propDefs: ResolverPropDef[], overrides: Record<string, unknown> | undefined): B[];
39
+ /**
40
+ * How deep a component may nest another component before we stop. Matches the guard the
41
+ * three live renderers already apply, so a tree that renders is a tree that snapshots.
42
+ */
43
+ declare const MAX_COMPONENT_DEPTH = 10;
44
+ /** What `resolveDeep` needs to look a nested component up by id. */
45
+ type ResolverComponentDef = {
46
+ blockJson: unknown;
47
+ props: ResolverPropDef[];
48
+ };
49
+ /**
50
+ * `applyOverrides`, then INLINE every nested `component` block into its resolved blocks.
51
+ *
52
+ * This is the deep/frozen counterpart to `applyOverrides`, and it exists for exactly one
53
+ * caller family: publishing. A published entry's `component-ref` freezes the resolved tree
54
+ * into `published_data` so that later edits to the component cannot silently rewrite content
55
+ * that was already published. `applyOverrides` alone does not deliver that promise once a
56
+ * component nests another (which a `slot` prop makes routine): the outer tree is frozen, but
57
+ * the nested `component` block is still a live REFERENCE that re-resolves at bake time.
58
+ *
59
+ * The three LIVE renderers deliberately keep using `applyOverrides` plus their own
60
+ * per-render recursion — they want the live reference. So there is still exactly one
61
+ * implementation of "apply overrides to a component"; this only adds the inlining.
62
+ *
63
+ * Fail-soft, matching the renderers: an empty id, an unknown component, a cycle, or a tree
64
+ * deeper than MAX_COMPONENT_DEPTH resolves to NOTHING rather than throwing — the same thing
65
+ * a visitor already sees for a deleted component.
66
+ *
67
+ * `seen` is the ancestry of the current branch and the caller MUST seed it with the id of
68
+ * the component being resolved, or a component that slots itself will not be caught. It is
69
+ * ancestry-scoped, not global: the same component placed twice as siblings renders twice.
70
+ */
71
+ declare function resolveDeep<B extends ResolverBlock>(blockJson: unknown, propDefs: ResolverPropDef[], overrides: Record<string, unknown> | undefined, lookup: (componentId: string) => ResolverComponentDef | undefined, seen?: readonly string[], depth?: number): B[];
39
72
 
40
- export { type ResolverBlock, type ResolverPropDef, applyOverrides, childArrays, findBlockById, setPath };
73
+ export { MAX_COMPONENT_DEPTH, type ResolverBlock, type ResolverComponentDef, type ResolverPropDef, applyOverrides, childArrays, findBlockById, resolveDeep, setPath };
package/dist/resolve.js CHANGED
@@ -49,10 +49,75 @@ function applyOverrides(blockJson, propDefs, overrides) {
49
49
  }
50
50
  return clone;
51
51
  }
52
+ var MAX_COMPONENT_DEPTH = 10;
53
+ function mapChildArrays(block, map) {
54
+ const p = block.props ?? {};
55
+ switch (block.type) {
56
+ case "columns":
57
+ if (!Array.isArray(p.columns)) return block;
58
+ return { ...block, props: { ...p, columns: p.columns.map(map) } };
59
+ case "section":
60
+ if (!Array.isArray(p.children)) return block;
61
+ return { ...block, props: { ...p, children: map(p.children) } };
62
+ case "slider":
63
+ if (!Array.isArray(p.slides)) return block;
64
+ return {
65
+ ...block,
66
+ props: {
67
+ ...p,
68
+ slides: p.slides.map(
69
+ (s) => Array.isArray(s.children) ? { ...s, children: map(s.children) } : s
70
+ )
71
+ }
72
+ };
73
+ case "tabs":
74
+ if (!Array.isArray(p.tabs)) return block;
75
+ return {
76
+ ...block,
77
+ props: {
78
+ ...p,
79
+ tabs: p.tabs.map(
80
+ (t) => Array.isArray(t.children) ? { ...t, children: map(t.children) } : t
81
+ )
82
+ }
83
+ };
84
+ default:
85
+ return block;
86
+ }
87
+ }
88
+ function resolveDeep(blockJson, propDefs, overrides, lookup, seen = [], depth = 0) {
89
+ const applied = applyOverrides(blockJson, propDefs, overrides);
90
+ const out = [];
91
+ for (const block of applied) {
92
+ if (block?.type !== "component") {
93
+ out.push(mapChildArrays(block, (children) => resolveDeep(children, [], void 0, lookup, seen, depth)));
94
+ continue;
95
+ }
96
+ if (depth >= MAX_COMPONENT_DEPTH) continue;
97
+ const p = block.props ?? {};
98
+ const cid = typeof p.componentId === "string" ? p.componentId : "";
99
+ if (!cid || seen.includes(cid)) continue;
100
+ const def = lookup(cid);
101
+ if (!def) continue;
102
+ out.push(
103
+ ...resolveDeep(
104
+ def.blockJson,
105
+ def.props,
106
+ p.overrides,
107
+ lookup,
108
+ [...seen, cid],
109
+ depth + 1
110
+ )
111
+ );
112
+ }
113
+ return out;
114
+ }
52
115
  export {
116
+ MAX_COMPONENT_DEPTH,
53
117
  applyOverrides,
54
118
  childArrays,
55
119
  findBlockById,
120
+ resolveDeep,
56
121
  setPath
57
122
  };
58
123
  //# sourceMappingURL=resolve.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/resolve.ts"],"sourcesContent":["/**\n * Pure component-instance resolver — clone a component's blockJson and write each\n * declared override at its target (blockId + dotted path), honoring the props[]\n * allowlist. The single source of truth shared by the dashboard canvas preview, the\n * backend live HTML renderer, and @bettercms-ai/next's SSG output, so all three resolve\n * instances identically. React-free and exported on its own subpath (@bettercms-ai/ui/\n * resolve) so the Hono server can import it without pulling in the block components.\n */\n\n/** Minimal block shape the resolver needs — every real block type is structurally wider.\n * `props` is `unknown` (not `Record<string, unknown>`) so typed block unions like\n * @bettercms-ai/types' ContentBlock — whose `props` are specific interfaces without an\n * index signature — still satisfy the constraint. */\nexport type ResolverBlock = { type: string; id?: string; props?: unknown };\n\n/** Minimal prop-def shape — the real ComponentPropDef is structurally wider. */\nexport type ResolverPropDef = { key: string; target: { blockId: string; path: string } };\n\n/** The child block arrays a container holds, one entry per nested slot (all container types). */\nexport function childArrays<B extends ResolverBlock>(block: B): B[][] {\n const p = (block.props ?? {}) as Record<string, unknown>;\n switch (block.type) {\n case \"columns\":\n return Array.isArray(p.columns) ? (p.columns as B[][]) : [];\n case \"section\":\n return Array.isArray(p.children) ? [p.children as B[]] : [];\n case \"slider\":\n return Array.isArray(p.slides)\n ? (p.slides as { children?: B[] }[]).map((s) => (Array.isArray(s.children) ? s.children : []))\n : [];\n case \"tabs\":\n return Array.isArray(p.tabs)\n ? (p.tabs as { children?: B[] }[]).map((t) => (Array.isArray(t.children) ? t.children : []))\n : [];\n default:\n return [];\n }\n}\n\n/** Find a block by id anywhere in a tree (descends every container). */\nexport function findBlockById<B extends ResolverBlock>(blocks: B[], id: string): B | null {\n for (const b of blocks) {\n if (b?.id === id) return b;\n for (const child of childArrays(b)) {\n const found = findBlockById(child, id);\n if (found) return found;\n }\n }\n return null;\n}\n\n/** Write `value` at a dotted path (e.g. \"props.text\"), creating objects as needed. */\nexport function setPath(obj: Record<string, unknown>, path: string, value: unknown): void {\n const keys = path.split(\".\");\n let cur: Record<string, unknown> = obj;\n for (let i = 0; i < keys.length - 1; i++) {\n const k = keys[i]!;\n if (typeof cur[k] !== \"object\" || cur[k] === null) cur[k] = {};\n cur = cur[k] as Record<string, unknown>;\n }\n cur[keys[keys.length - 1]!] = value;\n}\n\n/**\n * Apply an instance's `overrides` onto a clone of a component's blockJson, honoring the\n * declared `props[]` allowlist (key → target block + path). Returns the original array\n * untouched (referentially stable) when there's nothing to override; returns [] for a\n * non-array input.\n */\nexport function applyOverrides<B extends ResolverBlock>(\n blockJson: unknown,\n propDefs: ResolverPropDef[],\n overrides: Record<string, unknown> | undefined,\n): B[] {\n if (!Array.isArray(blockJson)) return [];\n if (!propDefs.length || !overrides || Object.keys(overrides).length === 0) {\n return blockJson as B[];\n }\n const clone = structuredClone(blockJson) as B[];\n for (const def of propDefs) {\n if (!(def.key in overrides)) continue;\n const target = findBlockById(clone, def.target.blockId);\n if (target) setPath(target as unknown as Record<string, unknown>, def.target.path, overrides[def.key]);\n }\n return clone;\n}\n"],"mappings":";;;AAmBO,SAAS,YAAqC,OAAiB;AACpE,QAAM,IAAK,MAAM,SAAS,CAAC;AAC3B,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK;AACH,aAAO,MAAM,QAAQ,EAAE,OAAO,IAAK,EAAE,UAAoB,CAAC;AAAA,IAC5D,KAAK;AACH,aAAO,MAAM,QAAQ,EAAE,QAAQ,IAAI,CAAC,EAAE,QAAe,IAAI,CAAC;AAAA,IAC5D,KAAK;AACH,aAAO,MAAM,QAAQ,EAAE,MAAM,IACxB,EAAE,OAAgC,IAAI,CAAC,MAAO,MAAM,QAAQ,EAAE,QAAQ,IAAI,EAAE,WAAW,CAAC,CAAE,IAC3F,CAAC;AAAA,IACP,KAAK;AACH,aAAO,MAAM,QAAQ,EAAE,IAAI,IACtB,EAAE,KAA8B,IAAI,CAAC,MAAO,MAAM,QAAQ,EAAE,QAAQ,IAAI,EAAE,WAAW,CAAC,CAAE,IACzF,CAAC;AAAA,IACP;AACE,aAAO,CAAC;AAAA,EACZ;AACF;AAGO,SAAS,cAAuC,QAAa,IAAsB;AACxF,aAAW,KAAK,QAAQ;AACtB,QAAI,GAAG,OAAO,GAAI,QAAO;AACzB,eAAW,SAAS,YAAY,CAAC,GAAG;AAClC,YAAM,QAAQ,cAAc,OAAO,EAAE;AACrC,UAAI,MAAO,QAAO;AAAA,IACpB;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,QAAQ,KAA8B,MAAc,OAAsB;AACxF,QAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,MAAI,MAA+B;AACnC,WAAS,IAAI,GAAG,IAAI,KAAK,SAAS,GAAG,KAAK;AACxC,UAAM,IAAI,KAAK,CAAC;AAChB,QAAI,OAAO,IAAI,CAAC,MAAM,YAAY,IAAI,CAAC,MAAM,KAAM,KAAI,CAAC,IAAI,CAAC;AAC7D,UAAM,IAAI,CAAC;AAAA,EACb;AACA,MAAI,KAAK,KAAK,SAAS,CAAC,CAAE,IAAI;AAChC;AAQO,SAAS,eACd,WACA,UACA,WACK;AACL,MAAI,CAAC,MAAM,QAAQ,SAAS,EAAG,QAAO,CAAC;AACvC,MAAI,CAAC,SAAS,UAAU,CAAC,aAAa,OAAO,KAAK,SAAS,EAAE,WAAW,GAAG;AACzE,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,gBAAgB,SAAS;AACvC,aAAW,OAAO,UAAU;AAC1B,QAAI,EAAE,IAAI,OAAO,WAAY;AAC7B,UAAM,SAAS,cAAc,OAAO,IAAI,OAAO,OAAO;AACtD,QAAI,OAAQ,SAAQ,QAA8C,IAAI,OAAO,MAAM,UAAU,IAAI,GAAG,CAAC;AAAA,EACvG;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/resolve.ts"],"sourcesContent":["/**\n * Pure component-instance resolver — clone a component's blockJson and write each\n * declared override at its target (blockId + dotted path), honoring the props[]\n * allowlist. The single source of truth shared by the dashboard canvas preview, the\n * backend live HTML renderer, and @bettercms-ai/next's SSG output, so all three resolve\n * instances identically. React-free and exported on its own subpath (@bettercms-ai/ui/\n * resolve) so the Hono server can import it without pulling in the block components.\n */\n\n/** Minimal block shape the resolver needs — every real block type is structurally wider.\n * `props` is `unknown` (not `Record<string, unknown>`) so typed block unions like\n * @bettercms-ai/types' ContentBlock — whose `props` are specific interfaces without an\n * index signature — still satisfy the constraint. */\nexport type ResolverBlock = { type: string; id?: string; props?: unknown };\n\n/** Minimal prop-def shape — the real ComponentPropDef is structurally wider. */\nexport type ResolverPropDef = { key: string; target: { blockId: string; path: string } };\n\n/** The child block arrays a container holds, one entry per nested slot (all container types). */\nexport function childArrays<B extends ResolverBlock>(block: B): B[][] {\n const p = (block.props ?? {}) as Record<string, unknown>;\n switch (block.type) {\n case \"columns\":\n return Array.isArray(p.columns) ? (p.columns as B[][]) : [];\n case \"section\":\n return Array.isArray(p.children) ? [p.children as B[]] : [];\n case \"slider\":\n return Array.isArray(p.slides)\n ? (p.slides as { children?: B[] }[]).map((s) => (Array.isArray(s.children) ? s.children : []))\n : [];\n case \"tabs\":\n return Array.isArray(p.tabs)\n ? (p.tabs as { children?: B[] }[]).map((t) => (Array.isArray(t.children) ? t.children : []))\n : [];\n default:\n return [];\n }\n}\n\n/** Find a block by id anywhere in a tree (descends every container). */\nexport function findBlockById<B extends ResolverBlock>(blocks: B[], id: string): B | null {\n for (const b of blocks) {\n if (b?.id === id) return b;\n for (const child of childArrays(b)) {\n const found = findBlockById(child, id);\n if (found) return found;\n }\n }\n return null;\n}\n\n/** Write `value` at a dotted path (e.g. \"props.text\"), creating objects as needed. */\nexport function setPath(obj: Record<string, unknown>, path: string, value: unknown): void {\n const keys = path.split(\".\");\n let cur: Record<string, unknown> = obj;\n for (let i = 0; i < keys.length - 1; i++) {\n const k = keys[i]!;\n if (typeof cur[k] !== \"object\" || cur[k] === null) cur[k] = {};\n cur = cur[k] as Record<string, unknown>;\n }\n cur[keys[keys.length - 1]!] = value;\n}\n\n/**\n * Apply an instance's `overrides` onto a clone of a component's blockJson, honoring the\n * declared `props[]` allowlist (key → target block + path). Returns the original array\n * untouched (referentially stable) when there's nothing to override; returns [] for a\n * non-array input.\n */\nexport function applyOverrides<B extends ResolverBlock>(\n blockJson: unknown,\n propDefs: ResolverPropDef[],\n overrides: Record<string, unknown> | undefined,\n): B[] {\n if (!Array.isArray(blockJson)) return [];\n if (!propDefs.length || !overrides || Object.keys(overrides).length === 0) {\n return blockJson as B[];\n }\n const clone = structuredClone(blockJson) as B[];\n for (const def of propDefs) {\n if (!(def.key in overrides)) continue;\n const target = findBlockById(clone, def.target.blockId);\n if (target) setPath(target as unknown as Record<string, unknown>, def.target.path, overrides[def.key]);\n }\n return clone;\n}\n\n/**\n * How deep a component may nest another component before we stop. Matches the guard the\n * three live renderers already apply, so a tree that renders is a tree that snapshots.\n */\nexport const MAX_COMPONENT_DEPTH = 10;\n\n/** What `resolveDeep` needs to look a nested component up by id. */\nexport type ResolverComponentDef = { blockJson: unknown; props: ResolverPropDef[] };\n\n/** Rebuild a container block with each of its child arrays mapped. The write-side twin of\n * `childArrays` — same four container types, same order. Non-containers pass through. */\nfunction mapChildArrays<B extends ResolverBlock>(block: B, map: (children: B[]) => B[]): B {\n const p = (block.props ?? {}) as Record<string, unknown>;\n switch (block.type) {\n case \"columns\":\n if (!Array.isArray(p.columns)) return block;\n return { ...block, props: { ...p, columns: (p.columns as B[][]).map(map) } };\n case \"section\":\n if (!Array.isArray(p.children)) return block;\n return { ...block, props: { ...p, children: map(p.children as B[]) } };\n case \"slider\":\n if (!Array.isArray(p.slides)) return block;\n return {\n ...block,\n props: {\n ...p,\n slides: (p.slides as { children?: B[] }[]).map((s) =>\n Array.isArray(s.children) ? { ...s, children: map(s.children) } : s,\n ),\n },\n };\n case \"tabs\":\n if (!Array.isArray(p.tabs)) return block;\n return {\n ...block,\n props: {\n ...p,\n tabs: (p.tabs as { children?: B[] }[]).map((t) =>\n Array.isArray(t.children) ? { ...t, children: map(t.children) } : t,\n ),\n },\n };\n default:\n return block;\n }\n}\n\n/**\n * `applyOverrides`, then INLINE every nested `component` block into its resolved blocks.\n *\n * This is the deep/frozen counterpart to `applyOverrides`, and it exists for exactly one\n * caller family: publishing. A published entry's `component-ref` freezes the resolved tree\n * into `published_data` so that later edits to the component cannot silently rewrite content\n * that was already published. `applyOverrides` alone does not deliver that promise once a\n * component nests another (which a `slot` prop makes routine): the outer tree is frozen, but\n * the nested `component` block is still a live REFERENCE that re-resolves at bake time.\n *\n * The three LIVE renderers deliberately keep using `applyOverrides` plus their own\n * per-render recursion — they want the live reference. So there is still exactly one\n * implementation of \"apply overrides to a component\"; this only adds the inlining.\n *\n * Fail-soft, matching the renderers: an empty id, an unknown component, a cycle, or a tree\n * deeper than MAX_COMPONENT_DEPTH resolves to NOTHING rather than throwing — the same thing\n * a visitor already sees for a deleted component.\n *\n * `seen` is the ancestry of the current branch and the caller MUST seed it with the id of\n * the component being resolved, or a component that slots itself will not be caught. It is\n * ancestry-scoped, not global: the same component placed twice as siblings renders twice.\n */\nexport function resolveDeep<B extends ResolverBlock>(\n blockJson: unknown,\n propDefs: ResolverPropDef[],\n overrides: Record<string, unknown> | undefined,\n lookup: (componentId: string) => ResolverComponentDef | undefined,\n seen: readonly string[] = [],\n depth = 0,\n): B[] {\n const applied = applyOverrides<B>(blockJson, propDefs, overrides);\n\n const out: B[] = [];\n for (const block of applied) {\n if (block?.type !== \"component\") {\n out.push(mapChildArrays(block, (children) => resolveDeep<B>(children, [], undefined, lookup, seen, depth)));\n continue;\n }\n // THE INVARIANT: the returned tree never contains a `component` block. Every branch below\n // either inlines it or drops it — including the depth cap, which must NOT bail out by\n // returning the tree as-is. Doing that would leave a live reference sitting inside a\n // \"frozen\" snapshot, which is the exact half-frozen state this function exists to prevent\n // and is harder to notice than rendering nothing.\n if (depth >= MAX_COMPONENT_DEPTH) continue; // too deep — drop, never leave a live ref\n const p = (block.props ?? {}) as { componentId?: unknown; overrides?: unknown };\n const cid = typeof p.componentId === \"string\" ? p.componentId : \"\";\n if (!cid || seen.includes(cid)) continue; // empty slot, or a cycle — render nothing\n const def = lookup(cid);\n if (!def) continue; // unknown/unpublished/deleted — render nothing\n out.push(\n ...resolveDeep<B>(\n def.blockJson,\n def.props,\n p.overrides as Record<string, unknown> | undefined,\n lookup,\n [...seen, cid],\n depth + 1,\n ),\n );\n }\n return out;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bettercms-ai/ui",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "BetterCMS block components + registry/renderer for rendering page block_json in React (dashboard builder preview + headless sites).",
6
6
  "sideEffects": false,
@@ -47,7 +47,7 @@
47
47
  "add": "echo 'Use npx shadcn@latest add -p ../.. <component>'"
48
48
  },
49
49
  "dependencies": {
50
- "@bettercms-ai/types": "^1.3.1",
50
+ "@bettercms-ai/types": "^1.9.0",
51
51
  "clsx": "^2.1.1",
52
52
  "tailwind-merge": "^2.6.0"
53
53
  },