@bettercms-ai/ui 0.16.0 → 0.17.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/blocks.js CHANGED
@@ -29,841 +29,1205 @@ function Heading({
29
29
  }
30
30
 
31
31
  // src/blocks/portableText.tsx
32
- import React from "react";
32
+ import React6 from "react";
33
33
  import { PortableText as PT } from "@portabletext/react";
34
34
 
35
- // ../richtext/src/types.ts
36
- var PT_TYPES = {
37
- /** A generic embedded block: `{schemaKey, schemaLabel, fields}`, the successor to Lexical's BlockNode. */
38
- block: "bcmsBlock",
39
- /** An inline mention. A PT inline object is natively void, which is what Lexical's `token` mode was faking. */
40
- mention: "bcmsMention",
41
- divider: "bcmsDivider",
42
- code: "bcmsCode",
43
- /**
44
- * A video — an uploaded file or a provider URL. DEDICATED rather than another `schemaKey` on
45
- * the generic block, because a video is the one media node whose rendition depends on its own
46
- * payload: the same field is a `<video>` for an mp4 and a provider `<iframe>` for a YouTube
47
- * link (@see resolveVideoSource). The generic block carries a reference this package cannot
48
- * resolve; a video carries a URL it can.
49
- */
50
- video: "video",
51
- /** A downloadable document — a PDF, a spec sheet. `<a href download>`, nothing more. */
52
- file: "file",
53
- /**
54
- * A table. Spelled `table`, unprefixed, because that is what every other Portable Text
55
- * producer calls one — the Sanity-shaped `{_type:"table", rows}` this platform used to refuse
56
- * as foreign content now lands on OUR type and renders, rather than being data nothing draws.
57
- */
58
- table: "table",
59
- /** The annotation carrying a hyperlink's `href`. Spelled `link` because that is the spec's convention. */
60
- link: "link",
61
- /**
62
- * The annotation carrying a site's OWN inline markup — `<span class>`, `<small>`, `<time
63
- * datetime>`. Unprefixed, like `table`, because it describes HTML rather than a BetterCMS
64
- * concept. See `styled.ts` for what it may hold and why the list is that short.
65
- */
66
- styled: "styled"
67
- };
68
- var BUILTIN_TABLE_KEY = "builtin:table";
69
- var STORABLE_BLOCK_TYPES = /* @__PURE__ */ new Set([
70
- "block",
71
- PT_TYPES.block,
72
- PT_TYPES.table,
73
- PT_TYPES.code,
74
- PT_TYPES.divider,
75
- PT_TYPES.mention,
76
- PT_TYPES.video,
77
- PT_TYPES.file
78
- ]);
35
+ // src/renderer.tsx
36
+ import { RESPONSIVE_CSS, hiddenEverywhere, hideOnAttr, splitButtonStyle, usesHideOn, visibleBlocks, windowState } from "@bettercms-ai/types";
79
37
 
80
- // ../richtext/src/styled.ts
81
- var STYLED_TAGS = [
82
- "span",
83
- "small",
84
- "sup",
85
- "sub",
86
- "kbd",
87
- "abbr",
88
- "time",
89
- "strong",
90
- "b",
91
- "em",
92
- "i",
93
- "u",
94
- "s",
95
- "mark",
96
- "code",
97
- "q",
98
- "cite",
99
- "dfn",
100
- "var",
101
- "samp",
102
- "ins",
103
- "del"
104
- ];
105
- var TAG_SET = new Set(STYLED_TAGS);
106
- var isStyledTag = (tag) => typeof tag === "string" && TAG_SET.has(tag);
107
- var ATTR_NAMES = /* @__PURE__ */ new Set(["title", "datetime", "style", "aria-label"]);
108
- var DATA_ATTR = /^data-[a-z][a-z0-9-]*$/;
109
- var isStyledAttr = (name) => ATTR_NAMES.has(name) || DATA_ATTR.test(name) && !name.startsWith("data-bcms-");
110
- var STYLE_PROPS = /* @__PURE__ */ new Set([
111
- "color",
112
- "background-color",
113
- "opacity",
114
- "font",
115
- "font-family",
116
- "font-size",
117
- "font-style",
118
- "font-weight",
119
- "font-variant",
120
- "font-feature-settings",
121
- "font-variation-settings",
122
- "letter-spacing",
123
- "line-height",
124
- "word-spacing",
125
- "white-space",
126
- "word-break",
127
- "text-align",
128
- "text-decoration",
129
- "text-decoration-color",
130
- "text-decoration-line",
131
- "text-decoration-style",
132
- "text-transform",
133
- "text-underline-offset",
134
- "vertical-align"
135
- ]);
136
- var STYLE_VALUE_BAN = /url\(|expression\(|@import|\\|\/\*|[<>]/i;
137
- function safeStyle(css) {
138
- const kept = [];
139
- for (const declaration of css.split(";")) {
140
- const at = declaration.indexOf(":");
141
- if (at < 0) continue;
142
- const name = declaration.slice(0, at).trim().toLowerCase();
143
- const value = declaration.slice(at + 1).trim();
144
- if (!name || !value) continue;
145
- if (!name.startsWith("--") && !STYLE_PROPS.has(name)) continue;
146
- if (STYLE_VALUE_BAN.test(value)) continue;
147
- kept.push(`${name}:${value}`);
148
- }
149
- return kept.length ? kept.join(";") : void 0;
150
- }
151
- function styledAttrEntries(attrs) {
152
- if (!attrs || typeof attrs !== "object" || Array.isArray(attrs)) return [];
153
- const out = [];
154
- for (const [name, raw] of Object.entries(attrs)) {
155
- if (!isStyledAttr(name) || typeof raw !== "string") continue;
156
- const value = name === "style" ? safeStyle(raw) : raw;
157
- if (value === void 0) continue;
158
- out.push([name, value]);
159
- }
160
- return out;
38
+ // src/lib/utils.ts
39
+ import { clsx } from "clsx";
40
+ import { twMerge } from "tailwind-merge";
41
+ function cn(...inputs) {
42
+ return twMerge(clsx(inputs));
161
43
  }
162
44
 
163
- // ../richtext/src/href.ts
164
- var SAFE_HREF_SCHEMES = ["http:", "https:", "mailto:", "tel:"];
165
- function safeHref(raw) {
166
- const trimmed = raw.trim();
167
- if (trimmed.startsWith("#")) return /^#[^\s<>"'`]*$/.test(trimmed) ? trimmed : null;
168
- if (trimmed.startsWith("/") && !trimmed.startsWith("//")) return trimmed;
169
- try {
170
- return SAFE_HREF_SCHEMES.includes(new URL(trimmed).protocol) ? trimmed : null;
171
- } catch {
172
- return null;
173
- }
45
+ // src/blocks/Image.tsx
46
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
47
+ function Image({
48
+ src,
49
+ alt,
50
+ width,
51
+ height,
52
+ caption,
53
+ className = "",
54
+ sizes,
55
+ "data-bcms-field": field,
56
+ "data-bcms-kind": kind
57
+ }) {
58
+ const sized = typeof width === "number" && width > 0;
59
+ return /* @__PURE__ */ jsxs("figure", { className: cn(sized ? "max-w-full" : "w-full", className), children: [
60
+ /* @__PURE__ */ jsx2(
61
+ "img",
62
+ {
63
+ src,
64
+ alt,
65
+ width,
66
+ height,
67
+ sizes,
68
+ "data-bcms-field": field,
69
+ "data-bcms-kind": kind,
70
+ style: sized ? { width: `${width}px`, maxWidth: "100%", height: "auto" } : void 0,
71
+ className: cn(!sized && "w-full", "h-auto rounded-md object-cover"),
72
+ loading: "lazy"
73
+ }
74
+ ),
75
+ caption && /* @__PURE__ */ jsx2("figcaption", { className: "mt-2 text-sm text-muted-foreground text-center italic", children: caption })
76
+ ] });
174
77
  }
175
78
 
176
- // ../richtext/src/key.ts
177
- var ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
178
- var KEY_PATTERN = /^[A-Za-z0-9-]{1,32}$/;
179
- var isValidKey = (k) => typeof k === "string" && KEY_PATTERN.test(k);
180
- function randomKey(length = 12) {
181
- let out = "";
182
- for (let i = 0; i < length; i += 1) {
183
- out += ALPHABET[Math.floor(Math.random() * ALPHABET.length)];
184
- }
185
- return out;
186
- }
187
- function deterministicKeys(prefix = "k") {
188
- let n = 0;
189
- return () => {
190
- n += 1;
191
- return `${prefix}${n}`;
79
+ // src/blocks/Button.tsx
80
+ import { LINK_RELS, buttonIconOf, buttonIconSvg, buttonVariantOf, isScriptHref } from "@bettercms-ai/types";
81
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
82
+ function Button({
83
+ text,
84
+ href,
85
+ variant = "primary",
86
+ size = "md",
87
+ fullWidth = false,
88
+ arrow = false,
89
+ icon: iconProp,
90
+ newTab = false,
91
+ rel,
92
+ action,
93
+ align = "left",
94
+ className = "",
95
+ style,
96
+ "data-bcms-field": field,
97
+ "data-bcms-kind": kind
98
+ }) {
99
+ 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";
100
+ const variantStyles = {
101
+ primary: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
102
+ secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
103
+ tertiary: "bg-transparent text-primary hover:bg-primary/10",
104
+ brand: "bg-accent text-accent-foreground shadow hover:bg-accent/90",
105
+ dark: "bg-foreground text-background shadow hover:bg-foreground/90",
106
+ light: "border bg-background text-foreground shadow-sm hover:bg-muted",
107
+ outline: "border border-current bg-transparent text-primary hover:bg-primary/10",
108
+ link: "h-auto bg-transparent p-0 text-primary underline underline-offset-4"
192
109
  };
193
- }
194
-
195
- // ../richtext/src/table.ts
196
- var isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
197
- var isPortableTextTable = (n) => isRecord(n) && n._type === PT_TYPES.table && Array.isArray(n.rows);
198
- var isBuiltInTableBlock = (n) => isRecord(n) && n._type === PT_TYPES.block && n.schemaKey === BUILTIN_TABLE_KEY;
199
- var asIs = (v) => Array.isArray(v) ? v : [];
200
- function keyMinter(base, reserved) {
201
- const prefix = (base.match(/[A-Za-z0-9-]/g) ?? []).join("").slice(0, 16) || "t";
202
- return (...parts) => {
203
- const candidate = `${prefix}-${parts.join("")}`.slice(0, 30);
204
- let key = candidate;
205
- for (let n = 1; reserved.has(key); n += 1) key = `${candidate}-${n}`;
206
- reserved.add(key);
207
- return key;
110
+ const icon = buttonIconOf(iconProp);
111
+ const alignClass = {
112
+ left: "justify-start",
113
+ center: "justify-center",
114
+ right: "justify-end"
115
+ }[align];
116
+ const sizeStyles = {
117
+ sm: "px-3 py-1.5 text-xs",
118
+ md: "px-4 py-2",
119
+ lg: "px-6 py-3 text-base"
208
120
  };
209
- }
210
- function reservedKeys(rows) {
211
- const out = /* @__PURE__ */ new Set();
212
- for (const row of rows) {
213
- if (!isRecord(row)) continue;
214
- if (isValidKey(row._key)) out.add(row._key);
215
- for (const cell of Array.isArray(row.cells) ? row.cells : []) {
216
- if (isRecord(cell) && isValidKey(cell._key)) out.add(cell._key);
217
- }
121
+ const sizeClass = size === "sm" || size === "lg" ? sizeStyles[size] : sizeStyles.md;
122
+ const fullClass = fullWidth === true ? "flex w-full justify-center" : "";
123
+ const relTokens = [...newTab === true ? ["noopener", "noreferrer"] : [], ...LINK_RELS.includes(rel) ? [rel] : []];
124
+ if (action === "code") {
125
+ return /* @__PURE__ */ jsxs2("button", { type: "button", "data-bcms-field": field, "data-bcms-kind": kind, style, className: cn(baseStyles, variantStyles[buttonVariantOf(variant)], alignClass, sizeClass, fullClass, className), children: [
126
+ icon && /* @__PURE__ */ jsx3("span", { className: "mr-2 inline-flex", dangerouslySetInnerHTML: { __html: buttonIconSvg(icon, "bcms-btn-icon") } }),
127
+ text,
128
+ arrow === true && /* @__PURE__ */ jsx3("span", { className: "ml-2 inline-flex", dangerouslySetInnerHTML: { __html: buttonIconSvg("arrow-right", "bcms-btn-arrow") } })
129
+ ] });
218
130
  }
219
- return out;
131
+ return /* @__PURE__ */ jsxs2(
132
+ "a",
133
+ {
134
+ href: typeof href === "string" && isScriptHref(href) ? "#" : href,
135
+ ...newTab === true ? { target: "_blank" } : {},
136
+ ...relTokens.length ? { rel: relTokens.join(" ") } : {},
137
+ "data-bcms-field": field,
138
+ "data-bcms-kind": kind,
139
+ style,
140
+ className: cn(baseStyles, variantStyles[buttonVariantOf(variant)], alignClass, sizeClass, fullClass, className),
141
+ children: [
142
+ icon && /* @__PURE__ */ jsx3("span", { className: "mr-2 inline-flex", dangerouslySetInnerHTML: { __html: buttonIconSvg(icon, "bcms-btn-icon") } }),
143
+ text,
144
+ arrow === true && /* @__PURE__ */ jsx3("span", { className: "ml-2 inline-flex", dangerouslySetInnerHTML: { __html: buttonIconSvg("arrow-right", "bcms-btn-arrow") } })
145
+ ]
146
+ }
147
+ );
220
148
  }
221
- var textContent = (text, key) => text === "" ? [] : [{
222
- _type: "block",
223
- _key: key(),
224
- style: "normal",
225
- markDefs: [],
226
- children: [{ _type: "span", _key: key(), text, marks: [] }]
227
- }];
228
- var spanOf = (value) => {
229
- const n = typeof value === "number" ? value : Number(value);
230
- return Number.isInteger(n) && n > 1 ? n : void 0;
231
- };
232
- function normalizeCell(raw, at, mint, read) {
233
- const [row, col] = at;
234
- const key = isRecord(raw) && isValidKey(raw._key) ? raw._key : mint("r", row, "c", col);
235
- const inner = deterministicKeys(`${key}-`);
236
- if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
237
- return { _key: key, content: textContent(String(raw), inner) };
238
- }
239
- if (!isRecord(raw)) return { _key: key, content: [] };
240
- const span = spanOf(raw.span ?? raw.colSpan ?? raw.colspan);
241
- const rowSpan = spanOf(raw.rowSpan ?? raw.rowspan);
242
- return {
243
- _key: key,
244
- // `text` is the spelling a string matrix uses once its cells have been given objects; it is
245
- // read here so a half-migrated value cannot lose its words.
246
- content: Array.isArray(raw.content) ? read(raw.content, inner) : textContent(typeof raw.text === "string" ? raw.text : "", inner),
247
- ...raw.header === true ? { header: true } : {},
248
- ...span === void 0 ? {} : { span },
249
- ...rowSpan === void 0 ? {} : { rowSpan }
250
- };
149
+
150
+ // src/blocks/Spacer.tsx
151
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
152
+ function Spacer({ height = 32, className = "" }) {
153
+ return /* @__PURE__ */ jsx4(
154
+ "div",
155
+ {
156
+ className: `w-full border border-dashed border-muted-foreground/20 rounded flex items-center justify-center ${className}`,
157
+ style: { height: `${height}px` },
158
+ "aria-hidden": "true",
159
+ children: /* @__PURE__ */ jsxs3("span", { className: "text-xs text-muted-foreground/40 select-none", children: [
160
+ height,
161
+ "px"
162
+ ] })
163
+ }
164
+ );
251
165
  }
252
- function normalizeRow(raw, at, mint, read) {
253
- const cells = Array.isArray(raw) ? raw : isRecord(raw) && Array.isArray(raw.cells) ? raw.cells : [];
254
- return {
255
- _key: isRecord(raw) && isValidKey(raw._key) ? raw._key : mint("r", at),
256
- cells: cells.map((c, col) => normalizeCell(c, [at, col], mint, read)),
257
- ...isRecord(raw) && raw.head === true ? { head: true } : {}
258
- };
166
+
167
+ // src/blocks/Columns.tsx
168
+ import "react";
169
+ import { jsx as jsx5 } from "react/jsx-runtime";
170
+ function Columns({
171
+ columns,
172
+ gap = 16,
173
+ className = "",
174
+ renderBlock
175
+ }) {
176
+ return /* @__PURE__ */ jsx5(
177
+ "div",
178
+ {
179
+ className: cn("grid w-full", className),
180
+ style: {
181
+ gridTemplateColumns: `repeat(${columns.length}, minmax(0, 1fr))`,
182
+ gap: `${gap}px`
183
+ },
184
+ children: columns.map((columnBlocks, colIndex) => /* @__PURE__ */ jsx5("div", { className: "flex flex-col gap-4 min-w-0", children: columnBlocks.map((block) => /* @__PURE__ */ jsx5("span", { children: renderBlock ? renderBlock(block) : null }, block.id)) }, colIndex))
185
+ }
186
+ );
259
187
  }
260
- function builtInTableToTable(node, key = randomKey) {
261
- if (!isBuiltInTableBlock(node)) return null;
262
- const fields = node.fields;
263
- const raw = isRecord(fields) && Array.isArray(fields.rows) ? fields.rows : [];
264
- const hasHeader = isRecord(fields) ? fields.hasHeader !== false : true;
265
- const own = node._key;
266
- const tableKey = isValidKey(own) ? own : key();
267
- const mint = keyMinter(tableKey, reservedKeys(raw));
268
- const rows = raw.map((row, i) => {
269
- const normalized = normalizeRow(row, i, mint, asIs);
270
- if (!(hasHeader && i === 0)) return normalized;
188
+
189
+ // src/blocks/Video.tsx
190
+ import { useState } from "react";
191
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
192
+ function getEmbedUrl(url) {
193
+ const ytMatch = url.match(
194
+ /(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
195
+ );
196
+ if (ytMatch) {
271
197
  return {
272
- ...normalized,
273
- head: true,
274
- cells: normalized.cells.map((c) => ({ ...c, header: true }))
198
+ embedUrl: `https://www.youtube.com/embed/${ytMatch[1]}`,
199
+ provider: "youtube"
200
+ };
201
+ }
202
+ const vimeoMatch = url.match(/vimeo\.com\/(\d+)/);
203
+ if (vimeoMatch) {
204
+ return {
205
+ embedUrl: `https://player.vimeo.com/video/${vimeoMatch[1]}`,
206
+ provider: "vimeo"
275
207
  };
276
- });
277
- const width = rows.reduce((max, r) => Math.max(max, r.cells.length), 0);
278
- for (const [i, row] of rows.entries()) {
279
- while (row.cells.length < width) {
280
- row.cells.push({
281
- _key: mint("r", i, "c", row.cells.length),
282
- content: [],
283
- ...row.head === true ? { header: true } : {}
284
- });
285
- }
286
208
  }
287
- return { _type: PT_TYPES.table, _key: tableKey, rows, legacy: true };
209
+ return { embedUrl: url, provider: "unknown" };
288
210
  }
289
- function migrateTable(node, key = randomKey, read = asIs) {
290
- const builtIn = builtInTableToTable(node, key);
291
- if (builtIn) return builtIn;
292
- if (!isPortableTextTable(node)) return null;
293
- const rows = node.rows;
294
- const tableKey = isValidKey(node._key) ? node._key : key();
295
- const mint = keyMinter(tableKey, reservedKeys(rows));
296
- return {
297
- _type: PT_TYPES.table,
298
- _key: tableKey,
299
- rows: rows.map((r, i) => normalizeRow(r, i, mint, read)),
300
- // Carried through: a migrated table that an author saves back is still the same table, and
301
- // dropping the flag on that save would change the page the next time it renders.
302
- ...node.legacy === true ? { legacy: true } : {}
303
- };
211
+ function Video({
212
+ url,
213
+ poster,
214
+ autoplay = false,
215
+ loop = false,
216
+ className = ""
217
+ }) {
218
+ const [isLoaded, setIsLoaded] = useState(false);
219
+ const { embedUrl, provider } = getEmbedUrl(url);
220
+ if (provider === "unknown") {
221
+ return /* @__PURE__ */ jsx6(
222
+ "video",
223
+ {
224
+ src: url,
225
+ poster,
226
+ autoPlay: autoplay,
227
+ loop,
228
+ controls: true,
229
+ className,
230
+ playsInline: true
231
+ }
232
+ );
233
+ }
234
+ return /* @__PURE__ */ jsxs4("div", { className: cn("w-full overflow-hidden rounded-md bg-black aspect-video", className), children: [
235
+ !isLoaded && poster && // eslint-disable-next-line @next/next/no-img-element
236
+ /* @__PURE__ */ jsx6(
237
+ "img",
238
+ {
239
+ src: poster,
240
+ alt: "Video thumbnail",
241
+ className: "w-full h-full object-cover absolute inset-0"
242
+ }
243
+ ),
244
+ /* @__PURE__ */ jsx6(
245
+ "iframe",
246
+ {
247
+ src: `${embedUrl}?autoplay=${autoplay ? 1 : 0}&loop=${loop ? 1 : 0}&mute=${autoplay ? 1 : 0}`,
248
+ allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
249
+ allowFullScreen: true,
250
+ loading: "lazy",
251
+ onLoad: () => setIsLoaded(true),
252
+ className: "w-full h-full"
253
+ }
254
+ )
255
+ ] });
304
256
  }
305
257
 
306
- // ../richtext/src/read.ts
307
- var isRecord2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
308
- function normalizePortableText(value, key = randomKey) {
309
- if (!Array.isArray(value)) return [];
310
- return value.flatMap((raw) => {
311
- if (!isRecord2(raw) || typeof raw._type !== "string") return [];
312
- const table = migrateTable(raw, key, (v, cellKey) => normalizePortableText(v, cellKey));
313
- if (table) return [table];
314
- const node = { ...raw, _key: isRecord2(raw) && typeof raw._key === "string" && raw._key ? raw._key : key() };
315
- if (node._type === "block") {
316
- if (!Array.isArray(node.markDefs)) node.markDefs = [];
317
- if (typeof node.style !== "string") node.style = "normal";
318
- node.children = (Array.isArray(node.children) ? node.children : []).map((c) => {
319
- if (!isRecord2(c)) return c;
320
- const child = { ...c, _key: typeof c._key === "string" && c._key ? c._key : key() };
321
- if (child._type === "span" && !Array.isArray(child.marks)) child.marks = [];
322
- return child;
323
- });
258
+ // src/blocks/Section.tsx
259
+ import "react";
260
+ import { jsx as jsx7 } from "react/jsx-runtime";
261
+ function Section({
262
+ children,
263
+ background,
264
+ paddingY,
265
+ maxWidth,
266
+ className = "",
267
+ renderBlock
268
+ }) {
269
+ const inner = (children ?? []).map((block) => /* @__PURE__ */ jsx7("span", { children: renderBlock ? renderBlock(block) : null }, block.id));
270
+ return /* @__PURE__ */ jsx7(
271
+ "section",
272
+ {
273
+ className: cn("w-full", className),
274
+ style: { background, paddingTop: paddingY, paddingBottom: paddingY },
275
+ children: maxWidth ? /* @__PURE__ */ jsx7("div", { style: { maxWidth, marginInline: "auto" }, children: inner }) : inner
324
276
  }
325
- return [node];
326
- });
327
- }
328
- function readStructuredContent(props) {
329
- if (!isRecord2(props)) return null;
330
- const candidate = props.content ?? props.value;
331
- const arr = Array.isArray(candidate) ? candidate : isRecord2(candidate) ? Array.isArray(candidate.content) ? candidate.content : Array.isArray(candidate.value) ? candidate.value : null : null;
332
- return arr && arr.length > 0 ? arr : null;
277
+ );
333
278
  }
334
279
 
335
- // src/blocks/portableText.tsx
336
- import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
337
- var TOKEN = /^[a-z0-9-]+$/i;
338
- function styleObject(css) {
339
- const out = {};
340
- for (const declaration of css.split(";")) {
341
- const at = declaration.indexOf(":");
342
- if (at < 0) continue;
343
- const name = declaration.slice(0, at).trim();
344
- const value = declaration.slice(at + 1).trim();
345
- if (!name || !value) continue;
346
- out[name.startsWith("--") ? name : name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = value;
347
- }
348
- return out;
280
+ // src/blocks/Navbar.tsx
281
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
282
+ function Navbar({ logo, links, cta, className = "" }) {
283
+ return /* @__PURE__ */ jsxs5("nav", { className: cn("flex items-center gap-6 py-4", className), children: [
284
+ logo?.src ? /* @__PURE__ */ jsx8("a", { href: logo.href ?? "/", className: "shrink-0", children: /* @__PURE__ */ jsx8("img", { src: logo.src, alt: logo.alt, className: "h-8 w-auto" }) }) : null,
285
+ /* @__PURE__ */ jsx8("div", { className: "flex flex-1 gap-5", children: (links ?? []).map((l, i) => /* @__PURE__ */ jsx8("a", { href: l.href, className: "text-inherit no-underline", children: l.label }, i)) }),
286
+ cta ? /* @__PURE__ */ jsx8(
287
+ "a",
288
+ {
289
+ href: cta.href,
290
+ className: "rounded-lg bg-black px-4 py-2 font-semibold text-white no-underline",
291
+ children: cta.text
292
+ }
293
+ ) : null
294
+ ] });
349
295
  }
350
- function Bound({
351
- base,
352
- nodeKey,
353
- children
354
- }) {
355
- if (!base || !nodeKey) return /* @__PURE__ */ jsx2(Fragment, { children });
356
- return /* @__PURE__ */ jsx2("div", { "data-bcms-field": `${base}#${nodeKey}`, "data-bcms-kind": "richtext", children });
296
+
297
+ // src/blocks/Footer.tsx
298
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
299
+ function Footer({ columns, copyright, className = "" }) {
300
+ return /* @__PURE__ */ jsxs6("footer", { className: cn("mt-12 border-t pt-8", className), children: [
301
+ /* @__PURE__ */ jsx9("div", { className: "flex flex-wrap gap-12", children: (columns ?? []).map((col, i) => /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-1.5", children: [
302
+ col.heading ? /* @__PURE__ */ jsx9("h4", { className: "mb-2 font-semibold", children: col.heading }) : null,
303
+ (col.links ?? []).map((l, j) => /* @__PURE__ */ jsx9("a", { href: l.href, className: "text-neutral-500 no-underline", children: l.label }, j))
304
+ ] }, i)) }),
305
+ copyright ? /* @__PURE__ */ jsx9("p", { className: "mt-6 text-sm text-neutral-500", children: copyright }) : null
306
+ ] });
357
307
  }
358
- function Table({ value }) {
359
- const components = React.useMemo(buildComponents, []);
360
- const inline = React.useMemo(
361
- () => ({ ...components, block: { normal: ({ children }) => /* @__PURE__ */ jsx2(Fragment, { children }) } }),
362
- [components]
363
- );
364
- const rows = Array.isArray(value?.rows) ? value.rows : [];
365
- if (rows.length === 0) return null;
366
- const cells = (row) => (Array.isArray(row?.cells) ? row.cells : []).map((cell) => {
367
- const Tag = cell?.header === true ? "th" : "td";
368
- const span = typeof cell?.span === "number" && cell.span > 1 ? cell.span : void 0;
369
- const rowSpan = typeof cell?.rowSpan === "number" && cell.rowSpan > 1 ? cell.rowSpan : void 0;
370
- const content = Array.isArray(cell?.content) ? cell.content : [];
371
- const lone = content.length === 1 && content[0]._type === "block" && content[0].style === "normal" && !content[0].listItem;
372
- return /* @__PURE__ */ jsx2(Tag, { ...span ? { colSpan: span } : {}, ...rowSpan ? { rowSpan } : {}, children: /* @__PURE__ */ jsx2(PT, { value: content, components: lone ? inline : components }) }, cell?._key);
373
- });
374
- const rendered = rows.map((row) => /* @__PURE__ */ jsx2("tr", { children: cells(row) }, row?._key));
375
- const headCount = rows.findIndex((r) => r?.head !== true);
376
- const head = headCount === -1 ? rows.length : headCount;
377
- return /* @__PURE__ */ jsxs("table", { ...value.legacy === true ? { className: "bcms-table" } : {}, children: [
378
- head > 0 && /* @__PURE__ */ jsx2("thead", { children: rendered.slice(0, head) }),
379
- head < rows.length && /* @__PURE__ */ jsx2("tbody", { children: rendered.slice(head) })
308
+
309
+ // src/blocks/Slider.tsx
310
+ import { useState as useState2 } from "react";
311
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
312
+ function Slider({ slides, className = "", renderBlock }) {
313
+ const [i, setI] = useState2(0);
314
+ const list = slides ?? [];
315
+ const go = (n) => setI((n + list.length) % (list.length || 1));
316
+ const btn = "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";
317
+ return /* @__PURE__ */ jsxs7("div", { className: cn("relative w-full", className), children: [
318
+ list.map((s, idx) => /* @__PURE__ */ jsx10("div", { hidden: idx !== i, className: "flex flex-col gap-4", children: (s.children ?? []).map((b) => /* @__PURE__ */ jsx10("span", { children: renderBlock ? renderBlock(b) : null }, b.id)) }, s.id)),
319
+ /* @__PURE__ */ jsx10("button", { type: "button", "aria-label": "Previous", onClick: () => go(i - 1), className: cn(btn, "left-2"), children: "\u2039" }),
320
+ /* @__PURE__ */ jsx10("button", { type: "button", "aria-label": "Next", onClick: () => go(i + 1), className: cn(btn, "right-2"), children: "\u203A" })
380
321
  ] });
381
322
  }
382
- function buildComponents(renderBlock) {
383
- return {
384
- types: {
385
- [PT_TYPES.divider]: () => /* @__PURE__ */ jsx2("hr", {}),
386
- [PT_TYPES.table]: ({ value }) => /* @__PURE__ */ jsx2(Table, { value }),
387
- [PT_TYPES.code]: ({ value }) => {
388
- const v = value;
389
- const lang = typeof v.language === "string" && TOKEN.test(v.language) ? v.language : void 0;
390
- return /* @__PURE__ */ jsx2("pre", { children: /* @__PURE__ */ jsx2("code", { className: lang ? `language-${lang}` : void 0, children: typeof v.code === "string" ? v.code : "" }) });
391
- },
392
- /**
393
- * A generic embedded block renders as a marker, not as content: its payload references
394
- * something (a component, a media id) that has to be RESOLVED against the project, and
395
- * this renderer has no project. A marker keeps the node visible and addressable rather
396
- * than rendering nothing at all, which is how embedded content used to vanish. A caller
397
- * that CAN resolve it passes `renderBlock`.
398
- */
399
- [PT_TYPES.block]: ({ value }) => {
400
- const rendered = renderBlock?.(value);
401
- if (rendered !== void 0) return /* @__PURE__ */ jsx2(Fragment, { children: rendered });
402
- const v = value;
403
- const key = typeof v.schemaKey === "string" ? v.schemaKey : "block";
404
- return /* @__PURE__ */ jsx2("div", { className: "bcms-block", "data-bcms-schema-key": key });
405
- },
406
- [PT_TYPES.mention]: ({ value }) => {
407
- const v = value;
408
- return /* @__PURE__ */ jsx2("span", { className: "bcms-mention", children: typeof v.label === "string" ? v.label : "" });
409
- }
410
- },
411
- marks: {
412
- /**
413
- * The only attribute this renderer writes from author data. An unsafe href renders the
414
- * LABEL rather than deleting it — blocking a link must not lose the words.
415
- */
416
- [PT_TYPES.link]: ({ value, children }) => {
417
- const v = value ?? {};
418
- const href = typeof v.href === "string" ? safeHref(v.href) : null;
419
- if (!href) return /* @__PURE__ */ jsx2(Fragment, { children });
420
- const blank = v.target === "_blank";
421
- return /* @__PURE__ */ jsx2("a", { href, ...blank ? { target: "_blank", rel: "noopener noreferrer" } : {}, children });
422
- },
423
- /**
424
- * The site's own inline markup, rendered as itself — the React half of
425
- * `portableTextToHtml`'s `styled` mark, and it has to stay the same shape: Astro and the
426
- * editor canvas render through that serialiser while a Next site renders through this
427
- * one, and two renderers of one value that disagree is the whole bug class.
428
- *
429
- * The tag is re-checked against `STYLED_TAGS` and the attribute bag re-filtered, because
430
- * a markDef is DATA and arrives from imports, the Management API and AI writes. React
431
- * escapes the values it writes; the allowlist is what keeps a `href` or an `onClick` out
432
- * of the props entirely. An unrecognised tag renders its CHILDREN — the words are the
433
- * author's, the wrapper is only the design.
434
- */
435
- [PT_TYPES.styled]: ({ value, children }) => {
436
- const v = value ?? {};
437
- const tag = typeof v.tag === "string" ? v.tag.toLowerCase() : "";
438
- if (!isStyledTag(tag)) return /* @__PURE__ */ jsx2(Fragment, { children });
439
- const Tag = tag;
440
- const className = typeof v.className === "string" && v.className ? v.className : void 0;
441
- const attrs = styledAttrEntries(v.attrs);
442
- const style = attrs.find(([name]) => name === "style")?.[1];
443
- return /* @__PURE__ */ jsx2(
444
- Tag,
445
- {
446
- className,
447
- ...Object.fromEntries(attrs.filter(([name]) => name !== "style")),
448
- ...style ? { style: styleObject(style) } : {},
449
- children
450
- }
451
- );
323
+
324
+ // src/blocks/Tabs.tsx
325
+ import { useState as useState3 } from "react";
326
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
327
+ function Tabs({ tabs, className = "", renderBlock }) {
328
+ const [active, setActive] = useState3(0);
329
+ const list = tabs ?? [];
330
+ return /* @__PURE__ */ jsxs8("div", { className: cn("w-full", className), children: [
331
+ /* @__PURE__ */ jsx11("div", { role: "tablist", className: "mb-4 flex gap-1 border-b", children: list.map((t, idx) => /* @__PURE__ */ jsx11(
332
+ "button",
333
+ {
334
+ role: "tab",
335
+ type: "button",
336
+ "aria-selected": idx === active,
337
+ onClick: () => setActive(idx),
338
+ className: cn(
339
+ "cursor-pointer border-b-2 border-transparent bg-transparent px-4 py-2",
340
+ idx === active && "border-black font-semibold"
341
+ ),
342
+ children: t.label
452
343
  },
453
- highlight: ({ children }) => /* @__PURE__ */ jsx2("mark", { children }),
454
- "strike-through": ({ children }) => /* @__PURE__ */ jsx2("s", { children }),
455
- /**
456
- * A colour mark carries a TOKEN name (`color:brand-accent`), never a baked hex, so the
457
- * consuming site styles it and a rebrand does not orphan the content.
458
- */
459
- color: ({ children, markType }) => {
460
- const token = markType.startsWith("color:") ? markType.slice(6) : "";
461
- if (!TOKEN.test(token)) return /* @__PURE__ */ jsx2(Fragment, { children });
462
- return /* @__PURE__ */ jsx2("span", { className: `bcms-color-${token}`, children });
463
- }
464
- },
465
- /** Unknown marks and types render their children. Never swallow content. */
466
- unknownMark: ({ children }) => /* @__PURE__ */ jsx2(Fragment, { children }),
467
- unknownType: ({ children }) => /* @__PURE__ */ jsx2(Fragment, { children: children ?? null }),
468
- unknownBlockStyle: ({ children }) => /* @__PURE__ */ jsx2("p", { children })
469
- };
344
+ t.id
345
+ )) }),
346
+ list.map((t, idx) => /* @__PURE__ */ jsx11("div", { role: "tabpanel", hidden: idx !== active, className: "flex flex-col gap-4", children: (t.children ?? []).map((b) => /* @__PURE__ */ jsx11("span", { children: renderBlock ? renderBlock(b) : null }, b.id)) }, t.id))
347
+ ] });
348
+ }
349
+
350
+ // src/blocks/Placeholders.tsx
351
+ import { Fragment, jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
352
+ var box = "rounded border border-dashed border-neutral-300 bg-neutral-50 p-4 text-sm text-neutral-500";
353
+ function ComponentInstance({
354
+ componentId,
355
+ className = ""
356
+ }) {
357
+ return /* @__PURE__ */ jsxs9("div", { className: cn(box, className), children: [
358
+ "Component instance: ",
359
+ /* @__PURE__ */ jsx12("code", { children: componentId })
360
+ ] });
361
+ }
362
+ function FormPlaceholder({ formId, className = "" }) {
363
+ return /* @__PURE__ */ jsxs9("div", { className: cn(box, className), children: [
364
+ "Form: ",
365
+ /* @__PURE__ */ jsx12("code", { children: formId })
366
+ ] });
470
367
  }
471
- var isListItem = (node) => node._type === "block" && typeof node.listItem === "string" && !!node.listItem;
472
- function runsOf(blocks) {
473
- const runs = [];
474
- for (const node of blocks) {
475
- const last = runs.at(-1);
476
- if (last && isListItem(node) && isListItem(last[0])) last.push(node);
477
- else runs.push([node]);
368
+ function CollectionPlaceholder({
369
+ cardComponentId,
370
+ className = ""
371
+ }) {
372
+ return /* @__PURE__ */ jsxs9("div", { className: cn(box, className), children: [
373
+ "Collection: entries of this page",
374
+ cardComponentId ? /* @__PURE__ */ jsxs9(Fragment, { children: [
375
+ " as ",
376
+ /* @__PURE__ */ jsx12("code", { children: cardComponentId })
377
+ ] }) : /* @__PURE__ */ jsx12(Fragment, { children: " (default card)" })
378
+ ] });
379
+ }
380
+
381
+ // src/registry.ts
382
+ var BLOCK_REGISTRY = {
383
+ // The entries of the page this block sits on. No preview here by design: the list is
384
+ // fetched per request by the site renderer, so the builder has nothing to show.
385
+ collection: {
386
+ component: CollectionPlaceholder,
387
+ label: "Collection",
388
+ icon: "list"
389
+ },
390
+ heading: {
391
+ component: Heading,
392
+ label: "Heading",
393
+ icon: "type"
394
+ },
395
+ text: {
396
+ component: Text,
397
+ label: "Text",
398
+ icon: "align-left"
399
+ },
400
+ // Same payload, same renderer — deliberately. `richtext` differs from `text` only in how
401
+ // an author EDITS it (a formatting surface vs a plain field); at delivery both are an HTML
402
+ // string, so duplicating the component would be two things to keep in sync for no gain.
403
+ richtext: {
404
+ component: Text,
405
+ label: "Rich text",
406
+ icon: "pilcrow"
407
+ },
408
+ image: {
409
+ component: Image,
410
+ label: "Image",
411
+ icon: "image"
412
+ },
413
+ button: {
414
+ component: Button,
415
+ label: "Button",
416
+ icon: "square"
417
+ },
418
+ spacer: {
419
+ component: Spacer,
420
+ label: "Spacer",
421
+ icon: "minus"
422
+ },
423
+ columns: {
424
+ component: Columns,
425
+ label: "Columns",
426
+ icon: "columns"
427
+ },
428
+ video: {
429
+ component: Video,
430
+ label: "Video",
431
+ icon: "play"
432
+ },
433
+ section: {
434
+ component: Section,
435
+ label: "Section",
436
+ icon: "layout"
437
+ },
438
+ navbar: {
439
+ component: Navbar,
440
+ label: "Navbar",
441
+ icon: "menu"
442
+ },
443
+ footer: {
444
+ component: Footer,
445
+ label: "Footer",
446
+ icon: "panel-bottom"
447
+ },
448
+ slider: {
449
+ component: Slider,
450
+ label: "Slider",
451
+ icon: "gallery-horizontal"
452
+ },
453
+ tabs: {
454
+ component: Tabs,
455
+ label: "Tabs",
456
+ icon: "folder"
457
+ },
458
+ component: {
459
+ component: ComponentInstance,
460
+ label: "Component",
461
+ icon: "component"
462
+ },
463
+ form: {
464
+ component: FormPlaceholder,
465
+ label: "Form",
466
+ icon: "clipboard-list"
478
467
  }
479
- return runs;
468
+ };
469
+ var BLOCK_TYPES = Object.keys(BLOCK_REGISTRY);
470
+
471
+ // src/blockStyle.ts
472
+ import { blockStyleToCss } from "@bettercms-ai/types";
473
+ function blockStyleToReactStyle(style) {
474
+ return blockStyleToCss(style);
480
475
  }
481
- function PortableText({ value, bindingBase, renderBlock }) {
482
- const blocks = React.useMemo(() => normalizePortableText(readValue(value)), [value]);
483
- const components = React.useMemo(() => buildComponents(renderBlock), [renderBlock]);
484
- const listComponents = React.useMemo(() => {
485
- if (!bindingBase) return null;
486
- const keys = new Set(blocks.map((b) => b._key));
487
- return {
488
- ...components,
489
- listItem: ({ value: item, children }) => keys.has(item._key) ? /* @__PURE__ */ jsx2("li", { "data-bcms-field": `${bindingBase}#${item._key}`, "data-bcms-kind": "richtext", children }) : /* @__PURE__ */ jsx2("li", { children })
490
- };
491
- }, [bindingBase, blocks, components]);
492
- if (blocks.length === 0) return null;
493
- if (!bindingBase || !listComponents) return /* @__PURE__ */ jsx2(PT, { value: blocks, components });
494
- return /* @__PURE__ */ jsx2(Fragment, { children: runsOf(blocks).map(
495
- (run) => isListItem(run[0]) ? /* @__PURE__ */ jsx2(PT, { value: run, components: listComponents }, run[0]._key) : /* @__PURE__ */ jsx2(Bound, { base: bindingBase, nodeKey: run[0]._key, children: /* @__PURE__ */ jsx2(PT, { value: run, components }) }, run[0]._key)
496
- ) });
476
+
477
+ // src/renderer.tsx
478
+ import "react";
479
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
480
+ var CONTAINER_TYPES = /* @__PURE__ */ new Set(["columns", "section", "slider", "tabs"]);
481
+ function styleOf(block) {
482
+ if (block.type !== "button") return { wrapper: blockStyleToReactStyle(block.style), paint: {} };
483
+ const { placement, paint } = splitButtonStyle(block.style);
484
+ return { wrapper: blockStyleToReactStyle(placement), paint: paint ? { style: paint } : {} };
497
485
  }
498
- function readValue(value) {
499
- if (Array.isArray(value)) return value;
500
- if (value && typeof value === "object") {
501
- const o = value;
502
- if (Array.isArray(o.value)) return o.value;
503
- if (Array.isArray(o.content)) return o.content;
486
+ function withHide(block, wrapperStyle, el, stampAt) {
487
+ const hide = hideOnAttr(block.style);
488
+ const state = stampAt ? windowState(block, stampAt) : "live";
489
+ const hidden = !!stampAt && hiddenEverywhere(block);
490
+ if (!wrapperStyle && !hide && state === "live" && !hidden) return el;
491
+ return /* @__PURE__ */ jsx13("div", { style: wrapperStyle, ...hide ? { "data-bcms-hide": hide } : {}, ...state === "live" ? {} : { "data-bcms-window": state }, ...hidden ? { "data-bcms-hidden": "all" } : {}, children: el });
492
+ }
493
+ function bindingProps(block, annotate) {
494
+ if (!annotate || !block.id) return {};
495
+ switch (block.type) {
496
+ case "heading":
497
+ case "button":
498
+ return { "data-bcms-field": `${block.id}__text`, "data-bcms-kind": "text" };
499
+ case "text":
500
+ case "richtext":
501
+ return { "data-bcms-field": `${block.id}__html`, "data-bcms-kind": "richtext" };
502
+ case "image":
503
+ return { "data-bcms-field": `${block.id}__src`, "data-bcms-kind": "image" };
504
+ default:
505
+ return {};
504
506
  }
505
- return [];
506
507
  }
507
-
508
- // src/blocks/Text.tsx
509
- import { jsx as jsx3 } from "react/jsx-runtime";
510
- function Text({
511
- html,
512
- content,
513
- align = "left",
508
+ function resolveBlock(block, className, annotate = false, stampAt) {
509
+ const entry = BLOCK_REGISTRY[block.type];
510
+ if (!entry) {
511
+ return /* @__PURE__ */ jsxs10(
512
+ "div",
513
+ {
514
+ className: `w-full p-4 border border-dashed border-red-400 bg-red-50 text-red-600 text-sm rounded ${className ?? ""}`,
515
+ children: [
516
+ "Unknown block type: ",
517
+ /* @__PURE__ */ jsx13("code", { children: block.type })
518
+ ]
519
+ }
520
+ );
521
+ }
522
+ const Component = entry.component;
523
+ const { wrapper: blockStyle, paint } = styleOf(block);
524
+ const el = /* @__PURE__ */ jsx13(Component, { ...block.props, ...paint, className, ...bindingProps(block, annotate) });
525
+ return withHide(block, blockStyle, el, stampAt);
526
+ }
527
+ function BlockRenderer({
528
+ block,
514
529
  className = "",
515
- "data-bcms-field": field,
516
- "data-bcms-kind": kind
530
+ renderBlock,
531
+ annotate = false,
532
+ stampAt
517
533
  }) {
518
- const alignClass = {
519
- left: "text-left",
520
- center: "text-center",
521
- right: "text-right"
522
- }[align];
523
- const cls = `text-muted-foreground leading-relaxed ${alignClass} ${className}`;
524
- const structured = readStructuredContent({ content });
525
- return /* @__PURE__ */ jsx3(
526
- "div",
534
+ const entry = BLOCK_REGISTRY[block.type];
535
+ if (!entry) {
536
+ return /* @__PURE__ */ jsxs10(
537
+ "div",
538
+ {
539
+ className: `w-full p-4 border border-dashed border-red-400 bg-red-50 text-red-600 text-sm rounded ${className}`,
540
+ children: [
541
+ "Unknown block type: ",
542
+ /* @__PURE__ */ jsx13("code", { children: block.type })
543
+ ]
544
+ }
545
+ );
546
+ }
547
+ const Component = entry.component;
548
+ const containerProps = CONTAINER_TYPES.has(block.type) ? { renderBlock: renderBlock ?? ((child) => resolveBlock(child, void 0, annotate, stampAt)) } : {};
549
+ const { wrapper: blockStyle, paint } = styleOf(block);
550
+ const el = /* @__PURE__ */ jsx13(
551
+ Component,
527
552
  {
528
- className: cls,
529
- ...structured ? {} : { "data-bcms-field": field, "data-bcms-kind": kind },
530
- children: structured ? /* @__PURE__ */ jsx3(PortableText, { value: structured, bindingBase: field }) : (
531
- // eslint-disable-next-line react/no-danger
532
- /* @__PURE__ */ jsx3("span", { dangerouslySetInnerHTML: { __html: html } })
533
- )
553
+ ...block.props,
554
+ ...paint,
555
+ className,
556
+ ...containerProps,
557
+ ...bindingProps(block, annotate)
534
558
  }
535
559
  );
560
+ return withHide(block, blockStyle, el, stampAt);
536
561
  }
537
-
538
- // src/lib/utils.ts
539
- import { clsx } from "clsx";
540
- import { twMerge } from "tailwind-merge";
541
- function cn(...inputs) {
542
- return twMerge(clsx(inputs));
543
- }
544
-
545
- // src/blocks/Image.tsx
546
- import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
547
- function Image({
548
- src,
549
- alt,
550
- width,
551
- height,
552
- caption,
562
+ function BlocksRenderer({
563
+ blocks: all,
553
564
  className = "",
554
- sizes,
555
- "data-bcms-field": field,
556
- "data-bcms-kind": kind
565
+ renderBlock,
566
+ annotate = false,
567
+ now
557
568
  }) {
558
- const sized = typeof width === "number" && width > 0;
559
- return /* @__PURE__ */ jsxs2("figure", { className: cn(sized ? "max-w-full" : "w-full", className), children: [
560
- /* @__PURE__ */ jsx4(
561
- "img",
569
+ const at = now ?? /* @__PURE__ */ new Date();
570
+ const blocks = annotate ? all : visibleBlocks(all, at);
571
+ if (blocks.length === 0) return null;
572
+ return /* @__PURE__ */ jsxs10("div", { className, children: [
573
+ usesHideOn(blocks) && /* @__PURE__ */ jsx13("style", { dangerouslySetInnerHTML: { __html: RESPONSIVE_CSS } }),
574
+ blocks.map((block) => /* @__PURE__ */ jsx13(
575
+ BlockRenderer,
562
576
  {
563
- src,
564
- alt,
565
- width,
566
- height,
567
- sizes,
568
- "data-bcms-field": field,
569
- "data-bcms-kind": kind,
570
- style: sized ? { width: `${width}px`, maxWidth: "100%", height: "auto" } : void 0,
571
- className: cn(!sized && "w-full", "h-auto rounded-md object-cover"),
572
- loading: "lazy"
573
- }
574
- ),
575
- caption && /* @__PURE__ */ jsx4("figcaption", { className: "mt-2 text-sm text-muted-foreground text-center italic", children: caption })
577
+ block,
578
+ renderBlock,
579
+ annotate,
580
+ stampAt: annotate ? at : void 0
581
+ },
582
+ block.id
583
+ ))
576
584
  ] });
577
585
  }
578
586
 
579
- // src/blocks/Button.tsx
580
- import { LINK_RELS, buttonIconOf, buttonIconSvg, buttonVariantOf, isScriptHref } from "@bettercms-ai/types";
581
- import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
582
- function Button({
583
- text,
584
- href,
585
- variant = "primary",
586
- size = "md",
587
- fullWidth = false,
588
- arrow = false,
589
- icon: iconProp,
590
- newTab = false,
591
- rel,
592
- action,
593
- align = "left",
594
- className = "",
595
- style,
596
- "data-bcms-field": field,
597
- "data-bcms-kind": kind
598
- }) {
599
- 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";
600
- const variantStyles = {
601
- primary: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
602
- secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
603
- tertiary: "bg-transparent text-primary hover:bg-primary/10",
604
- brand: "bg-accent text-accent-foreground shadow hover:bg-accent/90",
605
- dark: "bg-foreground text-background shadow hover:bg-foreground/90",
606
- light: "border bg-background text-foreground shadow-sm hover:bg-muted",
607
- outline: "border border-current bg-transparent text-primary hover:bg-primary/10",
608
- link: "h-auto bg-transparent p-0 text-primary underline underline-offset-4"
609
- };
610
- const icon = buttonIconOf(iconProp);
611
- const alignClass = {
612
- left: "justify-start",
613
- center: "justify-center",
614
- right: "justify-end"
615
- }[align];
616
- const sizeStyles = {
617
- sm: "px-3 py-1.5 text-xs",
618
- md: "px-4 py-2",
619
- lg: "px-6 py-3 text-base"
620
- };
621
- const sizeClass = size === "sm" || size === "lg" ? sizeStyles[size] : sizeStyles.md;
622
- const fullClass = fullWidth === true ? "flex w-full justify-center" : "";
623
- const relTokens = [...newTab === true ? ["noopener", "noreferrer"] : [], ...LINK_RELS.includes(rel) ? [rel] : []];
624
- if (action === "code") {
625
- return /* @__PURE__ */ jsxs3("button", { type: "button", "data-bcms-field": field, "data-bcms-kind": kind, style, className: cn(baseStyles, variantStyles[buttonVariantOf(variant)], alignClass, sizeClass, fullClass, className), children: [
626
- icon && /* @__PURE__ */ jsx5("span", { className: "mr-2 inline-flex", dangerouslySetInnerHTML: { __html: buttonIconSvg(icon, "bcms-btn-icon") } }),
627
- text,
628
- arrow === true && /* @__PURE__ */ jsx5("span", { className: "ml-2 inline-flex", dangerouslySetInnerHTML: { __html: buttonIconSvg("arrow-right", "bcms-btn-arrow") } })
629
- ] });
587
+ // ../richtext/src/types.ts
588
+ function pullQuoteHref(link) {
589
+ if (typeof link === "string") return link.trim();
590
+ if (link && typeof link === "object" && typeof link.href === "string") {
591
+ return link.href.trim();
592
+ }
593
+ return "";
594
+ }
595
+ var PT_TYPES = {
596
+ /** A generic embedded block: `{schemaKey, schemaLabel, fields}`, the successor to Lexical's BlockNode. */
597
+ block: "bcmsBlock",
598
+ /** An inline mention. A PT inline object is natively void, which is what Lexical's `token` mode was faking. */
599
+ mention: "bcmsMention",
600
+ divider: "bcmsDivider",
601
+ code: "bcmsCode",
602
+ /**
603
+ * A video — an uploaded file or a provider URL. DEDICATED rather than another `schemaKey` on
604
+ * the generic block, because a video is the one media node whose rendition depends on its own
605
+ * payload: the same field is a `<video>` for an mp4 and a provider `<iframe>` for a YouTube
606
+ * link (@see resolveVideoSource). The generic block carries a reference this package cannot
607
+ * resolve; a video carries a URL it can.
608
+ */
609
+ video: "video",
610
+ /** A downloadable document — a PDF, a spec sheet. `<a href download>`, nothing more. */
611
+ file: "file",
612
+ /** A pull quote — see `PortableTextPullQuote`. The V2 kit's `<figure data-rt="pullquote">`. */
613
+ pullQuote: "pullQuote",
614
+ /** Key takeaways — see `PortableTextKeyTakeaways`. The V2 kit's `<aside data-rt="takeaways">`. */
615
+ keyTakeaways: "keyTakeaways",
616
+ /**
617
+ * A table. Spelled `table`, unprefixed, because that is what every other Portable Text
618
+ * producer calls one — the Sanity-shaped `{_type:"table", rows}` this platform used to refuse
619
+ * as foreign content now lands on OUR type and renders, rather than being data nothing draws.
620
+ */
621
+ table: "table",
622
+ /** The annotation carrying a hyperlink's `href`. Spelled `link` because that is the spec's convention. */
623
+ link: "link",
624
+ /**
625
+ * The annotation carrying a site's OWN inline markup — `<span class>`, `<small>`, `<time
626
+ * datetime>`. Unprefixed, like `table`, because it describes HTML rather than a BetterCMS
627
+ * concept. See `styled.ts` for what it may hold and why the list is that short.
628
+ */
629
+ styled: "styled"
630
+ };
631
+ var BUILTIN_TABLE_KEY = "builtin:table";
632
+ var STORABLE_BLOCK_TYPES = /* @__PURE__ */ new Set([
633
+ "block",
634
+ PT_TYPES.block,
635
+ PT_TYPES.table,
636
+ PT_TYPES.code,
637
+ PT_TYPES.divider,
638
+ PT_TYPES.mention,
639
+ PT_TYPES.video,
640
+ PT_TYPES.file,
641
+ PT_TYPES.pullQuote,
642
+ PT_TYPES.keyTakeaways
643
+ ]);
644
+ var EMBEDDED_COMPONENT_KEY = "component";
645
+
646
+ // ../richtext/src/styled.ts
647
+ var STYLED_TAGS = [
648
+ "span",
649
+ "small",
650
+ "sup",
651
+ "sub",
652
+ "kbd",
653
+ "abbr",
654
+ "time",
655
+ "strong",
656
+ "b",
657
+ "em",
658
+ "i",
659
+ "u",
660
+ "s",
661
+ "mark",
662
+ "code",
663
+ "q",
664
+ "cite",
665
+ "dfn",
666
+ "var",
667
+ "samp",
668
+ "ins",
669
+ "del"
670
+ ];
671
+ var TAG_SET = new Set(STYLED_TAGS);
672
+ var isStyledTag = (tag) => typeof tag === "string" && TAG_SET.has(tag);
673
+ var ATTR_NAMES = /* @__PURE__ */ new Set(["title", "datetime", "style", "aria-label"]);
674
+ var DATA_ATTR = /^data-[a-z][a-z0-9-]*$/;
675
+ var isStyledAttr = (name) => ATTR_NAMES.has(name) || DATA_ATTR.test(name) && !name.startsWith("data-bcms-");
676
+ var STYLE_PROPS = /* @__PURE__ */ new Set([
677
+ "color",
678
+ "background-color",
679
+ "opacity",
680
+ "font",
681
+ "font-family",
682
+ "font-size",
683
+ "font-style",
684
+ "font-weight",
685
+ "font-variant",
686
+ "font-feature-settings",
687
+ "font-variation-settings",
688
+ "letter-spacing",
689
+ "line-height",
690
+ "word-spacing",
691
+ "white-space",
692
+ "word-break",
693
+ "text-align",
694
+ "text-decoration",
695
+ "text-decoration-color",
696
+ "text-decoration-line",
697
+ "text-decoration-style",
698
+ "text-transform",
699
+ "text-underline-offset",
700
+ "vertical-align"
701
+ ]);
702
+ var STYLE_VALUE_BAN = /url\(|expression\(|@import|\\|\/\*|[<>]/i;
703
+ function safeStyle(css) {
704
+ const kept = [];
705
+ for (const declaration of css.split(";")) {
706
+ const at = declaration.indexOf(":");
707
+ if (at < 0) continue;
708
+ const name = declaration.slice(0, at).trim().toLowerCase();
709
+ const value = declaration.slice(at + 1).trim();
710
+ if (!name || !value) continue;
711
+ if (!name.startsWith("--") && !STYLE_PROPS.has(name)) continue;
712
+ if (STYLE_VALUE_BAN.test(value)) continue;
713
+ kept.push(`${name}:${value}`);
630
714
  }
631
- return /* @__PURE__ */ jsxs3(
632
- "a",
633
- {
634
- href: typeof href === "string" && isScriptHref(href) ? "#" : href,
635
- ...newTab === true ? { target: "_blank" } : {},
636
- ...relTokens.length ? { rel: relTokens.join(" ") } : {},
637
- "data-bcms-field": field,
638
- "data-bcms-kind": kind,
639
- style,
640
- className: cn(baseStyles, variantStyles[buttonVariantOf(variant)], alignClass, sizeClass, fullClass, className),
641
- children: [
642
- icon && /* @__PURE__ */ jsx5("span", { className: "mr-2 inline-flex", dangerouslySetInnerHTML: { __html: buttonIconSvg(icon, "bcms-btn-icon") } }),
643
- text,
644
- arrow === true && /* @__PURE__ */ jsx5("span", { className: "ml-2 inline-flex", dangerouslySetInnerHTML: { __html: buttonIconSvg("arrow-right", "bcms-btn-arrow") } })
645
- ]
646
- }
647
- );
715
+ return kept.length ? kept.join(";") : void 0;
716
+ }
717
+ function styledAttrEntries(attrs) {
718
+ if (!attrs || typeof attrs !== "object" || Array.isArray(attrs)) return [];
719
+ const out = [];
720
+ for (const [name, raw] of Object.entries(attrs)) {
721
+ if (!isStyledAttr(name) || typeof raw !== "string") continue;
722
+ const value = name === "style" ? safeStyle(raw) : raw;
723
+ if (value === void 0) continue;
724
+ out.push([name, value]);
725
+ }
726
+ return out;
648
727
  }
649
728
 
650
- // src/blocks/Spacer.tsx
651
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
652
- function Spacer({ height = 32, className = "" }) {
653
- return /* @__PURE__ */ jsx6(
654
- "div",
655
- {
656
- className: `w-full border border-dashed border-muted-foreground/20 rounded flex items-center justify-center ${className}`,
657
- style: { height: `${height}px` },
658
- "aria-hidden": "true",
659
- children: /* @__PURE__ */ jsxs4("span", { className: "text-xs text-muted-foreground/40 select-none", children: [
660
- height,
661
- "px"
662
- ] })
663
- }
664
- );
729
+ // ../richtext/src/href.ts
730
+ var SAFE_HREF_SCHEMES = ["http:", "https:", "mailto:", "tel:"];
731
+ function safeHref(raw) {
732
+ const trimmed = raw.trim();
733
+ if (trimmed.startsWith("#")) return /^#[^\s<>"'`]*$/.test(trimmed) ? trimmed : null;
734
+ if (trimmed.startsWith("/") && !trimmed.startsWith("//")) return trimmed;
735
+ try {
736
+ return SAFE_HREF_SCHEMES.includes(new URL(trimmed).protocol) ? trimmed : null;
737
+ } catch {
738
+ return null;
739
+ }
665
740
  }
666
741
 
667
- // src/blocks/Columns.tsx
668
- import "react";
669
- import { jsx as jsx7 } from "react/jsx-runtime";
670
- function Columns({
671
- columns,
672
- gap = 16,
673
- className = "",
674
- renderBlock
675
- }) {
676
- return /* @__PURE__ */ jsx7(
677
- "div",
678
- {
679
- className: cn("grid w-full", className),
680
- style: {
681
- gridTemplateColumns: `repeat(${columns.length}, minmax(0, 1fr))`,
682
- gap: `${gap}px`
683
- },
684
- children: columns.map((columnBlocks, colIndex) => /* @__PURE__ */ jsx7("div", { className: "flex flex-col gap-4 min-w-0", children: columnBlocks.map((block) => /* @__PURE__ */ jsx7("span", { children: renderBlock ? renderBlock(block) : null }, block.id)) }, colIndex))
685
- }
686
- );
742
+ // ../richtext/src/key.ts
743
+ var ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
744
+ var KEY_PATTERN = /^[A-Za-z0-9-]{1,32}$/;
745
+ var isValidKey = (k) => typeof k === "string" && KEY_PATTERN.test(k);
746
+ function randomKey(length = 12) {
747
+ let out = "";
748
+ for (let i = 0; i < length; i += 1) {
749
+ out += ALPHABET[Math.floor(Math.random() * ALPHABET.length)];
750
+ }
751
+ return out;
752
+ }
753
+ function deterministicKeys(prefix = "k") {
754
+ let n = 0;
755
+ return () => {
756
+ n += 1;
757
+ return `${prefix}${n}`;
758
+ };
687
759
  }
688
760
 
689
- // src/blocks/Video.tsx
690
- import { useState } from "react";
691
- import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
692
- function getEmbedUrl(url) {
693
- const ytMatch = url.match(
694
- /(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
695
- );
696
- if (ytMatch) {
697
- return {
698
- embedUrl: `https://www.youtube.com/embed/${ytMatch[1]}`,
699
- provider: "youtube"
700
- };
761
+ // ../richtext/src/table.ts
762
+ var isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
763
+ var isPortableTextTable = (n) => isRecord(n) && n._type === PT_TYPES.table && Array.isArray(n.rows);
764
+ var isBuiltInTableBlock = (n) => isRecord(n) && n._type === PT_TYPES.block && n.schemaKey === BUILTIN_TABLE_KEY;
765
+ var asIs = (v) => Array.isArray(v) ? v : [];
766
+ function keyMinter(base, reserved) {
767
+ const prefix = (base.match(/[A-Za-z0-9-]/g) ?? []).join("").slice(0, 16) || "t";
768
+ return (...parts) => {
769
+ const candidate = `${prefix}-${parts.join("")}`.slice(0, 30);
770
+ let key = candidate;
771
+ for (let n = 1; reserved.has(key); n += 1) key = `${candidate}-${n}`;
772
+ reserved.add(key);
773
+ return key;
774
+ };
775
+ }
776
+ function reservedKeys(rows) {
777
+ const out = /* @__PURE__ */ new Set();
778
+ for (const row of rows) {
779
+ if (!isRecord(row)) continue;
780
+ if (isValidKey(row._key)) out.add(row._key);
781
+ for (const cell of Array.isArray(row.cells) ? row.cells : []) {
782
+ if (isRecord(cell) && isValidKey(cell._key)) out.add(cell._key);
783
+ }
701
784
  }
702
- const vimeoMatch = url.match(/vimeo\.com\/(\d+)/);
703
- if (vimeoMatch) {
785
+ return out;
786
+ }
787
+ var textContent = (text, key) => text === "" ? [] : [{
788
+ _type: "block",
789
+ _key: key(),
790
+ style: "normal",
791
+ markDefs: [],
792
+ children: [{ _type: "span", _key: key(), text, marks: [] }]
793
+ }];
794
+ var spanOf = (value) => {
795
+ const n = typeof value === "number" ? value : Number(value);
796
+ return Number.isInteger(n) && n > 1 ? n : void 0;
797
+ };
798
+ function normalizeCell(raw, at, mint, read) {
799
+ const [row, col] = at;
800
+ const key = isRecord(raw) && isValidKey(raw._key) ? raw._key : mint("r", row, "c", col);
801
+ const inner = deterministicKeys(`${key}-`);
802
+ if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
803
+ return { _key: key, content: textContent(String(raw), inner) };
804
+ }
805
+ if (!isRecord(raw)) return { _key: key, content: [] };
806
+ const span = spanOf(raw.span ?? raw.colSpan ?? raw.colspan);
807
+ const rowSpan = spanOf(raw.rowSpan ?? raw.rowspan);
808
+ return {
809
+ _key: key,
810
+ // `text` is the spelling a string matrix uses once its cells have been given objects; it is
811
+ // read here so a half-migrated value cannot lose its words.
812
+ content: Array.isArray(raw.content) ? read(raw.content, inner) : textContent(typeof raw.text === "string" ? raw.text : "", inner),
813
+ ...raw.header === true ? { header: true } : {},
814
+ ...span === void 0 ? {} : { span },
815
+ ...rowSpan === void 0 ? {} : { rowSpan },
816
+ ...typeof raw.group === "string" ? { group: raw.group } : {}
817
+ };
818
+ }
819
+ function normalizeRow(raw, at, mint, read) {
820
+ const cells = Array.isArray(raw) ? raw : isRecord(raw) && Array.isArray(raw.cells) ? raw.cells : [];
821
+ return {
822
+ _key: isRecord(raw) && isValidKey(raw._key) ? raw._key : mint("r", at),
823
+ cells: cells.map((c, col) => normalizeCell(c, [at, col], mint, read)),
824
+ ...isRecord(raw) && raw.head === true ? { head: true } : {},
825
+ ...isRecord(raw) && typeof raw.group === "string" ? { group: raw.group } : {}
826
+ };
827
+ }
828
+ function builtInTableToTable(node, key = randomKey) {
829
+ if (!isBuiltInTableBlock(node)) return null;
830
+ const fields = node.fields;
831
+ const raw = isRecord(fields) && Array.isArray(fields.rows) ? fields.rows : [];
832
+ const hasHeader = isRecord(fields) ? fields.hasHeader !== false : true;
833
+ const own = node._key;
834
+ const tableKey = isValidKey(own) ? own : key();
835
+ const mint = keyMinter(tableKey, reservedKeys(raw));
836
+ const rows = raw.map((row, i) => {
837
+ const normalized = normalizeRow(row, i, mint, asIs);
838
+ if (!(hasHeader && i === 0)) return normalized;
704
839
  return {
705
- embedUrl: `https://player.vimeo.com/video/${vimeoMatch[1]}`,
706
- provider: "vimeo"
840
+ ...normalized,
841
+ head: true,
842
+ cells: normalized.cells.map((c) => ({ ...c, header: true }))
707
843
  };
844
+ });
845
+ const width = rows.reduce((max, r) => Math.max(max, r.cells.length), 0);
846
+ for (const [i, row] of rows.entries()) {
847
+ while (row.cells.length < width) {
848
+ row.cells.push({
849
+ _key: mint("r", i, "c", row.cells.length),
850
+ content: [],
851
+ ...row.head === true ? { header: true } : {}
852
+ });
853
+ }
708
854
  }
709
- return { embedUrl: url, provider: "unknown" };
855
+ return { _type: PT_TYPES.table, _key: tableKey, rows, legacy: true };
710
856
  }
711
- function Video({
712
- url,
713
- poster,
714
- autoplay = false,
715
- loop = false,
716
- className = ""
717
- }) {
718
- const [isLoaded, setIsLoaded] = useState(false);
719
- const { embedUrl, provider } = getEmbedUrl(url);
720
- if (provider === "unknown") {
721
- return /* @__PURE__ */ jsx8(
722
- "video",
723
- {
724
- src: url,
725
- poster,
726
- autoPlay: autoplay,
727
- loop,
728
- controls: true,
729
- className,
730
- playsInline: true
731
- }
732
- );
857
+ function migrateTable(node, key = randomKey, read = asIs) {
858
+ const builtIn = builtInTableToTable(node, key);
859
+ if (builtIn) return builtIn;
860
+ if (!isPortableTextTable(node)) return null;
861
+ const rows = node.rows;
862
+ const tableKey = isValidKey(node._key) ? node._key : key();
863
+ const mint = keyMinter(tableKey, reservedKeys(rows));
864
+ return {
865
+ _type: PT_TYPES.table,
866
+ _key: tableKey,
867
+ rows: rows.map((r, i) => normalizeRow(r, i, mint, read)),
868
+ // Carried through: a migrated table that an author saves back is still the same table, and
869
+ // dropping the flag on that save would change the page the next time it renders.
870
+ ...node.legacy === true ? { legacy: true } : {},
871
+ ...tableExtras(node)
872
+ };
873
+ }
874
+ function tableExtras(node) {
875
+ const groups = (list) => Array.isArray(list) ? list.filter((g) => isRecord(g) && typeof g._key === "string" && g._key !== "").map((g) => ({ _key: g._key, label: typeof g.label === "string" ? g.label : "" })) : void 0;
876
+ const rowGroups = groups(node.rowGroups);
877
+ const columnGroups = groups(node.columnGroups);
878
+ return {
879
+ ...typeof node.caption === "string" ? { caption: node.caption } : {},
880
+ ...typeof node.notes === "string" ? { notes: node.notes } : {},
881
+ ...rowGroups ? { rowGroups } : {},
882
+ ...columnGroups ? { columnGroups } : {}
883
+ };
884
+ }
885
+ function tableGroupLayout(table) {
886
+ const rows = Array.isArray(table.rows) ? table.rows : [];
887
+ const labels = (list) => new Map(
888
+ (Array.isArray(list) ? list : []).filter((g) => isRecord(g) && typeof g._key === "string").map((g) => [g._key, typeof g.label === "string" ? g.label : ""])
889
+ );
890
+ const cellsOf = (row) => isRecord(row) && Array.isArray(row.cells) ? row.cells : [];
891
+ const spanOf2 = (cell) => isRecord(cell) && typeof cell.span === "number" && Number.isInteger(cell.span) && cell.span > 1 ? cell.span : 1;
892
+ const width = rows.reduce((max, row) => Math.max(max, cellsOf(row).reduce((n, c) => n + spanOf2(c), 0)), 0);
893
+ const colLabels = labels(table.columnGroups);
894
+ const columns = [];
895
+ for (const cell of cellsOf(rows[0])) {
896
+ const key = isRecord(cell) && typeof cell.group === "string" && colLabels.has(cell.group) ? cell.group : void 0;
897
+ const last = columns[columns.length - 1];
898
+ if (last && last.key === key) last.width += spanOf2(cell);
899
+ else columns.push({ width: spanOf2(cell), ...key === void 0 ? {} : { key, label: colLabels.get(key) } });
733
900
  }
734
- return /* @__PURE__ */ jsxs5("div", { className: cn("w-full overflow-hidden rounded-md bg-black aspect-video", className), children: [
735
- !isLoaded && poster && // eslint-disable-next-line @next/next/no-img-element
736
- /* @__PURE__ */ jsx8(
737
- "img",
738
- {
739
- src: poster,
740
- alt: "Video thumbnail",
741
- className: "w-full h-full object-cover absolute inset-0"
742
- }
743
- ),
744
- /* @__PURE__ */ jsx8(
745
- "iframe",
746
- {
747
- src: `${embedUrl}?autoplay=${autoplay ? 1 : 0}&loop=${loop ? 1 : 0}&mute=${autoplay ? 1 : 0}`,
748
- allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
749
- allowFullScreen: true,
750
- loading: "lazy",
751
- onLoad: () => setIsLoaded(true),
752
- className: "w-full h-full"
753
- }
754
- )
755
- ] });
901
+ const rowLabels = labels(table.rowGroups);
902
+ const headCount = rows.findIndex((r) => !isRecord(r) || r.head !== true);
903
+ const body = headCount === -1 ? [] : rows.slice(headCount);
904
+ const runs = [];
905
+ for (const row of body) {
906
+ const key = isRecord(row) && typeof row.group === "string" && rowLabels.has(row.group) ? row.group : void 0;
907
+ const last = runs[runs.length - 1];
908
+ if (last && last.key === key) last.rows.push(row);
909
+ else runs.push({ rows: [row], ...key === void 0 ? {} : { key, label: rowLabels.get(key) } });
910
+ }
911
+ return {
912
+ columns: columns.some((r) => r.key !== void 0) ? columns : [],
913
+ rows: runs.some((r) => r.key !== void 0) ? runs : [],
914
+ width
915
+ };
756
916
  }
757
917
 
758
- // src/blocks/Section.tsx
759
- import "react";
760
- import { jsx as jsx9 } from "react/jsx-runtime";
761
- function Section({
762
- children,
763
- background,
764
- paddingY,
765
- maxWidth,
766
- className = "",
767
- renderBlock
768
- }) {
769
- const inner = (children ?? []).map((block) => /* @__PURE__ */ jsx9("span", { children: renderBlock ? renderBlock(block) : null }, block.id));
770
- return /* @__PURE__ */ jsx9(
771
- "section",
772
- {
773
- className: cn("w-full", className),
774
- style: { background, paddingTop: paddingY, paddingBottom: paddingY },
775
- children: maxWidth ? /* @__PURE__ */ jsx9("div", { style: { maxWidth, marginInline: "auto" }, children: inner }) : inner
776
- }
777
- );
918
+ // ../richtext/src/key-takeaways.ts
919
+ var isRecord2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
920
+ function keyTakeawaysView(value) {
921
+ const v = isRecord2(value) ? value : {};
922
+ const title = typeof v.title === "string" ? v.title.trim() : "";
923
+ const points = (Array.isArray(v.points) ? v.points : []).map((p) => typeof p === "string" ? p.trim() : "").filter(Boolean);
924
+ return { title, points };
778
925
  }
779
926
 
780
- // src/blocks/Navbar.tsx
781
- import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
782
- function Navbar({ logo, links, cta, className = "" }) {
783
- return /* @__PURE__ */ jsxs6("nav", { className: cn("flex items-center gap-6 py-4", className), children: [
784
- logo?.src ? /* @__PURE__ */ jsx10("a", { href: logo.href ?? "/", className: "shrink-0", children: /* @__PURE__ */ jsx10("img", { src: logo.src, alt: logo.alt, className: "h-8 w-auto" }) }) : null,
785
- /* @__PURE__ */ jsx10("div", { className: "flex flex-1 gap-5", children: (links ?? []).map((l, i) => /* @__PURE__ */ jsx10("a", { href: l.href, className: "text-inherit no-underline", children: l.label }, i)) }),
786
- cta ? /* @__PURE__ */ jsx10(
787
- "a",
788
- {
789
- href: cta.href,
790
- className: "rounded-lg bg-black px-4 py-2 font-semibold text-white no-underline",
791
- children: cta.text
792
- }
793
- ) : null
794
- ] });
927
+ // ../richtext/src/component-copy.ts
928
+ var isRecord3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
929
+ var fieldsOf = (value) => isRecord3(value) && value.schemaKey === EMBEDDED_COMPONENT_KEY && isRecord3(value.fields) ? value.fields : null;
930
+ function detachedComponentCopy(value) {
931
+ const f = fieldsOf(value);
932
+ if (!f || f.detached !== true || !Array.isArray(f.blocks)) return null;
933
+ return {
934
+ id: isRecord3(value) && typeof value._key === "string" ? value._key : "",
935
+ componentId: typeof f.componentId === "string" ? f.componentId : "",
936
+ props: isRecord3(f.props) ? f.props : {},
937
+ blocks: f.blocks,
938
+ ...typeof f.sourcePublishedAt === "string" ? { sourcePublishedAt: f.sourcePublishedAt } : {},
939
+ ...typeof f.variant === "string" ? { variant: f.variant } : {}
940
+ };
795
941
  }
796
942
 
797
- // src/blocks/Footer.tsx
798
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
799
- function Footer({ columns, copyright, className = "" }) {
800
- return /* @__PURE__ */ jsxs7("footer", { className: cn("mt-12 border-t pt-8", className), children: [
801
- /* @__PURE__ */ jsx11("div", { className: "flex flex-wrap gap-12", children: (columns ?? []).map((col, i) => /* @__PURE__ */ jsxs7("div", { className: "flex flex-col gap-1.5", children: [
802
- col.heading ? /* @__PURE__ */ jsx11("h4", { className: "mb-2 font-semibold", children: col.heading }) : null,
803
- (col.links ?? []).map((l, j) => /* @__PURE__ */ jsx11("a", { href: l.href, className: "text-neutral-500 no-underline", children: l.label }, j))
804
- ] }, i)) }),
805
- copyright ? /* @__PURE__ */ jsx11("p", { className: "mt-6 text-sm text-neutral-500", children: copyright }) : null
806
- ] });
943
+ // ../richtext/src/read.ts
944
+ var isRecord4 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
945
+ function normalizePortableText(value, key = randomKey) {
946
+ if (!Array.isArray(value)) return [];
947
+ return value.flatMap((raw) => {
948
+ if (!isRecord4(raw) || typeof raw._type !== "string") return [];
949
+ const table = migrateTable(raw, key, (v, cellKey) => normalizePortableText(v, cellKey));
950
+ if (table) return [table];
951
+ const node = { ...raw, _key: isRecord4(raw) && typeof raw._key === "string" && raw._key ? raw._key : key() };
952
+ if (node._type === "block") {
953
+ if (!Array.isArray(node.markDefs)) node.markDefs = [];
954
+ if (typeof node.style !== "string") node.style = "normal";
955
+ node.children = (Array.isArray(node.children) ? node.children : []).map((c) => {
956
+ if (!isRecord4(c)) return c;
957
+ const child = { ...c, _key: typeof c._key === "string" && c._key ? c._key : key() };
958
+ if (child._type === "span" && !Array.isArray(child.marks)) child.marks = [];
959
+ return child;
960
+ });
961
+ }
962
+ return [node];
963
+ });
964
+ }
965
+ function readStructuredContent(props) {
966
+ if (!isRecord4(props)) return null;
967
+ const candidate = props.content ?? props.value;
968
+ const arr = Array.isArray(candidate) ? candidate : isRecord4(candidate) ? Array.isArray(candidate.content) ? candidate.content : Array.isArray(candidate.value) ? candidate.value : null : null;
969
+ return arr && arr.length > 0 ? arr : null;
807
970
  }
808
971
 
809
- // src/blocks/Slider.tsx
810
- import { useState as useState2 } from "react";
811
- import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
812
- function Slider({ slides, className = "", renderBlock }) {
813
- const [i, setI] = useState2(0);
814
- const list = slides ?? [];
815
- const go = (n) => setI((n + list.length) % (list.length || 1));
816
- const btn = "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";
817
- return /* @__PURE__ */ jsxs8("div", { className: cn("relative w-full", className), children: [
818
- list.map((s, idx) => /* @__PURE__ */ jsx12("div", { hidden: idx !== i, className: "flex flex-col gap-4", children: (s.children ?? []).map((b) => /* @__PURE__ */ jsx12("span", { children: renderBlock ? renderBlock(b) : null }, b.id)) }, s.id)),
819
- /* @__PURE__ */ jsx12("button", { type: "button", "aria-label": "Previous", onClick: () => go(i - 1), className: cn(btn, "left-2"), children: "\u2039" }),
820
- /* @__PURE__ */ jsx12("button", { type: "button", "aria-label": "Next", onClick: () => go(i + 1), className: cn(btn, "right-2"), children: "\u203A" })
972
+ // src/blocks/portableText.tsx
973
+ import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
974
+ var TOKEN = /^[a-z0-9-]+$/i;
975
+ function styleObject(css) {
976
+ const out = {};
977
+ for (const declaration of css.split(";")) {
978
+ const at = declaration.indexOf(":");
979
+ if (at < 0) continue;
980
+ const name = declaration.slice(0, at).trim();
981
+ const value = declaration.slice(at + 1).trim();
982
+ if (!name || !value) continue;
983
+ out[name.startsWith("--") ? name : name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = value;
984
+ }
985
+ return out;
986
+ }
987
+ function Bound({
988
+ base,
989
+ nodeKey,
990
+ children
991
+ }) {
992
+ if (!base || !nodeKey) return /* @__PURE__ */ jsx14(Fragment2, { children });
993
+ return /* @__PURE__ */ jsx14("div", { "data-bcms-field": `${base}#${nodeKey}`, "data-bcms-kind": "richtext", children });
994
+ }
995
+ function Table({ value }) {
996
+ const components = React6.useMemo(buildComponents, []);
997
+ const inline = React6.useMemo(
998
+ () => ({ ...components, block: { normal: ({ children }) => /* @__PURE__ */ jsx14(Fragment2, { children }) } }),
999
+ [components]
1000
+ );
1001
+ const rows = Array.isArray(value?.rows) ? value.rows : [];
1002
+ if (rows.length === 0) return null;
1003
+ const cells = (row) => (Array.isArray(row?.cells) ? row.cells : []).map((cell) => {
1004
+ const Tag = cell?.header === true ? "th" : "td";
1005
+ const span2 = typeof cell?.span === "number" && cell.span > 1 ? cell.span : void 0;
1006
+ const rowSpan = typeof cell?.rowSpan === "number" && cell.rowSpan > 1 ? cell.rowSpan : void 0;
1007
+ const content = Array.isArray(cell?.content) ? cell.content : [];
1008
+ const lone = content.length === 1 && content[0]._type === "block" && content[0].style === "normal" && !content[0].listItem;
1009
+ return /* @__PURE__ */ jsx14(Tag, { ...span2 ? { colSpan: span2 } : {}, ...rowSpan ? { rowSpan } : {}, children: /* @__PURE__ */ jsx14(PT, { value: content, components: lone ? inline : components }) }, cell?._key);
1010
+ });
1011
+ const rowEl = (row) => /* @__PURE__ */ jsx14("tr", { children: cells(row) }, row?._key);
1012
+ const headCount = rows.findIndex((r) => r?.head !== true);
1013
+ const head = headCount === -1 ? rows.length : headCount;
1014
+ const layout = tableGroupLayout(value);
1015
+ const caption = typeof value.caption === "string" ? value.caption.trim() : "";
1016
+ const notes = typeof value.notes === "string" ? value.notes.trim() : "";
1017
+ const notesId = notes && typeof value._key === "string" && value._key ? `${value._key}-notes` : void 0;
1018
+ const span = (n) => n > 1 ? n : void 0;
1019
+ const groupRow = layout.columns.length ? /* @__PURE__ */ jsx14("tr", { children: layout.columns.flatMap(
1020
+ (run, i) => run.label === void 0 ? Array.from({ length: run.width }, (_, k) => /* @__PURE__ */ jsx14("td", {}, `${i}-${k}`)) : [/* @__PURE__ */ jsx14("th", { scope: "colgroup", colSpan: span(run.width), "data-group": run.key, children: run.label }, i)]
1021
+ ) }) : null;
1022
+ const bodies = layout.rows.length ? layout.rows.map((run, i) => /* @__PURE__ */ jsxs11("tbody", { "data-key": run.key, children: [
1023
+ run.label !== void 0 && /* @__PURE__ */ jsx14("tr", { children: /* @__PURE__ */ jsx14("th", { scope: "rowgroup", colSpan: span(layout.width), children: run.label }) }),
1024
+ run.rows.map(rowEl)
1025
+ ] }, i)) : head < rows.length && /* @__PURE__ */ jsx14("tbody", { children: rows.slice(head).map(rowEl) });
1026
+ const table = /* @__PURE__ */ jsxs11("table", { ...value.legacy === true ? { className: "bcms-table" } : {}, ...notesId ? { "aria-describedby": notesId } : {}, children: [
1027
+ caption && /* @__PURE__ */ jsx14("caption", { children: caption }),
1028
+ layout.columns.map((run, i) => /* @__PURE__ */ jsx14("colgroup", { span: span(run.width) }, i)),
1029
+ (groupRow || head > 0) && /* @__PURE__ */ jsxs11("thead", { children: [
1030
+ groupRow,
1031
+ rows.slice(0, head).map(rowEl)
1032
+ ] }),
1033
+ bodies
821
1034
  ] });
1035
+ return notes ? /* @__PURE__ */ jsxs11("figure", { "data-rt": "table", children: [
1036
+ table,
1037
+ /* @__PURE__ */ jsx14("p", { "data-notes": "", id: notesId, children: notes })
1038
+ ] }) : table;
822
1039
  }
823
-
824
- // src/blocks/Tabs.tsx
825
- import { useState as useState3 } from "react";
826
- import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
827
- function Tabs({ tabs, className = "", renderBlock }) {
828
- const [active, setActive] = useState3(0);
829
- const list = tabs ?? [];
830
- return /* @__PURE__ */ jsxs9("div", { className: cn("w-full", className), children: [
831
- /* @__PURE__ */ jsx13("div", { role: "tablist", className: "mb-4 flex gap-1 border-b", children: list.map((t, idx) => /* @__PURE__ */ jsx13(
832
- "button",
833
- {
834
- role: "tab",
835
- type: "button",
836
- "aria-selected": idx === active,
837
- onClick: () => setActive(idx),
838
- className: cn(
839
- "cursor-pointer border-b-2 border-transparent bg-transparent px-4 py-2",
840
- idx === active && "border-black font-semibold"
841
- ),
842
- children: t.label
1040
+ function buildComponents(renderBlock) {
1041
+ return {
1042
+ types: {
1043
+ [PT_TYPES.divider]: () => /* @__PURE__ */ jsx14("hr", {}),
1044
+ [PT_TYPES.table]: ({ value }) => /* @__PURE__ */ jsx14(Table, { value }),
1045
+ [PT_TYPES.code]: ({ value }) => {
1046
+ const v = value;
1047
+ const lang = typeof v.language === "string" && TOKEN.test(v.language) ? v.language : void 0;
1048
+ return /* @__PURE__ */ jsx14("pre", { children: /* @__PURE__ */ jsx14("code", { className: lang ? `language-${lang}` : void 0, children: typeof v.code === "string" ? v.code : "" }) });
843
1049
  },
844
- t.id
845
- )) }),
846
- list.map((t, idx) => /* @__PURE__ */ jsx13("div", { role: "tabpanel", hidden: idx !== active, className: "flex flex-col gap-4", children: (t.children ?? []).map((b) => /* @__PURE__ */ jsx13("span", { children: renderBlock ? renderBlock(b) : null }, b.id)) }, t.id))
847
- ] });
1050
+ /**
1051
+ * A generic embedded block renders as a marker, not as content: its payload references
1052
+ * something (a component, a media id) that has to be RESOLVED against the project, and
1053
+ * this renderer has no project. A marker keeps the node visible and addressable rather
1054
+ * than rendering nothing at all, which is how embedded content used to vanish. A caller
1055
+ * that CAN resolve it passes `renderBlock`.
1056
+ */
1057
+ [PT_TYPES.block]: ({ value }) => {
1058
+ const rendered = renderBlock?.(value);
1059
+ if (rendered !== void 0) return /* @__PURE__ */ jsx14(Fragment2, { children: rendered });
1060
+ const copy = detachedComponentCopy(value);
1061
+ if (copy) {
1062
+ return /* @__PURE__ */ jsx14("div", { className: "bcms-block bcms-block--component", "data-bcms-component-copy": copy.componentId, "data-bcms-instance": copy.id, children: /* @__PURE__ */ jsx14(BlocksRenderer, { blocks: copy.blocks }) });
1063
+ }
1064
+ const v = value;
1065
+ const key = typeof v.schemaKey === "string" ? v.schemaKey : "block";
1066
+ return /* @__PURE__ */ jsx14("div", { className: "bcms-block", "data-bcms-schema-key": key });
1067
+ },
1068
+ /**
1069
+ * A pull quote — the React half of `portableTextToHtml`'s, and the same DOM: the V2 kit's
1070
+ * `<figure data-rt="pullquote">`, the name linked (and the quote cited) only through
1071
+ * `safeHref`. pull-quote.test.tsx compares the two.
1072
+ */
1073
+ [PT_TYPES.pullQuote]: ({ value }) => {
1074
+ const v = value;
1075
+ const text = (x) => typeof x === "string" ? x.trim() : "";
1076
+ const quote = typeof v.quote === "string" ? v.quote : "";
1077
+ const name = text(v.name);
1078
+ const role = text(v.role);
1079
+ const href = safeHref(pullQuoteHref(v.link)) ?? void 0;
1080
+ return /* @__PURE__ */ jsxs11("figure", { "data-rt": "pullquote", children: [
1081
+ /* @__PURE__ */ jsx14("blockquote", { cite: href, children: /* @__PURE__ */ jsx14("p", { children: quote }) }),
1082
+ name || role ? /* @__PURE__ */ jsxs11("figcaption", { children: [
1083
+ name ? href ? /* @__PURE__ */ jsx14("a", { href, "data-name": "", children: name }) : /* @__PURE__ */ jsx14("span", { "data-name": "", children: name }) : null,
1084
+ name && role ? ", " : null,
1085
+ role ? /* @__PURE__ */ jsx14("span", { "data-role": "", children: role }) : null
1086
+ ] }) : null
1087
+ ] });
1088
+ },
1089
+ /**
1090
+ * Key takeaways — the React half of `portableTextToHtml`'s, and the same DOM: the V2 kit's
1091
+ * `<aside data-rt="takeaways">`, read through the one `keyTakeawaysView` both renderers share.
1092
+ * portable-text-key-takeaways.test.tsx compares the two.
1093
+ */
1094
+ [PT_TYPES.keyTakeaways]: ({ value }) => {
1095
+ const { title, points } = keyTakeawaysView(value);
1096
+ return /* @__PURE__ */ jsxs11("aside", { "data-rt": "takeaways", children: [
1097
+ title ? /* @__PURE__ */ jsx14("p", { "data-title": "", children: title }) : null,
1098
+ /* @__PURE__ */ jsx14("ul", { children: points.map((p, i) => /* @__PURE__ */ jsx14("li", { children: p }, i)) })
1099
+ ] });
1100
+ },
1101
+ [PT_TYPES.mention]: ({ value }) => {
1102
+ const v = value;
1103
+ return /* @__PURE__ */ jsx14("span", { className: "bcms-mention", children: typeof v.label === "string" ? v.label : "" });
1104
+ }
1105
+ },
1106
+ marks: {
1107
+ /**
1108
+ * The only attribute this renderer writes from author data. An unsafe href renders the
1109
+ * LABEL rather than deleting it — blocking a link must not lose the words.
1110
+ */
1111
+ [PT_TYPES.link]: ({ value, children }) => {
1112
+ const v = value ?? {};
1113
+ const href = typeof v.href === "string" ? safeHref(v.href) : null;
1114
+ if (!href) return /* @__PURE__ */ jsx14(Fragment2, { children });
1115
+ const blank = v.target === "_blank";
1116
+ return /* @__PURE__ */ jsx14("a", { href, ...blank ? { target: "_blank", rel: "noopener noreferrer" } : {}, children });
1117
+ },
1118
+ /**
1119
+ * The site's own inline markup, rendered as itself — the React half of
1120
+ * `portableTextToHtml`'s `styled` mark, and it has to stay the same shape: Astro and the
1121
+ * editor canvas render through that serialiser while a Next site renders through this
1122
+ * one, and two renderers of one value that disagree is the whole bug class.
1123
+ *
1124
+ * The tag is re-checked against `STYLED_TAGS` and the attribute bag re-filtered, because
1125
+ * a markDef is DATA and arrives from imports, the Management API and AI writes. React
1126
+ * escapes the values it writes; the allowlist is what keeps a `href` or an `onClick` out
1127
+ * of the props entirely. An unrecognised tag renders its CHILDREN — the words are the
1128
+ * author's, the wrapper is only the design.
1129
+ */
1130
+ [PT_TYPES.styled]: ({ value, children }) => {
1131
+ const v = value ?? {};
1132
+ const tag = typeof v.tag === "string" ? v.tag.toLowerCase() : "";
1133
+ if (!isStyledTag(tag)) return /* @__PURE__ */ jsx14(Fragment2, { children });
1134
+ const Tag = tag;
1135
+ const className = typeof v.className === "string" && v.className ? v.className : void 0;
1136
+ const attrs = styledAttrEntries(v.attrs);
1137
+ const style = attrs.find(([name]) => name === "style")?.[1];
1138
+ return /* @__PURE__ */ jsx14(
1139
+ Tag,
1140
+ {
1141
+ className,
1142
+ ...Object.fromEntries(attrs.filter(([name]) => name !== "style")),
1143
+ ...style ? { style: styleObject(style) } : {},
1144
+ children
1145
+ }
1146
+ );
1147
+ },
1148
+ highlight: ({ children }) => /* @__PURE__ */ jsx14("mark", { children }),
1149
+ "strike-through": ({ children }) => /* @__PURE__ */ jsx14("s", { children }),
1150
+ /**
1151
+ * A colour mark carries a TOKEN name (`color:brand-accent`), never a baked hex, so the
1152
+ * consuming site styles it and a rebrand does not orphan the content.
1153
+ */
1154
+ color: ({ children, markType }) => {
1155
+ const token = markType.startsWith("color:") ? markType.slice(6) : "";
1156
+ if (!TOKEN.test(token)) return /* @__PURE__ */ jsx14(Fragment2, { children });
1157
+ return /* @__PURE__ */ jsx14("span", { className: `bcms-color-${token}`, children });
1158
+ }
1159
+ },
1160
+ /** Unknown marks and types render their children. Never swallow content. */
1161
+ unknownMark: ({ children }) => /* @__PURE__ */ jsx14(Fragment2, { children }),
1162
+ unknownType: ({ children }) => /* @__PURE__ */ jsx14(Fragment2, { children: children ?? null }),
1163
+ unknownBlockStyle: ({ children }) => /* @__PURE__ */ jsx14("p", { children })
1164
+ };
1165
+ }
1166
+ var isListItem = (node) => node._type === "block" && typeof node.listItem === "string" && !!node.listItem;
1167
+ function runsOf(blocks) {
1168
+ const runs = [];
1169
+ for (const node of blocks) {
1170
+ const last = runs.at(-1);
1171
+ if (last && isListItem(node) && isListItem(last[0])) last.push(node);
1172
+ else runs.push([node]);
1173
+ }
1174
+ return runs;
1175
+ }
1176
+ function PortableText({ value, bindingBase, renderBlock }) {
1177
+ const blocks = React6.useMemo(() => normalizePortableText(readValue(value)), [value]);
1178
+ const components = React6.useMemo(() => buildComponents(renderBlock), [renderBlock]);
1179
+ const listComponents = React6.useMemo(() => {
1180
+ if (!bindingBase) return null;
1181
+ const keys = new Set(blocks.map((b) => b._key));
1182
+ return {
1183
+ ...components,
1184
+ listItem: ({ value: item, children }) => keys.has(item._key) ? /* @__PURE__ */ jsx14("li", { "data-bcms-field": `${bindingBase}#${item._key}`, "data-bcms-kind": "richtext", children }) : /* @__PURE__ */ jsx14("li", { children })
1185
+ };
1186
+ }, [bindingBase, blocks, components]);
1187
+ if (blocks.length === 0) return null;
1188
+ if (!bindingBase || !listComponents) return /* @__PURE__ */ jsx14(PT, { value: blocks, components });
1189
+ return /* @__PURE__ */ jsx14(Fragment2, { children: runsOf(blocks).map(
1190
+ (run) => isListItem(run[0]) ? /* @__PURE__ */ jsx14(PT, { value: run, components: listComponents }, run[0]._key) : /* @__PURE__ */ jsx14(Bound, { base: bindingBase, nodeKey: run[0]._key, children: /* @__PURE__ */ jsx14(PT, { value: run, components }) }, run[0]._key)
1191
+ ) });
1192
+ }
1193
+ function readValue(value) {
1194
+ if (Array.isArray(value)) return value;
1195
+ if (value && typeof value === "object") {
1196
+ const o = value;
1197
+ if (Array.isArray(o.value)) return o.value;
1198
+ if (Array.isArray(o.content)) return o.content;
1199
+ }
1200
+ return [];
848
1201
  }
849
1202
 
850
- // src/blocks/Placeholders.tsx
851
- import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
852
- var box = "rounded border border-dashed border-neutral-300 bg-neutral-50 p-4 text-sm text-neutral-500";
853
- function ComponentInstance({
854
- componentId,
855
- className = ""
1203
+ // src/blocks/Text.tsx
1204
+ import { jsx as jsx15 } from "react/jsx-runtime";
1205
+ function Text({
1206
+ html,
1207
+ content,
1208
+ align = "left",
1209
+ className = "",
1210
+ "data-bcms-field": field,
1211
+ "data-bcms-kind": kind
856
1212
  }) {
857
- return /* @__PURE__ */ jsxs10("div", { className: cn(box, className), children: [
858
- "Component instance: ",
859
- /* @__PURE__ */ jsx14("code", { children: componentId })
860
- ] });
861
- }
862
- function FormPlaceholder({ formId, className = "" }) {
863
- return /* @__PURE__ */ jsxs10("div", { className: cn(box, className), children: [
864
- "Form: ",
865
- /* @__PURE__ */ jsx14("code", { children: formId })
866
- ] });
1213
+ const alignClass = {
1214
+ left: "text-left",
1215
+ center: "text-center",
1216
+ right: "text-right"
1217
+ }[align];
1218
+ const cls = `text-muted-foreground leading-relaxed ${alignClass} ${className}`;
1219
+ const structured = readStructuredContent({ content });
1220
+ return /* @__PURE__ */ jsx15(
1221
+ "div",
1222
+ {
1223
+ className: cls,
1224
+ ...structured ? {} : { "data-bcms-field": field, "data-bcms-kind": kind },
1225
+ children: structured ? /* @__PURE__ */ jsx15(PortableText, { value: structured, bindingBase: field }) : (
1226
+ // eslint-disable-next-line react/no-danger
1227
+ /* @__PURE__ */ jsx15("span", { dangerouslySetInnerHTML: { __html: html } })
1228
+ )
1229
+ }
1230
+ );
867
1231
  }
868
1232
  export {
869
1233
  Button,