@bettercms-ai/ui 0.16.0 → 0.17.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/dist/resolve.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  // src/resolve.ts
4
- import { imageValueUrl } from "@bettercms-ai/image-url";
4
+ import { imageValueUrl, normalizeImageCrop, normalizeImageFocal } from "@bettercms-ai/image-url";
5
5
  import { buttonBlockProps, isLinkObject, isScriptHref, isTableValue, linkAnchorOf, linkHref, renderTableHtml, slotItems, SLOT_ITEMS_CAP } from "@bettercms-ai/types";
6
6
  function childArrays(block) {
7
7
  const p = block.props ?? {};
@@ -39,6 +39,12 @@ function setPath(obj, path, value) {
39
39
  cur[keys[keys.length - 1]] = value;
40
40
  }
41
41
  var MARKUP_PATH = "props.html";
42
+ function embedsComponent(value) {
43
+ return Array.isArray(value) && value.some((node) => {
44
+ const n = node;
45
+ return !!n && typeof n === "object" && n.schemaKey === "component" && !!n.fields && typeof n.fields === "object";
46
+ });
47
+ }
42
48
  function writeRichTextOverride(target, value) {
43
49
  const block = target;
44
50
  if (typeof block.props !== "object" || block.props === null) block.props = {};
@@ -46,8 +52,15 @@ function writeRichTextOverride(target, value) {
46
52
  const envelope = value && typeof value === "object" && !Array.isArray(value) ? value : null;
47
53
  if (envelope && typeof envelope.html === "string") {
48
54
  props.html = envelope.html;
49
- if (envelope.content === void 0) delete props.content;
50
- else props.content = envelope.content;
55
+ if (envelope.content !== void 0) props.content = envelope.content;
56
+ else if (embedsComponent(envelope.value)) props.content = envelope.value;
57
+ else delete props.content;
58
+ return;
59
+ }
60
+ const portable = envelope ? envelope.value : void 0;
61
+ if (Array.isArray(portable)) {
62
+ props.html = "";
63
+ props.content = portable;
51
64
  return;
52
65
  }
53
66
  props.html = value;
@@ -63,6 +76,10 @@ function writeImageOverride(target, value) {
63
76
  const envelope = value;
64
77
  const url = typeof envelope.url === "string" ? envelope.url : envelope.src;
65
78
  if (typeof url === "string") props.src = imageValueUrl({ ...value, url }) ?? url;
79
+ const focal = normalizeImageFocal(value.focal);
80
+ if (focal) props.focal = focal;
81
+ const crop = normalizeImageCrop(value.crop);
82
+ if (crop && typeof props.src === "string" && !/[?&]rect=/.test(props.src)) props.crop = crop;
66
83
  const alt = typeof envelope.altText === "string" ? envelope.altText : envelope.alt;
67
84
  if (typeof alt === "string") props.alt = alt;
68
85
  return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/resolve.ts"],"sourcesContent":["/**\n * Pure component-instance resolver — clone a component's blockJson and write each\n * declared override at its target (blockId + dotted path), honoring the props[]\n * allowlist. The single source of truth shared by the dashboard canvas preview, the\n * backend live HTML renderer, and @bettercms-ai/next's SSG output, so all three resolve\n * instances identically. React-free and exported on its own subpath (@bettercms-ai/ui/\n * resolve) so the Hono server can import it without pulling in the block components.\n */\n\nimport { imageValueUrl } from \"@bettercms-ai/image-url\";\nimport { buttonBlockProps, isLinkObject, isScriptHref, isTableValue, linkAnchorOf, linkHref, renderTableHtml, slotItems, SLOT_ITEMS_CAP } from \"@bettercms-ai/types\";\n\n/** Minimal block shape the resolver needs — every real block type is structurally wider.\n * `props` is `unknown` (not `Record<string, unknown>`) so typed block unions like\n * @bettercms-ai/types' ContentBlock — whose `props` are specific interfaces without an\n * index signature — still satisfy the constraint. */\nexport type ResolverBlock = { type: string; id?: string; props?: unknown };\n\n/** Minimal prop-def shape — the real ComponentPropDef is structurally wider.\n *\n * `type` is carried for callers that need it (the backfill reads it to decide which values are\n * rich text). The resolver itself branches on the TARGET alone. @see writeRichTextOverride */\nexport type ResolverPropDef = {\n key: string;\n type?: string;\n target: { blockId: string; path: string };\n /** A `table`/`group` prop's row shape — the ORDERED columns a row holds. @see expandRows */\n config?: { fields?: ResolverSubField[] } & Record<string, unknown>;\n};\n\n/** One column of a `table` row. `fields` (or its `config.fields` mirror) makes it a nested table. */\nexport type ResolverSubField = {\n key: string;\n type?: string;\n fields?: ResolverSubField[];\n config?: { fields?: ResolverSubField[] } & Record<string, unknown>;\n};\n\n/** The child block arrays a container holds, one entry per nested slot (all container types). */\nexport function childArrays<B extends ResolverBlock>(block: B): B[][] {\n const p = (block.props ?? {}) as Record<string, unknown>;\n switch (block.type) {\n case \"columns\":\n return Array.isArray(p.columns) ? (p.columns as B[][]) : [];\n case \"section\":\n return Array.isArray(p.children) ? [p.children as B[]] : [];\n case \"slider\":\n return Array.isArray(p.slides)\n ? (p.slides as { children?: B[] }[]).map((s) => (Array.isArray(s.children) ? s.children : []))\n : [];\n case \"tabs\":\n return Array.isArray(p.tabs)\n ? (p.tabs as { children?: B[] }[]).map((t) => (Array.isArray(t.children) ? t.children : []))\n : [];\n default:\n return [];\n }\n}\n\n/** Find a block by id anywhere in a tree (descends every container). */\nexport function findBlockById<B extends ResolverBlock>(blocks: B[], id: string): B | null {\n for (const b of blocks) {\n if (b?.id === id) return b;\n for (const child of childArrays(b)) {\n const found = findBlockById(child, id);\n if (found) return found;\n }\n }\n return null;\n}\n\n/** Write `value` at a dotted path (e.g. \"props.text\"), creating objects as needed. */\nexport function setPath(obj: Record<string, unknown>, path: string, value: unknown): void {\n const keys = path.split(\".\");\n let cur: Record<string, unknown> = obj;\n for (let i = 0; i < keys.length - 1; i++) {\n const k = keys[i]!;\n if (typeof cur[k] !== \"object\" || cur[k] === null) cur[k] = {};\n cur = cur[k] as Record<string, unknown>;\n }\n cur[keys[keys.length - 1]!] = value;\n}\n\n/**\n * `props.html` — the one target path that holds MARKUP.\n *\n * Named the same thing, for the same reason, as `MARKUP_PATH` in the backend's\n * `lib/components/prop-value.ts`: the authority is the TARGET, never the declared type. A prop\n * can be declared `richtext` and still land on a plain-text slot like `heading.props.text`, and\n * treating that one as an envelope would write an object where the renderer escapes a string.\n */\nconst MARKUP_PATH = \"props.html\";\n\n/**\n * An override aimed at `props.html`, written as the block's own `{ html, content }` PAIR.\n *\n * 🔴 The two keys are siblings on `props`, not one value at `props.html`. A `text`/`richtext`\n * block stores `props.html` (the rendered cache) beside `props.content` (Portable Text, the\n * source of truth), and every reader — `packages/ui/src/blocks/Text.tsx`,\n * `packages/next/src/blocks.tsx`, the backend's `render-page.ts` via `readStructuredContent` —\n * PREFERS `content`. A single `setPath(…, \"props.html\", value)` therefore has two failure modes\n * and this function exists to close both:\n *\n * an ENVELOPE written whole ⇒ `props.html` is an object, the renderer does `String(p.html)`,\n * and the page shows the words \"[object Object]\".\n * a STRING written alone ⇒ the DEFINITION's `props.content` survives underneath it, and\n * since readers prefer `content`, the instance's new html is\n * invisible — the override silently does nothing.\n *\n * So an envelope splits into both keys, and anything else writes `html` and DELETES `content`.\n * Deleting is the whole point of the second branch: the stale structure must not outlive the\n * html it no longer describes.\n *\n * 🔴 THE TARGET DECIDES, NOT THE DECLARED TYPE. An earlier version gated this on\n * `type === \"richtext\"`, which put `type: \"text\"` overrides — a perfectly ordinary legacy\n * declaration — back on the plain `setPath`, writing `html` while the definition's `content`\n * stayed put. The instant the backfill gives that definition block a `content`, every such\n * instance renders the DEFINITION's words and the override becomes invisible. Invalidation is a\n * property of the SLOT (`props.html` has a `props.content` sibling that outranks it), so it\n * cannot be conditional on what a prop calls itself — the same argument `prop-value.ts` makes\n * about a `richtext` prop landing on a plain-text target, pointed the other way.\n */\nfunction writeRichTextOverride(target: ResolverBlock, value: unknown): void {\n const block = target as unknown as Record<string, unknown>;\n if (typeof block.props !== \"object\" || block.props === null) block.props = {};\n const props = block.props as Record<string, unknown>;\n\n const envelope = value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as { html?: unknown; content?: unknown })\n : null;\n\n if (envelope && typeof envelope.html === \"string\") {\n props.html = envelope.html;\n // An envelope carrying no `content` is an html-only value, and it clears the old structure\n // for the same reason a bare string does.\n if (envelope.content === undefined) delete props.content;\n else props.content = envelope.content;\n return;\n }\n props.html = value;\n delete props.content;\n}\n\n/** True for the one TARGET whose value is a `{ html, content }` pair. @see writeRichTextOverride */\nconst writesMarkupSlot = (def: ResolverPropDef): boolean => def.target.path === MARKUP_PATH;\n\n/** `props.src` — the target that holds an IMAGE, whose canonical value carries its own alt. */\nexport const IMAGE_PATH = \"props.src\";\n\n/**\n * An override aimed at `props.src`, written as the block's own `src` + `alt` PAIR.\n *\n * 🔴 ALT IS PART OF THE VALUE, and it belongs to the INSTANCE. The canonical image a field\n * stores is `{ id?, url, name?, altText? }` (field-value-normalize.ts), and an image prop\n * targets one path — so writing the value whole put an OBJECT in `src` (`src=\"[object Object]\"`)\n * while writing only the url left every placement inheriting the DEFINITION's alt: two instances\n * of one hero, two different photographs, one screen-reader description, and it belonged to\n * whichever page was componentized first.\n *\n * So an envelope splits into both keys, and a bare string writes `src` alone — a caller who sent\n * only a URL has said nothing about the alt, and silently blanking it would be a regression for\n * every component that has one today.\n */\nfunction writeImageOverride(target: ResolverBlock, value: unknown): void {\n const block = target as unknown as Record<string, unknown>;\n if (typeof block.props !== \"object\" || block.props === null) block.props = {};\n const props = block.props as Record<string, unknown>;\n\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n const envelope = value as { url?: unknown; src?: unknown; altText?: unknown; alt?: unknown };\n const url = typeof envelope.url === \"string\" ? envelope.url : envelope.src;\n // A crop set on THIS placement rides the URL as `rect` (image-url's imageValueUrl — the one\n // function every lane draws an image value through). With no crop, or on a non-BetterCMS\n // URL, the url comes back untouched. A focal point cannot ride a width-only src, so the block\n // lane keeps the asset's.\n if (typeof url === \"string\") props.src = imageValueUrl({ ...(value as object), url }) ?? url;\n // `altText` is the canonical key; `alt` is what a hand-written value uses. An envelope that\n // names neither leaves the block's own alt alone rather than blanking it.\n const alt = typeof envelope.altText === \"string\" ? envelope.altText : envelope.alt;\n if (typeof alt === \"string\") props.alt = alt;\n return;\n }\n props.src = value;\n}\n\n/** True for the target whose value is an image envelope. @see writeImageOverride */\nconst writesImageSlot = (def: ResolverPropDef): boolean => def.target.path === IMAGE_PATH;\n\n/**\n * A `button` value written onto a `button` block as ONE unit: label, href and look together.\n *\n * Merged, never assigned. A button prop targets the block's whole `props`, and writing the value\n * there verbatim would put `label` where the block reads `text` and drop every key the override\n * did not repeat, so a placement that changed only its words would lose its link.\n */\nfunction writeButtonOverride(target: ResolverBlock, value: unknown): void {\n const block = target as unknown as Record<string, unknown>;\n if (typeof block.props !== \"object\" || block.props === null) block.props = {};\n Object.assign(block.props as Record<string, unknown>, buttonBlockProps(value));\n}\n\n/**\n * A `video` value on a `video` block lands as the block's props, whatever path the prop names (a\n * video has one value): a bare string is its url, an object `{ url, poster?, autoplay?, loop?,\n * transcript? }` carries exactly the block's own props. Only those keys are written, so an open\n * override record cannot put anything else on the block.\n */\nconst VIDEO_BLOCK_PROPS = [\"url\", \"poster\", \"autoplay\", \"loop\", \"transcript\"] as const;\nfunction writeVideoOverride(target: ResolverBlock, value: unknown): void {\n const block = target as unknown as Record<string, unknown>;\n if (typeof block.props !== \"object\" || block.props === null) block.props = {};\n const props = block.props as Record<string, unknown>;\n if (typeof value === \"string\") {\n props.url = value;\n return;\n }\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return;\n for (const key of VIDEO_BLOCK_PROPS) if (key in value) props[key] = (value as Record<string, unknown>)[key];\n}\n\n/**\n * A `link` value on a `button` block lands on its `href`, whatever path the prop names: a link\n * has one place to go on a button. A link object `{ href }` is read down to its string, because\n * an object in an `href` attribute renders as \"[object Object]\".\n */\nfunction writeLinkOverride(target: ResolverBlock, value: unknown): void {\n const props = target as unknown as Record<string, unknown>;\n const href = linkHref(value);\n // An instance's overrides are an open record nobody validated: a script href renders as \"#\".\n if (href !== null && isScriptHref(href)) {\n setPath(props, \"props.href\", \"#\");\n return;\n }\n setPath(props, \"props.href\", href ?? value);\n if (!isLinkObject(value)) return;\n // A link object describes the WHOLE anchor (linkAnchorOf): where it opens and its rel replace\n // the block's own, so an editor's \"same tab\" or \"follow\" clears a base new tab or rel.\n const link = linkAnchorOf(value)!;\n setPath(props, \"props.newTab\", link.newTab);\n const blockProps = (props.props ?? {}) as Record<string, unknown>;\n if (link.rel) blockProps.rel = link.rel;\n else delete blockProps.rel;\n}\n\n/**\n * A `link` value on any block but a button lands at its path as a STRING: a navbar's\n * `links.N.href`, a footer link, a video's `url` all render the value in an attribute, where a\n * link object reads \"[object Object]\". A script href renders as \"#\".\n */\nfunction linkTargetValue(value: unknown): unknown {\n const href = linkHref(value);\n if (href === null) return value;\n return isScriptHref(href) ? \"#\" : href;\n}\n\n/**\n * `props.rows` — the one target path whose value is a LIST, and therefore REPEATS.\n *\n * 🔴 WHY THIS EXISTS. A repeater becomes one `table` prop pointing at a container block whose\n * children are ONE ROW's blocks — the row TEMPLATE. `setPath` assigned that array to\n * `props.rows`, and no renderer reads `props.rows`: `Section` renders `props.children`. So an\n * FAQ with nine questions rendered the single seeded row, on the canvas, in delivery and in the\n * SSG output alike, and the other eight were invisible everywhere except the dock that edited\n * them.\n */\nexport const ROWS_PATH = \"props.rows\";\n\n/**\n * Where one row COLUMN's value lands inside the block that renders it, by block type.\n *\n * The template's blocks mirror the row's declared sub-fields one for one and in order (that is\n * how the componentize lane builds them), so a column is written at the slot ITS OWN BLOCK TYPE\n * reads — the same pairing `blockFor`/`PROP_OF_KIND` make on the way in. A block type not listed\n * here is left exactly as the template had it rather than guessed at: writing a headline into an\n * unknown slot is worse than a row that renders its template value.\n */\n/**\n * Which BLOCK TYPES can hold a column of each declared type.\n *\n * 🔴 A SLOT IS NOT A TYPE. `ROW_SLOTS` says WHERE a cell is written on a given block; it says\n * nothing about whether the value belongs there. A component declaring a `richtext` column whose\n * template child is a `button` passed a check that only asked \"does this block have a slot?\" —\n * and then the rich-text envelope was written into `props.href` while the page field holding that\n * copy was marked hidden. So the column's own type decides which children can carry it, and the\n * lists are exactly the slots below read the right way round: text goes where text renders\n * (never into an href), a url goes to the one block whose slot IS an href.\n *\n * A column type this map does not know is not this rule's to judge — the child having a slot at\n * all is the whole check for it.\n */\nconst CELL_TYPES: Record<string, readonly string[]> = {\n text: [\"heading\", \"text\", \"richtext\"],\n richtext: [\"text\", \"richtext\"],\n url: [\"button\"],\n link: [\"button\"],\n button: [\"button\"],\n image: [\"image\", \"video\"],\n // Object values: no row slot reads one (each slot above is a string or an image value), so a\n // column of these never fits a template and the rows are not expanded into somebody's text.\n location: [],\n json: [],\n};\n\nconst ROW_SLOTS: Record<string, string> = {\n heading: \"props.text\",\n text: \"props.html\",\n richtext: \"props.html\",\n image: \"props.src\",\n button: \"props.href\",\n video: \"props.src\",\n};\n\n/** The columns a row declares — `fields` is the authority, `config.fields` its legacy mirror. */\nconst subFieldsOf = (node: { fields?: ResolverSubField[]; config?: { fields?: ResolverSubField[] } }):\n | ResolverSubField[]\n | null => node.fields ?? node.config?.fields ?? null;\n\n/**\n * Can this container's ROW TEMPLATE be repeated for these columns?\n *\n * 🔴 THE ONE STATEMENT OF WHAT `expandRows` NEEDS, exported because the componentize lane has to\n * ask the SAME question before it points a placement at an existing component. Keys and types\n * matching is not enough: a component whose FAQ template holds two children for a one-column row\n * (or a child nothing writes into) passes every prop check and then silently fails to expand —\n * rows do not render, while the page fields that hold their copy are marked hidden. Two\n * implementations of \"does this template fit\" would be two answers, and the pair that disagreed\n * is exactly that bug.\n *\n * ONE template child per declared column, in order; a `table` column's child is a container of\n * its own and recurses; every other child must be a block type a row cell can be written into.\n */\nexport function rowTemplateFits(target: ResolverBlock, fields: ResolverSubField[] | null): boolean {\n if (!fields) return false;\n const props = target.props as Record<string, unknown> | undefined;\n const template = Array.isArray(props?.children) ? (props!.children as ResolverBlock[]) : null;\n if (!template || template.length !== fields.length) return false;\n return fields.every((field, column) => {\n const child = template[column]!;\n const nested = subFieldsOf(field);\n if (field.type === \"table\" || nested) return rowTemplateFits(child, nested);\n if (ROW_SLOTS[child.type] === undefined) return false;\n const allowed = field.type === undefined ? undefined : CELL_TYPES[field.type];\n return allowed === undefined || allowed.includes(child.type);\n });\n}\n\n/**\n * Expand a container's ROW TEMPLATE once per override row, writing each column into the block\n * that renders it. Returns false when the shape does not match, and the caller then falls back\n * to the plain write — a mismatch must never mis-assign a value into somebody else's slot.\n *\n * REQUIRES the template's children to correspond to the declared columns one for one, in order,\n * because that is the only correspondence either side states. A nested `table` column is a\n * container of its own and recurses on the same rule, which is how a card list whose cards each\n * hold a bullet list renders every bullet of every card.\n *\n * Row copies get suffixed ids (`f0r1#2`) so a tree never carries the same block id twice —\n * `findBlockById` and every renderer's React key depend on that.\n */\nfunction expandRows(target: ResolverBlock, value: unknown, fields: ResolverSubField[] | null): boolean {\n if (!Array.isArray(value) || !fields) return false;\n if (!rowTemplateFits(target, fields)) return false;\n const props = target.props as Record<string, unknown>;\n const template = props.children as ResolverBlock[];\n\n const children: ResolverBlock[] = [];\n value.forEach((row, index) => {\n const cells = (row ?? {}) as Record<string, unknown>;\n template.forEach((block, column) => {\n const copy = structuredClone(block);\n suffixIds(copy, `#${index}`);\n const field = fields[column]!;\n const cell = cells[field.key];\n const nested = subFieldsOf(field);\n if (nested && Array.isArray(cell)) {\n // A column that is itself a table: the same expansion, one level down.\n if (!expandRows(copy, cell, nested)) writeCell(copy, cell);\n } else if (cell !== undefined) {\n writeCell(copy, cell);\n }\n children.push(copy);\n });\n });\n props.children = children;\n // Kept beside the expansion: it is what the dock reads back, and what says how many rows this\n // instance holds without walking the tree.\n props.rows = value;\n return true;\n}\n\n/** Write one row cell at the slot its block type reads. @see ROW_SLOTS */\nfunction writeCell(block: ResolverBlock, cell: unknown): void {\n const path = ROW_SLOTS[block.type];\n if (!path) return;\n if (path === MARKUP_PATH) writeRichTextOverride(block, cell);\n else if (path === IMAGE_PATH) writeImageOverride(block, cell);\n // A `button` column holds the whole unit; a `url`/`link` column holds the href alone.\n else if (block.type === \"button\" && cell && typeof cell === \"object\") writeButtonOverride(block, cell);\n else setPath(block as unknown as Record<string, unknown>, path, cell);\n}\n\n/**\n * Stamp a row coordinate onto EVERY id in a cloned row — the subtree, not just its root.\n *\n * 🔴 GRANDCHILDREN COLLIDE OTHERWISE. Suffixing only the clone's root left a nested row template\n * carrying its original ids under every outer row, so card 1's first bullet and card 2's first\n * bullet were both `c1r0#0`: `findBlockById` returns whichever comes first (so an override aimed\n * at one lands on the other) and React keys repeat. The nested expansion then appends its own\n * coordinate to these, which is what makes `c1r0#0#1` mean \"outer row 0, inner row 1\".\n */\nfunction suffixIds(block: ResolverBlock, suffix: string): void {\n if (typeof block.id === \"string\") block.id = `${block.id}${suffix}`;\n for (const children of childArrays(block)) for (const child of children) suffixIds(child, suffix);\n}\n\n/** The primitive blocks that render a prop's value as a STRING at a slot of their props. */\nconst PRIMITIVE_BLOCKS: ReadonlySet<string> = new Set([\"heading\", \"text\", \"richtext\", \"image\", \"button\", \"video\"]);\n\nconst escapeMarkup = (text: string): string =>\n text.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\").replace(/'/g, \"&#39;\");\n\n/**\n * A `reference` value as the slot it lands on can hold it.\n *\n * On a primitive block's slot (a heading's `props.text`, a button's label) a reference renders as\n * its entry's TITLE when the read hydrated it, else as the id it stores (a polymorphic value's\n * `entryId`): the rule an entry's own reference field follows on the site. `props.html` is markup,\n * so the title is escaped there. Anywhere else (`props`, or a key a code component reads) the\n * value lands as delivered: the hydrated entry, or the id.\n */\nfunction referenceSlotValue(target: ResolverBlock, path: string, value: unknown): unknown {\n if (!PRIMITIVE_BLOCKS.has(target.type) || path === \"props\" || !value || typeof value !== \"object\" || Array.isArray(value)) return value;\n const ref = value as { title?: unknown; entryId?: unknown; kind?: unknown; id?: unknown };\n if (typeof ref.title === \"string\") return path === MARKUP_PATH ? escapeMarkup(ref.title) : ref.title;\n if (typeof ref.entryId === \"string\") return ref.entryId;\n // An unresolved page / form / collection target ({ kind, id }) prints its id, as an entry does.\n return typeof ref.kind === \"string\" && typeof ref.id === \"string\" ? ref.id : value;\n}\n\n/** True for a `slot` prop whose value is a LIST of instances. @see slotChildren */\nconst isMultiSlot = (def: ResolverPropDef): boolean => def.type === \"slot\" && def.config?.multiple === true;\n\n/**\n * A `multiple` slot's value as the child blocks its container holds: one `component` block per\n * item, in order.\n *\n * Ordinary blocks on purpose. Every renderer and the publish freeze already walk a container's\n * children, resolve a `component` block, guard its depth and cycles, and honour its `style` and\n * `window`; a list the renderers had to learn would be a fifth thing for four lanes to agree on.\n *\n * The id is the prop key and the item's position, so it is the same on every render and never\n * collides with a block the definition declares. A single object is read as a list of one, and\n * the list stops at SLOT_ITEMS_CAP whatever a hand-edited row holds.\n */\nconst slotSettingsOfItem = (item: { overrides?: unknown }): Record<string, unknown> | undefined => {\n const settings = item.overrides && typeof item.overrides === \"object\" ? (item.overrides as Record<string, unknown>)._settings : undefined;\n return settings && typeof settings === \"object\" && !Array.isArray(settings) ? (settings as Record<string, unknown>) : undefined;\n};\n\nfunction slotChildren(def: ResolverPropDef, value: unknown): ResolverBlock[] {\n return slotItems(value).slice(0, SLOT_ITEMS_CAP).map((item, index) => ({\n type: \"component\",\n id: `${def.key}#${index}`,\n props: {\n componentId: item.componentId,\n ...(item.overrides ? { overrides: item.overrides } : {}),\n // A V2 child's `_settings` (the slot's slotSettings) ride on the block, for the renderer to carry.\n ...(slotSettingsOfItem(item) ? { slotSettings: slotSettingsOfItem(item) } : {}),\n },\n ...(item.style ? { style: item.style } : {}),\n ...(item.window ? { window: item.window } : {}),\n }));\n}\n\n/**\n * Apply an instance's `overrides` onto a clone of a component's blockJson, honoring the\n * declared `props[]` allowlist (key → target block + path). Returns the original array\n * untouched (referentially stable) when there's nothing to override; returns [] for a\n * non-array input.\n *\n * Every prop but seven is written verbatim at its `target.path`. The exceptions are the values\n * the renderer cannot read where they land: `props.html` is written as a `{ html, content }`\n * PAIR, `props.src` as a `src` + `alt` pair, `props.rows` REPEATS the target's row template once\n * per row, on a `button` block a `button` prop is merged as one unit and a `link` prop\n * writes `href`, a `video` prop on a `video` block writes the block's props, a data `table` prop on\n * `props.html` writes its HTML table, and a `multiple`\n * slot is written as one `component` block per item. A\n * `reference` on a primitive's string slot writes its entry's title (@see referenceSlotValue).\n * @see writeRichTextOverride · @see writeImageOverride · @see expandRows · @see writeButtonOverride\n * @see slotChildren\n */\nexport function applyOverrides<B extends ResolverBlock>(\n blockJson: unknown,\n propDefs: ResolverPropDef[],\n overrides: Record<string, unknown> | undefined,\n): B[] {\n if (!Array.isArray(blockJson)) return [];\n if (!propDefs.length || !overrides || Object.keys(overrides).length === 0) {\n return blockJson as B[];\n }\n const clone = structuredClone(blockJson) as B[];\n for (const def of propDefs) {\n if (!(def.key in overrides)) continue;\n const target = findBlockById(clone, def.target.blockId);\n if (!target) continue;\n const value = def.type === \"reference\" ? referenceSlotValue(target, def.target.path, overrides[def.key]) : overrides[def.key];\n if (isMultiSlot(def)) setPath(target as unknown as Record<string, unknown>, def.target.path, slotChildren(def, value));\n else if (def.type === \"video\" && target.type === \"video\") writeVideoOverride(target, value);\n // A data table (`config.mode: \"data\"`) on `props.html` lands as its semantic HTML table.\n else if (writesMarkupSlot(def) && def.type === \"table\" && def.config?.mode === \"data\") writeRichTextOverride(target, isTableValue(value) ? renderTableHtml(value) : \"\");\n else if (writesMarkupSlot(def)) writeRichTextOverride(target, value);\n else if (writesImageSlot(def)) writeImageOverride(target, value);\n else if (def.type === \"button\" && target.type === \"button\") writeButtonOverride(target, value);\n else if (def.type === \"link\" && target.type === \"button\") writeLinkOverride(target, value);\n else if (def.type === \"link\") setPath(target as unknown as Record<string, unknown>, def.target.path, linkTargetValue(value));\n else if (def.target.path === ROWS_PATH && expandRows(target, value, subFieldsOf(def))) {\n /* the template was repeated per row — see expandRows */\n } else setPath(target as unknown as Record<string, unknown>, def.target.path, value);\n }\n return clone;\n}\n\n/**\n * How deep a component may nest another component before we stop. Matches the guard the\n * three live renderers already apply, so a tree that renders is a tree that snapshots.\n */\nexport const MAX_COMPONENT_DEPTH = 10;\n\n/** What `resolveDeep` needs to look a nested component up by id. */\nexport type ResolverComponentDef = { blockJson: unknown; props: ResolverPropDef[] };\n\n/** Rebuild a container block with each of its child arrays mapped. The write-side twin of\n * `childArrays` — same four container types, same order. Non-containers pass through. */\nfunction mapChildArrays<B extends ResolverBlock>(block: B, map: (children: B[]) => B[]): B {\n const p = (block.props ?? {}) as Record<string, unknown>;\n switch (block.type) {\n case \"columns\":\n if (!Array.isArray(p.columns)) return block;\n return { ...block, props: { ...p, columns: (p.columns as B[][]).map(map) } };\n case \"section\":\n if (!Array.isArray(p.children)) return block;\n return { ...block, props: { ...p, children: map(p.children as B[]) } };\n case \"slider\":\n if (!Array.isArray(p.slides)) return block;\n return {\n ...block,\n props: {\n ...p,\n slides: (p.slides as { children?: B[] }[]).map((s) =>\n Array.isArray(s.children) ? { ...s, children: map(s.children) } : s,\n ),\n },\n };\n case \"tabs\":\n if (!Array.isArray(p.tabs)) return block;\n return {\n ...block,\n props: {\n ...p,\n tabs: (p.tabs as { children?: B[] }[]).map((t) =>\n Array.isArray(t.children) ? { ...t, children: map(t.children) } : t,\n ),\n },\n };\n default:\n return block;\n }\n}\n\n/**\n * `applyOverrides`, then INLINE every nested `component` block into its resolved blocks.\n *\n * This is the deep/frozen counterpart to `applyOverrides`, and it exists for exactly one\n * caller family: publishing. A published entry's `component-ref` freezes the resolved tree\n * into `published_data` so that later edits to the component cannot silently rewrite content\n * that was already published. `applyOverrides` alone does not deliver that promise once a\n * component nests another (which a `slot` prop makes routine): the outer tree is frozen, but\n * the nested `component` block is still a live REFERENCE that re-resolves at bake time.\n *\n * The three LIVE renderers deliberately keep using `applyOverrides` plus their own\n * per-render recursion — they want the live reference. So there is still exactly one\n * implementation of \"apply overrides to a component\"; this only adds the inlining.\n *\n * Fail-soft, matching the renderers: an empty id, an unknown component, a cycle, or a tree\n * deeper than MAX_COMPONENT_DEPTH resolves to NOTHING rather than throwing — the same thing\n * a visitor already sees for a deleted component.\n *\n * `seen` is the ancestry of the current branch and the caller MUST seed it with the id of\n * the component being resolved, or a component that slots itself will not be caught. It is\n * ancestry-scoped, not global: the same component placed twice as siblings renders twice.\n */\nexport function resolveDeep<B extends ResolverBlock>(\n blockJson: unknown,\n propDefs: ResolverPropDef[],\n overrides: Record<string, unknown> | undefined,\n lookup: (componentId: string) => ResolverComponentDef | undefined,\n seen: readonly string[] = [],\n depth = 0,\n): B[] {\n const applied = applyOverrides<B>(blockJson, propDefs, overrides);\n\n const out: B[] = [];\n for (const block of applied) {\n if (block?.type !== \"component\") {\n out.push(mapChildArrays(block, (children) => resolveDeep<B>(children, [], undefined, lookup, seen, depth)));\n continue;\n }\n // THE INVARIANT: the returned tree never contains a `component` block. Every branch below\n // either inlines it or drops it — including the depth cap, which must NOT bail out by\n // returning the tree as-is. Doing that would leave a live reference sitting inside a\n // \"frozen\" snapshot, which is the exact half-frozen state this function exists to prevent\n // and is harder to notice than rendering nothing.\n if (depth >= MAX_COMPONENT_DEPTH) continue; // too deep — drop, never leave a live ref\n const p = (block.props ?? {}) as { componentId?: unknown; overrides?: unknown };\n const cid = typeof p.componentId === \"string\" ? p.componentId : \"\";\n if (!cid || seen.includes(cid)) continue; // empty slot, or a cycle — render nothing\n const def = lookup(cid);\n if (!def) continue; // unknown/unpublished/deleted — render nothing\n out.push(\n ...resolveDeep<B>(\n def.blockJson,\n def.props,\n p.overrides as Record<string, unknown> | undefined,\n lookup,\n [...seen, cid],\n depth + 1,\n ),\n );\n }\n return out;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/resolve.ts"],"sourcesContent":["/**\n * Pure component-instance resolver — clone a component's blockJson and write each\n * declared override at its target (blockId + dotted path), honoring the props[]\n * allowlist. The single source of truth shared by the dashboard canvas preview, the\n * backend live HTML renderer, and @bettercms-ai/next's SSG output, so all three resolve\n * instances identically. React-free and exported on its own subpath (@bettercms-ai/ui/\n * resolve) so the Hono server can import it without pulling in the block components.\n */\n\nimport { imageValueUrl, normalizeImageCrop, normalizeImageFocal } from \"@bettercms-ai/image-url\";\nimport { buttonBlockProps, isLinkObject, isScriptHref, isTableValue, linkAnchorOf, linkHref, renderTableHtml, slotItems, SLOT_ITEMS_CAP } from \"@bettercms-ai/types\";\n\n/** Minimal block shape the resolver needs — every real block type is structurally wider.\n * `props` is `unknown` (not `Record<string, unknown>`) so typed block unions like\n * @bettercms-ai/types' ContentBlock — whose `props` are specific interfaces without an\n * index signature — still satisfy the constraint. */\nexport type ResolverBlock = { type: string; id?: string; props?: unknown };\n\n/** Minimal prop-def shape — the real ComponentPropDef is structurally wider.\n *\n * `type` is carried for callers that need it (the backfill reads it to decide which values are\n * rich text). The resolver itself branches on the TARGET alone. @see writeRichTextOverride */\nexport type ResolverPropDef = {\n key: string;\n type?: string;\n target: { blockId: string; path: string };\n /** A `table`/`group` prop's row shape — the ORDERED columns a row holds. @see expandRows */\n config?: { fields?: ResolverSubField[] } & Record<string, unknown>;\n};\n\n/** One column of a `table` row. `fields` (or its `config.fields` mirror) makes it a nested table. */\nexport type ResolverSubField = {\n key: string;\n type?: string;\n fields?: ResolverSubField[];\n config?: { fields?: ResolverSubField[] } & Record<string, unknown>;\n};\n\n/** The child block arrays a container holds, one entry per nested slot (all container types). */\nexport function childArrays<B extends ResolverBlock>(block: B): B[][] {\n const p = (block.props ?? {}) as Record<string, unknown>;\n switch (block.type) {\n case \"columns\":\n return Array.isArray(p.columns) ? (p.columns as B[][]) : [];\n case \"section\":\n return Array.isArray(p.children) ? [p.children as B[]] : [];\n case \"slider\":\n return Array.isArray(p.slides)\n ? (p.slides as { children?: B[] }[]).map((s) => (Array.isArray(s.children) ? s.children : []))\n : [];\n case \"tabs\":\n return Array.isArray(p.tabs)\n ? (p.tabs as { children?: B[] }[]).map((t) => (Array.isArray(t.children) ? t.children : []))\n : [];\n default:\n return [];\n }\n}\n\n/** Find a block by id anywhere in a tree (descends every container). */\nexport function findBlockById<B extends ResolverBlock>(blocks: B[], id: string): B | null {\n for (const b of blocks) {\n if (b?.id === id) return b;\n for (const child of childArrays(b)) {\n const found = findBlockById(child, id);\n if (found) return found;\n }\n }\n return null;\n}\n\n/** Write `value` at a dotted path (e.g. \"props.text\"), creating objects as needed. */\nexport function setPath(obj: Record<string, unknown>, path: string, value: unknown): void {\n const keys = path.split(\".\");\n let cur: Record<string, unknown> = obj;\n for (let i = 0; i < keys.length - 1; i++) {\n const k = keys[i]!;\n if (typeof cur[k] !== \"object\" || cur[k] === null) cur[k] = {};\n cur = cur[k] as Record<string, unknown>;\n }\n cur[keys[keys.length - 1]!] = value;\n}\n\n/**\n * `props.html` — the one target path that holds MARKUP.\n *\n * Named the same thing, for the same reason, as `MARKUP_PATH` in the backend's\n * `lib/components/prop-value.ts`: the authority is the TARGET, never the declared type. A prop\n * can be declared `richtext` and still land on a plain-text slot like `heading.props.text`, and\n * treating that one as an envelope would write an object where the renderer escapes a string.\n */\nconst MARKUP_PATH = \"props.html\";\n\n/**\n * An override aimed at `props.html`, written as the block's own `{ html, content }` PAIR.\n *\n * 🔴 The two keys are siblings on `props`, not one value at `props.html`. A `text`/`richtext`\n * block stores `props.html` (the rendered cache) beside `props.content` (Portable Text, the\n * source of truth), and every reader — `packages/ui/src/blocks/Text.tsx`,\n * `packages/next/src/blocks.tsx`, the backend's `render-page.ts` via `readStructuredContent` —\n * PREFERS `content`. A single `setPath(…, \"props.html\", value)` therefore has two failure modes\n * and this function exists to close both:\n *\n * an ENVELOPE written whole ⇒ `props.html` is an object, the renderer does `String(p.html)`,\n * and the page shows the words \"[object Object]\".\n * a STRING written alone ⇒ the DEFINITION's `props.content` survives underneath it, and\n * since readers prefer `content`, the instance's new html is\n * invisible — the override silently does nothing.\n *\n * So an envelope splits into both keys, and anything else writes `html` and DELETES `content`.\n * Deleting is the whole point of the second branch: the stale structure must not outlive the\n * html it no longer describes.\n *\n * 🔴 THE TARGET DECIDES, NOT THE DECLARED TYPE. An earlier version gated this on\n * `type === \"richtext\"`, which put `type: \"text\"` overrides — a perfectly ordinary legacy\n * declaration — back on the plain `setPath`, writing `html` while the definition's `content`\n * stayed put. The instant the backfill gives that definition block a `content`, every such\n * instance renders the DEFINITION's words and the override becomes invisible. Invalidation is a\n * property of the SLOT (`props.html` has a `props.content` sibling that outranks it), so it\n * cannot be conditional on what a prop calls itself — the same argument `prop-value.ts` makes\n * about a `richtext` prop landing on a plain-text target, pointed the other way.\n */\n/** A Portable Text value with a component embedded at its top level (`@bettercms-ai/richtext` `embeddedComponentRender`). */\nfunction embedsComponent(value: unknown): value is unknown[] {\n return Array.isArray(value) && value.some((node) => {\n const n = node as { schemaKey?: unknown; fields?: unknown } | null;\n return !!n && typeof n === \"object\" && n.schemaKey === \"component\" && !!n.fields && typeof n.fields === \"object\";\n });\n}\n\nfunction writeRichTextOverride(target: ResolverBlock, value: unknown): void {\n const block = target as unknown as Record<string, unknown>;\n if (typeof block.props !== \"object\" || block.props === null) block.props = {};\n const props = block.props as Record<string, unknown>;\n\n const envelope = value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as { html?: unknown; content?: unknown })\n : null;\n\n if (envelope && typeof envelope.html === \"string\") {\n props.html = envelope.html;\n // An envelope carrying no `content` is an html-only value, and it clears the old structure\n // for the same reason a bare string does.\n if (envelope.content !== undefined) props.content = envelope.content;\n // A stored rich-text value (`{ format, value, html }`) with a COMPONENT in it: its `html` holds\n // only the embed's empty marker, so the canvas drew nothing where the component sits. Its\n // Portable Text goes to `content`, where every renderer resolves the embed (a linked one\n // draws the live component, a separate copy its own blocks). Any other value keeps its html\n // render byte for byte.\n else if (embedsComponent((envelope as { value?: unknown }).value)) props.content = (envelope as { value: unknown[] }).value;\n else delete props.content;\n return;\n }\n // A stored rich-text value (`{ format: \"portabletext\", value }`) whose html cache is missing:\n // its Portable Text is the content, and the renderer draws it. Written verbatim it rendered as\n // `[object Object]`.\n const portable = envelope ? (envelope as { value?: unknown }).value : undefined;\n if (Array.isArray(portable)) {\n props.html = \"\";\n props.content = portable;\n return;\n }\n props.html = value;\n delete props.content;\n}\n\n/** True for the one TARGET whose value is a `{ html, content }` pair. @see writeRichTextOverride */\nconst writesMarkupSlot = (def: ResolverPropDef): boolean => def.target.path === MARKUP_PATH;\n\n/** `props.src` — the target that holds an IMAGE, whose canonical value carries its own alt. */\nexport const IMAGE_PATH = \"props.src\";\n\n/**\n * An override aimed at `props.src`, written as the block's own `src` + `alt` PAIR.\n *\n * 🔴 ALT IS PART OF THE VALUE, and it belongs to the INSTANCE. The canonical image a field\n * stores is `{ id?, url, name?, altText? }` (field-value-normalize.ts), and an image prop\n * targets one path — so writing the value whole put an OBJECT in `src` (`src=\"[object Object]\"`)\n * while writing only the url left every placement inheriting the DEFINITION's alt: two instances\n * of one hero, two different photographs, one screen-reader description, and it belonged to\n * whichever page was componentized first.\n *\n * So an envelope splits into both keys, and a bare string writes `src` alone — a caller who sent\n * only a URL has said nothing about the alt, and silently blanking it would be a regression for\n * every component that has one today.\n */\nfunction writeImageOverride(target: ResolverBlock, value: unknown): void {\n const block = target as unknown as Record<string, unknown>;\n if (typeof block.props !== \"object\" || block.props === null) block.props = {};\n const props = block.props as Record<string, unknown>;\n\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n const envelope = value as { url?: unknown; src?: unknown; altText?: unknown; alt?: unknown };\n const url = typeof envelope.url === \"string\" ? envelope.url : envelope.src;\n // A crop set on THIS placement rides the URL as `rect` (image-url's imageValueUrl — the one\n // function every lane draws an image value through). With no crop, or on a non-BetterCMS\n // URL, the url comes back untouched (the crop then rides the block's `crop`, below). A focal point cannot ride a width-only src: it lands on\n // the block's own `focal`, which the image block draws as object-position inside that crop\n // (render-page's image arm). Without it the point an author set on a placement was stored and\n // drawn by nobody.\n if (typeof url === \"string\") props.src = imageValueUrl({ ...(value as object), url }) ?? url;\n const focal = normalizeImageFocal((value as { focal?: unknown }).focal);\n if (focal) props.focal = focal;\n // A crop the URL could not carry (an address the byte server does not cut, so `rect` was never added) lands on the\n // block's own `crop`, which the image block draws in the browser (object-view-box). Without it a 1:1 crop on an\n // outside image was stored and drawn by nobody (QA w2-section 7).\n const crop = normalizeImageCrop((value as { crop?: unknown }).crop);\n if (crop && typeof props.src === \"string\" && !/[?&]rect=/.test(props.src)) props.crop = crop;\n // `altText` is the canonical key; `alt` is what a hand-written value uses. An envelope that\n // names neither leaves the block's own alt alone rather than blanking it.\n const alt = typeof envelope.altText === \"string\" ? envelope.altText : envelope.alt;\n if (typeof alt === \"string\") props.alt = alt;\n return;\n }\n props.src = value;\n}\n\n/** True for the target whose value is an image envelope. @see writeImageOverride */\nconst writesImageSlot = (def: ResolverPropDef): boolean => def.target.path === IMAGE_PATH;\n\n/**\n * A `button` value written onto a `button` block as ONE unit: label, href and look together.\n *\n * Merged, never assigned. A button prop targets the block's whole `props`, and writing the value\n * there verbatim would put `label` where the block reads `text` and drop every key the override\n * did not repeat, so a placement that changed only its words would lose its link.\n */\nfunction writeButtonOverride(target: ResolverBlock, value: unknown): void {\n const block = target as unknown as Record<string, unknown>;\n if (typeof block.props !== \"object\" || block.props === null) block.props = {};\n Object.assign(block.props as Record<string, unknown>, buttonBlockProps(value));\n}\n\n/**\n * A `video` value on a `video` block lands as the block's props, whatever path the prop names (a\n * video has one value): a bare string is its url, an object `{ url, poster?, autoplay?, loop?,\n * transcript? }` carries exactly the block's own props. Only those keys are written, so an open\n * override record cannot put anything else on the block.\n */\nconst VIDEO_BLOCK_PROPS = [\"url\", \"poster\", \"autoplay\", \"loop\", \"transcript\"] as const;\nfunction writeVideoOverride(target: ResolverBlock, value: unknown): void {\n const block = target as unknown as Record<string, unknown>;\n if (typeof block.props !== \"object\" || block.props === null) block.props = {};\n const props = block.props as Record<string, unknown>;\n if (typeof value === \"string\") {\n props.url = value;\n return;\n }\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return;\n for (const key of VIDEO_BLOCK_PROPS) if (key in value) props[key] = (value as Record<string, unknown>)[key];\n}\n\n/**\n * A `link` value on a `button` block lands on its `href`, whatever path the prop names: a link\n * has one place to go on a button. A link object `{ href }` is read down to its string, because\n * an object in an `href` attribute renders as \"[object Object]\".\n */\nfunction writeLinkOverride(target: ResolverBlock, value: unknown): void {\n const props = target as unknown as Record<string, unknown>;\n const href = linkHref(value);\n // An instance's overrides are an open record nobody validated: a script href renders as \"#\".\n if (href !== null && isScriptHref(href)) {\n setPath(props, \"props.href\", \"#\");\n return;\n }\n setPath(props, \"props.href\", href ?? value);\n if (!isLinkObject(value)) return;\n // A link object describes the WHOLE anchor (linkAnchorOf): where it opens and its rel replace\n // the block's own, so an editor's \"same tab\" or \"follow\" clears a base new tab or rel.\n const link = linkAnchorOf(value)!;\n setPath(props, \"props.newTab\", link.newTab);\n const blockProps = (props.props ?? {}) as Record<string, unknown>;\n if (link.rel) blockProps.rel = link.rel;\n else delete blockProps.rel;\n}\n\n/**\n * A `link` value on any block but a button lands at its path as a STRING: a navbar's\n * `links.N.href`, a footer link, a video's `url` all render the value in an attribute, where a\n * link object reads \"[object Object]\". A script href renders as \"#\".\n */\nfunction linkTargetValue(value: unknown): unknown {\n const href = linkHref(value);\n if (href === null) return value;\n return isScriptHref(href) ? \"#\" : href;\n}\n\n/**\n * `props.rows` — the one target path whose value is a LIST, and therefore REPEATS.\n *\n * 🔴 WHY THIS EXISTS. A repeater becomes one `table` prop pointing at a container block whose\n * children are ONE ROW's blocks — the row TEMPLATE. `setPath` assigned that array to\n * `props.rows`, and no renderer reads `props.rows`: `Section` renders `props.children`. So an\n * FAQ with nine questions rendered the single seeded row, on the canvas, in delivery and in the\n * SSG output alike, and the other eight were invisible everywhere except the dock that edited\n * them.\n */\nexport const ROWS_PATH = \"props.rows\";\n\n/**\n * Where one row COLUMN's value lands inside the block that renders it, by block type.\n *\n * The template's blocks mirror the row's declared sub-fields one for one and in order (that is\n * how the componentize lane builds them), so a column is written at the slot ITS OWN BLOCK TYPE\n * reads — the same pairing `blockFor`/`PROP_OF_KIND` make on the way in. A block type not listed\n * here is left exactly as the template had it rather than guessed at: writing a headline into an\n * unknown slot is worse than a row that renders its template value.\n */\n/**\n * Which BLOCK TYPES can hold a column of each declared type.\n *\n * 🔴 A SLOT IS NOT A TYPE. `ROW_SLOTS` says WHERE a cell is written on a given block; it says\n * nothing about whether the value belongs there. A component declaring a `richtext` column whose\n * template child is a `button` passed a check that only asked \"does this block have a slot?\" —\n * and then the rich-text envelope was written into `props.href` while the page field holding that\n * copy was marked hidden. So the column's own type decides which children can carry it, and the\n * lists are exactly the slots below read the right way round: text goes where text renders\n * (never into an href), a url goes to the one block whose slot IS an href.\n *\n * A column type this map does not know is not this rule's to judge — the child having a slot at\n * all is the whole check for it.\n */\nconst CELL_TYPES: Record<string, readonly string[]> = {\n text: [\"heading\", \"text\", \"richtext\"],\n richtext: [\"text\", \"richtext\"],\n url: [\"button\"],\n link: [\"button\"],\n button: [\"button\"],\n image: [\"image\", \"video\"],\n // Object values: no row slot reads one (each slot above is a string or an image value), so a\n // column of these never fits a template and the rows are not expanded into somebody's text.\n location: [],\n json: [],\n};\n\nconst ROW_SLOTS: Record<string, string> = {\n heading: \"props.text\",\n text: \"props.html\",\n richtext: \"props.html\",\n image: \"props.src\",\n button: \"props.href\",\n video: \"props.src\",\n};\n\n/** The columns a row declares — `fields` is the authority, `config.fields` its legacy mirror. */\nconst subFieldsOf = (node: { fields?: ResolverSubField[]; config?: { fields?: ResolverSubField[] } }):\n | ResolverSubField[]\n | null => node.fields ?? node.config?.fields ?? null;\n\n/**\n * Can this container's ROW TEMPLATE be repeated for these columns?\n *\n * 🔴 THE ONE STATEMENT OF WHAT `expandRows` NEEDS, exported because the componentize lane has to\n * ask the SAME question before it points a placement at an existing component. Keys and types\n * matching is not enough: a component whose FAQ template holds two children for a one-column row\n * (or a child nothing writes into) passes every prop check and then silently fails to expand —\n * rows do not render, while the page fields that hold their copy are marked hidden. Two\n * implementations of \"does this template fit\" would be two answers, and the pair that disagreed\n * is exactly that bug.\n *\n * ONE template child per declared column, in order; a `table` column's child is a container of\n * its own and recurses; every other child must be a block type a row cell can be written into.\n */\nexport function rowTemplateFits(target: ResolverBlock, fields: ResolverSubField[] | null): boolean {\n if (!fields) return false;\n const props = target.props as Record<string, unknown> | undefined;\n const template = Array.isArray(props?.children) ? (props!.children as ResolverBlock[]) : null;\n if (!template || template.length !== fields.length) return false;\n return fields.every((field, column) => {\n const child = template[column]!;\n const nested = subFieldsOf(field);\n if (field.type === \"table\" || nested) return rowTemplateFits(child, nested);\n if (ROW_SLOTS[child.type] === undefined) return false;\n const allowed = field.type === undefined ? undefined : CELL_TYPES[field.type];\n return allowed === undefined || allowed.includes(child.type);\n });\n}\n\n/**\n * Expand a container's ROW TEMPLATE once per override row, writing each column into the block\n * that renders it. Returns false when the shape does not match, and the caller then falls back\n * to the plain write — a mismatch must never mis-assign a value into somebody else's slot.\n *\n * REQUIRES the template's children to correspond to the declared columns one for one, in order,\n * because that is the only correspondence either side states. A nested `table` column is a\n * container of its own and recurses on the same rule, which is how a card list whose cards each\n * hold a bullet list renders every bullet of every card.\n *\n * Row copies get suffixed ids (`f0r1#2`) so a tree never carries the same block id twice —\n * `findBlockById` and every renderer's React key depend on that.\n */\nfunction expandRows(target: ResolverBlock, value: unknown, fields: ResolverSubField[] | null): boolean {\n if (!Array.isArray(value) || !fields) return false;\n if (!rowTemplateFits(target, fields)) return false;\n const props = target.props as Record<string, unknown>;\n const template = props.children as ResolverBlock[];\n\n const children: ResolverBlock[] = [];\n value.forEach((row, index) => {\n const cells = (row ?? {}) as Record<string, unknown>;\n template.forEach((block, column) => {\n const copy = structuredClone(block);\n suffixIds(copy, `#${index}`);\n const field = fields[column]!;\n const cell = cells[field.key];\n const nested = subFieldsOf(field);\n if (nested && Array.isArray(cell)) {\n // A column that is itself a table: the same expansion, one level down.\n if (!expandRows(copy, cell, nested)) writeCell(copy, cell);\n } else if (cell !== undefined) {\n writeCell(copy, cell);\n }\n children.push(copy);\n });\n });\n props.children = children;\n // Kept beside the expansion: it is what the dock reads back, and what says how many rows this\n // instance holds without walking the tree.\n props.rows = value;\n return true;\n}\n\n/** Write one row cell at the slot its block type reads. @see ROW_SLOTS */\nfunction writeCell(block: ResolverBlock, cell: unknown): void {\n const path = ROW_SLOTS[block.type];\n if (!path) return;\n if (path === MARKUP_PATH) writeRichTextOverride(block, cell);\n else if (path === IMAGE_PATH) writeImageOverride(block, cell);\n // A `button` column holds the whole unit; a `url`/`link` column holds the href alone.\n else if (block.type === \"button\" && cell && typeof cell === \"object\") writeButtonOverride(block, cell);\n else setPath(block as unknown as Record<string, unknown>, path, cell);\n}\n\n/**\n * Stamp a row coordinate onto EVERY id in a cloned row — the subtree, not just its root.\n *\n * 🔴 GRANDCHILDREN COLLIDE OTHERWISE. Suffixing only the clone's root left a nested row template\n * carrying its original ids under every outer row, so card 1's first bullet and card 2's first\n * bullet were both `c1r0#0`: `findBlockById` returns whichever comes first (so an override aimed\n * at one lands on the other) and React keys repeat. The nested expansion then appends its own\n * coordinate to these, which is what makes `c1r0#0#1` mean \"outer row 0, inner row 1\".\n */\nfunction suffixIds(block: ResolverBlock, suffix: string): void {\n if (typeof block.id === \"string\") block.id = `${block.id}${suffix}`;\n for (const children of childArrays(block)) for (const child of children) suffixIds(child, suffix);\n}\n\n/** The primitive blocks that render a prop's value as a STRING at a slot of their props. */\nconst PRIMITIVE_BLOCKS: ReadonlySet<string> = new Set([\"heading\", \"text\", \"richtext\", \"image\", \"button\", \"video\"]);\n\nconst escapeMarkup = (text: string): string =>\n text.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\").replace(/'/g, \"&#39;\");\n\n/**\n * A `reference` value as the slot it lands on can hold it.\n *\n * On a primitive block's slot (a heading's `props.text`, a button's label) a reference renders as\n * its entry's TITLE when the read hydrated it, else as the id it stores (a polymorphic value's\n * `entryId`): the rule an entry's own reference field follows on the site. `props.html` is markup,\n * so the title is escaped there. Anywhere else (`props`, or a key a code component reads) the\n * value lands as delivered: the hydrated entry, or the id.\n */\nfunction referenceSlotValue(target: ResolverBlock, path: string, value: unknown): unknown {\n if (!PRIMITIVE_BLOCKS.has(target.type) || path === \"props\" || !value || typeof value !== \"object\" || Array.isArray(value)) return value;\n const ref = value as { title?: unknown; entryId?: unknown; kind?: unknown; id?: unknown };\n if (typeof ref.title === \"string\") return path === MARKUP_PATH ? escapeMarkup(ref.title) : ref.title;\n if (typeof ref.entryId === \"string\") return ref.entryId;\n // An unresolved page / form / collection target ({ kind, id }) prints its id, as an entry does.\n return typeof ref.kind === \"string\" && typeof ref.id === \"string\" ? ref.id : value;\n}\n\n/** True for a `slot` prop whose value is a LIST of instances. @see slotChildren */\nconst isMultiSlot = (def: ResolverPropDef): boolean => def.type === \"slot\" && def.config?.multiple === true;\n\n/**\n * A `multiple` slot's value as the child blocks its container holds: one `component` block per\n * item, in order.\n *\n * Ordinary blocks on purpose. Every renderer and the publish freeze already walk a container's\n * children, resolve a `component` block, guard its depth and cycles, and honour its `style` and\n * `window`; a list the renderers had to learn would be a fifth thing for four lanes to agree on.\n *\n * The id is the prop key and the item's position, so it is the same on every render and never\n * collides with a block the definition declares. A single object is read as a list of one, and\n * the list stops at SLOT_ITEMS_CAP whatever a hand-edited row holds.\n */\nconst slotSettingsOfItem = (item: { overrides?: unknown }): Record<string, unknown> | undefined => {\n const settings = item.overrides && typeof item.overrides === \"object\" ? (item.overrides as Record<string, unknown>)._settings : undefined;\n return settings && typeof settings === \"object\" && !Array.isArray(settings) ? (settings as Record<string, unknown>) : undefined;\n};\n\nfunction slotChildren(def: ResolverPropDef, value: unknown): ResolverBlock[] {\n return slotItems(value).slice(0, SLOT_ITEMS_CAP).map((item, index) => ({\n type: \"component\",\n id: `${def.key}#${index}`,\n props: {\n componentId: item.componentId,\n ...(item.overrides ? { overrides: item.overrides } : {}),\n // A V2 child's `_settings` (the slot's slotSettings) ride on the block, for the renderer to carry.\n ...(slotSettingsOfItem(item) ? { slotSettings: slotSettingsOfItem(item) } : {}),\n },\n ...(item.style ? { style: item.style } : {}),\n ...(item.window ? { window: item.window } : {}),\n }));\n}\n\n/**\n * Apply an instance's `overrides` onto a clone of a component's blockJson, honoring the\n * declared `props[]` allowlist (key → target block + path). Returns the original array\n * untouched (referentially stable) when there's nothing to override; returns [] for a\n * non-array input.\n *\n * Every prop but seven is written verbatim at its `target.path`. The exceptions are the values\n * the renderer cannot read where they land: `props.html` is written as a `{ html, content }`\n * PAIR, `props.src` as a `src` + `alt` pair, `props.rows` REPEATS the target's row template once\n * per row, on a `button` block a `button` prop is merged as one unit and a `link` prop\n * writes `href`, a `video` prop on a `video` block writes the block's props, a data `table` prop on\n * `props.html` writes its HTML table, and a `multiple`\n * slot is written as one `component` block per item. A\n * `reference` on a primitive's string slot writes its entry's title (@see referenceSlotValue).\n * @see writeRichTextOverride · @see writeImageOverride · @see expandRows · @see writeButtonOverride\n * @see slotChildren\n */\nexport function applyOverrides<B extends ResolverBlock>(\n blockJson: unknown,\n propDefs: ResolverPropDef[],\n overrides: Record<string, unknown> | undefined,\n): B[] {\n if (!Array.isArray(blockJson)) return [];\n if (!propDefs.length || !overrides || Object.keys(overrides).length === 0) {\n return blockJson as B[];\n }\n const clone = structuredClone(blockJson) as B[];\n for (const def of propDefs) {\n if (!(def.key in overrides)) continue;\n const target = findBlockById(clone, def.target.blockId);\n if (!target) continue;\n const value = def.type === \"reference\" ? referenceSlotValue(target, def.target.path, overrides[def.key]) : overrides[def.key];\n if (isMultiSlot(def)) setPath(target as unknown as Record<string, unknown>, def.target.path, slotChildren(def, value));\n else if (def.type === \"video\" && target.type === \"video\") writeVideoOverride(target, value);\n // A data table (`config.mode: \"data\"`) on `props.html` lands as its semantic HTML table.\n else if (writesMarkupSlot(def) && def.type === \"table\" && def.config?.mode === \"data\") writeRichTextOverride(target, isTableValue(value) ? renderTableHtml(value) : \"\");\n else if (writesMarkupSlot(def)) writeRichTextOverride(target, value);\n else if (writesImageSlot(def)) writeImageOverride(target, value);\n else if (def.type === \"button\" && target.type === \"button\") writeButtonOverride(target, value);\n else if (def.type === \"link\" && target.type === \"button\") writeLinkOverride(target, value);\n else if (def.type === \"link\") setPath(target as unknown as Record<string, unknown>, def.target.path, linkTargetValue(value));\n else if (def.target.path === ROWS_PATH && expandRows(target, value, subFieldsOf(def))) {\n /* the template was repeated per row — see expandRows */\n } else setPath(target as unknown as Record<string, unknown>, def.target.path, value);\n }\n return clone;\n}\n\n/**\n * How deep a component may nest another component before we stop. Matches the guard the\n * three live renderers already apply, so a tree that renders is a tree that snapshots.\n */\nexport const MAX_COMPONENT_DEPTH = 10;\n\n/** What `resolveDeep` needs to look a nested component up by id. */\nexport type ResolverComponentDef = { blockJson: unknown; props: ResolverPropDef[] };\n\n/** Rebuild a container block with each of its child arrays mapped. The write-side twin of\n * `childArrays` — same four container types, same order. Non-containers pass through. */\nfunction mapChildArrays<B extends ResolverBlock>(block: B, map: (children: B[]) => B[]): B {\n const p = (block.props ?? {}) as Record<string, unknown>;\n switch (block.type) {\n case \"columns\":\n if (!Array.isArray(p.columns)) return block;\n return { ...block, props: { ...p, columns: (p.columns as B[][]).map(map) } };\n case \"section\":\n if (!Array.isArray(p.children)) return block;\n return { ...block, props: { ...p, children: map(p.children as B[]) } };\n case \"slider\":\n if (!Array.isArray(p.slides)) return block;\n return {\n ...block,\n props: {\n ...p,\n slides: (p.slides as { children?: B[] }[]).map((s) =>\n Array.isArray(s.children) ? { ...s, children: map(s.children) } : s,\n ),\n },\n };\n case \"tabs\":\n if (!Array.isArray(p.tabs)) return block;\n return {\n ...block,\n props: {\n ...p,\n tabs: (p.tabs as { children?: B[] }[]).map((t) =>\n Array.isArray(t.children) ? { ...t, children: map(t.children) } : t,\n ),\n },\n };\n default:\n return block;\n }\n}\n\n/**\n * `applyOverrides`, then INLINE every nested `component` block into its resolved blocks.\n *\n * This is the deep/frozen counterpart to `applyOverrides`, and it exists for exactly one\n * caller family: publishing. A published entry's `component-ref` freezes the resolved tree\n * into `published_data` so that later edits to the component cannot silently rewrite content\n * that was already published. `applyOverrides` alone does not deliver that promise once a\n * component nests another (which a `slot` prop makes routine): the outer tree is frozen, but\n * the nested `component` block is still a live REFERENCE that re-resolves at bake time.\n *\n * The three LIVE renderers deliberately keep using `applyOverrides` plus their own\n * per-render recursion — they want the live reference. So there is still exactly one\n * implementation of \"apply overrides to a component\"; this only adds the inlining.\n *\n * Fail-soft, matching the renderers: an empty id, an unknown component, a cycle, or a tree\n * deeper than MAX_COMPONENT_DEPTH resolves to NOTHING rather than throwing — the same thing\n * a visitor already sees for a deleted component.\n *\n * `seen` is the ancestry of the current branch and the caller MUST seed it with the id of\n * the component being resolved, or a component that slots itself will not be caught. It is\n * ancestry-scoped, not global: the same component placed twice as siblings renders twice.\n */\nexport function resolveDeep<B extends ResolverBlock>(\n blockJson: unknown,\n propDefs: ResolverPropDef[],\n overrides: Record<string, unknown> | undefined,\n lookup: (componentId: string) => ResolverComponentDef | undefined,\n seen: readonly string[] = [],\n depth = 0,\n): B[] {\n const applied = applyOverrides<B>(blockJson, propDefs, overrides);\n\n const out: B[] = [];\n for (const block of applied) {\n if (block?.type !== \"component\") {\n out.push(mapChildArrays(block, (children) => resolveDeep<B>(children, [], undefined, lookup, seen, depth)));\n continue;\n }\n // THE INVARIANT: the returned tree never contains a `component` block. Every branch below\n // either inlines it or drops it — including the depth cap, which must NOT bail out by\n // returning the tree as-is. Doing that would leave a live reference sitting inside a\n // \"frozen\" snapshot, which is the exact half-frozen state this function exists to prevent\n // and is harder to notice than rendering nothing.\n if (depth >= MAX_COMPONENT_DEPTH) continue; // too deep — drop, never leave a live ref\n const p = (block.props ?? {}) as { componentId?: unknown; overrides?: unknown };\n const cid = typeof p.componentId === \"string\" ? p.componentId : \"\";\n if (!cid || seen.includes(cid)) continue; // empty slot, or a cycle — render nothing\n const def = lookup(cid);\n if (!def) continue; // unknown/unpublished/deleted — render nothing\n out.push(\n ...resolveDeep<B>(\n def.blockJson,\n def.props,\n p.overrides as Record<string, unknown> | undefined,\n lookup,\n [...seen, cid],\n depth + 1,\n ),\n );\n }\n return out;\n}\n"],"mappings":";;;AASA,SAAS,eAAe,oBAAoB,2BAA2B;AACvE,SAAS,kBAAkB,cAAc,cAAc,cAAc,cAAc,UAAU,iBAAiB,WAAW,sBAAsB;AA6BxI,SAAS,YAAqC,OAAiB;AACpE,QAAM,IAAK,MAAM,SAAS,CAAC;AAC3B,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK;AACH,aAAO,MAAM,QAAQ,EAAE,OAAO,IAAK,EAAE,UAAoB,CAAC;AAAA,IAC5D,KAAK;AACH,aAAO,MAAM,QAAQ,EAAE,QAAQ,IAAI,CAAC,EAAE,QAAe,IAAI,CAAC;AAAA,IAC5D,KAAK;AACH,aAAO,MAAM,QAAQ,EAAE,MAAM,IACxB,EAAE,OAAgC,IAAI,CAAC,MAAO,MAAM,QAAQ,EAAE,QAAQ,IAAI,EAAE,WAAW,CAAC,CAAE,IAC3F,CAAC;AAAA,IACP,KAAK;AACH,aAAO,MAAM,QAAQ,EAAE,IAAI,IACtB,EAAE,KAA8B,IAAI,CAAC,MAAO,MAAM,QAAQ,EAAE,QAAQ,IAAI,EAAE,WAAW,CAAC,CAAE,IACzF,CAAC;AAAA,IACP;AACE,aAAO,CAAC;AAAA,EACZ;AACF;AAGO,SAAS,cAAuC,QAAa,IAAsB;AACxF,aAAW,KAAK,QAAQ;AACtB,QAAI,GAAG,OAAO,GAAI,QAAO;AACzB,eAAW,SAAS,YAAY,CAAC,GAAG;AAClC,YAAM,QAAQ,cAAc,OAAO,EAAE;AACrC,UAAI,MAAO,QAAO;AAAA,IACpB;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,QAAQ,KAA8B,MAAc,OAAsB;AACxF,QAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,MAAI,MAA+B;AACnC,WAAS,IAAI,GAAG,IAAI,KAAK,SAAS,GAAG,KAAK;AACxC,UAAM,IAAI,KAAK,CAAC;AAChB,QAAI,OAAO,IAAI,CAAC,MAAM,YAAY,IAAI,CAAC,MAAM,KAAM,KAAI,CAAC,IAAI,CAAC;AAC7D,UAAM,IAAI,CAAC;AAAA,EACb;AACA,MAAI,KAAK,KAAK,SAAS,CAAC,CAAE,IAAI;AAChC;AAUA,IAAM,cAAc;AAgCpB,SAAS,gBAAgB,OAAoC;AAC3D,SAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,KAAK,CAAC,SAAS;AAClD,UAAM,IAAI;AACV,WAAO,CAAC,CAAC,KAAK,OAAO,MAAM,YAAY,EAAE,cAAc,eAAe,CAAC,CAAC,EAAE,UAAU,OAAO,EAAE,WAAW;AAAA,EAC1G,CAAC;AACH;AAEA,SAAS,sBAAsB,QAAuB,OAAsB;AAC1E,QAAM,QAAQ;AACd,MAAI,OAAO,MAAM,UAAU,YAAY,MAAM,UAAU,KAAM,OAAM,QAAQ,CAAC;AAC5E,QAAM,QAAQ,MAAM;AAEpB,QAAM,WAAW,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IACtE,QACD;AAEJ,MAAI,YAAY,OAAO,SAAS,SAAS,UAAU;AACjD,UAAM,OAAO,SAAS;AAGtB,QAAI,SAAS,YAAY,OAAW,OAAM,UAAU,SAAS;AAAA,aAMpD,gBAAiB,SAAiC,KAAK,EAAG,OAAM,UAAW,SAAkC;AAAA,QACjH,QAAO,MAAM;AAClB;AAAA,EACF;AAIA,QAAM,WAAW,WAAY,SAAiC,QAAQ;AACtE,MAAI,MAAM,QAAQ,QAAQ,GAAG;AAC3B,UAAM,OAAO;AACb,UAAM,UAAU;AAChB;AAAA,EACF;AACA,QAAM,OAAO;AACb,SAAO,MAAM;AACf;AAGA,IAAM,mBAAmB,CAAC,QAAkC,IAAI,OAAO,SAAS;AAGzE,IAAM,aAAa;AAgB1B,SAAS,mBAAmB,QAAuB,OAAsB;AACvE,QAAM,QAAQ;AACd,MAAI,OAAO,MAAM,UAAU,YAAY,MAAM,UAAU,KAAM,OAAM,QAAQ,CAAC;AAC5E,QAAM,QAAQ,MAAM;AAEpB,MAAI,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,GAAG;AAC/D,UAAM,WAAW;AACjB,UAAM,MAAM,OAAO,SAAS,QAAQ,WAAW,SAAS,MAAM,SAAS;AAOvE,QAAI,OAAO,QAAQ,SAAU,OAAM,MAAM,cAAc,EAAE,GAAI,OAAkB,IAAI,CAAC,KAAK;AACzF,UAAM,QAAQ,oBAAqB,MAA8B,KAAK;AACtE,QAAI,MAAO,OAAM,QAAQ;AAIzB,UAAM,OAAO,mBAAoB,MAA6B,IAAI;AAClE,QAAI,QAAQ,OAAO,MAAM,QAAQ,YAAY,CAAC,YAAY,KAAK,MAAM,GAAG,EAAG,OAAM,OAAO;AAGxF,UAAM,MAAM,OAAO,SAAS,YAAY,WAAW,SAAS,UAAU,SAAS;AAC/E,QAAI,OAAO,QAAQ,SAAU,OAAM,MAAM;AACzC;AAAA,EACF;AACA,QAAM,MAAM;AACd;AAGA,IAAM,kBAAkB,CAAC,QAAkC,IAAI,OAAO,SAAS;AAS/E,SAAS,oBAAoB,QAAuB,OAAsB;AACxE,QAAM,QAAQ;AACd,MAAI,OAAO,MAAM,UAAU,YAAY,MAAM,UAAU,KAAM,OAAM,QAAQ,CAAC;AAC5E,SAAO,OAAO,MAAM,OAAkC,iBAAiB,KAAK,CAAC;AAC/E;AAQA,IAAM,oBAAoB,CAAC,OAAO,UAAU,YAAY,QAAQ,YAAY;AAC5E,SAAS,mBAAmB,QAAuB,OAAsB;AACvE,QAAM,QAAQ;AACd,MAAI,OAAO,MAAM,UAAU,YAAY,MAAM,UAAU,KAAM,OAAM,QAAQ,CAAC;AAC5E,QAAM,QAAQ,MAAM;AACpB,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,MAAM;AACZ;AAAA,EACF;AACA,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,EAAG;AACjE,aAAW,OAAO,kBAAmB,KAAI,OAAO,MAAO,OAAM,GAAG,IAAK,MAAkC,GAAG;AAC5G;AAOA,SAAS,kBAAkB,QAAuB,OAAsB;AACtE,QAAM,QAAQ;AACd,QAAM,OAAO,SAAS,KAAK;AAE3B,MAAI,SAAS,QAAQ,aAAa,IAAI,GAAG;AACvC,YAAQ,OAAO,cAAc,GAAG;AAChC;AAAA,EACF;AACA,UAAQ,OAAO,cAAc,QAAQ,KAAK;AAC1C,MAAI,CAAC,aAAa,KAAK,EAAG;AAG1B,QAAM,OAAO,aAAa,KAAK;AAC/B,UAAQ,OAAO,gBAAgB,KAAK,MAAM;AAC1C,QAAM,aAAc,MAAM,SAAS,CAAC;AACpC,MAAI,KAAK,IAAK,YAAW,MAAM,KAAK;AAAA,MAC/B,QAAO,WAAW;AACzB;AAOA,SAAS,gBAAgB,OAAyB;AAChD,QAAM,OAAO,SAAS,KAAK;AAC3B,MAAI,SAAS,KAAM,QAAO;AAC1B,SAAO,aAAa,IAAI,IAAI,MAAM;AACpC;AAYO,IAAM,YAAY;AAyBzB,IAAM,aAAgD;AAAA,EACpD,MAAM,CAAC,WAAW,QAAQ,UAAU;AAAA,EACpC,UAAU,CAAC,QAAQ,UAAU;AAAA,EAC7B,KAAK,CAAC,QAAQ;AAAA,EACd,MAAM,CAAC,QAAQ;AAAA,EACf,QAAQ,CAAC,QAAQ;AAAA,EACjB,OAAO,CAAC,SAAS,OAAO;AAAA;AAAA;AAAA,EAGxB,UAAU,CAAC;AAAA,EACX,MAAM,CAAC;AACT;AAEA,IAAM,YAAoC;AAAA,EACxC,SAAS;AAAA,EACT,MAAM;AAAA,EACN,UAAU;AAAA,EACV,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,OAAO;AACT;AAGA,IAAM,cAAc,CAAC,SAET,KAAK,UAAU,KAAK,QAAQ,UAAU;AAgB3C,SAAS,gBAAgB,QAAuB,QAA4C;AACjG,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,QAAQ,OAAO;AACrB,QAAM,WAAW,MAAM,QAAQ,OAAO,QAAQ,IAAK,MAAO,WAA+B;AACzF,MAAI,CAAC,YAAY,SAAS,WAAW,OAAO,OAAQ,QAAO;AAC3D,SAAO,OAAO,MAAM,CAAC,OAAO,WAAW;AACrC,UAAM,QAAQ,SAAS,MAAM;AAC7B,UAAM,SAAS,YAAY,KAAK;AAChC,QAAI,MAAM,SAAS,WAAW,OAAQ,QAAO,gBAAgB,OAAO,MAAM;AAC1E,QAAI,UAAU,MAAM,IAAI,MAAM,OAAW,QAAO;AAChD,UAAM,UAAU,MAAM,SAAS,SAAY,SAAY,WAAW,MAAM,IAAI;AAC5E,WAAO,YAAY,UAAa,QAAQ,SAAS,MAAM,IAAI;AAAA,EAC7D,CAAC;AACH;AAeA,SAAS,WAAW,QAAuB,OAAgB,QAA4C;AACrG,MAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,CAAC,OAAQ,QAAO;AAC7C,MAAI,CAAC,gBAAgB,QAAQ,MAAM,EAAG,QAAO;AAC7C,QAAM,QAAQ,OAAO;AACrB,QAAM,WAAW,MAAM;AAEvB,QAAM,WAA4B,CAAC;AACnC,QAAM,QAAQ,CAAC,KAAK,UAAU;AAC5B,UAAM,QAAS,OAAO,CAAC;AACvB,aAAS,QAAQ,CAAC,OAAO,WAAW;AAClC,YAAM,OAAO,gBAAgB,KAAK;AAClC,gBAAU,MAAM,IAAI,KAAK,EAAE;AAC3B,YAAM,QAAQ,OAAO,MAAM;AAC3B,YAAM,OAAO,MAAM,MAAM,GAAG;AAC5B,YAAM,SAAS,YAAY,KAAK;AAChC,UAAI,UAAU,MAAM,QAAQ,IAAI,GAAG;AAEjC,YAAI,CAAC,WAAW,MAAM,MAAM,MAAM,EAAG,WAAU,MAAM,IAAI;AAAA,MAC3D,WAAW,SAAS,QAAW;AAC7B,kBAAU,MAAM,IAAI;AAAA,MACtB;AACA,eAAS,KAAK,IAAI;AAAA,IACpB,CAAC;AAAA,EACH,CAAC;AACD,QAAM,WAAW;AAGjB,QAAM,OAAO;AACb,SAAO;AACT;AAGA,SAAS,UAAU,OAAsB,MAAqB;AAC5D,QAAM,OAAO,UAAU,MAAM,IAAI;AACjC,MAAI,CAAC,KAAM;AACX,MAAI,SAAS,YAAa,uBAAsB,OAAO,IAAI;AAAA,WAClD,SAAS,WAAY,oBAAmB,OAAO,IAAI;AAAA,WAEnD,MAAM,SAAS,YAAY,QAAQ,OAAO,SAAS,SAAU,qBAAoB,OAAO,IAAI;AAAA,MAChG,SAAQ,OAA6C,MAAM,IAAI;AACtE;AAWA,SAAS,UAAU,OAAsB,QAAsB;AAC7D,MAAI,OAAO,MAAM,OAAO,SAAU,OAAM,KAAK,GAAG,MAAM,EAAE,GAAG,MAAM;AACjE,aAAW,YAAY,YAAY,KAAK,EAAG,YAAW,SAAS,SAAU,WAAU,OAAO,MAAM;AAClG;AAGA,IAAM,mBAAwC,oBAAI,IAAI,CAAC,WAAW,QAAQ,YAAY,SAAS,UAAU,OAAO,CAAC;AAEjH,IAAM,eAAe,CAAC,SACpB,KAAK,QAAQ,MAAM,OAAO,EAAE,QAAQ,MAAM,MAAM,EAAE,QAAQ,MAAM,MAAM,EAAE,QAAQ,MAAM,QAAQ,EAAE,QAAQ,MAAM,OAAO;AAWvH,SAAS,mBAAmB,QAAuB,MAAc,OAAyB;AACxF,MAAI,CAAC,iBAAiB,IAAI,OAAO,IAAI,KAAK,SAAS,WAAW,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,EAAG,QAAO;AAClI,QAAM,MAAM;AACZ,MAAI,OAAO,IAAI,UAAU,SAAU,QAAO,SAAS,cAAc,aAAa,IAAI,KAAK,IAAI,IAAI;AAC/F,MAAI,OAAO,IAAI,YAAY,SAAU,QAAO,IAAI;AAEhD,SAAO,OAAO,IAAI,SAAS,YAAY,OAAO,IAAI,OAAO,WAAW,IAAI,KAAK;AAC/E;AAGA,IAAM,cAAc,CAAC,QAAkC,IAAI,SAAS,UAAU,IAAI,QAAQ,aAAa;AAcvG,IAAM,qBAAqB,CAAC,SAAuE;AACjG,QAAM,WAAW,KAAK,aAAa,OAAO,KAAK,cAAc,WAAY,KAAK,UAAsC,YAAY;AAChI,SAAO,YAAY,OAAO,aAAa,YAAY,CAAC,MAAM,QAAQ,QAAQ,IAAK,WAAuC;AACxH;AAEA,SAAS,aAAa,KAAsB,OAAiC;AAC3E,SAAO,UAAU,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,IAAI,CAAC,MAAM,WAAW;AAAA,IACrE,MAAM;AAAA,IACN,IAAI,GAAG,IAAI,GAAG,IAAI,KAAK;AAAA,IACvB,OAAO;AAAA,MACL,aAAa,KAAK;AAAA,MAClB,GAAI,KAAK,YAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;AAAA;AAAA,MAEtD,GAAI,mBAAmB,IAAI,IAAI,EAAE,cAAc,mBAAmB,IAAI,EAAE,IAAI,CAAC;AAAA,IAC/E;AAAA,IACA,GAAI,KAAK,QAAQ,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;AAAA,IAC1C,GAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;AAAA,EAC/C,EAAE;AACJ;AAmBO,SAAS,eACd,WACA,UACA,WACK;AACL,MAAI,CAAC,MAAM,QAAQ,SAAS,EAAG,QAAO,CAAC;AACvC,MAAI,CAAC,SAAS,UAAU,CAAC,aAAa,OAAO,KAAK,SAAS,EAAE,WAAW,GAAG;AACzE,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,gBAAgB,SAAS;AACvC,aAAW,OAAO,UAAU;AAC1B,QAAI,EAAE,IAAI,OAAO,WAAY;AAC7B,UAAM,SAAS,cAAc,OAAO,IAAI,OAAO,OAAO;AACtD,QAAI,CAAC,OAAQ;AACb,UAAM,QAAQ,IAAI,SAAS,cAAc,mBAAmB,QAAQ,IAAI,OAAO,MAAM,UAAU,IAAI,GAAG,CAAC,IAAI,UAAU,IAAI,GAAG;AAC5H,QAAI,YAAY,GAAG,EAAG,SAAQ,QAA8C,IAAI,OAAO,MAAM,aAAa,KAAK,KAAK,CAAC;AAAA,aAC5G,IAAI,SAAS,WAAW,OAAO,SAAS,QAAS,oBAAmB,QAAQ,KAAK;AAAA,aAEjF,iBAAiB,GAAG,KAAK,IAAI,SAAS,WAAW,IAAI,QAAQ,SAAS,OAAQ,uBAAsB,QAAQ,aAAa,KAAK,IAAI,gBAAgB,KAAK,IAAI,EAAE;AAAA,aAC7J,iBAAiB,GAAG,EAAG,uBAAsB,QAAQ,KAAK;AAAA,aAC1D,gBAAgB,GAAG,EAAG,oBAAmB,QAAQ,KAAK;AAAA,aACtD,IAAI,SAAS,YAAY,OAAO,SAAS,SAAU,qBAAoB,QAAQ,KAAK;AAAA,aACpF,IAAI,SAAS,UAAU,OAAO,SAAS,SAAU,mBAAkB,QAAQ,KAAK;AAAA,aAChF,IAAI,SAAS,OAAQ,SAAQ,QAA8C,IAAI,OAAO,MAAM,gBAAgB,KAAK,CAAC;AAAA,aAClH,IAAI,OAAO,SAAS,aAAa,WAAW,QAAQ,OAAO,YAAY,GAAG,CAAC,GAAG;AAAA,IAEvF,MAAO,SAAQ,QAA8C,IAAI,OAAO,MAAM,KAAK;AAAA,EACrF;AACA,SAAO;AACT;AAMO,IAAM,sBAAsB;AAOnC,SAAS,eAAwC,OAAU,KAAgC;AACzF,QAAM,IAAK,MAAM,SAAS,CAAC;AAC3B,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK;AACH,UAAI,CAAC,MAAM,QAAQ,EAAE,OAAO,EAAG,QAAO;AACtC,aAAO,EAAE,GAAG,OAAO,OAAO,EAAE,GAAG,GAAG,SAAU,EAAE,QAAkB,IAAI,GAAG,EAAE,EAAE;AAAA,IAC7E,KAAK;AACH,UAAI,CAAC,MAAM,QAAQ,EAAE,QAAQ,EAAG,QAAO;AACvC,aAAO,EAAE,GAAG,OAAO,OAAO,EAAE,GAAG,GAAG,UAAU,IAAI,EAAE,QAAe,EAAE,EAAE;AAAA,IACvE,KAAK;AACH,UAAI,CAAC,MAAM,QAAQ,EAAE,MAAM,EAAG,QAAO;AACrC,aAAO;AAAA,QACL,GAAG;AAAA,QACH,OAAO;AAAA,UACL,GAAG;AAAA,UACH,QAAS,EAAE,OAAgC;AAAA,YAAI,CAAC,MAC9C,MAAM,QAAQ,EAAE,QAAQ,IAAI,EAAE,GAAG,GAAG,UAAU,IAAI,EAAE,QAAQ,EAAE,IAAI;AAAA,UACpE;AAAA,QACF;AAAA,MACF;AAAA,IACF,KAAK;AACH,UAAI,CAAC,MAAM,QAAQ,EAAE,IAAI,EAAG,QAAO;AACnC,aAAO;AAAA,QACL,GAAG;AAAA,QACH,OAAO;AAAA,UACL,GAAG;AAAA,UACH,MAAO,EAAE,KAA8B;AAAA,YAAI,CAAC,MAC1C,MAAM,QAAQ,EAAE,QAAQ,IAAI,EAAE,GAAG,GAAG,UAAU,IAAI,EAAE,QAAQ,EAAE,IAAI;AAAA,UACpE;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACE,aAAO;AAAA,EACX;AACF;AAwBO,SAAS,YACd,WACA,UACA,WACA,QACA,OAA0B,CAAC,GAC3B,QAAQ,GACH;AACL,QAAM,UAAU,eAAkB,WAAW,UAAU,SAAS;AAEhE,QAAM,MAAW,CAAC;AAClB,aAAW,SAAS,SAAS;AAC3B,QAAI,OAAO,SAAS,aAAa;AAC/B,UAAI,KAAK,eAAe,OAAO,CAAC,aAAa,YAAe,UAAU,CAAC,GAAG,QAAW,QAAQ,MAAM,KAAK,CAAC,CAAC;AAC1G;AAAA,IACF;AAMA,QAAI,SAAS,oBAAqB;AAClC,UAAM,IAAK,MAAM,SAAS,CAAC;AAC3B,UAAM,MAAM,OAAO,EAAE,gBAAgB,WAAW,EAAE,cAAc;AAChE,QAAI,CAAC,OAAO,KAAK,SAAS,GAAG,EAAG;AAChC,UAAM,MAAM,OAAO,GAAG;AACtB,QAAI,CAAC,IAAK;AACV,QAAI;AAAA,MACF,GAAG;AAAA,QACD,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,EAAE;AAAA,QACF;AAAA,QACA,CAAC,GAAG,MAAM,GAAG;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
package/dist/tokens.d.ts CHANGED
@@ -30,7 +30,7 @@ type TokenPrefix = (typeof TOKEN_PREFIXES)[number];
30
30
  * is the PAGE background and renderer `muted` is the banded section. That inversion is the
31
31
  * emitter's business (see brandCss), but it is named here so nobody re-derives it wrongly.
32
32
  */
33
- declare const BCMS_PALETTE_TOKENS: readonly ["--bcms-bg-surface", "--bcms-bg-muted", "--bcms-bg-accent", "--bcms-bg-dark", "--bcms-text-muted", "--bcms-accent", "--bcms-primary", "--bcms-radius-card", "--bcms-radius-control"];
33
+ declare const BCMS_PALETTE_TOKENS: readonly ["--bcms-bg-surface", "--bcms-bg-muted", "--bcms-bg-accent", "--bcms-bg-dark", "--bcms-text-muted", "--bcms-accent", "--bcms-on-accent", "--bcms-accent-scheme", "--bcms-primary", "--bcms-radius-card", "--bcms-radius-control"];
34
34
  type BcmsPaletteToken = (typeof BCMS_PALETTE_TOKENS)[number];
35
35
  /** Every `--brand-*` family, with the shape of its variable part. A family with `keyed: false`
36
36
  * has exactly one name; the rest are open sets the kit decides. */
package/dist/tokens.js CHANGED
@@ -9,6 +9,9 @@ var BCMS_PALETTE_TOKENS = [
9
9
  "--bcms-bg-dark",
10
10
  "--bcms-text-muted",
11
11
  "--bcms-accent",
12
+ // Derived from the accent by WCAG contrast: the text on an accent fill, and its scheme.
13
+ "--bcms-on-accent",
14
+ "--bcms-accent-scheme",
12
15
  // `primary` is the BUTTON fill, NOT body text. Binding it to the brand hue is the documented
13
16
  // trap in brandCss — brand `primary` is the brand colour while the renderer's `primary` used
14
17
  // to mean near-black body text, and mapping the two repaints every heading.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tokens.ts"],"sourcesContent":["/**\n * The design-token NAME grammar — one spelling, shared by everything that writes or reads a\n * BetterCMS custom property.\n *\n * Token names are a CROSS-PACKAGE grammar. The backend's brand compiler mints them, starter\n * stylesheets and block components consume them, and until now the only thing keeping the two\n * in step was that somebody typed the same string twice. A grammar with no compiler is how\n * every prop-keyspace schism in this project started: the emitter renames `--brand-leading-*`,\n * every template that reads it silently falls back to its initial value, and nothing is red.\n *\n * So the names live here, in the package both sides already depend on, as CONSTRUCTORS rather\n * than a list of strings — an indexed family (`--brand-space-3`, `--brand-text-lg`) cannot be\n * enumerated ahead of time, but its shape can be, and a builder is the only spelling either\n * side can reach. `assertTokenName` closes the loop for anything generated dynamically.\n *\n * DTCG note (FLO-1178 Lane 2): this is the NAME grammar only. What a token means — its\n * `$type`/`$value` and who owns it — belongs to the template presentation manifest. Keeping\n * names separate is what lets a template declare new knobs without the emitter changing.\n */\n\n/** The two prefixes in the system. `bcms` is the renderer's own palette contract; `brand` is\n * the tenant's compiled design system. Never mix them: a `--bcms-*` name is guaranteed to\n * exist on every page, a `--brand-*` name only when the kit declares that group. */\nexport const TOKEN_PREFIXES = [\"bcms\", \"brand\"] as const;\nexport type TokenPrefix = (typeof TOKEN_PREFIXES)[number];\n\n/**\n * Fixed renderer palette. Emitted for every page that has a brand kit, so a consumer may use\n * these unconditionally.\n *\n * ⚠️ `bg-surface` and `bg-muted` invert against the kit's own vocabulary — renderer `surface`\n * is the PAGE background and renderer `muted` is the banded section. That inversion is the\n * emitter's business (see brandCss), but it is named here so nobody re-derives it wrongly.\n */\nexport const BCMS_PALETTE_TOKENS = [\n \"--bcms-bg-surface\",\n \"--bcms-bg-muted\",\n \"--bcms-bg-accent\",\n \"--bcms-bg-dark\",\n \"--bcms-text-muted\",\n \"--bcms-accent\",\n // `primary` is the BUTTON fill, NOT body text. Binding it to the brand hue is the documented\n // trap in brandCss — brand `primary` is the brand colour while the renderer's `primary` used\n // to mean near-black body text, and mapping the two repaints every heading.\n \"--bcms-primary\",\n // The corner language, per element ROLE. Only the two roles BASE_CSS consumes are emitted:\n // a `--bcms-radius-chip` with no consumer is dead flexibility wearing a token's clothes.\n \"--bcms-radius-card\",\n \"--bcms-radius-control\",\n] as const;\nexport type BcmsPaletteToken = (typeof BCMS_PALETTE_TOKENS)[number];\n\n/** Every `--brand-*` family, with the shape of its variable part. A family with `keyed: false`\n * has exactly one name; the rest are open sets the kit decides. */\nexport const BRAND_TOKEN_FAMILIES = {\n text: { keyed: true, unit: \"px\", from: \"typeScale[].size\" },\n leading: { keyed: true, unit: \"ratio\", from: \"typeScale[].lineHeight\" },\n weight: { keyed: true, unit: \"number\", from: \"typeScale[].weight\" },\n space: { keyed: true, unit: \"px\", from: \"spacing.steps[] (index-keyed) + `base`\" },\n shadow: { keyed: true, unit: \"box-shadow\", from: \"elevation[]\" },\n duration: { keyed: true, unit: \"ms\", from: \"motion.durations\" },\n color: { keyed: true, unit: \"color\", from: \"extras[]\" },\n // A gradient is a VALUE, not a file — which is why it lives in the kit rather than the media\n // library. The emitter composes the CSS from the kit's numbers and colour keys; nothing\n // author-written reaches the stylesheet but the key in this name.\n gradient: { keyed: true, unit: \"gradient\", from: \"graphics.gradients[]\" },\n ease: { keyed: false, unit: \"easing\", from: \"motion.ease\" },\n} as const;\nexport type BrandTokenFamily = keyof typeof BRAND_TOKEN_FAMILIES;\n\n/**\n * A key inside a family. Deliberately narrow — these names reach a stylesheet, and the\n * grammar is what stops an author-controlled key from becoming CSS. The emitter's values are\n * already composed from validated numbers and enums; this guards the other half, the name.\n */\nconst KEY = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\n/** `brandToken(\"space\", 3)` → `--brand-space-3`. Throws on a key the grammar forbids, because\n * the alternative is emitting a property no consumer can ever read. */\nexport function brandToken(family: BrandTokenFamily, key?: string | number): string {\n const definition = BRAND_TOKEN_FAMILIES[family];\n if (!definition.keyed) {\n if (key !== undefined) throw new Error(`Token family \"${family}\" takes no key`);\n return `--brand-${family}`;\n }\n const suffix = String(key ?? \"\");\n if (!KEY.test(suffix)) throw new Error(`Invalid ${family} token key: ${JSON.stringify(key)}`);\n return `--brand-${family}-${suffix}`;\n}\n\n/** The one un-suffixed member of a keyed family: `--brand-space-base`. */\nexport const BRAND_SPACE_BASE = \"--brand-space-base\";\n\n/** Does `name` belong to the grammar at all? The check both sides run so a drift is a failing\n * test rather than a property nothing reads. */\nexport function isTokenName(name: string): boolean {\n if ((BCMS_PALETTE_TOKENS as readonly string[]).includes(name)) return true;\n if (name === BRAND_SPACE_BASE) return true;\n const match = /^--brand-([a-z]+)(?:-(.+))?$/.exec(name);\n if (!match) return false;\n const family = match[1] as BrandTokenFamily;\n const definition = BRAND_TOKEN_FAMILIES[family];\n if (!definition) return false;\n return definition.keyed ? Boolean(match[2] && KEY.test(match[2])) : match[2] === undefined;\n}\n\nexport function assertTokenName(name: string): string {\n if (!isTokenName(name)) throw new Error(`Not a BetterCMS design token: ${name}`);\n return name;\n}\n"],"mappings":";;;AAuBO,IAAM,iBAAiB,CAAC,QAAQ,OAAO;AAWvC,IAAM,sBAAsB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA,EAIA;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AACF;AAKO,IAAM,uBAAuB;AAAA,EAClC,MAAM,EAAE,OAAO,MAAM,MAAM,MAAM,MAAM,mBAAmB;AAAA,EAC1D,SAAS,EAAE,OAAO,MAAM,MAAM,SAAS,MAAM,yBAAyB;AAAA,EACtE,QAAQ,EAAE,OAAO,MAAM,MAAM,UAAU,MAAM,qBAAqB;AAAA,EAClE,OAAO,EAAE,OAAO,MAAM,MAAM,MAAM,MAAM,yCAAyC;AAAA,EACjF,QAAQ,EAAE,OAAO,MAAM,MAAM,cAAc,MAAM,cAAc;AAAA,EAC/D,UAAU,EAAE,OAAO,MAAM,MAAM,MAAM,MAAM,mBAAmB;AAAA,EAC9D,OAAO,EAAE,OAAO,MAAM,MAAM,SAAS,MAAM,WAAW;AAAA;AAAA;AAAA;AAAA,EAItD,UAAU,EAAE,OAAO,MAAM,MAAM,YAAY,MAAM,uBAAuB;AAAA,EACxE,MAAM,EAAE,OAAO,OAAO,MAAM,UAAU,MAAM,cAAc;AAC5D;AAQA,IAAM,MAAM;AAIL,SAAS,WAAW,QAA0B,KAA+B;AAClF,QAAM,aAAa,qBAAqB,MAAM;AAC9C,MAAI,CAAC,WAAW,OAAO;AACrB,QAAI,QAAQ,OAAW,OAAM,IAAI,MAAM,iBAAiB,MAAM,gBAAgB;AAC9E,WAAO,WAAW,MAAM;AAAA,EAC1B;AACA,QAAM,SAAS,OAAO,OAAO,EAAE;AAC/B,MAAI,CAAC,IAAI,KAAK,MAAM,EAAG,OAAM,IAAI,MAAM,WAAW,MAAM,eAAe,KAAK,UAAU,GAAG,CAAC,EAAE;AAC5F,SAAO,WAAW,MAAM,IAAI,MAAM;AACpC;AAGO,IAAM,mBAAmB;AAIzB,SAAS,YAAY,MAAuB;AACjD,MAAK,oBAA0C,SAAS,IAAI,EAAG,QAAO;AACtE,MAAI,SAAS,iBAAkB,QAAO;AACtC,QAAM,QAAQ,+BAA+B,KAAK,IAAI;AACtD,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,SAAS,MAAM,CAAC;AACtB,QAAM,aAAa,qBAAqB,MAAM;AAC9C,MAAI,CAAC,WAAY,QAAO;AACxB,SAAO,WAAW,QAAQ,QAAQ,MAAM,CAAC,KAAK,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,MAAM;AACnF;AAEO,SAAS,gBAAgB,MAAsB;AACpD,MAAI,CAAC,YAAY,IAAI,EAAG,OAAM,IAAI,MAAM,iCAAiC,IAAI,EAAE;AAC/E,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/tokens.ts"],"sourcesContent":["/**\n * The design-token NAME grammar — one spelling, shared by everything that writes or reads a\n * BetterCMS custom property.\n *\n * Token names are a CROSS-PACKAGE grammar. The backend's brand compiler mints them, starter\n * stylesheets and block components consume them, and until now the only thing keeping the two\n * in step was that somebody typed the same string twice. A grammar with no compiler is how\n * every prop-keyspace schism in this project started: the emitter renames `--brand-leading-*`,\n * every template that reads it silently falls back to its initial value, and nothing is red.\n *\n * So the names live here, in the package both sides already depend on, as CONSTRUCTORS rather\n * than a list of strings — an indexed family (`--brand-space-3`, `--brand-text-lg`) cannot be\n * enumerated ahead of time, but its shape can be, and a builder is the only spelling either\n * side can reach. `assertTokenName` closes the loop for anything generated dynamically.\n *\n * DTCG note (FLO-1178 Lane 2): this is the NAME grammar only. What a token means — its\n * `$type`/`$value` and who owns it — belongs to the template presentation manifest. Keeping\n * names separate is what lets a template declare new knobs without the emitter changing.\n */\n\n/** The two prefixes in the system. `bcms` is the renderer's own palette contract; `brand` is\n * the tenant's compiled design system. Never mix them: a `--bcms-*` name is guaranteed to\n * exist on every page, a `--brand-*` name only when the kit declares that group. */\nexport const TOKEN_PREFIXES = [\"bcms\", \"brand\"] as const;\nexport type TokenPrefix = (typeof TOKEN_PREFIXES)[number];\n\n/**\n * Fixed renderer palette. Emitted for every page that has a brand kit, so a consumer may use\n * these unconditionally.\n *\n * ⚠️ `bg-surface` and `bg-muted` invert against the kit's own vocabulary — renderer `surface`\n * is the PAGE background and renderer `muted` is the banded section. That inversion is the\n * emitter's business (see brandCss), but it is named here so nobody re-derives it wrongly.\n */\nexport const BCMS_PALETTE_TOKENS = [\n \"--bcms-bg-surface\",\n \"--bcms-bg-muted\",\n \"--bcms-bg-accent\",\n \"--bcms-bg-dark\",\n \"--bcms-text-muted\",\n \"--bcms-accent\",\n // Derived from the accent by WCAG contrast: the text on an accent fill, and its scheme.\n \"--bcms-on-accent\",\n \"--bcms-accent-scheme\",\n // `primary` is the BUTTON fill, NOT body text. Binding it to the brand hue is the documented\n // trap in brandCss — brand `primary` is the brand colour while the renderer's `primary` used\n // to mean near-black body text, and mapping the two repaints every heading.\n \"--bcms-primary\",\n // The corner language, per element ROLE. Only the two roles BASE_CSS consumes are emitted:\n // a `--bcms-radius-chip` with no consumer is dead flexibility wearing a token's clothes.\n \"--bcms-radius-card\",\n \"--bcms-radius-control\",\n] as const;\nexport type BcmsPaletteToken = (typeof BCMS_PALETTE_TOKENS)[number];\n\n/** Every `--brand-*` family, with the shape of its variable part. A family with `keyed: false`\n * has exactly one name; the rest are open sets the kit decides. */\nexport const BRAND_TOKEN_FAMILIES = {\n text: { keyed: true, unit: \"px\", from: \"typeScale[].size\" },\n leading: { keyed: true, unit: \"ratio\", from: \"typeScale[].lineHeight\" },\n weight: { keyed: true, unit: \"number\", from: \"typeScale[].weight\" },\n space: { keyed: true, unit: \"px\", from: \"spacing.steps[] (index-keyed) + `base`\" },\n shadow: { keyed: true, unit: \"box-shadow\", from: \"elevation[]\" },\n duration: { keyed: true, unit: \"ms\", from: \"motion.durations\" },\n color: { keyed: true, unit: \"color\", from: \"extras[]\" },\n // A gradient is a VALUE, not a file — which is why it lives in the kit rather than the media\n // library. The emitter composes the CSS from the kit's numbers and colour keys; nothing\n // author-written reaches the stylesheet but the key in this name.\n gradient: { keyed: true, unit: \"gradient\", from: \"graphics.gradients[]\" },\n ease: { keyed: false, unit: \"easing\", from: \"motion.ease\" },\n} as const;\nexport type BrandTokenFamily = keyof typeof BRAND_TOKEN_FAMILIES;\n\n/**\n * A key inside a family. Deliberately narrow — these names reach a stylesheet, and the\n * grammar is what stops an author-controlled key from becoming CSS. The emitter's values are\n * already composed from validated numbers and enums; this guards the other half, the name.\n */\nconst KEY = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\n/** `brandToken(\"space\", 3)` → `--brand-space-3`. Throws on a key the grammar forbids, because\n * the alternative is emitting a property no consumer can ever read. */\nexport function brandToken(family: BrandTokenFamily, key?: string | number): string {\n const definition = BRAND_TOKEN_FAMILIES[family];\n if (!definition.keyed) {\n if (key !== undefined) throw new Error(`Token family \"${family}\" takes no key`);\n return `--brand-${family}`;\n }\n const suffix = String(key ?? \"\");\n if (!KEY.test(suffix)) throw new Error(`Invalid ${family} token key: ${JSON.stringify(key)}`);\n return `--brand-${family}-${suffix}`;\n}\n\n/** The one un-suffixed member of a keyed family: `--brand-space-base`. */\nexport const BRAND_SPACE_BASE = \"--brand-space-base\";\n\n/** Does `name` belong to the grammar at all? The check both sides run so a drift is a failing\n * test rather than a property nothing reads. */\nexport function isTokenName(name: string): boolean {\n if ((BCMS_PALETTE_TOKENS as readonly string[]).includes(name)) return true;\n if (name === BRAND_SPACE_BASE) return true;\n const match = /^--brand-([a-z]+)(?:-(.+))?$/.exec(name);\n if (!match) return false;\n const family = match[1] as BrandTokenFamily;\n const definition = BRAND_TOKEN_FAMILIES[family];\n if (!definition) return false;\n return definition.keyed ? Boolean(match[2] && KEY.test(match[2])) : match[2] === undefined;\n}\n\nexport function assertTokenName(name: string): string {\n if (!isTokenName(name)) throw new Error(`Not a BetterCMS design token: ${name}`);\n return name;\n}\n"],"mappings":";;;AAuBO,IAAM,iBAAiB,CAAC,QAAQ,OAAO;AAWvC,IAAM,sBAAsB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA,EAIA;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AACF;AAKO,IAAM,uBAAuB;AAAA,EAClC,MAAM,EAAE,OAAO,MAAM,MAAM,MAAM,MAAM,mBAAmB;AAAA,EAC1D,SAAS,EAAE,OAAO,MAAM,MAAM,SAAS,MAAM,yBAAyB;AAAA,EACtE,QAAQ,EAAE,OAAO,MAAM,MAAM,UAAU,MAAM,qBAAqB;AAAA,EAClE,OAAO,EAAE,OAAO,MAAM,MAAM,MAAM,MAAM,yCAAyC;AAAA,EACjF,QAAQ,EAAE,OAAO,MAAM,MAAM,cAAc,MAAM,cAAc;AAAA,EAC/D,UAAU,EAAE,OAAO,MAAM,MAAM,MAAM,MAAM,mBAAmB;AAAA,EAC9D,OAAO,EAAE,OAAO,MAAM,MAAM,SAAS,MAAM,WAAW;AAAA;AAAA;AAAA;AAAA,EAItD,UAAU,EAAE,OAAO,MAAM,MAAM,YAAY,MAAM,uBAAuB;AAAA,EACxE,MAAM,EAAE,OAAO,OAAO,MAAM,UAAU,MAAM,cAAc;AAC5D;AAQA,IAAM,MAAM;AAIL,SAAS,WAAW,QAA0B,KAA+B;AAClF,QAAM,aAAa,qBAAqB,MAAM;AAC9C,MAAI,CAAC,WAAW,OAAO;AACrB,QAAI,QAAQ,OAAW,OAAM,IAAI,MAAM,iBAAiB,MAAM,gBAAgB;AAC9E,WAAO,WAAW,MAAM;AAAA,EAC1B;AACA,QAAM,SAAS,OAAO,OAAO,EAAE;AAC/B,MAAI,CAAC,IAAI,KAAK,MAAM,EAAG,OAAM,IAAI,MAAM,WAAW,MAAM,eAAe,KAAK,UAAU,GAAG,CAAC,EAAE;AAC5F,SAAO,WAAW,MAAM,IAAI,MAAM;AACpC;AAGO,IAAM,mBAAmB;AAIzB,SAAS,YAAY,MAAuB;AACjD,MAAK,oBAA0C,SAAS,IAAI,EAAG,QAAO;AACtE,MAAI,SAAS,iBAAkB,QAAO;AACtC,QAAM,QAAQ,+BAA+B,KAAK,IAAI;AACtD,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,SAAS,MAAM,CAAC;AACtB,QAAM,aAAa,qBAAqB,MAAM;AAC9C,MAAI,CAAC,WAAY,QAAO;AACxB,SAAO,WAAW,QAAQ,QAAQ,MAAM,CAAC,KAAK,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,MAAM;AACnF;AAEO,SAAS,gBAAgB,MAAsB;AACpD,MAAI,CAAC,YAAY,IAAI,EAAG,OAAM,IAAI,MAAM,iCAAiC,IAAI,EAAE;AAC/E,SAAO;AACT;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bettercms-ai/ui",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "description": "BetterCMS block components + registry/renderer for rendering page block_json in React (dashboard builder preview + headless sites).",
6
6
  "sideEffects": false,
@@ -52,11 +52,11 @@
52
52
  "add": "echo 'Use npx shadcn@latest add -p ../.. <component>'"
53
53
  },
54
54
  "dependencies": {
55
- "@bettercms-ai/types": "^1.43.0",
55
+ "@bettercms-ai/types": "^1.44.0",
56
56
  "clsx": "^2.1.1",
57
57
  "tailwind-merge": "^2.6.0",
58
58
  "@portabletext/react": "^6.2.0",
59
- "@bettercms-ai/richtext": "^0.8.0",
59
+ "@bettercms-ai/richtext": "^0.9.0",
60
60
  "@bettercms-ai/image-url": "^0.4.0"
61
61
  },
62
62
  "peerDependencies": {