@bettercms-ai/next 0.23.0 → 0.25.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/README.md +361 -321
- package/dist/bettercms-snapshot.js +0 -0
- package/dist/bettercms-snapshot.js.map +1 -1
- package/dist/blocks.js.map +1 -1
- package/dist/draft-bridge.js.map +1 -1
- package/dist/form.js.map +1 -1
- package/dist/index.d.ts +143 -4
- package/dist/index.js +111 -0
- package/dist/index.js.map +1 -1
- package/dist/live.js.map +1 -1
- package/dist/phone-field.js.map +1 -1
- package/dist/search.js.map +1 -1
- package/dist/sections.d.ts +88 -0
- package/dist/sections.js +73 -0
- package/dist/sections.js.map +1 -0
- package/dist/visual-editing.js.map +1 -1
- package/package.json +99 -94
package/dist/phone-field.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/phone-field.tsx"],"sourcesContent":["\"use client\";\
|
|
1
|
+
{"version":3,"sources":["../src/phone-field.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * The phone control for a field with `ui.countryPicker`, in its own entry so that\n * libphonenumber's metadata NEVER reaches `dist/form.js`.\n *\n * This file is lazy-imported by `<BcmsForm>` and is listed in tsup's `external`, so the\n * dynamic import survives into the built output instead of being inlined. `splitting` is\n * false for this package, and with it false esbuild inlines a dynamic import outright —\n * every consumer of `@bettercms-ai/next/form` would then ship ~80KB of metadata whether or\n * not any of their fields enabled a picker. Nothing errors when that happens; `form.js`\n * just quietly grows. The dist greps in this package's tests are what keep it honest.\n *\n * Markup contract, shared with the vanilla renderers (see `DeliveryFormFieldUi` in\n * `@bettercms-ai/sdk`): a `.bcms-phone` wrapper holding a country `<select>` and a `tel`\n * input, submitting ONE E.164 string under the field's own key. Everything is unstyled and\n * class-driven, like the rest of this package.\n */\n\nimport { useMemo, useState } from \"react\";\nimport PhoneInputBase, {\n getCountries,\n getCountryCallingCode,\n isSupportedCountry,\n type Country,\n} from \"react-phone-number-input/input\";\n\n/** Region names for the option labels, built once. Falls back to the ISO code. */\nconst regionNames =\n typeof Intl !== \"undefined\" && typeof Intl.DisplayNames === \"function\"\n ? new Intl.DisplayNames([\"en\"], { type: \"region\" })\n : null;\n\nconst countryName = (iso: string): string => {\n try {\n return regionNames?.of(iso) ?? iso;\n } catch {\n return iso;\n }\n};\n\n/**\n * Every supported country, sorted by name.\n *\n * Each helper is wrapped in an arrow rather than passed as a bare callback: `.filter`/`.map`\n * hand the array INDEX as the second argument, which every libphonenumber helper reads as its\n * `metadata` parameter and throws on.\n */\nconst COUNTRIES: { iso: Country; name: string; code: string }[] = getCountries()\n .filter((iso) => isSupportedCountry(iso))\n .map((iso) => ({ iso, name: countryName(iso), code: getCountryCallingCode(iso) }))\n .sort((a, b) => a.name.localeCompare(b.name));\n\n/**\n * Collapse a value that is only a calling code to `\"\"`.\n *\n * With `withCountryCallingCode` the `+CC` prefix is permanently present in the input, so\n * clearing every national digit leaves `\"+91\"`. Posting that is worse than it looks: the\n * server's loose phone shape is 3–32 characters, so `\"+1\"` is REFUSED (an untouched optional\n * field starts failing) while `\"+91\"` is ACCEPTED and stored as a real-looking submission that\n * is nothing but a country code. The silent case is the dangerous one.\n */\nexport function normalizeOut(value: string | undefined, country: Country | null): string {\n if (!value) return \"\";\n const digits = value.replace(/\\D/g, \"\");\n if (!digits) return \"\";\n if (country && digits === getCountryCallingCode(country)) return \"\";\n return value;\n}\n\n/** The national part of `value`, so switching country can re-prefix it. */\nfunction nationalDigits(value: string, current: Country | null): string {\n const digits = value.replace(/\\D/g, \"\");\n if (!digits) return \"\";\n if (current) {\n const cc = getCountryCallingCode(current);\n if (digits.startsWith(cc)) return digits.slice(cc.length);\n }\n if (value.trim().startsWith(\"+\")) {\n const codes = [...new Set(COUNTRIES.map((c) => c.code))].sort((a, b) => b.length - a.length);\n const match = codes.find((cc) => digits.startsWith(cc));\n if (match) return digits.slice(match.length);\n }\n return digits;\n}\n\nexport interface PhoneFieldProps {\n name: string;\n value: string;\n onChange: (next: string) => void;\n placeholder?: string;\n required?: boolean;\n /** ISO 3166-1 alpha-2 the picker starts on. Absent means international entry. */\n defaultCountry?: string;\n}\n\nexport default function PhoneField({\n name,\n value,\n onChange,\n placeholder,\n required,\n defaultCountry,\n}: PhoneFieldProps) {\n const seed = useMemo(\n () => (defaultCountry && isSupportedCountry(defaultCountry as Country) ? (defaultCountry as Country) : null),\n [defaultCountry],\n );\n const [country, setCountry] = useState<Country | null>(seed);\n\n /**\n * Changing the library's `country` prop does NOT rewrite an existing value — it only\n * re-renders it — so the select and the number would disagree. Rewrite it here.\n */\n const pickCountry = (next: Country | null) => {\n const national = nationalDigits(value, country);\n onChange(national ? `+${next ? getCountryCallingCode(next) : \"\"}${national}` : \"\");\n setCountry(next);\n };\n\n // `country` + `international` + `withCountryCallingCode` is the library's only legal trio;\n // with no country, none of them may be passed at all.\n const modeProps = country ? ({ country, international: true, withCountryCallingCode: true } as const) : ({} as const);\n\n return (\n <span className=\"bcms-phone\">\n <select\n className=\"bcms-phone-country\"\n aria-label=\"Country code\"\n value={country ?? \"\"}\n onChange={(e) => pickCountry((e.target.value || null) as Country | null)}\n >\n <option value=\"\">International</option>\n {COUNTRIES.map((c) => (\n <option key={c.iso} value={c.iso}>\n {c.name} +{c.code}\n </option>\n ))}\n </select>\n <PhoneInputBase\n {...modeProps}\n name={name}\n value={value || undefined}\n onChange={(next?: string) => onChange(normalizeOut(next, country))}\n placeholder={placeholder}\n required={required}\n autoComplete=\"tel\"\n dir=\"ltr\"\n />\n </span>\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/search.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/search.tsx"],"sourcesContent":["\"use client\";\
|
|
1
|
+
{"version":3,"sources":["../src/search.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * <BcmsSearch> — an instant, typo-tolerant site-search box for React/Next.\n *\n * Debounced search-as-you-type against the public delivery search endpoint (via\n * `@bettercms-ai/sdk`'s `search`), rendering a dropdown of results under the input.\n * No API key — `project` (from the dashboard's Site Search settings) is the only\n * scope. All markup is unstyled and `bcms-search-*` class-driven, so the host\n * site owns the look. Mirrors <BcmsForm>.\n */\n\nimport { useEffect, useRef, useState, type ReactElement } from \"react\";\nimport { search, type SearchHit } from \"@bettercms-ai/sdk\";\n\nexport interface BcmsSearchProps {\n /** The project id (from Site Search settings). */\n project: string;\n /** API origin. Defaults to the SDK default (https://api.bettercms.ai). */\n apiBase?: string;\n /** Placeholder for the input. */\n placeholder?: string;\n /** Max results, 1–20 (default 8). */\n limit?: number;\n /** Debounce in ms (default 200). */\n debounceMs?: number;\n /** Class for the wrapper; inner elements use `bcms-search-*` classes. */\n className?: string;\n /** Called when a result is chosen; return false to suppress the default navigation. */\n onSelect?: (hit: SearchHit) => boolean | void;\n}\n\nexport function BcmsSearch({\n project,\n apiBase,\n placeholder = \"Search…\",\n limit,\n debounceMs = 200,\n className,\n onSelect,\n}: BcmsSearchProps): ReactElement {\n const [query, setQuery] = useState(\"\");\n const [hits, setHits] = useState<SearchHit[]>([]);\n const [open, setOpen] = useState(false);\n const [active, setActive] = useState(-1);\n const wrap = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const q = query.trim();\n if (q.length < 2) {\n setHits([]);\n setOpen(false);\n return;\n }\n const ctrl = new AbortController();\n const t = setTimeout(() => {\n search({ project, q, limit, baseUrl: apiBase, signal: ctrl.signal }).then((r) => {\n setHits(r);\n setActive(-1);\n setOpen(true);\n });\n }, debounceMs);\n return () => {\n clearTimeout(t);\n ctrl.abort();\n };\n }, [query, project, apiBase, limit, debounceMs]);\n\n // Close on outside click.\n useEffect(() => {\n const onClick = (e: MouseEvent) => {\n if (wrap.current && !wrap.current.contains(e.target as Node)) setOpen(false);\n };\n document.addEventListener(\"click\", onClick);\n return () => document.removeEventListener(\"click\", onClick);\n }, []);\n\n const choose = (hit: SearchHit) => {\n if (onSelect?.(hit) === false) return;\n window.location.href = hit.url;\n };\n\n const onKeyDown = (e: React.KeyboardEvent) => {\n if (!open || !hits.length) return;\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((i) => (i + 1) % hits.length);\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => (i - 1 + hits.length) % hits.length);\n } else if (e.key === \"Enter\" && active >= 0) {\n e.preventDefault();\n choose(hits[active]);\n } else if (e.key === \"Escape\") {\n setOpen(false);\n }\n };\n\n return (\n <div ref={wrap} className={`bcms-search${className ? ` ${className}` : \"\"}`} style={{ position: \"relative\" }}>\n <input\n type=\"search\"\n className=\"bcms-search-input\"\n placeholder={placeholder}\n aria-label=\"Search this site\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n onKeyDown={onKeyDown}\n onFocus={() => hits.length && setOpen(true)}\n />\n {open && (\n <div className=\"bcms-search-results\" role=\"listbox\">\n {hits.length === 0 ? (\n <div className=\"bcms-search-empty\">No results</div>\n ) : (\n hits.map((hit, i) => (\n <a\n key={`${hit.type}:${hit.slug}`}\n href={hit.url}\n role=\"option\"\n aria-selected={i === active}\n className={`bcms-search-hit${i === active ? \" is-active\" : \"\"}`}\n onClick={(e) => {\n e.preventDefault();\n choose(hit);\n }}\n >\n <span className=\"bcms-search-title\">{hit.title}</span>\n {hit.snippet && (\n <span\n className=\"bcms-search-snippet\"\n dangerouslySetInnerHTML={{ __html: hit.snippet }}\n />\n )}\n </a>\n ))\n )}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode, ReactElement } from 'react';
|
|
2
|
+
import { ContentBlock, DeliveryComponent } from '@bettercms-ai/types';
|
|
3
|
+
import { DeliveryForm } from '@bettercms-ai/sdk';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* C3/C4 — `sectionRootAttrs(block)`: the props a site's OWN section root spreads so the
|
|
7
|
+
* BetterCMS editor offers that section's spacing and style handles.
|
|
8
|
+
*
|
|
9
|
+
* <section className="hero" {...sectionRootAttrs(block)}>…</section>
|
|
10
|
+
*
|
|
11
|
+
* The editor shows those handles on a site-rendered page only when EVERY root the site stamps
|
|
12
|
+
* with `data-bcms-block` also carries `data-bcms-style`, which is the site's promise that "this
|
|
13
|
+
* root's inline style is `blockStyleToCss(block.style)`" — the same declarations `<BcmsBlocks>`
|
|
14
|
+
* puts on its own wrapper. The marker is present even when the style is EMPTY (then there is no
|
|
15
|
+
* `style` prop at all): an unstyled section is the one an author is about to give its first
|
|
16
|
+
* spacing edit. Twin: `sectionRootAttrs` in @bettercms-ai/astro (style as a string there).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface SectionRootAttrs {
|
|
20
|
+
"data-bcms-block"?: string;
|
|
21
|
+
"data-bcms-style": "";
|
|
22
|
+
/** `"mobile tablet"` when `style.hideOn` hides it there; the site ships `RESPONSIVE_CSS` for it. */
|
|
23
|
+
"data-bcms-hide"?: string;
|
|
24
|
+
/** `blockStyleToCss(block.style)`; absent when the block has no style. */
|
|
25
|
+
style?: CSSProperties;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* <BcmsSections> — a code-built page's sections, in the order its CMS page lists them.
|
|
30
|
+
*
|
|
31
|
+
* <BcmsSections {...sectionsFor("home")}>
|
|
32
|
+
* <BcmsSection bind="hero"><Hero /></BcmsSection>
|
|
33
|
+
* <BcmsSection bind="features">{(root) => <Features rootProps={root} />}</BcmsSection>
|
|
34
|
+
* </BcmsSections>
|
|
35
|
+
*
|
|
36
|
+
* Each section the page can draw is a `<BcmsSection>` child, named by its placement's bind: what is
|
|
37
|
+
* inside is the site's own and is rendered as written. The CMS page's block list decides the rest:
|
|
38
|
+
*
|
|
39
|
+
* - a block bound to a section renders it at the block's position, stamped (below);
|
|
40
|
+
* - a block with no bind (a section added from the library) renders through `<BcmsBlocks>`;
|
|
41
|
+
* - a section no block names: `code:<bind>` always renders where the source has it; a bare `<bind>`
|
|
42
|
+
* does too until the list names one of this page's sections, and from then on it is a removed one.
|
|
43
|
+
*
|
|
44
|
+
* With `blocks` null (a build without a snapshot) every child renders in source order, untouched.
|
|
45
|
+
*
|
|
46
|
+
* THE STAMP goes on the section's own root, never on a box of ours that changes layout:
|
|
47
|
+
* - one DOM element (`<section>`): `data-bcms-block`, `data-bcms-style` and the block's style are
|
|
48
|
+
* cloned onto it, its own style first;
|
|
49
|
+
* - a function child is handed those props and puts them on its root itself: the way a COMPONENT
|
|
50
|
+
* section takes its style, since a component may not pass unknown props on;
|
|
51
|
+
* - a component or several children: a box of ours carries the stamp. `display:contents` while
|
|
52
|
+
* the block has no style, so the page lays out as written; `display:flow-root` with the style on
|
|
53
|
+
* it once the block has one, because a box that draws nothing cannot show a padding. A styled
|
|
54
|
+
* box is ONE block box: in a flex or grid parent its children are one item, and their margins
|
|
55
|
+
* stop collapsing with the neighbours'. The function-child form has no box and no such limit.
|
|
56
|
+
*
|
|
57
|
+
* Server-safe (no hooks, no "use client"): a Server Component in the App Router, and plain React
|
|
58
|
+
* in the Pages Router, where `blocks` must come through page props so the client renders the same
|
|
59
|
+
* list (`sectionsStaticProps`). Twin: BcmsSections.astro in @bettercms-ai/astro; the order is
|
|
60
|
+
* `planSections` in @bettercms-ai/types.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/** What a function child receives: spread it on the section's root element. Empty when no block places the section. */
|
|
64
|
+
type SectionRootProps = Partial<SectionRootAttrs> & {
|
|
65
|
+
"data-bcms-sections"?: string;
|
|
66
|
+
};
|
|
67
|
+
interface BcmsSectionProps {
|
|
68
|
+
/** The placement's bind (`hero`), or `code:<bind>` for a section the CMS did not hold when the page was wrapped. */
|
|
69
|
+
bind: string;
|
|
70
|
+
children: ReactNode | ((root: SectionRootProps) => ReactNode);
|
|
71
|
+
}
|
|
72
|
+
/** One section of the page. Outside `<BcmsSections>` it renders its children as they are. */
|
|
73
|
+
declare function BcmsSection({ children }: BcmsSectionProps): ReactElement;
|
|
74
|
+
interface BcmsSectionsProps {
|
|
75
|
+
/** The CMS page's slug: `home`, `services`. */
|
|
76
|
+
page: string;
|
|
77
|
+
/** The page's block list: `sectionsFor(slug).blocks`, or a draft read. Null or absent renders the source order. */
|
|
78
|
+
blocks?: ContentBlock[] | null;
|
|
79
|
+
/** Component definitions and forms for the sections `<BcmsBlocks>` draws. */
|
|
80
|
+
components?: DeliveryComponent[];
|
|
81
|
+
forms?: DeliveryForm[];
|
|
82
|
+
/** The instant block windows are read against. Defaults to the render time. */
|
|
83
|
+
now?: Date;
|
|
84
|
+
children?: ReactNode;
|
|
85
|
+
}
|
|
86
|
+
declare function BcmsSections({ page, blocks, components, forms, now, children }: BcmsSectionsProps): ReactElement;
|
|
87
|
+
|
|
88
|
+
export { BcmsSection, type BcmsSectionProps, BcmsSections, type BcmsSectionsProps, type SectionRootProps };
|
package/dist/sections.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// src/sections.tsx
|
|
2
|
+
import { Children, Fragment, cloneElement, isValidElement } from "react";
|
|
3
|
+
import { CODE_SLOT_PREFIX, RESPONSIVE_CSS, planSections, usesHideOn, visibleBlocks } from "@bettercms-ai/types";
|
|
4
|
+
import { BcmsBlocks } from "./blocks.js";
|
|
5
|
+
|
|
6
|
+
// src/section-root.ts
|
|
7
|
+
import { blockStyleToColumnCss, blockStyleToCss, hideOnAttr } from "@bettercms-ai/types";
|
|
8
|
+
function sectionRootAttrs(block) {
|
|
9
|
+
const css = blockStyleToCss(block?.style ?? void 0);
|
|
10
|
+
const id = typeof block?.id === "string" && block.id !== "" ? block.id : void 0;
|
|
11
|
+
const hide = hideOnAttr(block?.style);
|
|
12
|
+
return {
|
|
13
|
+
...id !== void 0 ? { "data-bcms-block": id } : {},
|
|
14
|
+
"data-bcms-style": "",
|
|
15
|
+
...hide ? { "data-bcms-hide": hide } : {},
|
|
16
|
+
...css ? { style: css } : {}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/sections.tsx
|
|
21
|
+
import { Fragment as Fragment2, jsx, jsxs } from "react/jsx-runtime";
|
|
22
|
+
var contentOf = (children, root) => typeof children === "function" ? children(root) : children;
|
|
23
|
+
function BcmsSection({ children }) {
|
|
24
|
+
return /* @__PURE__ */ jsx(Fragment2, { children: contentOf(children, {}) });
|
|
25
|
+
}
|
|
26
|
+
function stamped(children, block, page) {
|
|
27
|
+
const attrs = sectionRootAttrs(block);
|
|
28
|
+
if (attrs["data-bcms-block"] === void 0) return contentOf(children, {});
|
|
29
|
+
const root = { ...attrs, "data-bcms-sections": page };
|
|
30
|
+
if (typeof children === "function") return children(root);
|
|
31
|
+
const only = Children.toArray(children);
|
|
32
|
+
const sole = only.length === 1 && isValidElement(only[0]) && typeof only[0].type === "string" ? only[0] : null;
|
|
33
|
+
if (sole) {
|
|
34
|
+
if (sole.props["data-bcms-block"] !== void 0) return sole;
|
|
35
|
+
const style = attrs.style ? { ...sole.props.style, ...attrs.style } : sole.props.style;
|
|
36
|
+
return cloneElement(sole, { ...root, style });
|
|
37
|
+
}
|
|
38
|
+
if (only.length === 0) return null;
|
|
39
|
+
const box = !attrs.style ? { display: "contents" } : attrs.style.display !== void 0 ? attrs.style : { display: "flow-root", ...attrs.style };
|
|
40
|
+
return /* @__PURE__ */ jsx("div", { style: box, "data-bcms-block": attrs["data-bcms-block"], "data-bcms-style": "", "data-bcms-hide": attrs["data-bcms-hide"], "data-bcms-box": "", "data-bcms-sections": page, children });
|
|
41
|
+
}
|
|
42
|
+
function BcmsSections({ page, blocks, components, forms, now, children }) {
|
|
43
|
+
const slots = /* @__PURE__ */ new Map();
|
|
44
|
+
Children.toArray(children).forEach((child, n) => {
|
|
45
|
+
const section = isValidElement(child) && child.type === BcmsSection ? child.props : null;
|
|
46
|
+
const name = section ? section.bind : `${CODE_SLOT_PREFIX}~${n}`;
|
|
47
|
+
if (!slots.has(name)) slots.set(name, section ? section.children : child);
|
|
48
|
+
});
|
|
49
|
+
const plan = planSections([...slots.keys()], Array.isArray(blocks) ? visibleBlocks(blocks, now ?? /* @__PURE__ */ new Date()) : null);
|
|
50
|
+
const parts = [];
|
|
51
|
+
for (const item of plan) {
|
|
52
|
+
if (item.slot !== null) {
|
|
53
|
+
const content = slots.get(item.slot);
|
|
54
|
+
parts.push({ node: item.block ? stamped(content, item.block, page) : contentOf(content, {}) });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const last = parts[parts.length - 1];
|
|
58
|
+
if (last && "blocks" in last) last.blocks.push(item.block);
|
|
59
|
+
else parts.push({ blocks: [item.block] });
|
|
60
|
+
}
|
|
61
|
+
const hides = usesHideOn(plan.filter((item) => item.slot !== null).map((item) => item.block));
|
|
62
|
+
return /* @__PURE__ */ jsxs(Fragment2, { children: [
|
|
63
|
+
hides && /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: RESPONSIVE_CSS } }),
|
|
64
|
+
parts.map(
|
|
65
|
+
(part, n) => "node" in part ? /* @__PURE__ */ jsx(Fragment, { children: part.node }, n) : /* @__PURE__ */ jsx(BcmsBlocks, { blocks: part.blocks, components, forms, now }, n)
|
|
66
|
+
)
|
|
67
|
+
] });
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
BcmsSection,
|
|
71
|
+
BcmsSections
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=sections.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sections.tsx","../src/section-root.ts"],"sourcesContent":["/**\n * <BcmsSections> — a code-built page's sections, in the order its CMS page lists them.\n *\n * <BcmsSections {...sectionsFor(\"home\")}>\n * <BcmsSection bind=\"hero\"><Hero /></BcmsSection>\n * <BcmsSection bind=\"features\">{(root) => <Features rootProps={root} />}</BcmsSection>\n * </BcmsSections>\n *\n * Each section the page can draw is a `<BcmsSection>` child, named by its placement's bind: what is\n * inside is the site's own and is rendered as written. The CMS page's block list decides the rest:\n *\n * - a block bound to a section renders it at the block's position, stamped (below);\n * - a block with no bind (a section added from the library) renders through `<BcmsBlocks>`;\n * - a section no block names: `code:<bind>` always renders where the source has it; a bare `<bind>`\n * does too until the list names one of this page's sections, and from then on it is a removed one.\n *\n * With `blocks` null (a build without a snapshot) every child renders in source order, untouched.\n *\n * THE STAMP goes on the section's own root, never on a box of ours that changes layout:\n * - one DOM element (`<section>`): `data-bcms-block`, `data-bcms-style` and the block's style are\n * cloned onto it, its own style first;\n * - a function child is handed those props and puts them on its root itself: the way a COMPONENT\n * section takes its style, since a component may not pass unknown props on;\n * - a component or several children: a box of ours carries the stamp. `display:contents` while\n * the block has no style, so the page lays out as written; `display:flow-root` with the style on\n * it once the block has one, because a box that draws nothing cannot show a padding. A styled\n * box is ONE block box: in a flex or grid parent its children are one item, and their margins\n * stop collapsing with the neighbours'. The function-child form has no box and no such limit.\n *\n * Server-safe (no hooks, no \"use client\"): a Server Component in the App Router, and plain React\n * in the Pages Router, where `blocks` must come through page props so the client renders the same\n * list (`sectionsStaticProps`). Twin: BcmsSections.astro in @bettercms-ai/astro; the order is\n * `planSections` in @bettercms-ai/types.\n */\nimport { Children, Fragment, cloneElement, isValidElement, type CSSProperties, type ReactElement, type ReactNode } from \"react\";\nimport { CODE_SLOT_PREFIX, RESPONSIVE_CSS, planSections, usesHideOn, visibleBlocks, type ContentBlock, type DeliveryComponent } from \"@bettercms-ai/types\";\nimport type { DeliveryForm } from \"@bettercms-ai/sdk\";\nimport { BcmsBlocks } from \"./blocks.js\";\nimport { sectionRootAttrs, type SectionRootAttrs } from \"./section-root.js\";\n\n/** What a function child receives: spread it on the section's root element. Empty when no block places the section. */\nexport type SectionRootProps = Partial<SectionRootAttrs> & { \"data-bcms-sections\"?: string };\n\nexport interface BcmsSectionProps {\n /** The placement's bind (`hero`), or `code:<bind>` for a section the CMS did not hold when the page was wrapped. */\n bind: string;\n children: ReactNode | ((root: SectionRootProps) => ReactNode);\n}\n\nconst contentOf = (children: BcmsSectionProps[\"children\"], root: SectionRootProps): ReactNode =>\n typeof children === \"function\" ? children(root) : children;\n\n/** One section of the page. Outside `<BcmsSections>` it renders its children as they are. */\nexport function BcmsSection({ children }: BcmsSectionProps): ReactElement {\n return <>{contentOf(children, {})}</>;\n}\n\nexport interface BcmsSectionsProps {\n /** The CMS page's slug: `home`, `services`. */\n page: string;\n /** The page's block list: `sectionsFor(slug).blocks`, or a draft read. Null or absent renders the source order. */\n blocks?: ContentBlock[] | null;\n /** Component definitions and forms for the sections `<BcmsBlocks>` draws. */\n components?: DeliveryComponent[];\n forms?: DeliveryForm[];\n /** The instant block windows are read against. Defaults to the render time. */\n now?: Date;\n children?: ReactNode;\n}\n\nfunction stamped(children: BcmsSectionProps[\"children\"], block: ContentBlock, page: string): ReactNode {\n const attrs = sectionRootAttrs(block);\n if (attrs[\"data-bcms-block\"] === undefined) return contentOf(children, {});\n const root: SectionRootProps = { ...attrs, \"data-bcms-sections\": page };\n if (typeof children === \"function\") return children(root);\n const only = Children.toArray(children);\n const sole = only.length === 1 && isValidElement(only[0]) && typeof only[0].type === \"string\" ? (only[0] as ReactElement<Record<string, unknown>>) : null;\n if (sole) {\n // A root that already stamps a block is the site's own work.\n if (sole.props[\"data-bcms-block\"] !== undefined) return sole;\n // The site's declarations first, the block's after, as the in-place painter orders them.\n const style = attrs.style ? { ...(sole.props.style as CSSProperties | undefined), ...attrs.style } : sole.props.style;\n return cloneElement(sole, { ...root, style });\n }\n if (only.length === 0) return null;\n // As `stampSectionRoot` in @bettercms-ai/astro writes it, attribute for attribute.\n const box: CSSProperties = !attrs.style ? { display: \"contents\" } : attrs.style.display !== undefined ? attrs.style : { display: \"flow-root\", ...attrs.style };\n return (\n <div style={box} data-bcms-block={attrs[\"data-bcms-block\"]} data-bcms-style=\"\" data-bcms-hide={attrs[\"data-bcms-hide\"]} data-bcms-box=\"\" data-bcms-sections={page}>\n {children}\n </div>\n );\n}\n\nexport function BcmsSections({ page, blocks, components, forms, now, children }: BcmsSectionsProps): ReactElement {\n // A child that is not a <BcmsSection> is the page's too: it keeps its place, like a `code:` section.\n const slots = new Map<string, BcmsSectionProps[\"children\"]>();\n Children.toArray(children).forEach((child, n) => {\n const section = isValidElement(child) && child.type === BcmsSection ? (child.props as BcmsSectionProps) : null;\n const name = section ? section.bind : `${CODE_SLOT_PREFIX}~${n}`;\n if (!slots.has(name)) slots.set(name, section ? section.children : child);\n });\n const plan = planSections([...slots.keys()], Array.isArray(blocks) ? visibleBlocks(blocks, now ?? new Date()) : null);\n\n // Consecutive CMS blocks share one <BcmsBlocks>, so its container wraps the run once.\n const parts: Array<{ node: ReactNode } | { blocks: ContentBlock[] }> = [];\n for (const item of plan) {\n if (item.slot !== null) {\n const content = slots.get(item.slot)!;\n parts.push({ node: item.block ? stamped(content, item.block, page) : contentOf(content, {}) });\n continue;\n }\n const last = parts[parts.length - 1];\n if (last && \"blocks\" in last) last.blocks.push(item.block);\n else parts.push({ blocks: [item.block] });\n }\n // Only when a placed section hides somewhere, so a page without `hideOn` renders exactly as before.\n const hides = usesHideOn(plan.filter((item) => item.slot !== null).map((item) => item.block));\n return (\n <>\n {hides && <style dangerouslySetInnerHTML={{ __html: RESPONSIVE_CSS }} />}\n {parts.map((part, n) =>\n \"node\" in part ? (\n <Fragment key={n}>{part.node}</Fragment>\n ) : (\n <BcmsBlocks key={n} blocks={part.blocks} components={components} forms={forms} now={now} />\n ),\n )}\n </>\n );\n}\n","/**\n * C3/C4 — `sectionRootAttrs(block)`: the props a site's OWN section root spreads so the\n * BetterCMS editor offers that section's spacing and style handles.\n *\n * <section className=\"hero\" {...sectionRootAttrs(block)}>…</section>\n *\n * The editor shows those handles on a site-rendered page only when EVERY root the site stamps\n * with `data-bcms-block` also carries `data-bcms-style`, which is the site's promise that \"this\n * root's inline style is `blockStyleToCss(block.style)`\" — the same declarations `<BcmsBlocks>`\n * puts on its own wrapper. The marker is present even when the style is EMPTY (then there is no\n * `style` prop at all): an unstyled section is the one an author is about to give its first\n * spacing edit. Twin: `sectionRootAttrs` in @bettercms-ai/astro (style as a string there).\n */\nimport type { CSSProperties } from \"react\";\nimport { blockStyleToColumnCss, blockStyleToCss, hideOnAttr, type BlockStyle } from \"@bettercms-ai/types\";\n\nexport interface SectionRootBlock {\n id?: string | null;\n style?: BlockStyle | Record<string, unknown> | null;\n}\n\nexport interface SectionRootAttrs {\n \"data-bcms-block\"?: string;\n \"data-bcms-style\": \"\";\n /** `\"mobile tablet\"` when `style.hideOn` hides it there; the site ships `RESPONSIVE_CSS` for it. */\n \"data-bcms-hide\"?: string;\n /** `blockStyleToCss(block.style)`; absent when the block has no style. */\n style?: CSSProperties;\n}\n\nexport function sectionRootAttrs(block: SectionRootBlock | null | undefined): SectionRootAttrs {\n const css = blockStyleToCss((block?.style ?? undefined) as BlockStyle | undefined);\n const id = typeof block?.id === \"string\" && block.id !== \"\" ? block.id : undefined;\n const hide = hideOnAttr(block?.style);\n return {\n ...(id !== undefined ? { \"data-bcms-block\": id } : {}),\n \"data-bcms-style\": \"\",\n ...(hide ? { \"data-bcms-hide\": hide } : {}),\n ...(css ? { style: css as CSSProperties } : {}),\n };\n}\n\nexport interface SectionColumnAttrs {\n \"data-bcms-column\": \"\";\n /** `blockStyleToColumnCss(block.style)`; absent when the section has no column. */\n style?: CSSProperties;\n}\n\n/**\n * C3 — `sectionColumnAttrs(block)`: the props a site's own INNER container spreads so the section's\n * content WIDTH ships (`style.contentWidth`, placed by `contentAlign`). The column BetterCMS draws as\n * `.bcms-block-inner`, never on the root (its background stays full-bleed); `data-bcms-column` is the\n * site's promise that this box's inline style is that column. Twin: @bettercms-ai/astro.\n */\nexport function sectionColumnAttrs(block: SectionRootBlock | null | undefined): SectionColumnAttrs {\n const css = blockStyleToColumnCss((block?.style ?? undefined) as BlockStyle | undefined);\n return { \"data-bcms-column\": \"\", ...(css ? { style: css as CSSProperties } : {}) };\n}\n"],"mappings":"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","names":["Fragment"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/visual-editing.tsx"],"sourcesContent":["\"use client\";\
|
|
1
|
+
{"version":3,"sources":["../src/visual-editing.tsx"],"sourcesContent":["\"use client\";\n/**\n * <VisualEditing> — click-to-edit overlay for draft preview. Alt+Click any value that\n * carries stega provenance to open the matching dashboard editor on that field.\n * Render it only in draft mode.\n *\n * import { VisualEditing } from \"@bettercms-ai/next/visual-editing\";\n * {isDraft && <VisualEditing studioUrl=\"https://bettercms.ai\" />}\n *\n * The decoder is NOT bundled here. It is one script served from the dashboard origin\n * (`/bcms-visual-editing.js`), shared with @bettercms-ai/astro and with plain-HTML sites\n * that install it by hand — a customer build is un-upgradable, so a copy baked into one\n * could never be fixed. Requires the read client to be in draft perspective with\n * `stega: true`; on a published build there is no provenance and the script is inert.\n */\nimport { useEffect } from \"react\";\n\nexport interface VisualEditingProps {\n /**\n * Studio origin, for the deep links and for the script itself. Defaults to\n * `https://bettercms.ai` — the same origin the dashboard hands out as\n * `NEXT_PUBLIC_BCMS_STUDIO_URL`, confirmed canonical 2026-08-16.\n * (`dashboard.bettercms.ai` resolves but 404s; `demo.bettercms.ai` still serves\n * but is a stale alias we no longer hand out.)\n */\n studioUrl?: string;\n}\n\nexport function VisualEditing({ studioUrl = \"https://bettercms.ai\" }: VisualEditingProps) {\n useEffect(() => {\n const src = `${studioUrl.replace(/\\/+$/, \"\")}/bcms-visual-editing.js`;\n if (document.querySelector(`script[src=\"${src}\"]`)) return;\n const el = document.createElement(\"script\");\n el.src = src;\n el.defer = true;\n el.dataset.bcmsStudio = studioUrl;\n document.head.appendChild(el);\n // No cleanup: removing the tag would not detach the listeners the script installed,\n // so a remove-and-remount would leave one set orphaned and add a second. The tag is\n // its own guard instead — this component is mounted once, in a draft-mode layout.\n }, [studioUrl]);\n return null;\n}\n"],"mappings":";;;;AAeA,SAAS,iBAAiB;AAanB,SAAS,cAAc,EAAE,YAAY,uBAAuB,GAAuB;AACxF,YAAU,MAAM;AACd,UAAM,MAAM,GAAG,UAAU,QAAQ,QAAQ,EAAE,CAAC;AAC5C,QAAI,SAAS,cAAc,eAAe,GAAG,IAAI,EAAG;AACpD,UAAM,KAAK,SAAS,cAAc,QAAQ;AAC1C,OAAG,MAAM;AACT,OAAG,QAAQ;AACX,OAAG,QAAQ,aAAa;AACxB,aAAS,KAAK,YAAY,EAAE;AAAA,EAI9B,GAAG,CAAC,SAAS,CAAC;AACd,SAAO;AACT;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,94 +1,99 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@bettercms-ai/next",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"description": "The BetterCMS adapter for Next.js — typed, cache-aware content reads (getEntry/listEntries), draft preview, build-time content snapshot, and llms.txt generation for the App Router. The /blocks, /form, /field, /search and /visual-editing entries are framework-free React and work in any React app (see the React+TS starter), which is why `next` is an OPTIONAL peer.",
|
|
6
|
-
"bin": {
|
|
7
|
-
"bettercms-snapshot": "./dist/bettercms-snapshot.js"
|
|
8
|
-
},
|
|
9
|
-
"main": "./dist/index.js",
|
|
10
|
-
"module": "./dist/index.js",
|
|
11
|
-
"types": "./dist/index.d.ts",
|
|
12
|
-
"sideEffects": false,
|
|
13
|
-
"exports": {
|
|
14
|
-
".": {
|
|
15
|
-
"types": "./dist/index.d.ts",
|
|
16
|
-
"import": "./dist/index.js",
|
|
17
|
-
"default": "./dist/index.js"
|
|
18
|
-
},
|
|
19
|
-
"./form": {
|
|
20
|
-
"types": "./dist/form.d.ts",
|
|
21
|
-
"import": "./dist/form.js",
|
|
22
|
-
"default": "./dist/form.js"
|
|
23
|
-
},
|
|
24
|
-
"./search": {
|
|
25
|
-
"types": "./dist/search.d.ts",
|
|
26
|
-
"import": "./dist/search.js",
|
|
27
|
-
"default": "./dist/search.js"
|
|
28
|
-
},
|
|
29
|
-
"./blocks": {
|
|
30
|
-
"types": "./dist/blocks.d.ts",
|
|
31
|
-
"import": "./dist/blocks.js",
|
|
32
|
-
"default": "./dist/blocks.js"
|
|
33
|
-
},
|
|
34
|
-
"./
|
|
35
|
-
"types": "./dist/
|
|
36
|
-
"import": "./dist/
|
|
37
|
-
"default": "./dist/
|
|
38
|
-
},
|
|
39
|
-
"./
|
|
40
|
-
"types": "./dist/
|
|
41
|
-
"import": "./dist/
|
|
42
|
-
"default": "./dist/
|
|
43
|
-
},
|
|
44
|
-
"./
|
|
45
|
-
"types": "./dist/
|
|
46
|
-
"import": "./dist/
|
|
47
|
-
"default": "./dist/
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"@bettercms-ai/
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@bettercms-ai/next",
|
|
3
|
+
"version": "0.25.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "The BetterCMS adapter for Next.js — typed, cache-aware content reads (getEntry/listEntries), draft preview, build-time content snapshot, and llms.txt generation for the App Router. The /blocks, /form, /field, /search and /visual-editing entries are framework-free React and work in any React app (see the React+TS starter), which is why `next` is an OPTIONAL peer.",
|
|
6
|
+
"bin": {
|
|
7
|
+
"bettercms-snapshot": "./dist/bettercms-snapshot.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./form": {
|
|
20
|
+
"types": "./dist/form.d.ts",
|
|
21
|
+
"import": "./dist/form.js",
|
|
22
|
+
"default": "./dist/form.js"
|
|
23
|
+
},
|
|
24
|
+
"./search": {
|
|
25
|
+
"types": "./dist/search.d.ts",
|
|
26
|
+
"import": "./dist/search.js",
|
|
27
|
+
"default": "./dist/search.js"
|
|
28
|
+
},
|
|
29
|
+
"./blocks": {
|
|
30
|
+
"types": "./dist/blocks.d.ts",
|
|
31
|
+
"import": "./dist/blocks.js",
|
|
32
|
+
"default": "./dist/blocks.js"
|
|
33
|
+
},
|
|
34
|
+
"./sections": {
|
|
35
|
+
"types": "./dist/sections.d.ts",
|
|
36
|
+
"import": "./dist/sections.js",
|
|
37
|
+
"default": "./dist/sections.js"
|
|
38
|
+
},
|
|
39
|
+
"./live": {
|
|
40
|
+
"types": "./dist/live.d.ts",
|
|
41
|
+
"import": "./dist/live.js",
|
|
42
|
+
"default": "./dist/live.js"
|
|
43
|
+
},
|
|
44
|
+
"./visual-editing": {
|
|
45
|
+
"types": "./dist/visual-editing.d.ts",
|
|
46
|
+
"import": "./dist/visual-editing.js",
|
|
47
|
+
"default": "./dist/visual-editing.js"
|
|
48
|
+
},
|
|
49
|
+
"./draft-bridge": {
|
|
50
|
+
"types": "./dist/draft-bridge.d.ts",
|
|
51
|
+
"import": "./dist/draft-bridge.js",
|
|
52
|
+
"default": "./dist/draft-bridge.js"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"files": [
|
|
56
|
+
"dist",
|
|
57
|
+
"README.md"
|
|
58
|
+
],
|
|
59
|
+
"scripts": {
|
|
60
|
+
"typecheck": "tsc --noEmit",
|
|
61
|
+
"build": "tsup",
|
|
62
|
+
"test": "vitest run",
|
|
63
|
+
"test:watch": "vitest"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@bettercms-ai/icons": "^0.3.0",
|
|
67
|
+
"@bettercms-ai/image-url": "^0.4.0",
|
|
68
|
+
"@bettercms-ai/richtext": "^0.8.2",
|
|
69
|
+
"@bettercms-ai/sdk": "^1.29.0",
|
|
70
|
+
"@bettercms-ai/types": "^1.42.0",
|
|
71
|
+
"@bettercms-ai/ui": "^0.15.1",
|
|
72
|
+
"react-phone-number-input": "^3.4.18"
|
|
73
|
+
},
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"next": ">=14",
|
|
76
|
+
"react": ">=18"
|
|
77
|
+
},
|
|
78
|
+
"peerDependenciesMeta": {
|
|
79
|
+
"next": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"react": {
|
|
83
|
+
"optional": true
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@types/node": "^20",
|
|
88
|
+
"@types/react": "^19.0.0",
|
|
89
|
+
"next": "^15.5.24",
|
|
90
|
+
"react": "^19.0.0",
|
|
91
|
+
"tsup": "^8.5.1",
|
|
92
|
+
"typescript": "^5",
|
|
93
|
+
"vitest": "^4.1.4"
|
|
94
|
+
},
|
|
95
|
+
"publishConfig": {
|
|
96
|
+
"registry": "https://registry.npmjs.org/",
|
|
97
|
+
"access": "public"
|
|
98
|
+
}
|
|
99
|
+
}
|