@bettercms-ai/ui 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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\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. */\nexport type ResolverPropDef = { key: string; target: { blockId: string; path: string } };\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 * 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 */\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) setPath(target as unknown as Record<string, unknown>, def.target.path, overrides[def.key]);\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\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 if (typeof url === \"string\") props.src = 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 * `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 image: [\"image\", \"video\"],\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 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/**\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 three is written verbatim at its `target.path`. The exceptions are the paths\n * whose value is not a scalar the renderer can read where it lands: `props.html` is written as a\n * `{ html, content }` PAIR, `props.src` as a `src` + `alt` pair, and `props.rows` REPEATS the\n * target's row template once per row.\n * @see writeRichTextOverride · @see writeImageOverride · @see expandRows\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 if (writesMarkupSlot(def)) writeRichTextOverride(target, overrides[def.key]);\n else if (writesImageSlot(def)) writeImageOverride(target, overrides[def.key]);\n else if (def.target.path === ROWS_PATH && expandRows(target, overrides[def.key], subFieldsOf(def))) {\n /* the template was repeated per row — see expandRows */\n } else setPath(target as unknown as Record<string, unknown>, def.target.path, overrides[def.key]);\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":[]}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The design-token NAME grammar — one spelling, shared by everything that writes or reads a
3
+ * BetterCMS custom property.
4
+ *
5
+ * Token names are a CROSS-PACKAGE grammar. The backend's brand compiler mints them, starter
6
+ * stylesheets and block components consume them, and until now the only thing keeping the two
7
+ * in step was that somebody typed the same string twice. A grammar with no compiler is how
8
+ * every prop-keyspace schism in this project started: the emitter renames `--brand-leading-*`,
9
+ * every template that reads it silently falls back to its initial value, and nothing is red.
10
+ *
11
+ * So the names live here, in the package both sides already depend on, as CONSTRUCTORS rather
12
+ * than a list of strings — an indexed family (`--brand-space-3`, `--brand-text-lg`) cannot be
13
+ * enumerated ahead of time, but its shape can be, and a builder is the only spelling either
14
+ * side can reach. `assertTokenName` closes the loop for anything generated dynamically.
15
+ *
16
+ * DTCG note (FLO-1178 Lane 2): this is the NAME grammar only. What a token means — its
17
+ * `$type`/`$value` and who owns it — belongs to the template presentation manifest. Keeping
18
+ * names separate is what lets a template declare new knobs without the emitter changing.
19
+ */
20
+ /** The two prefixes in the system. `bcms` is the renderer's own palette contract; `brand` is
21
+ * the tenant's compiled design system. Never mix them: a `--bcms-*` name is guaranteed to
22
+ * exist on every page, a `--brand-*` name only when the kit declares that group. */
23
+ declare const TOKEN_PREFIXES: readonly ["bcms", "brand"];
24
+ type TokenPrefix = (typeof TOKEN_PREFIXES)[number];
25
+ /**
26
+ * Fixed renderer palette. Emitted for every page that has a brand kit, so a consumer may use
27
+ * these unconditionally.
28
+ *
29
+ * ⚠️ `bg-surface` and `bg-muted` invert against the kit's own vocabulary — renderer `surface`
30
+ * is the PAGE background and renderer `muted` is the banded section. That inversion is the
31
+ * emitter's business (see brandCss), but it is named here so nobody re-derives it wrongly.
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"];
34
+ type BcmsPaletteToken = (typeof BCMS_PALETTE_TOKENS)[number];
35
+ /** Every `--brand-*` family, with the shape of its variable part. A family with `keyed: false`
36
+ * has exactly one name; the rest are open sets the kit decides. */
37
+ declare const BRAND_TOKEN_FAMILIES: {
38
+ readonly text: {
39
+ readonly keyed: true;
40
+ readonly unit: "px";
41
+ readonly from: "typeScale[].size";
42
+ };
43
+ readonly leading: {
44
+ readonly keyed: true;
45
+ readonly unit: "ratio";
46
+ readonly from: "typeScale[].lineHeight";
47
+ };
48
+ readonly weight: {
49
+ readonly keyed: true;
50
+ readonly unit: "number";
51
+ readonly from: "typeScale[].weight";
52
+ };
53
+ readonly space: {
54
+ readonly keyed: true;
55
+ readonly unit: "px";
56
+ readonly from: "spacing.steps[] (index-keyed) + `base`";
57
+ };
58
+ readonly shadow: {
59
+ readonly keyed: true;
60
+ readonly unit: "box-shadow";
61
+ readonly from: "elevation[]";
62
+ };
63
+ readonly duration: {
64
+ readonly keyed: true;
65
+ readonly unit: "ms";
66
+ readonly from: "motion.durations";
67
+ };
68
+ readonly color: {
69
+ readonly keyed: true;
70
+ readonly unit: "color";
71
+ readonly from: "extras[]";
72
+ };
73
+ readonly ease: {
74
+ readonly keyed: false;
75
+ readonly unit: "easing";
76
+ readonly from: "motion.ease";
77
+ };
78
+ };
79
+ type BrandTokenFamily = keyof typeof BRAND_TOKEN_FAMILIES;
80
+ /** `brandToken("space", 3)` → `--brand-space-3`. Throws on a key the grammar forbids, because
81
+ * the alternative is emitting a property no consumer can ever read. */
82
+ declare function brandToken(family: BrandTokenFamily, key?: string | number): string;
83
+ /** The one un-suffixed member of a keyed family: `--brand-space-base`. */
84
+ declare const BRAND_SPACE_BASE = "--brand-space-base";
85
+ /** Does `name` belong to the grammar at all? The check both sides run so a drift is a failing
86
+ * test rather than a property nothing reads. */
87
+ declare function isTokenName(name: string): boolean;
88
+ declare function assertTokenName(name: string): string;
89
+
90
+ export { BCMS_PALETTE_TOKENS, BRAND_SPACE_BASE, BRAND_TOKEN_FAMILIES, type BcmsPaletteToken, type BrandTokenFamily, TOKEN_PREFIXES, type TokenPrefix, assertTokenName, brandToken, isTokenName };
package/dist/tokens.js ADDED
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ // src/tokens.ts
4
+ var TOKEN_PREFIXES = ["bcms", "brand"];
5
+ var BCMS_PALETTE_TOKENS = [
6
+ "--bcms-bg-surface",
7
+ "--bcms-bg-muted",
8
+ "--bcms-bg-accent",
9
+ "--bcms-bg-dark",
10
+ "--bcms-text-muted",
11
+ "--bcms-accent",
12
+ // `primary` is the BUTTON fill, NOT body text. Binding it to the brand hue is the documented
13
+ // trap in brandCss — brand `primary` is the brand colour while the renderer's `primary` used
14
+ // to mean near-black body text, and mapping the two repaints every heading.
15
+ "--bcms-primary",
16
+ // The corner language, per element ROLE. Only the two roles BASE_CSS consumes are emitted:
17
+ // a `--bcms-radius-chip` with no consumer is dead flexibility wearing a token's clothes.
18
+ "--bcms-radius-card",
19
+ "--bcms-radius-control"
20
+ ];
21
+ var BRAND_TOKEN_FAMILIES = {
22
+ text: { keyed: true, unit: "px", from: "typeScale[].size" },
23
+ leading: { keyed: true, unit: "ratio", from: "typeScale[].lineHeight" },
24
+ weight: { keyed: true, unit: "number", from: "typeScale[].weight" },
25
+ space: { keyed: true, unit: "px", from: "spacing.steps[] (index-keyed) + `base`" },
26
+ shadow: { keyed: true, unit: "box-shadow", from: "elevation[]" },
27
+ duration: { keyed: true, unit: "ms", from: "motion.durations" },
28
+ color: { keyed: true, unit: "color", from: "extras[]" },
29
+ ease: { keyed: false, unit: "easing", from: "motion.ease" }
30
+ };
31
+ var KEY = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
32
+ function brandToken(family, key) {
33
+ const definition = BRAND_TOKEN_FAMILIES[family];
34
+ if (!definition.keyed) {
35
+ if (key !== void 0) throw new Error(`Token family "${family}" takes no key`);
36
+ return `--brand-${family}`;
37
+ }
38
+ const suffix = String(key ?? "");
39
+ if (!KEY.test(suffix)) throw new Error(`Invalid ${family} token key: ${JSON.stringify(key)}`);
40
+ return `--brand-${family}-${suffix}`;
41
+ }
42
+ var BRAND_SPACE_BASE = "--brand-space-base";
43
+ function isTokenName(name) {
44
+ if (BCMS_PALETTE_TOKENS.includes(name)) return true;
45
+ if (name === BRAND_SPACE_BASE) return true;
46
+ const match = /^--brand-([a-z]+)(?:-(.+))?$/.exec(name);
47
+ if (!match) return false;
48
+ const family = match[1];
49
+ const definition = BRAND_TOKEN_FAMILIES[family];
50
+ if (!definition) return false;
51
+ return definition.keyed ? Boolean(match[2] && KEY.test(match[2])) : match[2] === void 0;
52
+ }
53
+ function assertTokenName(name) {
54
+ if (!isTokenName(name)) throw new Error(`Not a BetterCMS design token: ${name}`);
55
+ return name;
56
+ }
57
+ export {
58
+ BCMS_PALETTE_TOKENS,
59
+ BRAND_SPACE_BASE,
60
+ BRAND_TOKEN_FAMILIES,
61
+ TOKEN_PREFIXES,
62
+ assertTokenName,
63
+ brandToken,
64
+ isTokenName
65
+ };
66
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +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 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,EACtD,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.5.0",
3
+ "version": "0.6.1",
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,
@@ -35,6 +35,11 @@
35
35
  "types": "./dist/resolve.d.ts",
36
36
  "import": "./dist/resolve.js",
37
37
  "default": "./dist/resolve.js"
38
+ },
39
+ "./tokens": {
40
+ "types": "./dist/tokens.d.ts",
41
+ "import": "./dist/tokens.js",
42
+ "default": "./dist/tokens.js"
38
43
  }
39
44
  },
40
45
  "scripts": {
@@ -47,9 +52,11 @@
47
52
  "add": "echo 'Use npx shadcn@latest add -p ../.. <component>'"
48
53
  },
49
54
  "dependencies": {
50
- "@bettercms-ai/types": "^1.9.0",
55
+ "@bettercms-ai/types": "^1.10.0",
51
56
  "clsx": "^2.1.1",
52
- "tailwind-merge": "^2.6.0"
57
+ "tailwind-merge": "^2.6.0",
58
+ "@portabletext/react": "^6.2.0",
59
+ "@bettercms-ai/richtext": "^0.2.0"
53
60
  },
54
61
  "peerDependencies": {
55
62
  "react": ">=18",