@bettercms-ai/astro 0.8.3 → 0.9.1
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/components/BcmsBlocks.astro +119 -21
- package/package.json +4 -4
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
* actually contains that block (mirrors render-page.ts's `ctx.flags` gating).
|
|
27
27
|
*/
|
|
28
28
|
import type { ContentBlock } from "@bettercms-ai/types";
|
|
29
|
-
import { blockStyleToCss } from "@bettercms-ai/types";
|
|
29
|
+
import { blockStyleToCss, componentInstanceAddresses } from "@bettercms-ai/types";
|
|
30
|
+
import { bind, bindProps } from "../src/bindings.js";
|
|
30
31
|
import type { DeliveryComponent, DeliveryForm } from "@bettercms-ai/sdk";
|
|
31
32
|
import { applyOverrides } from "@bettercms-ai/ui/resolve";
|
|
32
33
|
import BcmsImage from "./BcmsImage.astro";
|
|
@@ -50,6 +51,11 @@ interface Props {
|
|
|
50
51
|
* `component` case, so a component instance's marker rides the container its own
|
|
51
52
|
* recursion already produces instead of gaining a second <div>. Internal; do not pass. */
|
|
52
53
|
containerAttrs?: Record<string, string | undefined>;
|
|
54
|
+
/** Inside a component instance, the DEFINITION's `<blockId>__<prop>` mapped to this
|
|
55
|
+
* instance's `<id>__overrides.<key>`. AUTHORITATIVE where present: a prop the definition
|
|
56
|
+
* does not declare gets no address, rather than one that binds to no field.
|
|
57
|
+
* Internal; do not pass. */
|
|
58
|
+
fieldAddresses?: Map<string, string>;
|
|
53
59
|
class?: string;
|
|
54
60
|
}
|
|
55
61
|
|
|
@@ -65,8 +71,11 @@ const {
|
|
|
65
71
|
seen = [],
|
|
66
72
|
nested = false,
|
|
67
73
|
containerAttrs = {},
|
|
74
|
+
fieldAddresses,
|
|
68
75
|
class: className,
|
|
69
76
|
} = Astro.props;
|
|
77
|
+
/** What `bind`/`bindProps` read. One object, so every call site cannot disagree. */
|
|
78
|
+
const bctx = { fieldAddresses };
|
|
70
79
|
const formMap = new Map(forms.map((f) => [f.id, f]));
|
|
71
80
|
const componentMap = new Map(components.map((c) => [c.id, c]));
|
|
72
81
|
|
|
@@ -96,7 +105,9 @@ function styleAttr(block: ContentBlock): string | undefined {
|
|
|
96
105
|
* template expression as JSX — `applyOverrides<ContentBlock>(…)` inline emitted a bogus
|
|
97
106
|
* `<ContentBlock>` element and the file failed to compile at all.
|
|
98
107
|
*/
|
|
99
|
-
function resolveComponent(
|
|
108
|
+
function resolveComponent(
|
|
109
|
+
p: Record<string, any>,
|
|
110
|
+
): { id: string; blocks: ContentBlock[]; props: DeliveryComponent["props"] } | null {
|
|
100
111
|
const def = componentMap.get(String(p.componentId ?? ""));
|
|
101
112
|
if (!def) return null;
|
|
102
113
|
if (depth >= MAX_COMPONENT_DEPTH || seen.includes(def.id)) return null;
|
|
@@ -105,7 +116,9 @@ function resolveComponent(p: Record<string, any>): { id: string; blocks: Content
|
|
|
105
116
|
def.props ?? [],
|
|
106
117
|
(p.overrides as Record<string, unknown>) ?? {},
|
|
107
118
|
);
|
|
108
|
-
|
|
119
|
+
// `props` comes back too: the caller needs the DECLARATION (which block each prop drives) to
|
|
120
|
+
// build this instance's addresses, not just the resolved tree.
|
|
121
|
+
return { id: def.id, blocks: resolved, props: def.props ?? [] };
|
|
109
122
|
}
|
|
110
123
|
|
|
111
124
|
/**
|
|
@@ -139,7 +152,10 @@ const needsSlider = !nested && containsType(blocks, "slider");
|
|
|
139
152
|
const needsTabs = !nested && containsType(blocks, "tabs");
|
|
140
153
|
|
|
141
154
|
/** Props every self-recursion forwards unchanged. */
|
|
142
|
-
|
|
155
|
+
// `fieldAddresses` rides along: a slide or tab panel is still inside the SAME component
|
|
156
|
+
// instance, so its children keep that instance's addresses. The `component` case below
|
|
157
|
+
// overrides it with the NEW instance's map.
|
|
158
|
+
const pass = { forms, components, turnstileSiteKey, nested: true, fieldAddresses };
|
|
143
159
|
---
|
|
144
160
|
|
|
145
161
|
{/*
|
|
@@ -185,30 +201,41 @@ const pass = { forms, components, turnstileSiteKey, nested: true };
|
|
|
185
201
|
switch (block.type) {
|
|
186
202
|
case "heading": {
|
|
187
203
|
const Tag = `h${clamp(Number(p.level) || 2, 1, 6)}` as keyof HTMLElementTagNameMap;
|
|
188
|
-
return <Tag
|
|
204
|
+
return <Tag {...bind(block.id, "text", "text", bctx)} {...blockAttrs}>{p.text}</Tag>;
|
|
189
205
|
}
|
|
190
206
|
// `richtext` and `text` carry the same payload and differ only in the authoring
|
|
191
207
|
// surface, so delivery renders them identically — the hosted renderer does the same.
|
|
192
208
|
case "text":
|
|
193
209
|
case "richtext":
|
|
194
|
-
return <div class="bcms-text"
|
|
210
|
+
return <div class="bcms-text" {...bind(block.id, "html", "richtext", bctx)} {...blockAttrs} set:html={p.html ?? ""} />;
|
|
195
211
|
case "image": {
|
|
196
212
|
// Matches the Next adapter: the image block is click-to-select on the Visual Editor
|
|
197
213
|
// canvas like heading/text/button, rather than dock-only. `kind="image"` means select
|
|
198
214
|
// + live src reflection, never contentEditable.
|
|
199
|
-
|
|
215
|
+
// `alt` rides the props lane: this element already carries the src binding and one
|
|
216
|
+
// element cannot hold two addresses.
|
|
217
|
+
const imgBind = {
|
|
218
|
+
...bind(block.id, "src", "image", bctx),
|
|
219
|
+
...bindProps(block.id, [{ prop: "alt", kind: "text", attr: "alt", present: p.alt != null }], bctx),
|
|
220
|
+
};
|
|
200
221
|
return p.caption ? (
|
|
201
222
|
<figure {...blockAttrs}>
|
|
202
|
-
<BcmsImage src={p.src} alt={p.alt ?? ""} width={p.width} height={p.height} {...
|
|
203
|
-
<figcaption>{p.caption}</figcaption>
|
|
223
|
+
<BcmsImage src={p.src} alt={p.alt ?? ""} width={p.width} height={p.height} {...imgBind} />
|
|
224
|
+
<figcaption {...bind(block.id, "caption", "text", bctx)}>{p.caption}</figcaption>
|
|
204
225
|
</figure>
|
|
205
226
|
) : (
|
|
206
|
-
<BcmsImage src={p.src} alt={p.alt ?? ""} width={p.width} height={p.height} {...
|
|
227
|
+
<BcmsImage src={p.src} alt={p.alt ?? ""} width={p.width} height={p.height} {...imgBind} {...blockAttrs} />
|
|
207
228
|
);
|
|
208
229
|
}
|
|
209
230
|
case "button":
|
|
210
231
|
return (
|
|
211
|
-
<a
|
|
232
|
+
<a
|
|
233
|
+
class={`bcms-btn bcms-btn-${p.variant === "secondary" ? "secondary" : "primary"}`}
|
|
234
|
+
href={p.href ?? "#"}
|
|
235
|
+
{...bind(block.id, "text", "text", bctx)}
|
|
236
|
+
{...bindProps(block.id, [{ prop: "href", kind: "url", attr: "href", present: p.href != null }], bctx)}
|
|
237
|
+
{...blockAttrs}
|
|
238
|
+
>
|
|
212
239
|
{p.text}
|
|
213
240
|
</a>
|
|
214
241
|
);
|
|
@@ -216,7 +243,24 @@ const pass = { forms, components, turnstileSiteKey, nested: true };
|
|
|
216
243
|
return <div class="bcms-spacer" style={`height:${Number(p.height) || 0}px`} {...blockAttrs} />;
|
|
217
244
|
case "video":
|
|
218
245
|
return (
|
|
219
|
-
<video
|
|
246
|
+
<video
|
|
247
|
+
src={p.url}
|
|
248
|
+
poster={p.poster}
|
|
249
|
+
controls
|
|
250
|
+
autoplay={!!p.autoplay}
|
|
251
|
+
muted={!!p.autoplay}
|
|
252
|
+
loop={!!p.loop}
|
|
253
|
+
playsinline
|
|
254
|
+
{...bindProps(
|
|
255
|
+
block.id,
|
|
256
|
+
[
|
|
257
|
+
{ prop: "url", kind: "url", attr: "src", present: p.url != null },
|
|
258
|
+
{ prop: "poster", kind: "url", attr: "poster", present: p.poster != null },
|
|
259
|
+
],
|
|
260
|
+
bctx,
|
|
261
|
+
)}
|
|
262
|
+
{...blockAttrs}
|
|
263
|
+
/>
|
|
220
264
|
);
|
|
221
265
|
case "columns": {
|
|
222
266
|
const cols: ContentBlock[][] = Array.isArray(p.columns) ? p.columns : [];
|
|
@@ -260,13 +304,37 @@ const pass = { forms, components, turnstileSiteKey, nested: true };
|
|
|
260
304
|
<nav class="bcms-navbar" {...blockAttrs}>
|
|
261
305
|
{logo?.src ? (
|
|
262
306
|
<a class="bcms-navbar-logo" href={logo.href ?? "/"}>
|
|
263
|
-
<img
|
|
307
|
+
<img
|
|
308
|
+
src={logo.src}
|
|
309
|
+
alt={logo.alt ?? ""}
|
|
310
|
+
{...bind(block.id, "logo.src", "image", bctx)}
|
|
311
|
+
{...bindProps(block.id, [{ prop: "logo.alt", kind: "text", attr: "alt", present: logo.alt != null }], bctx)}
|
|
312
|
+
/>
|
|
264
313
|
</a>
|
|
265
314
|
) : null}
|
|
266
315
|
<div class="bcms-navbar-links">
|
|
267
|
-
{
|
|
316
|
+
{/* Each link addressed by index. The old "nested under arrays" exclusion was
|
|
317
|
+
about a writer ceiling that no longer exists: setFieldPath walks N indices. */}
|
|
318
|
+
{arr(p.links).map((l: any, i: number) => (
|
|
319
|
+
<a
|
|
320
|
+
href={l?.href ?? "#"}
|
|
321
|
+
{...bind(block.id, `links[${i}].label`, "text", bctx)}
|
|
322
|
+
{...bindProps(block.id, [{ prop: `links[${i}].href`, kind: "url", attr: "href", present: l?.href != null }], bctx)}
|
|
323
|
+
>
|
|
324
|
+
{l?.label ?? ""}
|
|
325
|
+
</a>
|
|
326
|
+
))}
|
|
268
327
|
</div>
|
|
269
|
-
{p.cta?.text ?
|
|
328
|
+
{p.cta?.text ? (
|
|
329
|
+
<a
|
|
330
|
+
class="bcms-btn bcms-btn-primary"
|
|
331
|
+
href={p.cta.href ?? "#"}
|
|
332
|
+
{...bind(block.id, "cta.text", "text", bctx)}
|
|
333
|
+
{...bindProps(block.id, [{ prop: "cta.href", kind: "url", attr: "href", present: p.cta.href != null }], bctx)}
|
|
334
|
+
>
|
|
335
|
+
{p.cta.text}
|
|
336
|
+
</a>
|
|
337
|
+
) : null}
|
|
270
338
|
</nav>
|
|
271
339
|
);
|
|
272
340
|
}
|
|
@@ -274,16 +342,27 @@ const pass = { forms, components, turnstileSiteKey, nested: true };
|
|
|
274
342
|
return (
|
|
275
343
|
<footer class="bcms-footer" {...blockAttrs}>
|
|
276
344
|
<div class="bcms-footer-cols">
|
|
277
|
-
{arr(p.columns).map((col: any) => (
|
|
345
|
+
{arr(p.columns).map((col: any, i: number) => (
|
|
278
346
|
<div class="bcms-footer-col">
|
|
279
|
-
{col?.heading ? <h4>{col.heading}</h4> : null}
|
|
347
|
+
{col?.heading ? <h4 {...bind(block.id, `columns[${i}].heading`, "text", bctx)}>{col.heading}</h4> : null}
|
|
280
348
|
<ul>
|
|
281
|
-
{
|
|
349
|
+
{/* Two indices deep — the deepest addressable thing on a page. */}
|
|
350
|
+
{arr(col?.links).map((l: any, j: number) => (
|
|
351
|
+
<li>
|
|
352
|
+
<a
|
|
353
|
+
href={l?.href ?? "#"}
|
|
354
|
+
{...bind(block.id, `columns[${i}].links[${j}].label`, "text", bctx)}
|
|
355
|
+
{...bindProps(block.id, [{ prop: `columns[${i}].links[${j}].href`, kind: "url", attr: "href", present: l?.href != null }], bctx)}
|
|
356
|
+
>
|
|
357
|
+
{l?.label ?? ""}
|
|
358
|
+
</a>
|
|
359
|
+
</li>
|
|
360
|
+
))}
|
|
282
361
|
</ul>
|
|
283
362
|
</div>
|
|
284
363
|
))}
|
|
285
364
|
</div>
|
|
286
|
-
{p.copyright ? <p class="bcms-footer-copy">{p.copyright}</p> : null}
|
|
365
|
+
{p.copyright ? <p class="bcms-footer-copy" {...bind(block.id, "copyright", "text", bctx)}>{p.copyright}</p> : null}
|
|
287
366
|
</footer>
|
|
288
367
|
);
|
|
289
368
|
case "slider":
|
|
@@ -315,7 +394,13 @@ const pass = { forms, components, turnstileSiteKey, nested: true };
|
|
|
315
394
|
<div class="bcms-tabs" data-bcms-tabs {...blockAttrs}>
|
|
316
395
|
<div class="bcms-tablist" role="tablist">
|
|
317
396
|
{arr(p.tabs).map((t: any, i: number) => (
|
|
318
|
-
<button
|
|
397
|
+
<button
|
|
398
|
+
role="tab"
|
|
399
|
+
class="bcms-tab"
|
|
400
|
+
aria-selected={i === 0 ? "true" : "false"}
|
|
401
|
+
data-tab={String(i)}
|
|
402
|
+
{...bind(block.id, `tabs[${i}].label`, "text", bctx)}
|
|
403
|
+
>
|
|
319
404
|
{t?.label ?? ""}
|
|
320
405
|
</button>
|
|
321
406
|
))}
|
|
@@ -330,7 +415,20 @@ const pass = { forms, components, turnstileSiteKey, nested: true };
|
|
|
330
415
|
case "component": {
|
|
331
416
|
const resolved = resolveComponent(p);
|
|
332
417
|
if (!resolved) return null;
|
|
333
|
-
|
|
418
|
+
// 🔴 `fieldAddresses` AFTER `{...pass}`: the instance's own map must replace the
|
|
419
|
+
// enclosing one, not inherit it. Every stamp inside this subtree otherwise names a
|
|
420
|
+
// block belonging to the DEFINITION — shared by every instance of it, so it resolves
|
|
421
|
+
// to no field on one placement and collides into a dropped duplicate on two.
|
|
422
|
+
return (
|
|
423
|
+
<Astro.self
|
|
424
|
+
blocks={resolved.blocks}
|
|
425
|
+
depth={depth + 1}
|
|
426
|
+
seen={[...seen, resolved.id]}
|
|
427
|
+
containerAttrs={blockAttrs}
|
|
428
|
+
{...pass}
|
|
429
|
+
fieldAddresses={componentInstanceAddresses(block.id, resolved.props)}
|
|
430
|
+
/>
|
|
431
|
+
);
|
|
334
432
|
}
|
|
335
433
|
default:
|
|
336
434
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bettercms-ai/astro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The BetterCMS adapter for Astro — a `bettercms()` integration, a `bettercms:client` virtual module, typed content loaders, draft preview, and native .astro rendering components.",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
"test:watch": "vitest"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@bettercms-ai/image-url": "^0.
|
|
51
|
-
"@bettercms-ai/sdk": "^1.13.
|
|
52
|
-
"@bettercms-ai/types": "^1.
|
|
50
|
+
"@bettercms-ai/image-url": "^0.3.0",
|
|
51
|
+
"@bettercms-ai/sdk": "^1.13.1",
|
|
52
|
+
"@bettercms-ai/types": "^1.11.0",
|
|
53
53
|
"@bettercms-ai/ui": "^0.6.0"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|