@bettercms-ai/astro 0.23.0 → 0.24.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.
@@ -1,112 +1,112 @@
1
- ---
2
- /**
3
- * <BcmsImage> — renders an optimized image via the BetterCMS media transform
4
- * endpoint, with a 1x/2x srcset. Accepts an asset id, a MediaAsset, a URL, or an image
5
- * VALUE (`{ url, altText, focal?, crop? }`) — whose per-placement crop and focal point are
6
- * applied by image-url itself (`rect`, and fx/fy inside the crop), the same as on every lane.
7
- */
8
- import config from "bettercms:config";
9
- import imageUrlBuilder, {
10
- focalInCrop,
11
- normalizeImageCrop,
12
- normalizeImageFocal,
13
- objectPosition,
14
- type ImageSource,
15
- type ImageFormat,
16
- type ImageFit,
17
- } from "@bettercms-ai/image-url";
18
-
19
- interface Props {
20
- src: ImageSource;
21
- alt?: string;
22
- width?: number;
23
- height?: number;
24
- format?: ImageFormat;
25
- quality?: number;
26
- fit?: ImageFit;
27
- sizes?: string;
28
- loading?: "lazy" | "eager";
29
- class?: string;
30
- /**
31
- * Focal point override, 0–1. Normally omitted — the asset carries its own and the
32
- * server applies it. Pass a MediaAsset with focalX/focalY as `src` and it is picked
33
- * up automatically.
34
- */
35
- focalX?: number | null;
36
- focalY?: number | null;
37
- /** Any other attribute (e.g. `data-bcms-field`) is forwarded to the <img>. */
38
- [attr: string]: unknown;
39
- }
40
-
41
- const {
42
- src,
43
- alt = "",
44
- width,
45
- height,
46
- format = "webp",
47
- quality,
48
- fit,
49
- sizes,
50
- loading = "lazy",
51
- class: className,
52
- focalX,
53
- focalY,
54
- style,
55
- ...rest
56
- } = Astro.props;
57
-
58
- // An explicit prop wins; then the value's own `focal`; then the asset's focalX/focalY. The
59
- // explicit prop is taken as given (window-relative when the value is cropped); the other two are
60
- // SOURCE-relative, so with a crop they are re-expressed inside it — image-url's focalInCrop.
61
- const asset = (typeof src === "object" && src !== null ? src : {}) as Record<string, unknown>;
62
- const explicit = focalX != null || focalY != null;
63
- const num = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
64
- const stored =
65
- normalizeImageFocal(asset.focal) ??
66
- (num(asset.focalX) != null || num(asset.focalY) != null
67
- ? { x: num(asset.focalX) ?? 0.5, y: num(asset.focalY) ?? 0.5 }
68
- : null);
69
- const inWindow = stored ? focalInCrop(stored, normalizeImageCrop(asset.crop)) : null;
70
- const fx = explicit ? focalX : inWindow?.x;
71
- const fy = explicit ? focalY : inWindow?.y;
72
- const hasFocal = fx != null || fy != null;
73
-
74
- const builder = imageUrlBuilder({ baseUrl: config.mediaUrl });
75
- const make = () => {
76
- let b = builder(src);
77
- if (width) b = b.width(width);
78
- if (height) b = b.height(height);
79
- if (format) b = b.format(format);
80
- if (quality) b = b.quality(quality);
81
- if (fit) b = b.fit(fit);
82
- if (hasFocal) b = b.focal(fx ?? 0.5, fy ?? 0.5);
83
- return b;
84
- };
85
-
86
- const src1x = make().url();
87
- // A pixel width is the author's size for THIS image, so it goes on as a style (a site rule like
88
- // `img { width: 100% }` beats a bare attribute) — paired with max-width so it never overflows a
89
- // narrower column, and height:auto so shrinking keeps the aspect ratio.
90
- const widthStyle = width ? `width:${width}px;max-width:100%;height:auto` : "";
91
- const src2x = make().dpr(2).url();
92
-
93
- // The CSS half of the same idea, and it is free. When only one dimension is pinned the
94
- // server does not crop, so the browser does — and `object-position` keeps the subject in
95
- // frame there too, but only if the stylesheet actually set `object-fit: cover`. Emitting
96
- // it unconditionally is harmless: `object-position` is inert without `object-fit`.
97
- const focalStyle = hasFocal ? `object-position:${objectPosition({ x: fx, y: fy })}` : "";
98
- const mergedStyle = [widthStyle, focalStyle, typeof style === "string" ? style : ""].filter(Boolean).join(";");
99
- ---
100
-
101
- <img
102
- src={src1x}
103
- srcset={`${src1x} 1x, ${src2x} 2x`}
104
- alt={alt}
105
- width={width}
106
- height={height}
107
- sizes={sizes}
108
- loading={loading}
109
- class={className}
110
- style={mergedStyle || undefined}
111
- {...rest}
112
- />
1
+ ---
2
+ /**
3
+ * <BcmsImage> — renders an optimized image via the BetterCMS media transform
4
+ * endpoint, with a 1x/2x srcset. Accepts an asset id, a MediaAsset, a URL, or an image
5
+ * VALUE (`{ url, altText, focal?, crop? }`) — whose per-placement crop and focal point are
6
+ * applied by image-url itself (`rect`, and fx/fy inside the crop), the same as on every lane.
7
+ */
8
+ import config from "bettercms:config";
9
+ import imageUrlBuilder, {
10
+ focalInCrop,
11
+ normalizeImageCrop,
12
+ normalizeImageFocal,
13
+ objectPosition,
14
+ type ImageSource,
15
+ type ImageFormat,
16
+ type ImageFit,
17
+ } from "@bettercms-ai/image-url";
18
+
19
+ interface Props {
20
+ src: ImageSource;
21
+ alt?: string;
22
+ width?: number;
23
+ height?: number;
24
+ format?: ImageFormat;
25
+ quality?: number;
26
+ fit?: ImageFit;
27
+ sizes?: string;
28
+ loading?: "lazy" | "eager";
29
+ class?: string;
30
+ /**
31
+ * Focal point override, 0–1. Normally omitted — the asset carries its own and the
32
+ * server applies it. Pass a MediaAsset with focalX/focalY as `src` and it is picked
33
+ * up automatically.
34
+ */
35
+ focalX?: number | null;
36
+ focalY?: number | null;
37
+ /** Any other attribute (e.g. `data-bcms-field`) is forwarded to the <img>. */
38
+ [attr: string]: unknown;
39
+ }
40
+
41
+ const {
42
+ src,
43
+ alt = "",
44
+ width,
45
+ height,
46
+ format = "webp",
47
+ quality,
48
+ fit,
49
+ sizes,
50
+ loading = "lazy",
51
+ class: className,
52
+ focalX,
53
+ focalY,
54
+ style,
55
+ ...rest
56
+ } = Astro.props;
57
+
58
+ // An explicit prop wins; then the value's own `focal`; then the asset's focalX/focalY. The
59
+ // explicit prop is taken as given (window-relative when the value is cropped); the other two are
60
+ // SOURCE-relative, so with a crop they are re-expressed inside it — image-url's focalInCrop.
61
+ const asset = (typeof src === "object" && src !== null ? src : {}) as Record<string, unknown>;
62
+ const explicit = focalX != null || focalY != null;
63
+ const num = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
64
+ const stored =
65
+ normalizeImageFocal(asset.focal) ??
66
+ (num(asset.focalX) != null || num(asset.focalY) != null
67
+ ? { x: num(asset.focalX) ?? 0.5, y: num(asset.focalY) ?? 0.5 }
68
+ : null);
69
+ const inWindow = stored ? focalInCrop(stored, normalizeImageCrop(asset.crop)) : null;
70
+ const fx = explicit ? focalX : inWindow?.x;
71
+ const fy = explicit ? focalY : inWindow?.y;
72
+ const hasFocal = fx != null || fy != null;
73
+
74
+ const builder = imageUrlBuilder({ baseUrl: config.mediaUrl });
75
+ const make = () => {
76
+ let b = builder(src);
77
+ if (width) b = b.width(width);
78
+ if (height) b = b.height(height);
79
+ if (format) b = b.format(format);
80
+ if (quality) b = b.quality(quality);
81
+ if (fit) b = b.fit(fit);
82
+ if (hasFocal) b = b.focal(fx ?? 0.5, fy ?? 0.5);
83
+ return b;
84
+ };
85
+
86
+ const src1x = make().url();
87
+ // A pixel width is the author's size for THIS image, so it goes on as a style (a site rule like
88
+ // `img { width: 100% }` beats a bare attribute) — paired with max-width so it never overflows a
89
+ // narrower column, and height:auto so shrinking keeps the aspect ratio.
90
+ const widthStyle = width ? `width:${width}px;max-width:100%;height:auto` : "";
91
+ const src2x = make().dpr(2).url();
92
+
93
+ // The CSS half of the same idea, and it is free. When only one dimension is pinned the
94
+ // server does not crop, so the browser does — and `object-position` keeps the subject in
95
+ // frame there too, but only if the stylesheet actually set `object-fit: cover`. Emitting
96
+ // it unconditionally is harmless: `object-position` is inert without `object-fit`.
97
+ const focalStyle = hasFocal ? `object-position:${objectPosition({ x: fx, y: fy })}` : "";
98
+ const mergedStyle = [widthStyle, focalStyle, typeof style === "string" ? style : ""].filter(Boolean).join(";");
99
+ ---
100
+
101
+ <img
102
+ src={src1x}
103
+ srcset={`${src1x} 1x, ${src2x} 2x`}
104
+ alt={alt}
105
+ width={width}
106
+ height={height}
107
+ sizes={sizes}
108
+ loading={loading}
109
+ class={className}
110
+ style={mergedStyle || undefined}
111
+ {...rest}
112
+ />
@@ -1,66 +1,66 @@
1
- ---
2
- /**
3
- * <BcmsLive> — opens the BetterCMS delivery live SSE stream in the browser and
4
- * keeps the page current. Drop it once, e.g. in your layout. Build `src` with
5
- * `liveSrc({ apiUrl, workspace, apiKey })` from `@bettercms-ai/astro` (use a
6
- * content:read key — it is browser-exposed).
7
- *
8
- * Every change frame is dispatched on `window` as a cancelable `bcms:change`
9
- * CustomEvent whose `detail` is the frame — including `detail.doc`, the finished
10
- * delivery document the server already hydrated for you. Patch the DOM from it
11
- * and call `preventDefault()` to keep the page as it is:
12
- *
13
- * window.addEventListener("bcms:change", (e) => {
14
- * if (e.detail.slug !== "home") return;
15
- * document.querySelector("h1").textContent = e.detail.doc.data.title;
16
- * e.preventDefault(); // handled — skip the reload
17
- * });
18
- *
19
- * Nothing handles it → the page reloads, as it always did. That fallback is why
20
- * this is safe to drop into an existing site unchanged.
21
- *
22
- * `tags` narrows which changes count at all. Without it a publish anywhere in the
23
- * workspace reloads every open tab, including pages that render none of it.
24
- */
25
- interface Props {
26
- src: string;
27
- /** Only react to changes carrying one of these tags. Omit = react to all. */
28
- tags?: string[];
29
- }
30
- const { src, tags } = Astro.props;
31
- ---
32
-
33
- <script is:inline define:vars={{ src, tags }}>
34
- try {
35
- const es = new EventSource(src);
36
- es.addEventListener("change", (e) => {
37
- let detail;
38
- try {
39
- detail = JSON.parse(e.data);
40
- } catch {
41
- return;
42
- }
43
- if (tags?.length && !detail.tags?.some((t) => tags.includes(t))) return;
44
- const handled = !window.dispatchEvent(
45
- new CustomEvent("bcms:change", { detail, cancelable: true }),
46
- );
47
- if (!handled) window.location.reload();
48
- });
49
- } catch (e) {
50
- /* EventSource unsupported — no live refresh */
51
- }
52
-
53
- // Soft refresh for the BetterCMS canvas. The editor knows which of its writes are
54
- // STRUCTURAL and asks for a re-render only for those; the ack is what tells it this page
55
- // answered, so it does not remount the iframe (which throws away an inline text session).
56
- // Same acceptance predicate as the Next draft bridge: own origin, and the parent frame.
57
- if (window.parent !== window) {
58
- window.addEventListener("message", (e) => {
59
- if (e.origin !== location.origin || e.source !== window.parent) return;
60
- if (e.data?.type !== "bcms:refresh") return;
61
- // Ack BEFORE reloading — after it, this frame is gone and the editor has timed out.
62
- window.parent.postMessage({ type: "bcms:refreshed" }, location.origin);
63
- window.location.reload();
64
- });
65
- }
66
- </script>
1
+ ---
2
+ /**
3
+ * <BcmsLive> — opens the BetterCMS delivery live SSE stream in the browser and
4
+ * keeps the page current. Drop it once, e.g. in your layout. Build `src` with
5
+ * `liveSrc({ apiUrl, workspace, apiKey })` from `@bettercms-ai/astro` (use a
6
+ * content:read key — it is browser-exposed).
7
+ *
8
+ * Every change frame is dispatched on `window` as a cancelable `bcms:change`
9
+ * CustomEvent whose `detail` is the frame — including `detail.doc`, the finished
10
+ * delivery document the server already hydrated for you. Patch the DOM from it
11
+ * and call `preventDefault()` to keep the page as it is:
12
+ *
13
+ * window.addEventListener("bcms:change", (e) => {
14
+ * if (e.detail.slug !== "home") return;
15
+ * document.querySelector("h1").textContent = e.detail.doc.data.title;
16
+ * e.preventDefault(); // handled — skip the reload
17
+ * });
18
+ *
19
+ * Nothing handles it → the page reloads, as it always did. That fallback is why
20
+ * this is safe to drop into an existing site unchanged.
21
+ *
22
+ * `tags` narrows which changes count at all. Without it a publish anywhere in the
23
+ * workspace reloads every open tab, including pages that render none of it.
24
+ */
25
+ interface Props {
26
+ src: string;
27
+ /** Only react to changes carrying one of these tags. Omit = react to all. */
28
+ tags?: string[];
29
+ }
30
+ const { src, tags } = Astro.props;
31
+ ---
32
+
33
+ <script is:inline define:vars={{ src, tags }}>
34
+ try {
35
+ const es = new EventSource(src);
36
+ es.addEventListener("change", (e) => {
37
+ let detail;
38
+ try {
39
+ detail = JSON.parse(e.data);
40
+ } catch {
41
+ return;
42
+ }
43
+ if (tags?.length && !detail.tags?.some((t) => tags.includes(t))) return;
44
+ const handled = !window.dispatchEvent(
45
+ new CustomEvent("bcms:change", { detail, cancelable: true }),
46
+ );
47
+ if (!handled) window.location.reload();
48
+ });
49
+ } catch (e) {
50
+ /* EventSource unsupported — no live refresh */
51
+ }
52
+
53
+ // Soft refresh for the BetterCMS canvas. The editor knows which of its writes are
54
+ // STRUCTURAL and asks for a re-render only for those; the ack is what tells it this page
55
+ // answered, so it does not remount the iframe (which throws away an inline text session).
56
+ // Same acceptance predicate as the Next draft bridge: own origin, and the parent frame.
57
+ if (window.parent !== window) {
58
+ window.addEventListener("message", (e) => {
59
+ if (e.origin !== location.origin || e.source !== window.parent) return;
60
+ if (e.data?.type !== "bcms:refresh") return;
61
+ // Ack BEFORE reloading — after it, this frame is gone and the editor has timed out.
62
+ window.parent.postMessage({ type: "bcms:refreshed" }, location.origin);
63
+ window.location.reload();
64
+ });
65
+ }
66
+ </script>
@@ -1,72 +1,72 @@
1
- ---
2
- /**
3
- * <BcmsLiveBlocks> — <BcmsBlocks>, but as a hydrated island that speaks the visual editor's
4
- * draft bridge (E5 / FLO-1178).
5
- *
6
- * WHY IT IS NOT A .astro RENDERER. An .astro component is server-only: once the page is built
7
- * there is nothing on the client to hand a new document to, so an Astro site can only show the
8
- * editor a STATIC build and the canvas falls back to the platform's approximate renderer for
9
- * every draft. The bridge needs a live component tree, and the only one that already exists,
10
- * is at parity with this package, and speaks the protocol is <BcmsDraftBridge> from
11
- * @bettercms-ai/next — which imports nothing from `next/*` (React and @bettercms-ai/* only),
12
- * so it mounts here unchanged. Reused rather than reimplemented: one protocol, one twin to
13
- * keep in step instead of two.
14
- *
15
- * RUNTIME: Preact + `compat`, verified by the P2-1 spike (see ../SPIKE.md — SSR and hydration
16
- * of a heading + PortableText richtext + BcmsForm tree, both clean). The cost is real and worth
17
- * naming: preact plus its compat aliases on the island (~10 kB of runtime, so `react` itself is
18
- * never bundled even though @bettercms-ai/ui declares React peers), and `@bettercms-ai/next` as a
19
- * dependency of this package.
20
- *
21
- * 🔴 THE CLASS NAMES ARE THE REACT GRAMMAR, NOT THIS PACKAGE'S. <BcmsBlocks> in @bettercms-ai/next
22
- * emits `bcms-block bcms-block--text`, `bcms-button--primary`, `bcms-column`; BcmsBlocks.astro
23
- * emits `bcms-text`, `bcms-btn-primary`, `bcms-cols`. Same markup shape, different hooks — a
24
- * stylesheet written against the .astro grammar styles NOTHING here, and swapping a page from
25
- * <BcmsBlocks> to <BcmsLiveBlocks> unstyles it. Either cover both spellings (the starters use
26
- * `:is()` for exactly this) or read the React classes off @bettercms-ai/next's src/blocks.tsx.
27
- *
28
- * The consumer supplies the renderer, because an integration cannot be added from a component:
29
- *
30
- * // astro.config.mjs
31
- * import preact from "@astrojs/preact";
32
- * export default defineConfig({ integrations: [bettercms(), preact({ compat: true })] });
33
- *
34
- * ---
35
- * import BcmsLiveBlocks from "@bettercms-ai/astro/components/BcmsLiveBlocks.astro";
36
- * const page = await getPage(Astro.params.slug);
37
- * ---
38
- * <BcmsLiveBlocks slug={page.slug} blocks={page.blocks} forms={forms} />
39
- *
40
- * Standalone the island renders exactly what <BcmsBlocks> renders (inside the
41
- * `data-bcms-canvas="bridge"` marker <div> that <BcmsDraftBridge> emits); the bridge only
42
- * wakes when the editor frames the page and pushes a draft. This only helps pages whose body
43
- * IS a block tree — a hand-written .astro layout is unreachable from here by construction.
44
- */
45
- import { BcmsDraftBridge } from "@bettercms-ai/next/draft-bridge";
46
- import type { ContentBlock } from "@bettercms-ai/types";
47
- import type { DeliveryComponent, DeliveryForm } from "@bettercms-ai/sdk";
48
-
49
- interface Props {
50
- /** The page's slug. A pushed draft is applied only when its slug matches. */
51
- slug: string;
52
- blocks: ContentBlock[];
53
- forms?: DeliveryForm[];
54
- components?: DeliveryComponent[];
55
- turnstileSiteKey?: string;
56
- apiBase?: string;
57
- class?: string;
58
- }
59
-
60
- const { slug, blocks, forms, components, turnstileSiteKey, apiBase, class: className } = Astro.props;
61
- ---
62
-
63
- <BcmsDraftBridge
64
- client:load
65
- slug={slug}
66
- blocks={blocks}
67
- forms={forms}
68
- components={components}
69
- turnstileSiteKey={turnstileSiteKey}
70
- apiBase={apiBase}
71
- className={className}
72
- />
1
+ ---
2
+ /**
3
+ * <BcmsLiveBlocks> — <BcmsBlocks>, but as a hydrated island that speaks the visual editor's
4
+ * draft bridge (E5 / FLO-1178).
5
+ *
6
+ * WHY IT IS NOT A .astro RENDERER. An .astro component is server-only: once the page is built
7
+ * there is nothing on the client to hand a new document to, so an Astro site can only show the
8
+ * editor a STATIC build and the canvas falls back to the platform's approximate renderer for
9
+ * every draft. The bridge needs a live component tree, and the only one that already exists,
10
+ * is at parity with this package, and speaks the protocol is <BcmsDraftBridge> from
11
+ * @bettercms-ai/next — which imports nothing from `next/*` (React and @bettercms-ai/* only),
12
+ * so it mounts here unchanged. Reused rather than reimplemented: one protocol, one twin to
13
+ * keep in step instead of two.
14
+ *
15
+ * RUNTIME: Preact + `compat`, verified by the P2-1 spike (see ../SPIKE.md — SSR and hydration
16
+ * of a heading + PortableText richtext + BcmsForm tree, both clean). The cost is real and worth
17
+ * naming: preact plus its compat aliases on the island (~10 kB of runtime, so `react` itself is
18
+ * never bundled even though @bettercms-ai/ui declares React peers), and `@bettercms-ai/next` as a
19
+ * dependency of this package.
20
+ *
21
+ * 🔴 THE CLASS NAMES ARE THE REACT GRAMMAR, NOT THIS PACKAGE'S. <BcmsBlocks> in @bettercms-ai/next
22
+ * emits `bcms-block bcms-block--text`, `bcms-button--primary`, `bcms-column`; BcmsBlocks.astro
23
+ * emits `bcms-text`, `bcms-btn-primary`, `bcms-cols`. Same markup shape, different hooks — a
24
+ * stylesheet written against the .astro grammar styles NOTHING here, and swapping a page from
25
+ * <BcmsBlocks> to <BcmsLiveBlocks> unstyles it. Either cover both spellings (the starters use
26
+ * `:is()` for exactly this) or read the React classes off @bettercms-ai/next's src/blocks.tsx.
27
+ *
28
+ * The consumer supplies the renderer, because an integration cannot be added from a component:
29
+ *
30
+ * // astro.config.mjs
31
+ * import preact from "@astrojs/preact";
32
+ * export default defineConfig({ integrations: [bettercms(), preact({ compat: true })] });
33
+ *
34
+ * ---
35
+ * import BcmsLiveBlocks from "@bettercms-ai/astro/components/BcmsLiveBlocks.astro";
36
+ * const page = await getPage(Astro.params.slug);
37
+ * ---
38
+ * <BcmsLiveBlocks slug={page.slug} blocks={page.blocks} forms={forms} />
39
+ *
40
+ * Standalone the island renders exactly what <BcmsBlocks> renders (inside the
41
+ * `data-bcms-canvas="bridge"` marker <div> that <BcmsDraftBridge> emits); the bridge only
42
+ * wakes when the editor frames the page and pushes a draft. This only helps pages whose body
43
+ * IS a block tree — a hand-written .astro layout is unreachable from here by construction.
44
+ */
45
+ import { BcmsDraftBridge } from "@bettercms-ai/next/draft-bridge";
46
+ import type { ContentBlock } from "@bettercms-ai/types";
47
+ import type { DeliveryComponent, DeliveryForm } from "@bettercms-ai/sdk";
48
+
49
+ interface Props {
50
+ /** The page's slug. A pushed draft is applied only when its slug matches. */
51
+ slug: string;
52
+ blocks: ContentBlock[];
53
+ forms?: DeliveryForm[];
54
+ components?: DeliveryComponent[];
55
+ turnstileSiteKey?: string;
56
+ apiBase?: string;
57
+ class?: string;
58
+ }
59
+
60
+ const { slug, blocks, forms, components, turnstileSiteKey, apiBase, class: className } = Astro.props;
61
+ ---
62
+
63
+ <BcmsDraftBridge
64
+ client:load
65
+ slug={slug}
66
+ blocks={blocks}
67
+ forms={forms}
68
+ components={components}
69
+ turnstileSiteKey={turnstileSiteKey}
70
+ apiBase={apiBase}
71
+ className={className}
72
+ />
@@ -1,23 +1,23 @@
1
- ---
2
- /**
3
- * <BcmsSearch> — an instant, typo-tolerant site-search box for Astro.
4
- *
5
- * Renders the search container and loads the hosted widget bundle, so the markup
6
- * and behaviour are identical to the universal HTML embed (DRY — one widget, one
7
- * place). No API key — `project` (from the dashboard's Site Search settings) is
8
- * the only scope. Styling is class-driven (`bcms-search-*`); the host can override.
9
- */
10
- import config from "bettercms:config";
11
-
12
- interface Props {
13
- project: string;
14
- placeholder?: string;
15
- class?: string;
16
- }
17
-
18
- const { project, placeholder = "Search…", class: className } = Astro.props;
19
- const widgetSrc = `${config.apiUrl}/widgets/search.js`;
20
- ---
21
-
22
- <div data-bcms-search data-project={project} data-placeholder={placeholder} class={className}></div>
23
- <script is:inline src={widgetSrc} async></script>
1
+ ---
2
+ /**
3
+ * <BcmsSearch> — an instant, typo-tolerant site-search box for Astro.
4
+ *
5
+ * Renders the search container and loads the hosted widget bundle, so the markup
6
+ * and behaviour are identical to the universal HTML embed (DRY — one widget, one
7
+ * place). No API key — `project` (from the dashboard's Site Search settings) is
8
+ * the only scope. Styling is class-driven (`bcms-search-*`); the host can override.
9
+ */
10
+ import config from "bettercms:config";
11
+
12
+ interface Props {
13
+ project: string;
14
+ placeholder?: string;
15
+ class?: string;
16
+ }
17
+
18
+ const { project, placeholder = "Search…", class: className } = Astro.props;
19
+ const widgetSrc = `${config.apiUrl}/widgets/search.js`;
20
+ ---
21
+
22
+ <div data-bcms-search data-project={project} data-placeholder={placeholder} class={className}></div>
23
+ <script is:inline src={widgetSrc} async></script>
@@ -1,26 +1,26 @@
1
- ---
2
- /**
3
- * <BcmsVisualEditing> — click-to-edit overlay for draft preview. Alt+Click any value
4
- * that carries stega provenance to open the matching dashboard editor on that field.
5
- * Render it only in draft mode.
6
- *
7
- * import config from "bettercms:config";
8
- * {Astro.locals.bcms?.draft && <BcmsVisualEditing studioUrl={config.studioUrl} />}
9
- *
10
- * The decoder is NOT inlined here. It is one script served from the dashboard origin
11
- * (`/bcms-visual-editing.js`), shared with @bettercms-ai/next and with plain-HTML sites
12
- * that install it by hand — a customer build is un-upgradable, so a copy baked into one
13
- * could never be fixed. Requires the read client to be in draft perspective with
14
- * `stega: true`; on a published build there is no provenance and the script is inert.
15
- */
16
- interface Props {
17
- studioUrl?: string;
18
- }
19
- // `bettercms.ai` is the studio origin — the one the dashboard's integration snippets hand out
20
- // as `PUBLIC_BCMS_STUDIO_URL`, and the default of @bettercms-ai/next's <VisualEditing>.
21
- // `demo.bettercms.ai` is a stale alias; `dashboard.bettercms.ai` resolves but 404s.
22
- const { studioUrl = "https://bettercms.ai" } = Astro.props;
23
- const src = `${studioUrl.replace(/\/+$/, "")}/bcms-visual-editing.js`;
24
- ---
25
-
26
- <script is:inline src={src} data-bcms-studio={studioUrl} defer></script>
1
+ ---
2
+ /**
3
+ * <BcmsVisualEditing> — click-to-edit overlay for draft preview. Alt+Click any value
4
+ * that carries stega provenance to open the matching dashboard editor on that field.
5
+ * Render it only in draft mode.
6
+ *
7
+ * import config from "bettercms:config";
8
+ * {Astro.locals.bcms?.draft && <BcmsVisualEditing studioUrl={config.studioUrl} />}
9
+ *
10
+ * The decoder is NOT inlined here. It is one script served from the dashboard origin
11
+ * (`/bcms-visual-editing.js`), shared with @bettercms-ai/next and with plain-HTML sites
12
+ * that install it by hand — a customer build is un-upgradable, so a copy baked into one
13
+ * could never be fixed. Requires the read client to be in draft perspective with
14
+ * `stega: true`; on a published build there is no provenance and the script is inert.
15
+ */
16
+ interface Props {
17
+ studioUrl?: string;
18
+ }
19
+ // `bettercms.ai` is the studio origin — the one the dashboard's integration snippets hand out
20
+ // as `PUBLIC_BCMS_STUDIO_URL`, and the default of @bettercms-ai/next's <VisualEditing>.
21
+ // `demo.bettercms.ai` is a stale alias; `dashboard.bettercms.ai` resolves but 404s.
22
+ const { studioUrl = "https://bettercms.ai" } = Astro.props;
23
+ const src = `${studioUrl.replace(/\/+$/, "")}/bcms-visual-editing.js`;
24
+ ---
25
+
26
+ <script is:inline src={src} data-bcms-studio={studioUrl} defer></script>
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/bindings.ts"],"sourcesContent":["/**\n * Visual Editor addresses for the Astro renderer.\n *\n * Identical contract to `@bettercms-ai/next`'s `bindings.ts`, and the SAME reason for existing:\n * inside a component instance the definition's block ids address nothing, because they are\n * shared by every instance of it. Stamping them produces an orphan on one placement and a\n * duplicate that drops BOTH on two.\n *\n * Kept as a `.ts` module rather than inlined in the `.astro` frontmatter so the logic is\n * testable without an Astro compiler, and so the two SDKs cannot drift from each other by\n * having one file edited and not the other.\n */\nimport type { BindingKind } from \"@bettercms-ai/types\";\nimport { formatPropsAttribute } from \"@bettercms-ai/types\";\n\nexport interface BindingCtx {\n /** Present only inside a component instance, where it is AUTHORITATIVE. */\n fieldAddresses?: Map<string, string>;\n}\n\n/** The address a write lands on, or null when none exists. */\nexport function addressFor(id: string | undefined, prop: string, ctx: BindingCtx): string | null {\n if (!id) return null;\n const key = `${id}__${prop}`;\n if (ctx.fieldAddresses) return ctx.fieldAddresses.get(key) ?? null;\n return key;\n}\n\n/**\n * Attributes for a value rendered as this element's own content. Astro omits `undefined`\n * attributes, so an empty object spreads to nothing.\n */\nexport function bind(\n id: string | undefined,\n prop: string,\n kind: BindingKind,\n ctx: BindingCtx,\n): Record<string, string> {\n const address = addressFor(id, prop, ctx);\n return address ? { \"data-bcms-field\": address, \"data-bcms-kind\": kind } : {};\n}\n\n/**\n * Attributes for values that live in an ATTRIBUTE of this element. `data-bcms-field` addresses\n * one element and an element cannot hold two, so these ride `data-bcms-props`:\n * `<address>|<kind>|<domAttribute>`, semicolon separated.\n *\n * An entry whose value is absent is dropped — `setFieldPath` refuses to create a final key, so\n * the address would be a write the editor rejects.\n */\nexport function bindProps(\n id: string | undefined,\n entries: readonly { prop: string; kind: BindingKind; attr: string; present: boolean }[],\n ctx: BindingCtx,\n): Record<string, string> {\n const resolved = entries.flatMap(({ prop, kind, attr, present }) => {\n if (!present) return [];\n const address = addressFor(id, prop, ctx);\n return address ? [{ address, kind, attr }] : [];\n });\n return resolved.length ? { \"data-bcms-props\": formatPropsAttribute(resolved) } : {};\n}\n"],"mappings":";AAaA,SAAS,4BAA4B;AAQ9B,SAAS,WAAW,IAAwB,MAAc,KAAgC;AAC/F,MAAI,CAAC,GAAI,QAAO;AAChB,QAAM,MAAM,GAAG,EAAE,KAAK,IAAI;AAC1B,MAAI,IAAI,eAAgB,QAAO,IAAI,eAAe,IAAI,GAAG,KAAK;AAC9D,SAAO;AACT;AAMO,SAAS,KACd,IACA,MACA,MACA,KACwB;AACxB,QAAM,UAAU,WAAW,IAAI,MAAM,GAAG;AACxC,SAAO,UAAU,EAAE,mBAAmB,SAAS,kBAAkB,KAAK,IAAI,CAAC;AAC7E;AAUO,SAAS,UACd,IACA,SACA,KACwB;AACxB,QAAM,WAAW,QAAQ,QAAQ,CAAC,EAAE,MAAM,MAAM,MAAM,QAAQ,MAAM;AAClE,QAAI,CAAC,QAAS,QAAO,CAAC;AACtB,UAAM,UAAU,WAAW,IAAI,MAAM,GAAG;AACxC,WAAO,UAAU,CAAC,EAAE,SAAS,MAAM,KAAK,CAAC,IAAI,CAAC;AAAA,EAChD,CAAC;AACD,SAAO,SAAS,SAAS,EAAE,mBAAmB,qBAAqB,QAAQ,EAAE,IAAI,CAAC;AACpF;","names":[]}
1
+ {"version":3,"sources":["../src/bindings.ts"],"sourcesContent":["/**\r\n * Visual Editor addresses for the Astro renderer.\r\n *\r\n * Identical contract to `@bettercms-ai/next`'s `bindings.ts`, and the SAME reason for existing:\r\n * inside a component instance the definition's block ids address nothing, because they are\r\n * shared by every instance of it. Stamping them produces an orphan on one placement and a\r\n * duplicate that drops BOTH on two.\r\n *\r\n * Kept as a `.ts` module rather than inlined in the `.astro` frontmatter so the logic is\r\n * testable without an Astro compiler, and so the two SDKs cannot drift from each other by\r\n * having one file edited and not the other.\r\n */\r\nimport type { BindingKind } from \"@bettercms-ai/types\";\r\nimport { formatPropsAttribute } from \"@bettercms-ai/types\";\r\n\r\nexport interface BindingCtx {\r\n /** Present only inside a component instance, where it is AUTHORITATIVE. */\r\n fieldAddresses?: Map<string, string>;\r\n}\r\n\r\n/** The address a write lands on, or null when none exists. */\r\nexport function addressFor(id: string | undefined, prop: string, ctx: BindingCtx): string | null {\r\n if (!id) return null;\r\n const key = `${id}__${prop}`;\r\n if (ctx.fieldAddresses) return ctx.fieldAddresses.get(key) ?? null;\r\n return key;\r\n}\r\n\r\n/**\r\n * Attributes for a value rendered as this element's own content. Astro omits `undefined`\r\n * attributes, so an empty object spreads to nothing.\r\n */\r\nexport function bind(\r\n id: string | undefined,\r\n prop: string,\r\n kind: BindingKind,\r\n ctx: BindingCtx,\r\n): Record<string, string> {\r\n const address = addressFor(id, prop, ctx);\r\n return address ? { \"data-bcms-field\": address, \"data-bcms-kind\": kind } : {};\r\n}\r\n\r\n/**\r\n * Attributes for values that live in an ATTRIBUTE of this element. `data-bcms-field` addresses\r\n * one element and an element cannot hold two, so these ride `data-bcms-props`:\r\n * `<address>|<kind>|<domAttribute>`, semicolon separated.\r\n *\r\n * An entry whose value is absent is dropped — `setFieldPath` refuses to create a final key, so\r\n * the address would be a write the editor rejects.\r\n */\r\nexport function bindProps(\r\n id: string | undefined,\r\n entries: readonly { prop: string; kind: BindingKind; attr: string; present: boolean }[],\r\n ctx: BindingCtx,\r\n): Record<string, string> {\r\n const resolved = entries.flatMap(({ prop, kind, attr, present }) => {\r\n if (!present) return [];\r\n const address = addressFor(id, prop, ctx);\r\n return address ? [{ address, kind, attr }] : [];\r\n });\r\n return resolved.length ? { \"data-bcms-props\": formatPropsAttribute(resolved) } : {};\r\n}\r\n"],"mappings":";AAaA,SAAS,4BAA4B;AAQ9B,SAAS,WAAW,IAAwB,MAAc,KAAgC;AAC/F,MAAI,CAAC,GAAI,QAAO;AAChB,QAAM,MAAM,GAAG,EAAE,KAAK,IAAI;AAC1B,MAAI,IAAI,eAAgB,QAAO,IAAI,eAAe,IAAI,GAAG,KAAK;AAC9D,SAAO;AACT;AAMO,SAAS,KACd,IACA,MACA,MACA,KACwB;AACxB,QAAM,UAAU,WAAW,IAAI,MAAM,GAAG;AACxC,SAAO,UAAU,EAAE,mBAAmB,SAAS,kBAAkB,KAAK,IAAI,CAAC;AAC7E;AAUO,SAAS,UACd,IACA,SACA,KACwB;AACxB,QAAM,WAAW,QAAQ,QAAQ,CAAC,EAAE,MAAM,MAAM,MAAM,QAAQ,MAAM;AAClE,QAAI,CAAC,QAAS,QAAO,CAAC;AACtB,UAAM,UAAU,WAAW,IAAI,MAAM,GAAG;AACxC,WAAO,UAAU,CAAC,EAAE,SAAS,MAAM,KAAK,CAAC,IAAI,CAAC;AAAA,EAChD,CAAC;AACD,SAAO,SAAS,SAAS,EAAE,mBAAmB,qBAAqB,QAAQ,EAAE,IAAI,CAAC;AACpF;","names":[]}
package/dist/index.d.ts CHANGED
@@ -129,7 +129,8 @@ declare function sectionColumnAttrs(block: SectionRootBlock | null | undefined):
129
129
 
130
130
  interface FieldAttrs {
131
131
  "data-bcms-field": string;
132
- "data-bcms-style": "";
132
+ /** The marker's revision: "2" promises `size` (zoom) too. Section roots keep "". */
133
+ "data-bcms-style": "2";
133
134
  /** `fieldStyleToCss(fieldStyles[path])` as a CSS string; absent when the field has no style. */
134
135
  style?: string;
135
136
  }
package/dist/index.js CHANGED
@@ -214,7 +214,7 @@ function fieldStyleOf(path, fieldStyles) {
214
214
  }
215
215
  function fieldAttrs(path, fieldStyles) {
216
216
  const style = fieldStyleCssText(fieldStyleOf(path, fieldStyles));
217
- return { "data-bcms-field": path, "data-bcms-style": "", ...style ? { style } : {} };
217
+ return { "data-bcms-field": path, "data-bcms-style": "2", ...style ? { style } : {} };
218
218
  }
219
219
 
220
220
  // src/index.ts