@bettercms-ai/next 0.28.0 → 0.30.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/field.tsx","../src/image.tsx","../src/button.tsx"],"sourcesContent":["/**\n * <BcmsField> — tag an entry field's rendered value with its identity so the\n * BetterCMS visual editor can bind click-to-edit deterministically (mirrors\n * Sanity's `data-sanity` / BaseHub's `data-basehub`). Entry-backed templates\n * hand-render field values (`f.title`, `f.body.html`), so the field `path` is\n * declared here by the author — the delivery payload carries no source-map.\n *\n * Server-safe (no hooks): renders in a Next Server Component. When the child is ONE\n * DOM element (`<h1>`, `<p>`), the attributes go on that element and nothing is added to\n * the page: `.hero > h1` and every animation target stay what the author wrote. Text,\n * several children, or a component (which may not pass unknown props on) are wrapped in a\n * `display:contents` span as before; pass `as` to carry the attributes on a real wrapper.\n * Twin: BcmsField.astro in @bettercms-ai/astro.\n *\n * `path` follows the editor grammar: `title` · `hero.title` · `columns[0].links[2].href`\n * (dotted keys and any number of `[n]` indices). For images, put the attributes on the\n * `<img>` itself (`kind=\"image\"` reflection patches `.src`), not on a wrapper:\n * <img src={imageValueUrl(f.cover, { w: 1200 })} data-bcms-field=\"cover\" data-bcms-kind=\"image\" />\n * `imageValueUrl` (re-exported from this package) applies the value's per-placement `crop` and\n * `focal`; `.url` alone would drop them. For a cover-fit box add\n * `style={{ objectPosition: imageValueObjectPosition(f.cover) ?? undefined }}`.\n */\nimport { Children, cloneElement, isValidElement, type ElementType, type ReactElement, type ReactNode } from \"react\";\n\nexport interface BcmsFieldProps {\n /** Field path in the editor grammar, e.g. `title` or `facts[2].label`. */\n path: string;\n /** Kind hint the editor's reflection branches on. Omit for plain text/number. */\n kind?: \"richtext\" | \"image\";\n /** Wrapper element. Without it: no wrapper around a single DOM element, a `display:contents` span around anything else. */\n as?: ElementType;\n children: ReactNode;\n}\n\nexport function BcmsField({ path, kind, as, children }: BcmsFieldProps) {\n const Tag = as ?? \"span\";\n const only = as ? [] : 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 // An element that already declares a field keeps its own address: wrapped, as before.\n if (sole && sole.props[\"data-bcms-field\"] === undefined && sole.props[\"data-bcms-layout-field\"] === undefined) {\n return cloneElement(sole, { \"data-bcms-field\": path, \"data-bcms-kind\": kind });\n }\n return (\n <Tag\n data-bcms-field={path}\n data-bcms-kind={kind}\n style={as ? undefined : { display: \"contents\" }}\n >\n {children}\n </Tag>\n );\n}\n","/**\n * <BcmsImage> — render an image field as one responsive `<img>` on the BetterCMS media transform\n * endpoint, declared so the visual editor binds it and a publish writes the new picture into it.\n * The same markup as the Astro SDK's <BcmsImage>.\n *\n * <BcmsImage path=\"hero.image\" src={page.hero.image} sizes=\"(min-width: 800px) 50vw, 100vw\" />\n *\n * `src` is an image VALUE (`{ url, altText, width?, focal?, crop? }`), a media url or an asset id.\n * The value's per-placement crop and focal point ride every url, and the focal point is also the\n * `object-position` (inert without `object-fit`). With `sizes` and no `width` the srcset is a WIDTH\n * ladder, never wider than the picture; with a pixel `width` it is 1x/2x. The value's `altText` is\n * the alt unless `alt` says otherwise. Any other prop reaches the `<img>` as written.\n *\n * A plain `<img>`, deliberately: `next/image` refuses a remote host the app has not allow-listed,\n * so an editor's replacement would break the build. Server-safe (no hooks).\n */\nimport imageUrlBuilder, {\n focalInCrop,\n normalizeImageCrop,\n normalizeImageFocal,\n objectPosition,\n type ImageFit,\n type ImageFormat,\n type ImageSource,\n} from \"@bettercms-ai/image-url\";\nimport type { CSSProperties, ImgHTMLAttributes } from \"react\";\n\n/** What delivery adds to an image value beside its url: the facts this component reads. */\ntype ImageFacts = { altText?: string | null; alt?: string | null; width?: number | null; height?: number | null; focalX?: number | null; focalY?: number | null };\n\nexport interface BcmsImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, \"src\" | \"srcSet\" | \"width\" | \"height\"> {\n src: ImageSource | (Exclude<ImageSource, string> & ImageFacts);\n /** The field this image renders, in the editor grammar (`cover`, `hero.image`). */\n path?: string;\n width?: number;\n height?: number;\n format?: ImageFormat;\n quality?: number;\n fit?: ImageFit;\n /** Focal point override, 0–1. Normally omitted: the value and the asset carry their own. */\n focalX?: number | null;\n focalY?: number | null;\n /** Media host for a `src` that is only an asset id. Defaults to the platform's. */\n baseUrl?: string;\n}\n\n/** The widths a `sizes` image offers: phone to desktop, and never an upscale of the picture. */\nconst LADDER = [480, 768, 1080, 1440, 1920];\n\nconst num = (v: unknown): number | undefined => (typeof v === \"number\" && Number.isFinite(v) ? v : undefined);\n\nexport function BcmsImage({\n src,\n path,\n alt,\n width,\n height,\n format = \"webp\",\n quality,\n fit,\n sizes,\n loading = \"lazy\",\n focalX,\n focalY,\n baseUrl,\n style,\n ...rest\n}: BcmsImageProps) {\n // An explicit prop wins; then the value's own `focal`; then the asset's focalX/focalY. The last\n // two are SOURCE-relative, so with a crop they are re-expressed inside it.\n const asset = (typeof src === \"object\" && src !== null ? src : {}) as Record<string, unknown>;\n const explicit = focalX != null || focalY != null;\n const stored =\n normalizeImageFocal(asset.focal) ??\n (num(asset.focalX) != null || num(asset.focalY) != null ? { x: num(asset.focalX) ?? 0.5, y: num(asset.focalY) ?? 0.5 } : null);\n const inWindow = stored ? focalInCrop(stored, normalizeImageCrop(asset.crop)) : null;\n const fx = explicit ? focalX : inWindow?.x;\n const fy = explicit ? focalY : inWindow?.y;\n const hasFocal = fx != null || fy != null;\n\n const builder = imageUrlBuilder({ baseUrl });\n const make = () => {\n let b = builder(src);\n if (width) b = b.width(width);\n if (height) b = b.height(height);\n if (format) b = b.format(format);\n if (quality) b = b.quality(quality);\n if (fit) b = b.fit(fit);\n if (hasFocal) b = b.focal(fx ?? 0.5, fy ?? 0.5);\n return b;\n };\n const src1x = make().url();\n const own = num(asset.width);\n const ladder = own ? [...new Set([...LADDER.filter((w) => w < own), Math.min(own, LADDER[LADDER.length - 1]!)])] : LADDER;\n const srcSet =\n sizes && !width ? ladder.map((w) => `${make().width(w).url()} ${w}w`).join(\", \") : `${src1x} 1x, ${make().dpr(2).url()} 2x`;\n // A pixel width is the author's size for THIS image, so it also goes on as a style (a site rule\n // like `img { width: 100% }` beats a bare attribute), capped at the column it sits in.\n const merged: CSSProperties = {\n ...(width ? { width: `${width}px`, maxWidth: \"100%\", height: \"auto\" } : {}),\n ...(hasFocal ? { objectPosition: objectPosition({ x: fx, y: fy }) } : {}),\n ...style,\n };\n\n return (\n <img\n src={src1x}\n srcSet={srcSet}\n alt={alt ?? (typeof asset.altText === \"string\" ? asset.altText : typeof asset.alt === \"string\" ? asset.alt : \"\")}\n data-bcms-field={path}\n data-bcms-kind={path ? \"image\" : undefined}\n width={width}\n height={height}\n sizes={sizes}\n loading={loading}\n style={Object.keys(merged).length > 0 ? merged : undefined}\n {...rest}\n />\n );\n}\n","/**\n * <BcmsButton> — render a `button` field ({ label, href, variant?, size?, arrow?, icon?,\n * newTab?, fullWidth? }) as one editable button, with the hooks the BetterCMS visual editor repaints and\n * in-place publishing writes. The same markup as the Astro SDK's <BcmsButton>:\n *\n * <a data-bcms-button=\"<path>\" data-variant data-size [data-full-width] href [target rel] data-bcms-props=\"<path>.href|url|href\">\n * [<svg class=\"bcms-btn-icon\">]<span data-bcms-field=\"<path>.label\">Label</span>[<svg class=\"bcms-btn-arrow\">]\n * </a>\n *\n * A value with `action: \"code\"` (no href: the site's script handles the click) renders the same\n * children in `<button type=\"button\" data-bcms-button=\"<path>\" data-variant data-size>`.\n *\n * Style it by `data-variant` and `data-size` in your own CSS. A full-width button also carries\n * `display:flex;width:100%;justify-content:center` inline, so it stretches with no rule of yours.\n * Server-safe (no hooks).\n *\n * <BcmsButton path=\"hero.cta\" value={page.hero.cta} className=\"btn\" />\n */\nimport { formatPropsAttribute, readButtonValue } from \"@bettercms-ai/types\";\nimport { Icon } from \"@bettercms-ai/icons/map\";\n\n/** A full-width button stretches across its column, label centred: the hosted renderer's `bcms-btn-full`. */\nconst FULL_WIDTH_STYLE = { display: \"flex\", width: \"100%\", justifyContent: \"center\" } as const;\n\nexport interface BcmsButtonProps {\n /** The field path in the editor grammar, e.g. `cta` or `hero.cta`. */\n path: string;\n /** The stored value. Missing keys render as the defaults (Primary, medium, no arrow, no icon). */\n value: unknown;\n className?: string;\n}\n\nexport function BcmsButton({ path, value, className }: BcmsButtonProps) {\n const b = readButtonValue(value);\n if (!b.label) return null;\n const content = (\n <>\n {/* Direct children of the root, as the editor and in-place publishing find them. */}\n {b.icon && <Icon symbol={b.icon} className=\"bcms-btn-icon\" aria-hidden focusable=\"false\" />}\n <span data-bcms-field={`${path}.label`} data-bcms-kind=\"text\">\n {b.label}\n </span>\n {b.arrow && <Icon symbol=\"arrow-right\" className=\"bcms-btn-arrow\" aria-hidden focusable=\"false\" />}\n </>\n );\n // `action: \"code\"`: your own handler owns the click (delegate on `[data-bcms-button]`), so no href.\n if (b.action === \"code\") {\n return (\n <button type=\"button\" className={className} data-bcms-button={path} data-variant={b.variant} data-size={b.size} {...(b.fullWidth ? { \"data-full-width\": \"\", style: FULL_WIDTH_STYLE } : {})}>\n {content}\n </button>\n );\n }\n return (\n <a\n className={className}\n href={b.href || \"#\"}\n data-bcms-button={path}\n data-variant={b.variant}\n data-size={b.size}\n {...(b.fullWidth ? { \"data-full-width\": \"\", style: FULL_WIDTH_STYLE } : {})}\n data-bcms-props={formatPropsAttribute([{ address: `${path}.href`, kind: \"url\", attr: \"href\" }])}\n {...(b.newTab ? { target: \"_blank\", rel: \"noopener noreferrer\" } : {})}\n >\n {content}\n </a>\n );\n}\n"],"mappings":"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","names":["jsx","jsx"]}
package/dist/form.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactElement } from 'react';
2
- import { DeliveryForm } from '@bettercms-ai/sdk';
2
+ import { DeliveryForm } from '@bettercms-ai/sdk/read';
3
3
 
4
4
  /**
5
5
  * <BcmsForm> — render a BetterCMS form natively in React, the headless way.
package/dist/form.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  formInitialValues,
10
10
  asStringArray,
11
11
  toggleOption
12
- } from "@bettercms-ai/sdk";
12
+ } from "@bettercms-ai/sdk/read";
13
13
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
14
14
  var TURNSTILE_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js";
15
15
  var PhoneField = lazy(() => import("./phone-field.js"));
package/dist/form.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/form.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * <BcmsForm> — render a BetterCMS form natively in React, the headless way.\n *\n * Pass a form straight from your content snapshot (`readSnapshot().forms[n]`) or\n * the delivery API. It renders real inputs (no iframe), handles conditional\n * `showIf` fields, URL-query prefill, the honeypot, an optional Turnstile widget,\n * and submits via `@bettercms-ai/sdk`'s `submitForm`. All visible markup is\n * unstyled and class-driven, so the host site owns the look.\n */\n\nimport { lazy, Suspense, useEffect, useMemo, useRef, useState, type FormEvent, type ReactElement } from \"react\";\nimport {\n submitForm,\n shouldShowField,\n formInitialValues,\n asStringArray,\n toggleOption,\n type DeliveryForm,\n type DeliveryFormField,\n type FormValue,\n type FormValues,\n type FormSubmitError,\n} from \"@bettercms-ai/sdk\";\n\nconst TURNSTILE_SRC = \"https://challenges.cloudflare.com/turnstile/v0/api.js\";\n\n/**\n * The country-picker phone control, loaded only when a form actually has one.\n *\n * It carries libphonenumber's metadata (~80KB), which no form without a picker should pay\n * for. The import specifier must survive into `dist/form.js` rather than being inlined —\n * see the `external` entry in tsup.config.ts and the dist greps in the tests.\n */\nconst PhoneField = lazy(() => import(\"./phone-field.js\"));\n\n// Field type → native <input type>. Other types render their own element.\nconst INPUT_TYPES: Partial<Record<DeliveryFormField[\"type\"], string>> = {\n text: \"text\",\n email: \"email\",\n phone: \"tel\",\n url: \"url\",\n number: \"number\",\n date: \"date\",\n};\n\nexport interface BcmsFormProps {\n /** A form from your content snapshot or the delivery API. */\n form: DeliveryForm;\n /** API origin. Defaults to the SDK default (https://api.bettercms.ai). */\n apiBase?: string;\n /** Turnstile site key — needed to render the widget when `form.turnstileEnabled`. */\n turnstileSiteKey?: string;\n /** Called with the new submission id after a successful submit (before any redirect). */\n onSubmitted?: (submissionId: string) => void;\n /** Class for the <form> element; field wrappers/status use `bcms-*` classes. */\n className?: string;\n}\n\ntype Status = { state: \"idle\" | \"submitting\" | \"error\" | \"success\"; message?: string };\n\nfunction readPrefill(): Record<string, string> {\n if (typeof window === \"undefined\") return {};\n const out: Record<string, string> = {};\n new URLSearchParams(window.location.search).forEach((v, k) => {\n out[k] = v;\n });\n return out;\n}\n\ntype Page = { label: string | null; key: string | null; fields: DeliveryFormField[] };\n\n/**\n * Cut the flat field list into pages at its `step` dividers, or null when it has none. Fields\n * before the first divider are page 1; an empty page is dropped. The hosted renderer and\n * BcmsForm.astro carry the same rule: each package publishes on its own, so none of them can\n * count on a newer shared helper arriving with it.\n */\nfunction splitSteps(fields: DeliveryFormField[]): Page[] | null {\n if (!fields.some((f) => f.type === \"step\")) return null;\n const pages: Page[] = [];\n let cur: Page = { label: null, key: null, fields: [] };\n for (const f of fields) {\n if (f.type !== \"step\") {\n cur.fields.push(f);\n continue;\n }\n if (cur.fields.length) pages.push(cur);\n cur = { label: f.label, key: f.key, fields: [] };\n }\n if (cur.fields.length) pages.push(cur);\n return pages;\n}\n\nfunction resetTurnstile(): void {\n const t = (globalThis as { turnstile?: { reset: () => void } }).turnstile;\n if (t) {\n try {\n t.reset();\n } catch {\n /* widget not mounted yet */\n }\n }\n}\n\nexport function BcmsForm({\n form,\n apiBase,\n turnstileSiteKey,\n onSubmitted,\n className,\n}: BcmsFormProps): ReactElement {\n const showTurnstile = Boolean(form.turnstileEnabled && turnstileSiteKey);\n const [values, setValues] = useState<FormValues>(() => {\n const init = formInitialValues(form, readPrefill());\n if (form.honeypotField) init[form.honeypotField] = \"\";\n return init;\n });\n const [status, setStatus] = useState<Status>({ state: \"idle\" });\n const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});\n\n // Steps. Until the component has mounted every page is shown, so server-rendered output (and a\n // visitor without JavaScript) gets one long form; `enhanced` then turns it into pages.\n const pages = useMemo(() => splitSteps(form.fields), [form.fields]);\n const [enhanced, setEnhanced] = useState(false);\n const [step, setStep] = useState(0);\n const pageRefs = useRef<(HTMLFieldSetElement | null)[]>([]);\n const headingRefs = useRef<(HTMLHeadingElement | null)[]>([]);\n const moveFocus = useRef(false);\n const last = pages ? pages.length - 1 : 0;\n const paged = Boolean(pages && enhanced);\n useEffect(() => setEnhanced(true), []);\n // Focus follows the visitor to the new page's heading, never on first paint.\n useEffect(() => {\n if (!moveFocus.current) return;\n moveFocus.current = false;\n headingRefs.current[step]?.focus();\n }, [step]);\n\n // The browser's own validity, for ONE page: the first control that fails reports its message.\n function pageInvalid(i: number): boolean {\n const controls = pageRefs.current[i]?.querySelectorAll<HTMLInputElement>(\"input,select,textarea\") ?? [];\n for (const c of controls) {\n if (!c.checkValidity()) {\n c.reportValidity();\n return true;\n }\n }\n return false;\n }\n function goTo(i: number): void {\n moveFocus.current = true;\n setStep(i);\n }\n function next(): void {\n if (!pageInvalid(step)) goTo(step + 1);\n }\n\n // Cloudflare auto-renders any `.cf-turnstile` element once its script is present.\n useEffect(() => {\n if (!showTurnstile || document.querySelector(`script[src=\"${TURNSTILE_SRC}\"]`)) return;\n const script = document.createElement(\"script\");\n script.src = TURNSTILE_SRC;\n script.async = true;\n script.defer = true;\n document.head.appendChild(script);\n }, [showTurnstile]);\n\n const visibleFields = useMemo(\n () => form.fields.filter((f) => f.type !== \"hidden\" && f.type !== \"step\" && shouldShowField(f, values)),\n [form.fields, values],\n );\n\n const setValue = (key: string, value: FormValue) =>\n setValues((prev) => ({ ...prev, [key]: value }));\n\n async function handleSubmit(e: FormEvent<HTMLFormElement>): Promise<void> {\n e.preventDefault();\n // Enter inside a field before the last page moves on rather than submitting half a form.\n if (paged && step < last) return next();\n setStatus({ state: \"submitting\" });\n setFieldErrors({});\n\n const tokenEl = e.currentTarget.querySelector<HTMLInputElement>(\n '[name=\"cf-turnstile-response\"]',\n );\n const turnstileToken = tokenEl?.value || undefined;\n\n // Drop conditionally-hidden fields so a hidden required field can't block submit;\n // keep hidden-type fields and the honeypot (the API classifies bots on it).\n const data: FormValues = {};\n for (const field of form.fields) {\n if (field.type === \"step\") continue; // a divider is never an answer\n if (field.type === \"hidden\" || shouldShowField(field, values)) {\n data[field.key] = values[field.key];\n }\n }\n if (form.honeypotField) data[form.honeypotField] = values[form.honeypotField] ?? \"\";\n\n try {\n const { id } = await submitForm({ formId: form.id, data, turnstileToken, baseUrl: apiBase });\n onSubmitted?.(id);\n if (form.redirectUrl) {\n window.location.href = form.redirectUrl;\n return;\n }\n const reset = formInitialValues(form);\n if (form.honeypotField) reset[form.honeypotField] = \"\";\n setValues(reset);\n setStep(0);\n setStatus({\n state: \"success\",\n message: form.successMessage ?? \"Thanks! Your submission was received.\",\n });\n resetTurnstile();\n } catch (err) {\n const e2 = err as FormSubmitError;\n if (e2.fieldErrors) {\n setFieldErrors(e2.fieldErrors);\n // The server found a problem on a page the visitor has left: take them back to it.\n const bad = pages?.findIndex((p) => p.fields.some((f) => e2.fieldErrors?.[f.key]));\n if (bad !== undefined && bad >= 0) goTo(bad);\n }\n setStatus({ state: \"error\", message: e2.message || \"Something went wrong. Please try again.\" });\n resetTurnstile();\n }\n }\n\n return (\n // `data-bcms-form` carries the id so the Visual Editor can find THIS form in the\n // preview frame and patch its copy live. Valued rather than bare: the runtime\n // selectors are `form[data-bcms-form]`, which a valued attribute still matches.\n <form className={className} onSubmit={handleSubmit} noValidate data-bcms-form={form.id}>\n {pages\n ? pages.map((page, i) => (\n <fieldset\n key={page.key ?? \"first\"}\n ref={(el) => {\n pageRefs.current[i] = el;\n }}\n className=\"bcms-step\"\n data-bcms-step={i + 1}\n hidden={paged && i !== step}\n aria-labelledby={`bcms-step-${form.id}-${i + 1}`}\n >\n <h3\n id={`bcms-step-${form.id}-${i + 1}`}\n className=\"bcms-step-title\"\n tabIndex={-1}\n ref={(el) => {\n headingRefs.current[i] = el;\n }}\n data-bcms-form-field={page.key ?? undefined}\n >\n <span className=\"bcms-step-count\">\n Step {i + 1} of {pages.length}\n </span>\n {page.label ? (\n <>\n {\" \"}\n <span data-bcms-form-part=\"label\">{page.label}</span>\n </>\n ) : null}\n </h3>\n {visibleFields\n .filter((f) => page.fields.includes(f))\n .map((field) => (\n <Field\n key={field.key}\n field={field}\n value={values[field.key]}\n error={fieldErrors[field.key]}\n onChange={(v) => setValue(field.key, v)}\n />\n ))}\n {paged ? (\n <div className=\"bcms-step-nav\">\n {i > 0 ? (\n <button type=\"button\" data-bcms-step-back onClick={() => goTo(i - 1)}>\n Back\n </button>\n ) : null}\n {i < last ? (\n <button type=\"button\" data-bcms-step-next onClick={next}>\n Next\n </button>\n ) : null}\n </div>\n ) : null}\n </fieldset>\n ))\n : visibleFields.map((field) => (\n <Field\n key={field.key}\n field={field}\n value={values[field.key]}\n error={fieldErrors[field.key]}\n onChange={(v) => setValue(field.key, v)}\n />\n ))}\n\n {form.honeypotField ? (\n <span aria-hidden=\"true\" style={{ position: \"absolute\", left: \"-9999px\" }}>\n <input\n type=\"text\"\n name={form.honeypotField}\n tabIndex={-1}\n autoComplete=\"off\"\n value={String(values[form.honeypotField] ?? \"\")}\n onChange={(e) => setValue(form.honeypotField as string, e.target.value)}\n />\n </span>\n ) : null}\n\n {showTurnstile ? <div className=\"cf-turnstile\" data-sitekey={turnstileSiteKey} /> : null}\n\n <button\n type=\"submit\"\n hidden={paged && step < last}\n disabled={status.state === \"submitting\"}\n data-bcms-form-part=\"submit\"\n >\n {status.state === \"submitting\" ? \"Submitting…\" : form.submitLabel ?? \"Submit\"}\n </button>\n\n {status.message ? (\n <p\n className={`bcms-form-status bcms-form-status--${status.state}`}\n role=\"status\"\n aria-live=\"polite\"\n >\n {status.message}\n </p>\n ) : null}\n </form>\n );\n}\n\ninterface FieldProps {\n field: DeliveryFormField;\n value: FormValue;\n error?: string;\n onChange: (value: FormValue) => void;\n}\n\nfunction Field({ field, value, error, onChange }: FieldProps): ReactElement {\n const required = field.required;\n const errorNode = error ? (\n <span className=\"bcms-field-error\" role=\"alert\">\n {error}\n </span>\n ) : null;\n // Every author-editable string carries a `data-bcms-form-part` so the Visual Editor can\n // repaint it live. A string the dock can edit but cannot mark shows stale on the canvas.\n const helpNode = field.helpText ? (\n <span className=\"bcms-field-help\" data-bcms-form-part=\"help\">\n {field.helpText}\n </span>\n ) : null;\n\n if (field.type === \"checkbox\" || field.type === \"consent\") {\n return (\n <label\n className=\"bcms-field bcms-field--checkbox\"\n data-field={field.key}\n data-bcms-form-field={field.key}\n >\n <input\n type=\"checkbox\"\n name={field.key}\n required={required}\n checked={Boolean(value)}\n onChange={(e) => onChange(e.target.checked)}\n />{\" \"}\n <span data-bcms-form-part=\"label\">\n {field.label}\n {required ? \" *\" : \"\"}\n </span>\n {helpNode}\n {errorNode}\n </label>\n );\n }\n\n // Pick-one and pick-many are groups of controls, not a single element, so they\n // return their own <fieldset> rather than flowing into the label+control shape\n // below. Without these two branches both fell through to `<input type=\"text\">`.\n if (field.type === \"radio\" || field.type === \"checkboxes\") {\n const multi = field.type === \"checkboxes\";\n const selected = asStringArray(value);\n return (\n <fieldset\n className={`bcms-field bcms-field--${multi ? \"checkboxes\" : \"radio\"}`}\n data-field={field.key}\n data-bcms-form-field={field.key}\n >\n <legend data-bcms-form-part=\"label\">\n {field.label}\n {required ? \" *\" : \"\"}\n </legend>\n {(field.options ?? []).map((opt) => (\n <label key={opt} className=\"bcms-option\">\n <input\n type={multi ? \"checkbox\" : \"radio\"}\n name={field.key}\n value={opt}\n // `required` on every radio in a group is the native way to demand one\n // choice. On checkboxes it would demand ALL of them, so it is omitted —\n // the API enforces non-empty instead.\n required={!multi && required}\n checked={multi ? selected.includes(opt) : value === opt}\n onChange={(e) =>\n onChange(multi ? toggleOption(value, opt, e.target.checked) : opt)\n }\n />{\" \"}\n <span data-bcms-form-part=\"option\">{opt}</span>\n </label>\n ))}\n {helpNode}\n {errorNode}\n </fieldset>\n );\n }\n\n const str = String(value ?? \"\");\n let control: ReactElement;\n if (field.type === \"textarea\") {\n control = (\n <textarea\n name={field.key}\n placeholder={field.placeholder}\n required={required}\n value={str}\n onChange={(e) => onChange(e.target.value)}\n />\n );\n } else if (field.type === \"select\") {\n control = (\n <select name={field.key} required={required} value={str} onChange={(e) => onChange(e.target.value)}>\n <option value=\"\" disabled>\n Choose…\n </option>\n {(field.options ?? []).map((opt) => (\n <option key={opt} value={opt} data-bcms-form-part=\"option\">\n {opt}\n </option>\n ))}\n </select>\n );\n } else if (field.type === \"phone\" && field.ui?.countryPicker) {\n // The fallback is a usable tel input inside the SAME wrapper, so the host's `.bcms-phone`\n // rules apply before and after the chunk lands and the swap is not a layout surprise.\n control = (\n <Suspense\n fallback={\n <span className=\"bcms-phone\">\n <input type=\"tel\" name={field.key} placeholder={field.placeholder} required={required} value={str} onChange={(e) => onChange(e.target.value)} autoComplete=\"tel\" dir=\"ltr\" />\n </span>\n }\n >\n <PhoneField\n name={field.key}\n value={str}\n onChange={onChange}\n placeholder={field.placeholder}\n required={required}\n defaultCountry={field.ui.defaultCountry}\n />\n </Suspense>\n );\n } else {\n control = (\n <input\n type={INPUT_TYPES[field.type] ?? \"text\"}\n name={field.key}\n placeholder={field.placeholder}\n required={required}\n value={str}\n onChange={(e) => onChange(e.target.value)}\n />\n );\n }\n\n return (\n <p className=\"bcms-field\" data-field={field.key} data-bcms-form-field={field.key}>\n <label data-bcms-form-part=\"label\">\n {field.label}\n {required ? \" *\" : \"\"}\n </label>\n <br />\n {control}\n {helpNode}\n {errorNode}\n </p>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/form.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * <BcmsForm> — render a BetterCMS form natively in React, the headless way.\n *\n * Pass a form straight from your content snapshot (`readSnapshot().forms[n]`) or\n * the delivery API. It renders real inputs (no iframe), handles conditional\n * `showIf` fields, URL-query prefill, the honeypot, an optional Turnstile widget,\n * and submits via `@bettercms-ai/sdk`'s `submitForm`. All visible markup is\n * unstyled and class-driven, so the host site owns the look.\n */\n\nimport { lazy, Suspense, useEffect, useMemo, useRef, useState, type FormEvent, type ReactElement } from \"react\";\nimport {\n submitForm,\n shouldShowField,\n formInitialValues,\n asStringArray,\n toggleOption,\n type DeliveryForm,\n type DeliveryFormField,\n type FormValue,\n type FormValues,\n type FormSubmitError,\n} from \"@bettercms-ai/sdk/read\";\n\nconst TURNSTILE_SRC = \"https://challenges.cloudflare.com/turnstile/v0/api.js\";\n\n/**\n * The country-picker phone control, loaded only when a form actually has one.\n *\n * It carries libphonenumber's metadata (~80KB), which no form without a picker should pay\n * for. The import specifier must survive into `dist/form.js` rather than being inlined —\n * see the `external` entry in tsup.config.ts and the dist greps in the tests.\n */\nconst PhoneField = lazy(() => import(\"./phone-field.js\"));\n\n// Field type → native <input type>. Other types render their own element.\nconst INPUT_TYPES: Partial<Record<DeliveryFormField[\"type\"], string>> = {\n text: \"text\",\n email: \"email\",\n phone: \"tel\",\n url: \"url\",\n number: \"number\",\n date: \"date\",\n};\n\nexport interface BcmsFormProps {\n /** A form from your content snapshot or the delivery API. */\n form: DeliveryForm;\n /** API origin. Defaults to the SDK default (https://api.bettercms.ai). */\n apiBase?: string;\n /** Turnstile site key — needed to render the widget when `form.turnstileEnabled`. */\n turnstileSiteKey?: string;\n /** Called with the new submission id after a successful submit (before any redirect). */\n onSubmitted?: (submissionId: string) => void;\n /** Class for the <form> element; field wrappers/status use `bcms-*` classes. */\n className?: string;\n}\n\ntype Status = { state: \"idle\" | \"submitting\" | \"error\" | \"success\"; message?: string };\n\nfunction readPrefill(): Record<string, string> {\n if (typeof window === \"undefined\") return {};\n const out: Record<string, string> = {};\n new URLSearchParams(window.location.search).forEach((v, k) => {\n out[k] = v;\n });\n return out;\n}\n\ntype Page = { label: string | null; key: string | null; fields: DeliveryFormField[] };\n\n/**\n * Cut the flat field list into pages at its `step` dividers, or null when it has none. Fields\n * before the first divider are page 1; an empty page is dropped. The hosted renderer and\n * BcmsForm.astro carry the same rule: each package publishes on its own, so none of them can\n * count on a newer shared helper arriving with it.\n */\nfunction splitSteps(fields: DeliveryFormField[]): Page[] | null {\n if (!fields.some((f) => f.type === \"step\")) return null;\n const pages: Page[] = [];\n let cur: Page = { label: null, key: null, fields: [] };\n for (const f of fields) {\n if (f.type !== \"step\") {\n cur.fields.push(f);\n continue;\n }\n if (cur.fields.length) pages.push(cur);\n cur = { label: f.label, key: f.key, fields: [] };\n }\n if (cur.fields.length) pages.push(cur);\n return pages;\n}\n\nfunction resetTurnstile(): void {\n const t = (globalThis as { turnstile?: { reset: () => void } }).turnstile;\n if (t) {\n try {\n t.reset();\n } catch {\n /* widget not mounted yet */\n }\n }\n}\n\nexport function BcmsForm({\n form,\n apiBase,\n turnstileSiteKey,\n onSubmitted,\n className,\n}: BcmsFormProps): ReactElement {\n const showTurnstile = Boolean(form.turnstileEnabled && turnstileSiteKey);\n const [values, setValues] = useState<FormValues>(() => {\n const init = formInitialValues(form, readPrefill());\n if (form.honeypotField) init[form.honeypotField] = \"\";\n return init;\n });\n const [status, setStatus] = useState<Status>({ state: \"idle\" });\n const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});\n\n // Steps. Until the component has mounted every page is shown, so server-rendered output (and a\n // visitor without JavaScript) gets one long form; `enhanced` then turns it into pages.\n const pages = useMemo(() => splitSteps(form.fields), [form.fields]);\n const [enhanced, setEnhanced] = useState(false);\n const [step, setStep] = useState(0);\n const pageRefs = useRef<(HTMLFieldSetElement | null)[]>([]);\n const headingRefs = useRef<(HTMLHeadingElement | null)[]>([]);\n const moveFocus = useRef(false);\n const last = pages ? pages.length - 1 : 0;\n const paged = Boolean(pages && enhanced);\n useEffect(() => setEnhanced(true), []);\n // Focus follows the visitor to the new page's heading, never on first paint.\n useEffect(() => {\n if (!moveFocus.current) return;\n moveFocus.current = false;\n headingRefs.current[step]?.focus();\n }, [step]);\n\n // The browser's own validity, for ONE page: the first control that fails reports its message.\n function pageInvalid(i: number): boolean {\n const controls = pageRefs.current[i]?.querySelectorAll<HTMLInputElement>(\"input,select,textarea\") ?? [];\n for (const c of controls) {\n if (!c.checkValidity()) {\n c.reportValidity();\n return true;\n }\n }\n return false;\n }\n function goTo(i: number): void {\n moveFocus.current = true;\n setStep(i);\n }\n function next(): void {\n if (!pageInvalid(step)) goTo(step + 1);\n }\n\n // Cloudflare auto-renders any `.cf-turnstile` element once its script is present.\n useEffect(() => {\n if (!showTurnstile || document.querySelector(`script[src=\"${TURNSTILE_SRC}\"]`)) return;\n const script = document.createElement(\"script\");\n script.src = TURNSTILE_SRC;\n script.async = true;\n script.defer = true;\n document.head.appendChild(script);\n }, [showTurnstile]);\n\n const visibleFields = useMemo(\n () => form.fields.filter((f) => f.type !== \"hidden\" && f.type !== \"step\" && shouldShowField(f, values)),\n [form.fields, values],\n );\n\n const setValue = (key: string, value: FormValue) =>\n setValues((prev) => ({ ...prev, [key]: value }));\n\n async function handleSubmit(e: FormEvent<HTMLFormElement>): Promise<void> {\n e.preventDefault();\n // Enter inside a field before the last page moves on rather than submitting half a form.\n if (paged && step < last) return next();\n setStatus({ state: \"submitting\" });\n setFieldErrors({});\n\n const tokenEl = e.currentTarget.querySelector<HTMLInputElement>(\n '[name=\"cf-turnstile-response\"]',\n );\n const turnstileToken = tokenEl?.value || undefined;\n\n // Drop conditionally-hidden fields so a hidden required field can't block submit;\n // keep hidden-type fields and the honeypot (the API classifies bots on it).\n const data: FormValues = {};\n for (const field of form.fields) {\n if (field.type === \"step\") continue; // a divider is never an answer\n if (field.type === \"hidden\" || shouldShowField(field, values)) {\n data[field.key] = values[field.key];\n }\n }\n if (form.honeypotField) data[form.honeypotField] = values[form.honeypotField] ?? \"\";\n\n try {\n const { id } = await submitForm({ formId: form.id, data, turnstileToken, baseUrl: apiBase });\n onSubmitted?.(id);\n if (form.redirectUrl) {\n window.location.href = form.redirectUrl;\n return;\n }\n const reset = formInitialValues(form);\n if (form.honeypotField) reset[form.honeypotField] = \"\";\n setValues(reset);\n setStep(0);\n setStatus({\n state: \"success\",\n message: form.successMessage ?? \"Thanks! Your submission was received.\",\n });\n resetTurnstile();\n } catch (err) {\n const e2 = err as FormSubmitError;\n if (e2.fieldErrors) {\n setFieldErrors(e2.fieldErrors);\n // The server found a problem on a page the visitor has left: take them back to it.\n const bad = pages?.findIndex((p) => p.fields.some((f) => e2.fieldErrors?.[f.key]));\n if (bad !== undefined && bad >= 0) goTo(bad);\n }\n setStatus({ state: \"error\", message: e2.message || \"Something went wrong. Please try again.\" });\n resetTurnstile();\n }\n }\n\n return (\n // `data-bcms-form` carries the id so the Visual Editor can find THIS form in the\n // preview frame and patch its copy live. Valued rather than bare: the runtime\n // selectors are `form[data-bcms-form]`, which a valued attribute still matches.\n <form className={className} onSubmit={handleSubmit} noValidate data-bcms-form={form.id}>\n {pages\n ? pages.map((page, i) => (\n <fieldset\n key={page.key ?? \"first\"}\n ref={(el) => {\n pageRefs.current[i] = el;\n }}\n className=\"bcms-step\"\n data-bcms-step={i + 1}\n hidden={paged && i !== step}\n aria-labelledby={`bcms-step-${form.id}-${i + 1}`}\n >\n <h3\n id={`bcms-step-${form.id}-${i + 1}`}\n className=\"bcms-step-title\"\n tabIndex={-1}\n ref={(el) => {\n headingRefs.current[i] = el;\n }}\n data-bcms-form-field={page.key ?? undefined}\n >\n <span className=\"bcms-step-count\">\n Step {i + 1} of {pages.length}\n </span>\n {page.label ? (\n <>\n {\" \"}\n <span data-bcms-form-part=\"label\">{page.label}</span>\n </>\n ) : null}\n </h3>\n {visibleFields\n .filter((f) => page.fields.includes(f))\n .map((field) => (\n <Field\n key={field.key}\n field={field}\n value={values[field.key]}\n error={fieldErrors[field.key]}\n onChange={(v) => setValue(field.key, v)}\n />\n ))}\n {paged ? (\n <div className=\"bcms-step-nav\">\n {i > 0 ? (\n <button type=\"button\" data-bcms-step-back onClick={() => goTo(i - 1)}>\n Back\n </button>\n ) : null}\n {i < last ? (\n <button type=\"button\" data-bcms-step-next onClick={next}>\n Next\n </button>\n ) : null}\n </div>\n ) : null}\n </fieldset>\n ))\n : visibleFields.map((field) => (\n <Field\n key={field.key}\n field={field}\n value={values[field.key]}\n error={fieldErrors[field.key]}\n onChange={(v) => setValue(field.key, v)}\n />\n ))}\n\n {form.honeypotField ? (\n <span aria-hidden=\"true\" style={{ position: \"absolute\", left: \"-9999px\" }}>\n <input\n type=\"text\"\n name={form.honeypotField}\n tabIndex={-1}\n autoComplete=\"off\"\n value={String(values[form.honeypotField] ?? \"\")}\n onChange={(e) => setValue(form.honeypotField as string, e.target.value)}\n />\n </span>\n ) : null}\n\n {showTurnstile ? <div className=\"cf-turnstile\" data-sitekey={turnstileSiteKey} /> : null}\n\n <button\n type=\"submit\"\n hidden={paged && step < last}\n disabled={status.state === \"submitting\"}\n data-bcms-form-part=\"submit\"\n >\n {status.state === \"submitting\" ? \"Submitting…\" : form.submitLabel ?? \"Submit\"}\n </button>\n\n {status.message ? (\n <p\n className={`bcms-form-status bcms-form-status--${status.state}`}\n role=\"status\"\n aria-live=\"polite\"\n >\n {status.message}\n </p>\n ) : null}\n </form>\n );\n}\n\ninterface FieldProps {\n field: DeliveryFormField;\n value: FormValue;\n error?: string;\n onChange: (value: FormValue) => void;\n}\n\nfunction Field({ field, value, error, onChange }: FieldProps): ReactElement {\n const required = field.required;\n const errorNode = error ? (\n <span className=\"bcms-field-error\" role=\"alert\">\n {error}\n </span>\n ) : null;\n // Every author-editable string carries a `data-bcms-form-part` so the Visual Editor can\n // repaint it live. A string the dock can edit but cannot mark shows stale on the canvas.\n const helpNode = field.helpText ? (\n <span className=\"bcms-field-help\" data-bcms-form-part=\"help\">\n {field.helpText}\n </span>\n ) : null;\n\n if (field.type === \"checkbox\" || field.type === \"consent\") {\n return (\n <label\n className=\"bcms-field bcms-field--checkbox\"\n data-field={field.key}\n data-bcms-form-field={field.key}\n >\n <input\n type=\"checkbox\"\n name={field.key}\n required={required}\n checked={Boolean(value)}\n onChange={(e) => onChange(e.target.checked)}\n />{\" \"}\n <span data-bcms-form-part=\"label\">\n {field.label}\n {required ? \" *\" : \"\"}\n </span>\n {helpNode}\n {errorNode}\n </label>\n );\n }\n\n // Pick-one and pick-many are groups of controls, not a single element, so they\n // return their own <fieldset> rather than flowing into the label+control shape\n // below. Without these two branches both fell through to `<input type=\"text\">`.\n if (field.type === \"radio\" || field.type === \"checkboxes\") {\n const multi = field.type === \"checkboxes\";\n const selected = asStringArray(value);\n return (\n <fieldset\n className={`bcms-field bcms-field--${multi ? \"checkboxes\" : \"radio\"}`}\n data-field={field.key}\n data-bcms-form-field={field.key}\n >\n <legend data-bcms-form-part=\"label\">\n {field.label}\n {required ? \" *\" : \"\"}\n </legend>\n {(field.options ?? []).map((opt) => (\n <label key={opt} className=\"bcms-option\">\n <input\n type={multi ? \"checkbox\" : \"radio\"}\n name={field.key}\n value={opt}\n // `required` on every radio in a group is the native way to demand one\n // choice. On checkboxes it would demand ALL of them, so it is omitted —\n // the API enforces non-empty instead.\n required={!multi && required}\n checked={multi ? selected.includes(opt) : value === opt}\n onChange={(e) =>\n onChange(multi ? toggleOption(value, opt, e.target.checked) : opt)\n }\n />{\" \"}\n <span data-bcms-form-part=\"option\">{opt}</span>\n </label>\n ))}\n {helpNode}\n {errorNode}\n </fieldset>\n );\n }\n\n const str = String(value ?? \"\");\n let control: ReactElement;\n if (field.type === \"textarea\") {\n control = (\n <textarea\n name={field.key}\n placeholder={field.placeholder}\n required={required}\n value={str}\n onChange={(e) => onChange(e.target.value)}\n />\n );\n } else if (field.type === \"select\") {\n control = (\n <select name={field.key} required={required} value={str} onChange={(e) => onChange(e.target.value)}>\n <option value=\"\" disabled>\n Choose…\n </option>\n {(field.options ?? []).map((opt) => (\n <option key={opt} value={opt} data-bcms-form-part=\"option\">\n {opt}\n </option>\n ))}\n </select>\n );\n } else if (field.type === \"phone\" && field.ui?.countryPicker) {\n // The fallback is a usable tel input inside the SAME wrapper, so the host's `.bcms-phone`\n // rules apply before and after the chunk lands and the swap is not a layout surprise.\n control = (\n <Suspense\n fallback={\n <span className=\"bcms-phone\">\n <input type=\"tel\" name={field.key} placeholder={field.placeholder} required={required} value={str} onChange={(e) => onChange(e.target.value)} autoComplete=\"tel\" dir=\"ltr\" />\n </span>\n }\n >\n <PhoneField\n name={field.key}\n value={str}\n onChange={onChange}\n placeholder={field.placeholder}\n required={required}\n defaultCountry={field.ui.defaultCountry}\n />\n </Suspense>\n );\n } else {\n control = (\n <input\n type={INPUT_TYPES[field.type] ?? \"text\"}\n name={field.key}\n placeholder={field.placeholder}\n required={required}\n value={str}\n onChange={(e) => onChange(e.target.value)}\n />\n );\n }\n\n return (\n <p className=\"bcms-field\" data-field={field.key} data-bcms-form-field={field.key}>\n <label data-bcms-form-part=\"label\">\n {field.label}\n {required ? \" *\" : \"\"}\n </label>\n <br />\n {control}\n {helpNode}\n {errorNode}\n </p>\n );\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { ResolvedSeo, SeoInput, resolveSeo } from '@bettercms-ai/sdk';
6
6
  import { CSSProperties, ReactElement, ReactNode, ElementType, ImgHTMLAttributes } from 'react';
7
7
  import { ImageSource, ImageFormat, ImageFit } from '@bettercms-ai/image-url';
8
8
  export { ImageCrop, ImageFit, ImageFocal, ImageFormat, ImageSource, ImageUrlBuilder, ImageValueUrlOptions, imageUrl, default as imageUrlBuilder, imageValueObjectPosition, imageValueUrl } from '@bettercms-ai/image-url';
9
+ import { DeliveryForm as DeliveryForm$1 } from '@bettercms-ai/sdk/read';
9
10
  import * as react_jsx_runtime from 'react/jsx-runtime';
10
11
 
11
12
  /**
@@ -184,7 +185,8 @@ declare function createBetterCMS<Schema extends Record<string, unknown> = Record
184
185
  * Pass `location` — `{ siteUrl, path }` — to get a canonical (and `og:url`) on pages that set
185
186
  * none of their own. Without it, `BETTERCMS_SITE_URL` plus the page's slug stand in; with
186
187
  * neither, both tags are absent and development logs one warning. The hosted renderer always
187
- * emits them. A `noindex` page gets `robots: { index: false }`.
188
+ * emits them. A `noindex` page gets `robots: { index: false }`. Every page gets
189
+ * `generator: "BetterCMS"`; spread over the result to replace it.
188
190
  */
189
191
  declare function buildMetadata(page: SeoInput, siteDefaults?: SiteSeoDefaults, location?: SeoLocation): Metadata;
190
192
 
@@ -563,7 +565,7 @@ declare function getComponent(idOrSlug: string, path?: string): DeliveryComponen
563
565
 
564
566
  interface BcmsFormProps {
565
567
  /** A form from your content snapshot or the delivery API. */
566
- form: DeliveryForm;
568
+ form: DeliveryForm$1;
567
569
  /** API origin. Defaults to the SDK default (https://api.bettercms.ai). */
568
570
  apiBase?: string;
569
571
  /** Turnstile site key — needed to render the widget when `form.turnstileEnabled`. */
package/dist/index.js CHANGED
@@ -232,6 +232,8 @@ function buildMetadata(page, siteDefaults = {}, location = {}) {
232
232
  const seo = resolveSeo(page, siteDefaults, withSiteUrlFallback(page, location));
233
233
  if (!seo.canonical) warnNoCanonical();
234
234
  return {
235
+ // `<meta name="generator">`: what Wappalyzer and BuiltWith read to name the CMS.
236
+ generator: "BetterCMS",
235
237
  title: seo.title || void 0,
236
238
  description: seo.description || void 0,
237
239
  robots: seo.noindex ? { index: false } : void 0,