@bettercms-ai/ui 0.6.0 → 0.7.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/{Video-DRC_fPjV.d.ts → Video-CZkWQ3Hm.d.ts} +1 -1
- package/dist/blocks.d.ts +9 -2
- package/dist/blocks.js +434 -55
- package/dist/blocks.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +438 -60
- package/dist/index.js.map +1 -1
- package/dist/registry.js +435 -57
- package/dist/registry.js.map +1 -1
- package/dist/renderer.js +438 -60
- package/dist/renderer.js.map +1 -1
- package/dist/resolve.d.ts +53 -2
- package/dist/resolve.js +107 -1
- package/dist/resolve.js.map +1 -1
- package/package.json +4 -2
package/dist/renderer.js
CHANGED
|
@@ -30,42 +30,420 @@ function Heading({
|
|
|
30
30
|
|
|
31
31
|
// src/blocks/portableText.tsx
|
|
32
32
|
import React from "react";
|
|
33
|
-
import {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
import { PortableText as PT } from "@portabletext/react";
|
|
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 table. Spelled `table`, unprefixed, because that is what every other Portable Text
|
|
45
|
+
* producer calls one — the Sanity-shaped `{_type:"table", rows}` this platform used to refuse
|
|
46
|
+
* as foreign content now lands on OUR type and renders, rather than being data nothing draws.
|
|
47
|
+
*/
|
|
48
|
+
table: "table",
|
|
49
|
+
/** The annotation carrying a hyperlink's `href`. Spelled `link` because that is the spec's convention. */
|
|
50
|
+
link: "link",
|
|
51
|
+
/**
|
|
52
|
+
* The annotation carrying a site's OWN inline markup — `<span class>`, `<small>`, `<time
|
|
53
|
+
* datetime>`. Unprefixed, like `table`, because it describes HTML rather than a BetterCMS
|
|
54
|
+
* concept. See `styled.ts` for what it may hold and why the list is that short.
|
|
55
|
+
*/
|
|
56
|
+
styled: "styled"
|
|
40
57
|
};
|
|
41
|
-
var
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
var BUILTIN_TABLE_KEY = "builtin:table";
|
|
59
|
+
var STORABLE_BLOCK_TYPES = /* @__PURE__ */ new Set([
|
|
60
|
+
"block",
|
|
61
|
+
PT_TYPES.block,
|
|
62
|
+
PT_TYPES.table,
|
|
63
|
+
PT_TYPES.code,
|
|
64
|
+
PT_TYPES.divider,
|
|
65
|
+
PT_TYPES.mention
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
// ../richtext/src/styled.ts
|
|
69
|
+
var STYLED_TAGS = ["span", "small", "sup", "sub", "kbd", "abbr", "time"];
|
|
70
|
+
var TAG_SET = new Set(STYLED_TAGS);
|
|
71
|
+
var isStyledTag = (tag) => typeof tag === "string" && TAG_SET.has(tag);
|
|
72
|
+
var ATTR_NAMES = /* @__PURE__ */ new Set(["title", "datetime", "style", "aria-label"]);
|
|
73
|
+
var DATA_ATTR = /^data-[a-z][a-z0-9-]*$/;
|
|
74
|
+
var isStyledAttr = (name) => ATTR_NAMES.has(name) || DATA_ATTR.test(name) && !name.startsWith("data-bcms-");
|
|
75
|
+
var STYLE_PROPS = /* @__PURE__ */ new Set([
|
|
76
|
+
"color",
|
|
77
|
+
"background-color",
|
|
78
|
+
"opacity",
|
|
79
|
+
"font",
|
|
80
|
+
"font-family",
|
|
81
|
+
"font-size",
|
|
82
|
+
"font-style",
|
|
83
|
+
"font-weight",
|
|
84
|
+
"font-variant",
|
|
85
|
+
"font-feature-settings",
|
|
86
|
+
"font-variation-settings",
|
|
87
|
+
"letter-spacing",
|
|
88
|
+
"line-height",
|
|
89
|
+
"word-spacing",
|
|
90
|
+
"white-space",
|
|
91
|
+
"word-break",
|
|
92
|
+
"text-align",
|
|
93
|
+
"text-decoration",
|
|
94
|
+
"text-decoration-color",
|
|
95
|
+
"text-decoration-line",
|
|
96
|
+
"text-decoration-style",
|
|
97
|
+
"text-transform",
|
|
98
|
+
"text-underline-offset",
|
|
99
|
+
"vertical-align"
|
|
100
|
+
]);
|
|
101
|
+
var STYLE_VALUE_BAN = /url\(|expression\(|@import|\\|\/\*|[<>]/i;
|
|
102
|
+
function safeStyle(css) {
|
|
103
|
+
const kept = [];
|
|
104
|
+
for (const declaration of css.split(";")) {
|
|
105
|
+
const at = declaration.indexOf(":");
|
|
106
|
+
if (at < 0) continue;
|
|
107
|
+
const name = declaration.slice(0, at).trim().toLowerCase();
|
|
108
|
+
const value = declaration.slice(at + 1).trim();
|
|
109
|
+
if (!name || !value) continue;
|
|
110
|
+
if (!name.startsWith("--") && !STYLE_PROPS.has(name)) continue;
|
|
111
|
+
if (STYLE_VALUE_BAN.test(value)) continue;
|
|
112
|
+
kept.push(`${name}:${value}`);
|
|
113
|
+
}
|
|
114
|
+
return kept.length ? kept.join(";") : void 0;
|
|
115
|
+
}
|
|
116
|
+
function styledAttrEntries(attrs) {
|
|
117
|
+
if (!attrs || typeof attrs !== "object" || Array.isArray(attrs)) return [];
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const [name, raw] of Object.entries(attrs)) {
|
|
120
|
+
if (!isStyledAttr(name) || typeof raw !== "string") continue;
|
|
121
|
+
const value = name === "style" ? safeStyle(raw) : raw;
|
|
122
|
+
if (value === void 0) continue;
|
|
123
|
+
out.push([name, value]);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ../richtext/src/href.ts
|
|
129
|
+
var SAFE_HREF_SCHEMES = ["http:", "https:", "mailto:", "tel:"];
|
|
130
|
+
function safeHref(raw) {
|
|
131
|
+
const trimmed = raw.trim();
|
|
132
|
+
if (trimmed.startsWith("/") && !trimmed.startsWith("//")) return trimmed;
|
|
133
|
+
try {
|
|
134
|
+
return SAFE_HREF_SCHEMES.includes(new URL(trimmed).protocol) ? trimmed : null;
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ../richtext/src/key.ts
|
|
141
|
+
var ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
142
|
+
var KEY_PATTERN = /^[A-Za-z0-9-]{1,32}$/;
|
|
143
|
+
var isValidKey = (k) => typeof k === "string" && KEY_PATTERN.test(k);
|
|
144
|
+
function randomKey(length = 12) {
|
|
145
|
+
let out = "";
|
|
146
|
+
for (let i = 0; i < length; i += 1) {
|
|
147
|
+
out += ALPHABET[Math.floor(Math.random() * ALPHABET.length)];
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
function deterministicKeys(prefix = "k") {
|
|
152
|
+
let n = 0;
|
|
153
|
+
return () => {
|
|
154
|
+
n += 1;
|
|
155
|
+
return `${prefix}${n}`;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ../richtext/src/table.ts
|
|
160
|
+
var isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
161
|
+
var isPortableTextTable = (n) => isRecord(n) && n._type === PT_TYPES.table && Array.isArray(n.rows);
|
|
162
|
+
var isBuiltInTableBlock = (n) => isRecord(n) && n._type === PT_TYPES.block && n.schemaKey === BUILTIN_TABLE_KEY;
|
|
163
|
+
var asIs = (v) => Array.isArray(v) ? v : [];
|
|
164
|
+
function keyMinter(base, reserved) {
|
|
165
|
+
const prefix = (base.match(/[A-Za-z0-9-]/g) ?? []).join("").slice(0, 16) || "t";
|
|
166
|
+
return (...parts) => {
|
|
167
|
+
const candidate = `${prefix}-${parts.join("")}`.slice(0, 30);
|
|
168
|
+
let key = candidate;
|
|
169
|
+
for (let n = 1; reserved.has(key); n += 1) key = `${candidate}-${n}`;
|
|
170
|
+
reserved.add(key);
|
|
171
|
+
return key;
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function reservedKeys(rows) {
|
|
175
|
+
const out = /* @__PURE__ */ new Set();
|
|
176
|
+
for (const row of rows) {
|
|
177
|
+
if (!isRecord(row)) continue;
|
|
178
|
+
if (isValidKey(row._key)) out.add(row._key);
|
|
179
|
+
for (const cell of Array.isArray(row.cells) ? row.cells : []) {
|
|
180
|
+
if (isRecord(cell) && isValidKey(cell._key)) out.add(cell._key);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
var textContent = (text, key) => text === "" ? [] : [{
|
|
186
|
+
_type: "block",
|
|
187
|
+
_key: key(),
|
|
188
|
+
style: "normal",
|
|
189
|
+
markDefs: [],
|
|
190
|
+
children: [{ _type: "span", _key: key(), text, marks: [] }]
|
|
191
|
+
}];
|
|
192
|
+
var spanOf = (value) => {
|
|
193
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
194
|
+
return Number.isInteger(n) && n > 1 ? n : void 0;
|
|
50
195
|
};
|
|
51
|
-
function
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
196
|
+
function normalizeCell(raw, at, mint, read) {
|
|
197
|
+
const [row, col] = at;
|
|
198
|
+
const key = isRecord(raw) && isValidKey(raw._key) ? raw._key : mint("r", row, "c", col);
|
|
199
|
+
const inner = deterministicKeys(`${key}-`);
|
|
200
|
+
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
|
|
201
|
+
return { _key: key, content: textContent(String(raw), inner) };
|
|
202
|
+
}
|
|
203
|
+
if (!isRecord(raw)) return { _key: key, content: [] };
|
|
204
|
+
const span = spanOf(raw.span ?? raw.colSpan ?? raw.colspan);
|
|
205
|
+
const rowSpan = spanOf(raw.rowSpan ?? raw.rowspan);
|
|
206
|
+
return {
|
|
207
|
+
_key: key,
|
|
208
|
+
// `text` is the spelling a string matrix uses once its cells have been given objects; it is
|
|
209
|
+
// read here so a half-migrated value cannot lose its words.
|
|
210
|
+
content: Array.isArray(raw.content) ? read(raw.content, inner) : textContent(typeof raw.text === "string" ? raw.text : "", inner),
|
|
211
|
+
...raw.header === true ? { header: true } : {},
|
|
212
|
+
...span === void 0 ? {} : { span },
|
|
213
|
+
...rowSpan === void 0 ? {} : { rowSpan }
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function normalizeRow(raw, at, mint, read) {
|
|
217
|
+
const cells = Array.isArray(raw) ? raw : isRecord(raw) && Array.isArray(raw.cells) ? raw.cells : [];
|
|
218
|
+
return {
|
|
219
|
+
_key: isRecord(raw) && isValidKey(raw._key) ? raw._key : mint("r", at),
|
|
220
|
+
cells: cells.map((c, col) => normalizeCell(c, [at, col], mint, read)),
|
|
221
|
+
...isRecord(raw) && raw.head === true ? { head: true } : {}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function builtInTableToTable(node, key = randomKey) {
|
|
225
|
+
if (!isBuiltInTableBlock(node)) return null;
|
|
226
|
+
const fields = node.fields;
|
|
227
|
+
const raw = isRecord(fields) && Array.isArray(fields.rows) ? fields.rows : [];
|
|
228
|
+
const hasHeader = isRecord(fields) ? fields.hasHeader !== false : true;
|
|
229
|
+
const own = node._key;
|
|
230
|
+
const tableKey = isValidKey(own) ? own : key();
|
|
231
|
+
const mint = keyMinter(tableKey, reservedKeys(raw));
|
|
232
|
+
const rows = raw.map((row, i) => {
|
|
233
|
+
const normalized = normalizeRow(row, i, mint, asIs);
|
|
234
|
+
if (!(hasHeader && i === 0)) return normalized;
|
|
235
|
+
return {
|
|
236
|
+
...normalized,
|
|
237
|
+
head: true,
|
|
238
|
+
cells: normalized.cells.map((c) => ({ ...c, header: true }))
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
const width = rows.reduce((max, r) => Math.max(max, r.cells.length), 0);
|
|
242
|
+
for (const [i, row] of rows.entries()) {
|
|
243
|
+
while (row.cells.length < width) {
|
|
244
|
+
row.cells.push({
|
|
245
|
+
_key: mint("r", i, "c", row.cells.length),
|
|
246
|
+
content: [],
|
|
247
|
+
...row.head === true ? { header: true } : {}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
58
250
|
}
|
|
59
|
-
|
|
60
|
-
|
|
251
|
+
return { _type: PT_TYPES.table, _key: tableKey, rows, legacy: true };
|
|
252
|
+
}
|
|
253
|
+
function migrateTable(node, key = randomKey, read = asIs) {
|
|
254
|
+
const builtIn = builtInTableToTable(node, key);
|
|
255
|
+
if (builtIn) return builtIn;
|
|
256
|
+
if (!isPortableTextTable(node)) return null;
|
|
257
|
+
const rows = node.rows;
|
|
258
|
+
const tableKey = isValidKey(node._key) ? node._key : key();
|
|
259
|
+
const mint = keyMinter(tableKey, reservedKeys(rows));
|
|
260
|
+
return {
|
|
261
|
+
_type: PT_TYPES.table,
|
|
262
|
+
_key: tableKey,
|
|
263
|
+
rows: rows.map((r, i) => normalizeRow(r, i, mint, read)),
|
|
264
|
+
// Carried through: a migrated table that an author saves back is still the same table, and
|
|
265
|
+
// dropping the flag on that save would change the page the next time it renders.
|
|
266
|
+
...node.legacy === true ? { legacy: true } : {}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ../richtext/src/read.ts
|
|
271
|
+
var isRecord2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
272
|
+
function normalizePortableText(value, key = randomKey) {
|
|
273
|
+
if (!Array.isArray(value)) return [];
|
|
274
|
+
return value.flatMap((raw) => {
|
|
275
|
+
if (!isRecord2(raw) || typeof raw._type !== "string") return [];
|
|
276
|
+
const table = migrateTable(raw, key, (v, cellKey) => normalizePortableText(v, cellKey));
|
|
277
|
+
if (table) return [table];
|
|
278
|
+
const node = { ...raw, _key: isRecord2(raw) && typeof raw._key === "string" && raw._key ? raw._key : key() };
|
|
279
|
+
if (node._type === "block") {
|
|
280
|
+
if (!Array.isArray(node.markDefs)) node.markDefs = [];
|
|
281
|
+
if (typeof node.style !== "string") node.style = "normal";
|
|
282
|
+
node.children = (Array.isArray(node.children) ? node.children : []).map((c) => {
|
|
283
|
+
if (!isRecord2(c)) return c;
|
|
284
|
+
const child = { ...c, _key: typeof c._key === "string" && c._key ? c._key : key() };
|
|
285
|
+
if (child._type === "span" && !Array.isArray(child.marks)) child.marks = [];
|
|
286
|
+
return child;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return [node];
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
function readStructuredContent(props) {
|
|
293
|
+
if (!isRecord2(props)) return null;
|
|
294
|
+
const candidate = props.content ?? props.value;
|
|
295
|
+
const arr = Array.isArray(candidate) ? candidate : isRecord2(candidate) ? Array.isArray(candidate.content) ? candidate.content : Array.isArray(candidate.value) ? candidate.value : null : null;
|
|
296
|
+
return arr && arr.length > 0 ? arr : null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/blocks/portableText.tsx
|
|
300
|
+
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
301
|
+
var TOKEN = /^[a-z0-9-]+$/i;
|
|
302
|
+
function styleObject(css) {
|
|
303
|
+
const out = {};
|
|
304
|
+
for (const declaration of css.split(";")) {
|
|
305
|
+
const at = declaration.indexOf(":");
|
|
306
|
+
if (at < 0) continue;
|
|
307
|
+
const name = declaration.slice(0, at).trim();
|
|
308
|
+
const value = declaration.slice(at + 1).trim();
|
|
309
|
+
if (!name || !value) continue;
|
|
310
|
+
out[name.startsWith("--") ? name : name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = value;
|
|
61
311
|
}
|
|
62
|
-
return
|
|
312
|
+
return out;
|
|
63
313
|
}
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
314
|
+
function Bound({
|
|
315
|
+
base,
|
|
316
|
+
nodeKey,
|
|
317
|
+
children
|
|
318
|
+
}) {
|
|
319
|
+
if (!base || !nodeKey) return /* @__PURE__ */ jsx2(Fragment, { children });
|
|
320
|
+
return /* @__PURE__ */ jsx2("div", { "data-bcms-field": `${base}#${nodeKey}`, "data-bcms-kind": "richtext", children });
|
|
321
|
+
}
|
|
322
|
+
function Table({ value }) {
|
|
323
|
+
const components = React.useMemo(buildComponents, []);
|
|
324
|
+
const inline = React.useMemo(
|
|
325
|
+
() => ({ ...components, block: { normal: ({ children }) => /* @__PURE__ */ jsx2(Fragment, { children }) } }),
|
|
326
|
+
[components]
|
|
327
|
+
);
|
|
328
|
+
const rows = Array.isArray(value?.rows) ? value.rows : [];
|
|
329
|
+
if (rows.length === 0) return null;
|
|
330
|
+
const cells = (row) => (Array.isArray(row?.cells) ? row.cells : []).map((cell) => {
|
|
331
|
+
const Tag = cell?.header === true ? "th" : "td";
|
|
332
|
+
const span = typeof cell?.span === "number" && cell.span > 1 ? cell.span : void 0;
|
|
333
|
+
const rowSpan = typeof cell?.rowSpan === "number" && cell.rowSpan > 1 ? cell.rowSpan : void 0;
|
|
334
|
+
const content = Array.isArray(cell?.content) ? cell.content : [];
|
|
335
|
+
const lone = content.length === 1 && content[0]._type === "block" && content[0].style === "normal" && !content[0].listItem;
|
|
336
|
+
return /* @__PURE__ */ jsx2(Tag, { ...span ? { colSpan: span } : {}, ...rowSpan ? { rowSpan } : {}, children: /* @__PURE__ */ jsx2(PT, { value: content, components: lone ? inline : components }) }, cell?._key);
|
|
337
|
+
});
|
|
338
|
+
const rendered = rows.map((row) => /* @__PURE__ */ jsx2("tr", { children: cells(row) }, row?._key));
|
|
339
|
+
const headCount = rows.findIndex((r) => r?.head !== true);
|
|
340
|
+
const head = headCount === -1 ? rows.length : headCount;
|
|
341
|
+
return /* @__PURE__ */ jsxs("table", { ...value.legacy === true ? { className: "bcms-table" } : {}, children: [
|
|
342
|
+
head > 0 && /* @__PURE__ */ jsx2("thead", { children: rendered.slice(0, head) }),
|
|
343
|
+
head < rows.length && /* @__PURE__ */ jsx2("tbody", { children: rendered.slice(head) })
|
|
344
|
+
] });
|
|
345
|
+
}
|
|
346
|
+
function buildComponents() {
|
|
347
|
+
return {
|
|
348
|
+
types: {
|
|
349
|
+
[PT_TYPES.divider]: () => /* @__PURE__ */ jsx2("hr", {}),
|
|
350
|
+
[PT_TYPES.table]: ({ value }) => /* @__PURE__ */ jsx2(Table, { value }),
|
|
351
|
+
[PT_TYPES.code]: ({ value }) => {
|
|
352
|
+
const v = value;
|
|
353
|
+
const lang = typeof v.language === "string" && TOKEN.test(v.language) ? v.language : void 0;
|
|
354
|
+
return /* @__PURE__ */ jsx2("pre", { children: /* @__PURE__ */ jsx2("code", { className: lang ? `language-${lang}` : void 0, children: typeof v.code === "string" ? v.code : "" }) });
|
|
355
|
+
},
|
|
356
|
+
/**
|
|
357
|
+
* A generic embedded block renders as a marker, not as content: its payload references
|
|
358
|
+
* something (a component, a media id) that has to be RESOLVED against the project, and
|
|
359
|
+
* this renderer has no project. A marker keeps the node visible and addressable rather
|
|
360
|
+
* than rendering nothing at all, which is how embedded content used to vanish.
|
|
361
|
+
*/
|
|
362
|
+
[PT_TYPES.block]: ({ value }) => {
|
|
363
|
+
const v = value;
|
|
364
|
+
const key = typeof v.schemaKey === "string" ? v.schemaKey : "block";
|
|
365
|
+
return /* @__PURE__ */ jsx2("div", { className: "bcms-block", "data-bcms-schema-key": key });
|
|
366
|
+
},
|
|
367
|
+
[PT_TYPES.mention]: ({ value }) => {
|
|
368
|
+
const v = value;
|
|
369
|
+
return /* @__PURE__ */ jsx2("span", { className: "bcms-mention", children: typeof v.label === "string" ? v.label : "" });
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
marks: {
|
|
373
|
+
/**
|
|
374
|
+
* The only attribute this renderer writes from author data. An unsafe href renders the
|
|
375
|
+
* LABEL rather than deleting it — blocking a link must not lose the words.
|
|
376
|
+
*/
|
|
377
|
+
[PT_TYPES.link]: ({ value, children }) => {
|
|
378
|
+
const v = value ?? {};
|
|
379
|
+
const href = typeof v.href === "string" ? safeHref(v.href) : null;
|
|
380
|
+
if (!href) return /* @__PURE__ */ jsx2(Fragment, { children });
|
|
381
|
+
const blank = v.target === "_blank";
|
|
382
|
+
return /* @__PURE__ */ jsx2("a", { href, ...blank ? { target: "_blank", rel: "noopener noreferrer" } : {}, children });
|
|
383
|
+
},
|
|
384
|
+
/**
|
|
385
|
+
* The site's own inline markup, rendered as itself — the React half of
|
|
386
|
+
* `portableTextToHtml`'s `styled` mark, and it has to stay the same shape: Astro and the
|
|
387
|
+
* editor canvas render through that serialiser while a Next site renders through this
|
|
388
|
+
* one, and two renderers of one value that disagree is the whole bug class.
|
|
389
|
+
*
|
|
390
|
+
* The tag is re-checked against `STYLED_TAGS` and the attribute bag re-filtered, because
|
|
391
|
+
* a markDef is DATA and arrives from imports, the Management API and AI writes. React
|
|
392
|
+
* escapes the values it writes; the allowlist is what keeps a `href` or an `onClick` out
|
|
393
|
+
* of the props entirely. An unrecognised tag renders its CHILDREN — the words are the
|
|
394
|
+
* author's, the wrapper is only the design.
|
|
395
|
+
*/
|
|
396
|
+
[PT_TYPES.styled]: ({ value, children }) => {
|
|
397
|
+
const v = value ?? {};
|
|
398
|
+
const tag = typeof v.tag === "string" ? v.tag.toLowerCase() : "";
|
|
399
|
+
if (!isStyledTag(tag)) return /* @__PURE__ */ jsx2(Fragment, { children });
|
|
400
|
+
const Tag = tag;
|
|
401
|
+
const className = typeof v.className === "string" && v.className ? v.className : void 0;
|
|
402
|
+
const attrs = styledAttrEntries(v.attrs);
|
|
403
|
+
const style = attrs.find(([name]) => name === "style")?.[1];
|
|
404
|
+
return /* @__PURE__ */ jsx2(
|
|
405
|
+
Tag,
|
|
406
|
+
{
|
|
407
|
+
className,
|
|
408
|
+
...Object.fromEntries(attrs.filter(([name]) => name !== "style")),
|
|
409
|
+
...style ? { style: styleObject(style) } : {},
|
|
410
|
+
children
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
},
|
|
414
|
+
highlight: ({ children }) => /* @__PURE__ */ jsx2("mark", { children }),
|
|
415
|
+
"strike-through": ({ children }) => /* @__PURE__ */ jsx2("s", { children }),
|
|
416
|
+
/**
|
|
417
|
+
* A colour mark carries a TOKEN name (`color:brand-accent`), never a baked hex, so the
|
|
418
|
+
* consuming site styles it and a rebrand does not orphan the content.
|
|
419
|
+
*/
|
|
420
|
+
color: ({ children, markType }) => {
|
|
421
|
+
const token = markType.startsWith("color:") ? markType.slice(6) : "";
|
|
422
|
+
if (!TOKEN.test(token)) return /* @__PURE__ */ jsx2(Fragment, { children });
|
|
423
|
+
return /* @__PURE__ */ jsx2("span", { className: `bcms-color-${token}`, children });
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
/** Unknown marks and types render their children. Never swallow content. */
|
|
427
|
+
unknownMark: ({ children }) => /* @__PURE__ */ jsx2(Fragment, { children }),
|
|
428
|
+
unknownType: ({ children }) => /* @__PURE__ */ jsx2(Fragment, { children: children ?? null }),
|
|
429
|
+
unknownBlockStyle: ({ children }) => /* @__PURE__ */ jsx2("p", { children })
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function PortableText({ value, bindingBase }) {
|
|
433
|
+
const blocks = React.useMemo(() => normalizePortableText(readValue(value)), [value]);
|
|
434
|
+
const components = React.useMemo(buildComponents, []);
|
|
435
|
+
if (blocks.length === 0) return null;
|
|
436
|
+
if (!bindingBase) return /* @__PURE__ */ jsx2(PT, { value: blocks, components });
|
|
437
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: blocks.map((node) => /* @__PURE__ */ jsx2(Bound, { base: bindingBase, nodeKey: node._key, children: /* @__PURE__ */ jsx2(PT, { value: [node], components }) }, node._key)) });
|
|
438
|
+
}
|
|
439
|
+
function readValue(value) {
|
|
440
|
+
if (Array.isArray(value)) return value;
|
|
441
|
+
if (value && typeof value === "object") {
|
|
442
|
+
const o = value;
|
|
443
|
+
if (Array.isArray(o.value)) return o.value;
|
|
444
|
+
if (Array.isArray(o.content)) return o.content;
|
|
445
|
+
}
|
|
446
|
+
return [];
|
|
69
447
|
}
|
|
70
448
|
|
|
71
449
|
// src/blocks/Text.tsx
|
|
@@ -84,16 +462,16 @@ function Text({
|
|
|
84
462
|
right: "text-right"
|
|
85
463
|
}[align];
|
|
86
464
|
const cls = `text-muted-foreground leading-relaxed ${alignClass} ${className}`;
|
|
87
|
-
|
|
88
|
-
return /* @__PURE__ */ jsx3("div", { className: cls, children: /* @__PURE__ */ jsx3(PortableText, { doc: content }) });
|
|
89
|
-
}
|
|
465
|
+
const structured = readStructuredContent({ content });
|
|
90
466
|
return /* @__PURE__ */ jsx3(
|
|
91
467
|
"div",
|
|
92
468
|
{
|
|
93
469
|
className: cls,
|
|
94
|
-
"data-bcms-field": field,
|
|
95
|
-
|
|
96
|
-
|
|
470
|
+
...structured ? {} : { "data-bcms-field": field, "data-bcms-kind": kind },
|
|
471
|
+
children: structured ? /* @__PURE__ */ jsx3(PortableText, { value: structured, bindingBase: field }) : (
|
|
472
|
+
// eslint-disable-next-line react/no-danger
|
|
473
|
+
/* @__PURE__ */ jsx3("span", { dangerouslySetInnerHTML: { __html: html } })
|
|
474
|
+
)
|
|
97
475
|
}
|
|
98
476
|
);
|
|
99
477
|
}
|
|
@@ -106,7 +484,7 @@ function cn(...inputs) {
|
|
|
106
484
|
}
|
|
107
485
|
|
|
108
486
|
// src/blocks/Image.tsx
|
|
109
|
-
import { jsx as jsx4, jsxs } from "react/jsx-runtime";
|
|
487
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
110
488
|
function Image({
|
|
111
489
|
src,
|
|
112
490
|
alt,
|
|
@@ -118,7 +496,7 @@ function Image({
|
|
|
118
496
|
"data-bcms-field": field,
|
|
119
497
|
"data-bcms-kind": kind
|
|
120
498
|
}) {
|
|
121
|
-
return /* @__PURE__ */
|
|
499
|
+
return /* @__PURE__ */ jsxs2("figure", { className: cn("w-full", className), children: [
|
|
122
500
|
/* @__PURE__ */ jsx4(
|
|
123
501
|
"img",
|
|
124
502
|
{
|
|
@@ -171,7 +549,7 @@ function Button({
|
|
|
171
549
|
}
|
|
172
550
|
|
|
173
551
|
// src/blocks/Spacer.tsx
|
|
174
|
-
import { jsx as jsx6, jsxs as
|
|
552
|
+
import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
175
553
|
function Spacer({ height = 32, className = "" }) {
|
|
176
554
|
return /* @__PURE__ */ jsx6(
|
|
177
555
|
"div",
|
|
@@ -179,7 +557,7 @@ function Spacer({ height = 32, className = "" }) {
|
|
|
179
557
|
className: `w-full border border-dashed border-muted-foreground/20 rounded flex items-center justify-center ${className}`,
|
|
180
558
|
style: { height: `${height}px` },
|
|
181
559
|
"aria-hidden": "true",
|
|
182
|
-
children: /* @__PURE__ */
|
|
560
|
+
children: /* @__PURE__ */ jsxs3("span", { className: "text-xs text-muted-foreground/40 select-none", children: [
|
|
183
561
|
height,
|
|
184
562
|
"px"
|
|
185
563
|
] })
|
|
@@ -211,7 +589,7 @@ function Columns({
|
|
|
211
589
|
|
|
212
590
|
// src/blocks/Video.tsx
|
|
213
591
|
import { useState } from "react";
|
|
214
|
-
import { jsx as jsx8, jsxs as
|
|
592
|
+
import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
215
593
|
function getEmbedUrl(url) {
|
|
216
594
|
const ytMatch = url.match(
|
|
217
595
|
/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
|
|
@@ -254,7 +632,7 @@ function Video({
|
|
|
254
632
|
}
|
|
255
633
|
);
|
|
256
634
|
}
|
|
257
|
-
return /* @__PURE__ */
|
|
635
|
+
return /* @__PURE__ */ jsxs4("div", { className: cn("w-full overflow-hidden rounded-md bg-black aspect-video", className), children: [
|
|
258
636
|
!isLoaded && poster && // eslint-disable-next-line @next/next/no-img-element
|
|
259
637
|
/* @__PURE__ */ jsx8(
|
|
260
638
|
"img",
|
|
@@ -301,9 +679,9 @@ function Section({
|
|
|
301
679
|
}
|
|
302
680
|
|
|
303
681
|
// src/blocks/Navbar.tsx
|
|
304
|
-
import { jsx as jsx10, jsxs as
|
|
682
|
+
import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
305
683
|
function Navbar({ logo, links, cta, className = "" }) {
|
|
306
|
-
return /* @__PURE__ */
|
|
684
|
+
return /* @__PURE__ */ jsxs5("nav", { className: cn("flex items-center gap-6 py-4", className), children: [
|
|
307
685
|
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,
|
|
308
686
|
/* @__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)) }),
|
|
309
687
|
cta ? /* @__PURE__ */ jsx10(
|
|
@@ -318,10 +696,10 @@ function Navbar({ logo, links, cta, className = "" }) {
|
|
|
318
696
|
}
|
|
319
697
|
|
|
320
698
|
// src/blocks/Footer.tsx
|
|
321
|
-
import { jsx as jsx11, jsxs as
|
|
699
|
+
import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
322
700
|
function Footer({ columns, copyright, className = "" }) {
|
|
323
|
-
return /* @__PURE__ */
|
|
324
|
-
/* @__PURE__ */ jsx11("div", { className: "flex flex-wrap gap-12", children: (columns ?? []).map((col, i) => /* @__PURE__ */
|
|
701
|
+
return /* @__PURE__ */ jsxs6("footer", { className: cn("mt-12 border-t pt-8", className), children: [
|
|
702
|
+
/* @__PURE__ */ jsx11("div", { className: "flex flex-wrap gap-12", children: (columns ?? []).map((col, i) => /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-1.5", children: [
|
|
325
703
|
col.heading ? /* @__PURE__ */ jsx11("h4", { className: "mb-2 font-semibold", children: col.heading }) : null,
|
|
326
704
|
(col.links ?? []).map((l, j) => /* @__PURE__ */ jsx11("a", { href: l.href, className: "text-neutral-500 no-underline", children: l.label }, j))
|
|
327
705
|
] }, i)) }),
|
|
@@ -331,13 +709,13 @@ function Footer({ columns, copyright, className = "" }) {
|
|
|
331
709
|
|
|
332
710
|
// src/blocks/Slider.tsx
|
|
333
711
|
import { useState as useState2 } from "react";
|
|
334
|
-
import { jsx as jsx12, jsxs as
|
|
712
|
+
import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
335
713
|
function Slider({ slides, className = "", renderBlock }) {
|
|
336
714
|
const [i, setI] = useState2(0);
|
|
337
715
|
const list = slides ?? [];
|
|
338
716
|
const go = (n) => setI((n + list.length) % (list.length || 1));
|
|
339
717
|
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";
|
|
340
|
-
return /* @__PURE__ */
|
|
718
|
+
return /* @__PURE__ */ jsxs7("div", { className: cn("relative w-full", className), children: [
|
|
341
719
|
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)),
|
|
342
720
|
/* @__PURE__ */ jsx12("button", { type: "button", "aria-label": "Previous", onClick: () => go(i - 1), className: cn(btn, "left-2"), children: "\u2039" }),
|
|
343
721
|
/* @__PURE__ */ jsx12("button", { type: "button", "aria-label": "Next", onClick: () => go(i + 1), className: cn(btn, "right-2"), children: "\u203A" })
|
|
@@ -346,11 +724,11 @@ function Slider({ slides, className = "", renderBlock }) {
|
|
|
346
724
|
|
|
347
725
|
// src/blocks/Tabs.tsx
|
|
348
726
|
import { useState as useState3 } from "react";
|
|
349
|
-
import { jsx as jsx13, jsxs as
|
|
727
|
+
import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
350
728
|
function Tabs({ tabs, className = "", renderBlock }) {
|
|
351
729
|
const [active, setActive] = useState3(0);
|
|
352
730
|
const list = tabs ?? [];
|
|
353
|
-
return /* @__PURE__ */
|
|
731
|
+
return /* @__PURE__ */ jsxs8("div", { className: cn("w-full", className), children: [
|
|
354
732
|
/* @__PURE__ */ jsx13("div", { role: "tablist", className: "mb-4 flex gap-1 border-b", children: list.map((t, idx) => /* @__PURE__ */ jsx13(
|
|
355
733
|
"button",
|
|
356
734
|
{
|
|
@@ -371,19 +749,19 @@ function Tabs({ tabs, className = "", renderBlock }) {
|
|
|
371
749
|
}
|
|
372
750
|
|
|
373
751
|
// src/blocks/Placeholders.tsx
|
|
374
|
-
import { Fragment as Fragment2, jsx as jsx14, jsxs as
|
|
752
|
+
import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
375
753
|
var box = "rounded border border-dashed border-neutral-300 bg-neutral-50 p-4 text-sm text-neutral-500";
|
|
376
754
|
function ComponentInstance({
|
|
377
755
|
componentId,
|
|
378
756
|
className = ""
|
|
379
757
|
}) {
|
|
380
|
-
return /* @__PURE__ */
|
|
758
|
+
return /* @__PURE__ */ jsxs9("div", { className: cn(box, className), children: [
|
|
381
759
|
"Component instance: ",
|
|
382
760
|
/* @__PURE__ */ jsx14("code", { children: componentId })
|
|
383
761
|
] });
|
|
384
762
|
}
|
|
385
763
|
function FormPlaceholder({ formId, className = "" }) {
|
|
386
|
-
return /* @__PURE__ */
|
|
764
|
+
return /* @__PURE__ */ jsxs9("div", { className: cn(box, className), children: [
|
|
387
765
|
"Form: ",
|
|
388
766
|
/* @__PURE__ */ jsx14("code", { children: formId })
|
|
389
767
|
] });
|
|
@@ -392,9 +770,9 @@ function CollectionPlaceholder({
|
|
|
392
770
|
cardComponentId,
|
|
393
771
|
className = ""
|
|
394
772
|
}) {
|
|
395
|
-
return /* @__PURE__ */
|
|
773
|
+
return /* @__PURE__ */ jsxs9("div", { className: cn(box, className), children: [
|
|
396
774
|
"Collection: entries of this page",
|
|
397
|
-
cardComponentId ? /* @__PURE__ */
|
|
775
|
+
cardComponentId ? /* @__PURE__ */ jsxs9(Fragment2, { children: [
|
|
398
776
|
" as ",
|
|
399
777
|
/* @__PURE__ */ jsx14("code", { children: cardComponentId })
|
|
400
778
|
] }) : /* @__PURE__ */ jsx14(Fragment2, { children: " (default card)" })
|
|
@@ -499,7 +877,7 @@ function blockStyleToReactStyle(style) {
|
|
|
499
877
|
|
|
500
878
|
// src/renderer.tsx
|
|
501
879
|
import "react";
|
|
502
|
-
import { jsx as jsx15, jsxs as
|
|
880
|
+
import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
503
881
|
var CONTAINER_TYPES = /* @__PURE__ */ new Set(["columns", "section", "slider", "tabs"]);
|
|
504
882
|
function bindingProps(block, annotate) {
|
|
505
883
|
if (!annotate || !block.id) return {};
|
|
@@ -519,7 +897,7 @@ function bindingProps(block, annotate) {
|
|
|
519
897
|
function resolveBlock(block, className, annotate = false) {
|
|
520
898
|
const entry = BLOCK_REGISTRY[block.type];
|
|
521
899
|
if (!entry) {
|
|
522
|
-
return /* @__PURE__ */
|
|
900
|
+
return /* @__PURE__ */ jsxs10(
|
|
523
901
|
"div",
|
|
524
902
|
{
|
|
525
903
|
className: `w-full p-4 border border-dashed border-red-400 bg-red-50 text-red-600 text-sm rounded ${className ?? ""}`,
|
|
@@ -543,7 +921,7 @@ function BlockRenderer({
|
|
|
543
921
|
}) {
|
|
544
922
|
const entry = BLOCK_REGISTRY[block.type];
|
|
545
923
|
if (!entry) {
|
|
546
|
-
return /* @__PURE__ */
|
|
924
|
+
return /* @__PURE__ */ jsxs10(
|
|
547
925
|
"div",
|
|
548
926
|
{
|
|
549
927
|
className: `w-full p-4 border border-dashed border-red-400 bg-red-50 text-red-600 text-sm rounded ${className}`,
|