@bettercms-ai/astro 0.26.0 → 0.27.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 +54 -19
- package/components/BcmsButton.astro +16 -4
- package/package.json +5 -5
|
@@ -26,13 +26,13 @@
|
|
|
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 { RESPONSIVE_CSS, blockStyleToCss, hideOnAttr, usesHideOn, visibleBlocks, windowState, blockStyleToInnerCss, splitButtonStyle, buttonIconOf, buttonIconSvg, buttonVariantOf, componentInstanceAddresses, headingPaintCss, withButtonLookProps } from "@bettercms-ai/types";
|
|
29
|
+
import { RESPONSIVE_CSS, imageFrameCss, blockStyleToCss, hiddenEverywhere, hideOnAttr, usesHideOn, visibleBlocks, windowState, blockStyleToInnerCss, splitButtonStyle, buttonIconOf, buttonIconSvg, buttonVariantOf, componentInstanceAddresses, headingPaintCss, withButtonLookProps } from "@bettercms-ai/types";
|
|
30
30
|
import { bind, bindProps } from "../dist/bindings.js";
|
|
31
31
|
import type { DeliveryComponent, DeliveryForm } from "@bettercms-ai/sdk";
|
|
32
32
|
import { applyOverrides } from "@bettercms-ai/ui/resolve";
|
|
33
33
|
import { addressFor } from "../dist/bindings.js";
|
|
34
34
|
import { portableTextToHtml } from "@bettercms-ai/richtext/html";
|
|
35
|
-
import {
|
|
35
|
+
import { embeddedComponentRender, readStructuredContent } from "@bettercms-ai/richtext";
|
|
36
36
|
import { headingParts, richtextHeadingLevel } from "@bettercms-ai/richtext/heading";
|
|
37
37
|
import BcmsImage from "./BcmsImage.astro";
|
|
38
38
|
import BcmsForm from "./BcmsForm.astro";
|
|
@@ -69,8 +69,9 @@ interface Props {
|
|
|
69
69
|
/** The instant block windows (`block.window`) are read against. Defaults to the render time:
|
|
70
70
|
* the build for a static page, the request for a server-rendered one. */
|
|
71
71
|
now?: Date;
|
|
72
|
-
/** Draw the blocks outside their window too, marked `data-bcms-window="pending|expired"`,
|
|
73
|
-
*
|
|
72
|
+
/** Draw the blocks outside their window too, marked `data-bcms-window="pending|expired"`, and the
|
|
73
|
+
* blocks hidden on every breakpoint, marked `data-bcms-hidden="all"`, for a draft preview. Off,
|
|
74
|
+
* neither is rendered, at any depth. */
|
|
74
75
|
showScheduled?: boolean;
|
|
75
76
|
}
|
|
76
77
|
|
|
@@ -174,13 +175,18 @@ const EMBED_SLOT = "<!--bcms-embed-->";
|
|
|
174
175
|
* `chunks` has one more entry than `embeds`.
|
|
175
176
|
*/
|
|
176
177
|
function richTextParts(structured: NonNullable<ReturnType<typeof readStructuredContent>>, base: string | null) {
|
|
177
|
-
|
|
178
|
+
/** A linked embed is its `component` block; a separate copy, its own frozen blocks (it never looks the component up). */
|
|
179
|
+
const embeds: ({ kind: "linked"; block: ContentBlock } | { kind: "copy"; componentId: string; id: string; blocks: ContentBlock[] })[] = [];
|
|
178
180
|
const html = portableTextToHtml(structured, {
|
|
179
181
|
...(base ? { bindingBase: base } : {}),
|
|
180
182
|
renderBlock: (v) => {
|
|
181
|
-
const
|
|
182
|
-
if (!
|
|
183
|
-
embeds.push(
|
|
183
|
+
const embed = embeddedComponentRender(v);
|
|
184
|
+
if (!embed) return undefined;
|
|
185
|
+
embeds.push(
|
|
186
|
+
embed.kind === "linked"
|
|
187
|
+
? { kind: "linked", block: embed.block as ContentBlock }
|
|
188
|
+
: { kind: "copy", componentId: embed.copy.componentId, id: embed.copy.id, blocks: embed.copy.blocks as ContentBlock[] },
|
|
189
|
+
);
|
|
184
190
|
return EMBED_SLOT;
|
|
185
191
|
},
|
|
186
192
|
});
|
|
@@ -270,7 +276,9 @@ const pass = { forms, components, turnstileSiteKey, nested: true, fieldAddresses
|
|
|
270
276
|
*/
|
|
271
277
|
/** `showScheduled` only: which side of its window a drawn block is on. Rides the block's root, like the id. */
|
|
272
278
|
const scheduled = showScheduled && windowState(block, at) !== "live" ? windowState(block, at) : undefined;
|
|
273
|
-
|
|
279
|
+
/** `showScheduled` only: hidden on every breakpoint ("Hide on this page"), drawn so its author can find it. */
|
|
280
|
+
const hiddenAll = showScheduled && hiddenEverywhere(block) ? "all" : undefined;
|
|
281
|
+
const blockAttrs = style || inner || hide ? {} : nested ? { "data-bcms-window": scheduled, "data-bcms-hidden": hiddenAll } : { "data-bcms-block": block.id, "data-bcms-window": scheduled, "data-bcms-hidden": hiddenAll };
|
|
274
282
|
const node = (() => {
|
|
275
283
|
switch (block.type) {
|
|
276
284
|
case "heading": {
|
|
@@ -321,24 +329,37 @@ const pass = { forms, components, turnstileSiteKey, nested: true, fieldAddresses
|
|
|
321
329
|
const { chunks, embeds } = richTextParts(structured, base);
|
|
322
330
|
return (
|
|
323
331
|
<div class="bcms-text" {...blockAttrs}>
|
|
324
|
-
{chunks.map((html, i) =>
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
332
|
+
{chunks.map((html, i) => {
|
|
333
|
+
const e = embeds[i];
|
|
334
|
+
return (
|
|
335
|
+
<Fragment>
|
|
336
|
+
<Fragment set:html={html} />
|
|
337
|
+
{e?.kind === "linked" && <Astro.self blocks={[e.block]} depth={depth} seen={seen} {...pass} owner={base} />}
|
|
338
|
+
{e?.kind === "copy" && (
|
|
339
|
+
<div class="bcms-block bcms-block--component" data-bcms-component-copy={e.componentId} data-bcms-instance={e.id}>
|
|
340
|
+
<Astro.self blocks={e.blocks} depth={depth + 1} seen={seen} {...pass} fieldAddresses={new Map()} />
|
|
341
|
+
</div>
|
|
342
|
+
)}
|
|
343
|
+
</Fragment>
|
|
344
|
+
);
|
|
345
|
+
})}
|
|
330
346
|
</div>
|
|
331
347
|
);
|
|
332
348
|
}
|
|
333
349
|
return <div class="bcms-text" {...bind(block.id, "html", "richtext", bctx)} {...blockAttrs} set:html={p.html ?? ""} />;
|
|
334
350
|
}
|
|
335
351
|
case "image": {
|
|
352
|
+
// An unset src renders nothing, wrapper included, as on the hosted lane (render-page D3).
|
|
353
|
+
if (!String(p.src ?? "").trim()) return null;
|
|
336
354
|
// Matches the Next adapter: the image block is click-to-select on the Visual Editor
|
|
337
355
|
// canvas like heading/text/button, rather than dock-only. `kind="image"` means select
|
|
338
356
|
// + live src reflection, never contentEditable.
|
|
339
357
|
// `alt` rides the props lane: this element already carries the src binding and one
|
|
340
358
|
// element cannot hold two addresses.
|
|
359
|
+
// V2's media frame (`style.aspect`), drawn as the hosted lane draws it.
|
|
360
|
+
const frame = imageFrameCss(block.style);
|
|
341
361
|
const imgBind = {
|
|
362
|
+
...(frame ? { style: [`width:${frame.width}`, `aspect-ratio:${frame.aspectRatio}`, `object-fit:${frame.objectFit}`, frame.borderRadius ? `border-radius:${frame.borderRadius}` : ""].filter(Boolean).join(";") } : {}),
|
|
342
363
|
...bind(block.id, "src", "image", bctx),
|
|
343
364
|
...bindProps(block.id, [{ prop: "alt", kind: "text", attr: "alt", present: p.alt != null }], bctx),
|
|
344
365
|
};
|
|
@@ -352,6 +373,20 @@ const pass = { forms, components, turnstileSiteKey, nested: true, fieldAddresses
|
|
|
352
373
|
);
|
|
353
374
|
}
|
|
354
375
|
case "button":
|
|
376
|
+
// The site's script owns the click: a `<button type="button">`, never an anchor with an invented href.
|
|
377
|
+
if (p.action === "code") return (
|
|
378
|
+
<button
|
|
379
|
+
type="button"
|
|
380
|
+
class={`bcms-btn bcms-btn-${buttonVariantOf(p.variant)}${p.size === "sm" || p.size === "lg" ? ` bcms-btn-${p.size}` : ""}${p.fullWidth === true ? " bcms-btn-full" : ""}`}
|
|
381
|
+
style={buttonPaintAttr(block)}
|
|
382
|
+
{...bind(block.id, "text", "text", bctx)}
|
|
383
|
+
{...blockAttrs}
|
|
384
|
+
>
|
|
385
|
+
{buttonIconOf(p.icon) && <Fragment set:html={buttonIconSvg(buttonIconOf(p.icon)!, "bcms-btn-icon")} />}
|
|
386
|
+
{p.text}
|
|
387
|
+
{p.arrow === true && <Fragment set:html={buttonIconSvg("arrow-right", "bcms-btn-arrow")} />}
|
|
388
|
+
</button>
|
|
389
|
+
);
|
|
355
390
|
return (
|
|
356
391
|
<a
|
|
357
392
|
class={`bcms-btn bcms-btn-${buttonVariantOf(p.variant)}${p.size === "sm" || p.size === "lg" ? ` bcms-btn-${p.size}` : ""}${p.fullWidth === true ? " bcms-btn-full" : ""}`}
|
|
@@ -585,10 +620,10 @@ const pass = { forms, components, turnstileSiteKey, nested: true, fieldAddresses
|
|
|
585
620
|
* box is the root and carries the marker instead.
|
|
586
621
|
*/
|
|
587
622
|
const stamp = nested ? undefined : block.id;
|
|
588
|
-
if (style && inner) return <div style={style} data-bcms-hide={hide} data-bcms-block={stamp} data-bcms-window={scheduled}><div class="bcms-block-inner" style={inner}>{node}</div></div>;
|
|
589
|
-
if (hide) return <div style={style} data-bcms-hide={hide} data-bcms-block={stamp} data-bcms-window={scheduled}>{inner ? <div class="bcms-block-inner" style={inner}>{node}</div> : node}</div>;
|
|
590
|
-
if (inner) return <div class="bcms-block-inner" style={inner} data-bcms-block={stamp} data-bcms-window={scheduled}>{node}</div>;
|
|
591
|
-
if (style) return <div style={style} data-bcms-block={stamp} data-bcms-window={scheduled}>{node}</div>;
|
|
623
|
+
if (style && inner) return <div style={style} data-bcms-hide={hide} data-bcms-block={stamp} data-bcms-window={scheduled} data-bcms-hidden={hiddenAll}><div class="bcms-block-inner" style={inner}>{node}</div></div>;
|
|
624
|
+
if (hide) return <div style={style} data-bcms-hide={hide} data-bcms-block={stamp} data-bcms-window={scheduled} data-bcms-hidden={hiddenAll}>{inner ? <div class="bcms-block-inner" style={inner}>{node}</div> : node}</div>;
|
|
625
|
+
if (inner) return <div class="bcms-block-inner" style={inner} data-bcms-block={stamp} data-bcms-window={scheduled} data-bcms-hidden={hiddenAll}>{node}</div>;
|
|
626
|
+
if (style) return <div style={style} data-bcms-block={stamp} data-bcms-window={scheduled} data-bcms-hidden={hiddenAll}>{node}</div>;
|
|
592
627
|
return node;
|
|
593
628
|
})}
|
|
594
629
|
</div>
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
3
|
* <BcmsButton> — render a `button` field ({ label, href, variant?, size?, arrow?, icon?,
|
|
4
|
-
* newTab? }) as one editable button, the way the BetterCMS visual editor and in-place publishing
|
|
4
|
+
* newTab?, fullWidth? }) as one editable button, the way the BetterCMS visual editor and in-place publishing
|
|
5
5
|
* expect to find it:
|
|
6
6
|
*
|
|
7
|
-
* <a data-bcms-button="<path>" data-variant data-size href [target rel] data-bcms-props="<path>.href|url|href">
|
|
7
|
+
* <a data-bcms-button="<path>" data-variant data-size [data-full-width] href [target rel] data-bcms-props="<path>.href|url|href">
|
|
8
8
|
* [<svg class="bcms-btn-icon">]<span data-bcms-field="<path>.label">Label</span>[<svg class="bcms-btn-arrow">]
|
|
9
9
|
* </a>
|
|
10
10
|
*
|
|
11
|
+
* A value with `action: "code"` (no href: the site's script handles the click) renders the same
|
|
12
|
+
* children in `<button type="button" data-bcms-button="<path>" data-variant data-size>`.
|
|
13
|
+
*
|
|
11
14
|
* Style it by `data-variant` (primary · secondary · tertiary · brand · dark · light · outline ·
|
|
12
15
|
* link) and `data-size` (sm · md · lg) in your own CSS: the editor repaints those attributes as the
|
|
13
16
|
* author picks, and publishing writes them into the live page, so your stylesheet decides what a
|
|
14
|
-
* look looks like.
|
|
17
|
+
* look looks like. A full-width button also carries `display:flex;width:100%;justify-content:center`
|
|
18
|
+
* inline, so it stretches across its column with no rule of yours. `class` is passed through untouched.
|
|
15
19
|
*
|
|
16
20
|
* <BcmsButton path="hero.cta" value={page.hero.cta} class="btn" />
|
|
17
21
|
*/
|
|
@@ -27,13 +31,21 @@ interface Props {
|
|
|
27
31
|
const { path, value, class: className } = Astro.props;
|
|
28
32
|
const b = readButtonValue(value);
|
|
29
33
|
---
|
|
30
|
-
{b.label && (
|
|
34
|
+
{b.label && b.action === "code" && (
|
|
35
|
+
<button type="button" class={className} data-bcms-button={path} data-variant={b.variant} data-size={b.size} {...(b.fullWidth ? { "data-full-width": "", style: "display:flex;width:100%;justify-content:center" } : {})}>
|
|
36
|
+
{b.icon && <Fragment set:html={buttonIconSvg(b.icon, "bcms-btn-icon")} />}
|
|
37
|
+
<span data-bcms-field={`${path}.label`} data-bcms-kind="text">{b.label}</span>
|
|
38
|
+
{b.arrow && <Fragment set:html={buttonIconSvg("arrow-right", "bcms-btn-arrow")} />}
|
|
39
|
+
</button>
|
|
40
|
+
)}
|
|
41
|
+
{b.label && b.action !== "code" && (
|
|
31
42
|
<a
|
|
32
43
|
class={className}
|
|
33
44
|
href={b.href || "#"}
|
|
34
45
|
data-bcms-button={path}
|
|
35
46
|
data-variant={b.variant}
|
|
36
47
|
data-size={b.size}
|
|
48
|
+
{...(b.fullWidth ? { "data-full-width": "", style: "display:flex;width:100%;justify-content:center" } : {})}
|
|
37
49
|
data-bcms-props={formatPropsAttribute([{ address: `${path}.href`, kind: "url", attr: "href" }])}
|
|
38
50
|
{...(b.newTab ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
|
39
51
|
>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bettercms-ai/astro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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",
|
|
@@ -61,11 +61,11 @@
|
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@bettercms-ai/image-url": "^0.4.0",
|
|
64
|
-
"@bettercms-ai/next": "^0.
|
|
65
|
-
"@bettercms-ai/richtext": "^0.
|
|
64
|
+
"@bettercms-ai/next": "^0.26.1",
|
|
65
|
+
"@bettercms-ai/richtext": "^0.9.0",
|
|
66
66
|
"@bettercms-ai/sdk": "^1.29.0",
|
|
67
|
-
"@bettercms-ai/types": "^1.
|
|
68
|
-
"@bettercms-ai/ui": "^0.
|
|
67
|
+
"@bettercms-ai/types": "^1.44.0",
|
|
68
|
+
"@bettercms-ai/ui": "^0.17.0",
|
|
69
69
|
"libphonenumber-js": "^1.13.13"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|