@bettercms-ai/kit 2.0.0-next.1 → 2.0.0-next.2

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.
Files changed (37) hide show
  1. package/dist/adapters/lossy.d.ts +4 -0
  2. package/dist/adapters/lossy.js +8 -2
  3. package/dist/adapters/lossy.js.map +1 -1
  4. package/dist/adapters/schema.js +28 -0
  5. package/dist/adapters/schema.js.map +1 -1
  6. package/dist/controls/rich-text/blocks.d.ts +6 -0
  7. package/dist/controls/rich-text/blocks.js +187 -0
  8. package/dist/controls/rich-text/blocks.js.map +1 -1
  9. package/dist/controls/rich-text/catalog.d.ts +2 -5
  10. package/dist/controls/rich-text/catalog.js +104 -6
  11. package/dist/controls/rich-text/catalog.js.map +1 -1
  12. package/dist/controls/rich-text/codec.d.ts +10 -1
  13. package/dist/controls/rich-text/codec.js +7 -0
  14. package/dist/controls/rich-text/codec.js.map +1 -1
  15. package/dist/controls/rich-text/control.js +1 -0
  16. package/dist/controls/rich-text/control.js.map +1 -1
  17. package/dist/controls/rich-text/dom.d.ts +79 -0
  18. package/dist/controls/rich-text/dom.js +159 -7
  19. package/dist/controls/rich-text/dom.js.map +1 -1
  20. package/dist/controls/rich-text/editor.d.ts +4 -2
  21. package/dist/controls/rich-text/editor.js +93 -3
  22. package/dist/controls/rich-text/editor.js.map +1 -1
  23. package/dist/controls/rich-text/ops.js +6 -0
  24. package/dist/controls/rich-text/ops.js.map +1 -1
  25. package/dist/controls/rich-text/toc.d.ts +3 -2
  26. package/dist/controls/rich-text/toc.js +1 -2
  27. package/dist/controls/rich-text/toc.js.map +1 -1
  28. package/dist/core/core.d.ts +1 -0
  29. package/dist/core/core.js +2 -0
  30. package/dist/core/core.js.map +1 -1
  31. package/dist/core/facets.d.ts +8 -0
  32. package/dist/core/facets.js +8 -0
  33. package/dist/core/facets.js.map +1 -1
  34. package/dist/library/kit-catalog.json +4 -2
  35. package/dist/schema/types.d.ts +8 -0
  36. package/dist/schema/types.js.map +1 -1
  37. package/package.json +2 -2
@@ -152,6 +152,12 @@ function insertBasic(root, id) {
152
152
  case "toggle":
153
153
  case "video":
154
154
  case "file":
155
+ case "gallery":
156
+ case "embed":
157
+ case "toc":
158
+ case "columns":
159
+ case "related":
160
+ case "author":
155
161
  insertIsland(root, id);
156
162
  return;
157
163
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ops.js","names":[],"sources":["../../../src/controls/rich-text/ops.ts"],"sourcesContent":["/**\n * RICH TEXT · pure DOM block operations on the editor surface (V2 HEAD controls/rich-text/ops.ts; no React, no\n * emitting). The editor wraps each one, then serializes and emits the new value.\n */\nimport { closestBlock, cloneModel, createIsland, defaultModel, esc, placeCaret, textBeforeCaret, topBlock, type IslandEl, type IslandKind, type IslandModels } from \"./dom\";\nimport type { RichTextFeature } from \"./codec\";\nimport type { BasicId } from \"./catalog\";\n\nconst emptyP = () => {\n const p = document.createElement(\"p\");\n p.innerHTML = \"<br>\";\n return p;\n};\nconst isWritable = (el: Element | null): el is HTMLElement => !!el && !el.hasAttribute(\"data-rt-island\") && el.tagName !== \"HR\";\n\n/** The writable line after a block, created when the block is last or followed by another rich block. */\nexport function ensureLineAfter(b: Element): HTMLElement {\n const next = b.nextElementSibling;\n if (isWritable(next)) return next;\n const p = emptyP();\n b.after(p);\n return p;\n}\n\nexport function duplicateBlock(b: HTMLElement): HTMLElement {\n let c: HTMLElement;\n if (b.hasAttribute(\"data-rt-island\")) {\n const k = b.getAttribute(\"data-rt-island\") as IslandKind;\n c = createIsland(k, cloneModel(k, (b as IslandEl).__rt as never));\n } else {\n c = b.cloneNode(true) as HTMLElement;\n c.removeAttribute(\"data-hint\");\n }\n b.after(c);\n return c;\n}\n\n/** Moves a block one step, keeping the caret where it was. Returns false at the edge. */\nexport function moveBlock(b: HTMLElement, dir: -1 | 1): boolean {\n const sib = dir < 0 ? b.previousElementSibling : b.nextElementSibling;\n if (!sib) return false;\n const s = window.getSelection();\n // ranges are live (a move collapses them), so keep the raw caret position\n const at = s && s.rangeCount ? { n: s.getRangeAt(0).startContainer, o: s.getRangeAt(0).startOffset } : null;\n const host = document.activeElement as HTMLElement | null;\n if (dir < 0) sib.before(b);\n else sib.after(b);\n if (at && s && b.contains(at.n)) {\n host?.focus({ preventScroll: true });\n const r = document.createRange();\n r.setStart(at.n, Math.min(at.o, at.n.nodeType === 3 ? (at.n as Text).length : at.n.childNodes.length));\n r.collapse(true);\n s.removeAllRanges();\n s.addRange(r);\n }\n return true;\n}\n\n/** Text / Heading / Quote / Code. Returns the new element, or null when nothing changed. */\nexport function turnInto(root: HTMLElement, b: HTMLElement | null, tag: string): HTMLElement | null {\n if (!b || b.parentNode !== root || b.hasAttribute(\"data-rt-island\") || b.tagName === \"HR\") return null;\n if (b.tagName.toLowerCase() === tag) return null;\n const inner =\n b.tagName === \"UL\" || b.tagName === \"OL\"\n ? Array.from(b.children).map((c) => c.innerHTML).join(\"<br>\")\n : b.tagName === \"PRE\"\n ? esc(b.textContent ?? \"\").replace(/\\n/g, \"<br>\")\n : b.innerHTML;\n const n = document.createElement(tag);\n n.innerHTML = tag === \"pre\" ? esc((b.textContent ?? \"\").replace(/​/g, \"\")) || \"<br>\" : inner || \"<br>\";\n b.replaceWith(n);\n root.focus({ preventScroll: true });\n placeCaret(n);\n return n;\n}\n\n/** An empty paragraph at the caret to build a block from (reuses the current one when it is empty). */\nexport function takeEmptyBlock(root: HTMLElement): HTMLElement {\n const s = window.getSelection();\n const anchor = s && s.rangeCount && root.contains(s.anchorNode) ? s.anchorNode : null;\n const li = anchor ? closestBlock(root, anchor) : null;\n let b = topBlock(root, anchor);\n if (li && li.tagName === \"LI\" && !li.textContent?.trim()) {\n const list = li.parentElement!;\n li.remove();\n if (!list.children.length) {\n const p = emptyP();\n list.replaceWith(p);\n b = p;\n } else b = list;\n }\n root.focus({ preventScroll: true });\n if (b && b.tagName === \"P\" && !b.textContent?.replace(/​/g, \"\").trim() && !b.querySelector(\"img\")) {\n b.innerHTML = \"<br>\";\n placeCaret(b);\n return b;\n }\n // an empty heading, quote or code line (e.g. \"/\" typed on one) is reused too, so no empty block is left behind\n if (b && /^(H[1-6]|BLOCKQUOTE|PRE)$/.test(b.tagName) && !b.textContent?.replace(/​/g, \"\").trim()) {\n const p = emptyP();\n b.replaceWith(p);\n placeCaret(p);\n return p;\n }\n const p = emptyP();\n if (b) b.after(p);\n else root.append(p);\n placeCaret(p);\n return p;\n}\n\nexport function insertIsland<K extends IslandKind>(root: HTMLElement, kind: K, model: IslandModels[K] = defaultModel(kind)): IslandEl {\n const target = takeEmptyBlock(root);\n const isl = createIsland(kind, model);\n isl.__rtFocus = true;\n target.before(isl);\n // keep the document tidy: reuse the line that already follows instead of leaving an empty one\n const next = target.nextElementSibling;\n if (isWritable(next)) {\n target.remove();\n placeCaret(next, false);\n } else placeCaret(target);\n return isl;\n}\n\n/** Inserts a basic block at the caret. */\nexport function insertBasic(root: HTMLElement, id: BasicId) {\n switch (id) {\n case \"p\":\n takeEmptyBlock(root);\n return;\n case \"h1\": case \"h2\": case \"h3\": case \"h4\": case \"h5\": case \"h6\":\n turnInto(root, takeEmptyBlock(root), id);\n return;\n case \"quote\":\n turnInto(root, takeEmptyBlock(root), \"blockquote\");\n return;\n case \"code\":\n turnInto(root, takeEmptyBlock(root), \"pre\");\n return;\n case \"ul\": case \"ol\": {\n const t = takeEmptyBlock(root);\n const l = document.createElement(id);\n l.innerHTML = \"<li><br></li>\";\n t.replaceWith(l);\n placeCaret(l.firstElementChild!);\n return;\n }\n case \"hr\": {\n const t = takeEmptyBlock(root);\n t.before(document.createElement(\"hr\"));\n placeCaret(t);\n return;\n }\n case \"image\": case \"table\": case \"pullquote\": case \"takeaways\": case \"callout\": case \"toggle\": case \"video\": case \"file\":\n insertIsland(root, id);\n return;\n }\n}\n\n/**\n * \"# \", \"> \", \"- \", \"1. \" and \"---\" at the start of a paragraph, each only when the field accepts what it makes.\n * Returns true when it converted.\n */\nexport function liveMarkdown(root: HTMLElement, data: string | null, allows: (f: RichTextFeature) => boolean = () => true): boolean {\n const s = window.getSelection();\n if (!s || !s.rangeCount || !s.isCollapsed) return false;\n const blk = topBlock(root, s.anchorNode);\n if (!blk || blk.tagName !== \"P\") return false;\n const before = textBeforeCaret(blk)?.replace(/ /g, \" \");\n if (before == null) return false;\n if (data === \"-\" && before === \"---\" && (blk.textContent ?? \"\").replace(/ /g, \" \") === \"---\") {\n if (!allows(\"hr\")) return false;\n const p = emptyP();\n blk.replaceWith(document.createElement(\"hr\"), p);\n placeCaret(p);\n return true;\n }\n if (data !== \" \") return false;\n const h = before.match(/^(#{1,6}) $/);\n let tag = h ? `h${h[1].length}` : before === \"> \" ? \"blockquote\" : null;\n let list = before === \"- \" || before === \"* \" ? \"ul\" : /^1[.)] $/.test(before) ? \"ol\" : null;\n if (tag && !allows(tag as RichTextFeature)) tag = null;\n if (list && !allows(list as RichTextFeature)) list = null;\n if (!tag && !list) return false;\n const r = document.createRange();\n r.selectNodeContents(blk);\n r.setEnd(s.anchorNode!, s.anchorOffset);\n r.deleteContents();\n const html = blk.textContent || blk.querySelector(\"img\") ? blk.innerHTML : \"<br>\";\n let target: HTMLElement;\n if (tag) {\n target = document.createElement(tag);\n target.innerHTML = html;\n blk.replaceWith(target);\n } else {\n const l = document.createElement(list!);\n target = document.createElement(\"li\");\n target.innerHTML = html;\n l.append(target);\n blk.replaceWith(l);\n }\n placeCaret(target, false);\n return true;\n}\n\n/** `text` typed between backticks becomes inline code. */\nexport function liveInlineCode(): boolean {\n const s = window.getSelection();\n if (!s || !s.rangeCount || !s.isCollapsed) return false;\n const r = s.getRangeAt(0);\n if (r.startContainer.nodeType !== 3 || r.startContainer.parentElement?.closest(\"code,pre\")) return false;\n const node = r.startContainer as Text;\n const m = node.data.slice(0, r.startOffset).match(/`([^`\\n]+)`$/);\n if (!m) return false;\n const del = document.createRange();\n del.setStart(node, r.startOffset - m[0].length);\n del.setEnd(node, r.startOffset);\n del.deleteContents();\n const code = document.createElement(\"code\");\n code.textContent = m[1];\n del.insertNode(code);\n const next = code.nextSibling;\n if (next && next.nodeType === 3 && next.textContent) {\n const c = document.createRange();\n c.setStart(next, 0);\n c.collapse(true);\n s.removeAllRanges();\n s.addRange(c);\n } else {\n const after = document.createTextNode(\"​\");\n code.after(after);\n placeCaret(after);\n }\n return true;\n}\n\n/* ── What a field accepts, applied to HTML that arrives from outside (paste, Markdown) ── */\nconst MARK_FEATURE: Record<string, RichTextFeature> = {\n STRONG: \"bold\", B: \"bold\", EM: \"italic\", I: \"italic\", U: \"underline\", S: \"strike\", STRIKE: \"strike\", DEL: \"strike\", CODE: \"code\", A: \"link\",\n};\nconst BLOCK_FEATURE: Record<string, RichTextFeature> = {\n H1: \"h1\", H2: \"h2\", H3: \"h3\", H4: \"h4\", H5: \"h5\", H6: \"h6\", BLOCKQUOTE: \"blockquote\", UL: \"ul\", OL: \"ol\", PRE: \"pre\", HR: \"hr\", FIGURE: \"image\", IMG: \"image\",\n};\nconst FEATURE_WORD: Partial<Record<RichTextFeature, string>> = {\n image: \"images\", hr: \"dividers\", pre: \"code blocks\", ul: \"bulleted lists\", ol: \"numbered lists\", blockquote: \"quotes\", link: \"links\",\n};\n\n/**\n * HTML with every element the field does not accept turned into what it does: a heading / quote / code block becomes a\n * paragraph, a list becomes one paragraph per item, a mark or link is unwrapped to its text, a divider or image is\n * removed. `dropped` names what was taken out entirely (said to the author, never silent).\n */\nexport function restrictHtml(html: string, allows: (f: RichTextFeature) => boolean): { html: string; dropped: string[] } {\n if (!html) return { html, dropped: [] };\n const doc = new DOMParser().parseFromString(`<body>${html}</body>`, \"text/html\");\n const dropped = new Set<string>();\n const walk = (el: Element) => {\n for (const ch of Array.from(el.children)) {\n walk(ch);\n const mark = MARK_FEATURE[ch.tagName];\n const block = BLOCK_FEATURE[ch.tagName];\n if (mark && !allows(mark)) ch.replaceWith(...Array.from(ch.childNodes));\n else if (block && !allows(block)) {\n if (block === \"hr\" || block === \"image\") {\n dropped.add(FEATURE_WORD[block]!);\n ch.remove();\n } else if (block === \"ul\" || block === \"ol\") {\n const ps = Array.from(ch.children).map((li) => {\n const p = doc.createElement(\"p\");\n p.innerHTML = li.innerHTML;\n return p;\n });\n ch.replaceWith(...ps);\n } else {\n const p = doc.createElement(\"p\");\n p.innerHTML = block === \"pre\" ? esc(ch.textContent ?? \"\").replace(/\\n/g, \"<br>\") : ch.innerHTML;\n ch.replaceWith(p);\n }\n }\n }\n };\n walk(doc.body);\n return { html: doc.body.innerHTML, dropped: [...dropped] };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ops.js","names":[],"sources":["../../../src/controls/rich-text/ops.ts"],"sourcesContent":["/**\n * RICH TEXT · pure DOM block operations on the editor surface (V2 HEAD controls/rich-text/ops.ts; no React, no\n * emitting). The editor wraps each one, then serializes and emits the new value.\n */\nimport { closestBlock, cloneModel, createIsland, defaultModel, esc, placeCaret, textBeforeCaret, topBlock, type IslandEl, type IslandKind, type IslandModels } from \"./dom\";\nimport type { RichTextFeature } from \"./codec\";\nimport type { BasicId } from \"./catalog\";\n\nconst emptyP = () => {\n const p = document.createElement(\"p\");\n p.innerHTML = \"<br>\";\n return p;\n};\nconst isWritable = (el: Element | null): el is HTMLElement => !!el && !el.hasAttribute(\"data-rt-island\") && el.tagName !== \"HR\";\n\n/** The writable line after a block, created when the block is last or followed by another rich block. */\nexport function ensureLineAfter(b: Element): HTMLElement {\n const next = b.nextElementSibling;\n if (isWritable(next)) return next;\n const p = emptyP();\n b.after(p);\n return p;\n}\n\nexport function duplicateBlock(b: HTMLElement): HTMLElement {\n let c: HTMLElement;\n if (b.hasAttribute(\"data-rt-island\")) {\n const k = b.getAttribute(\"data-rt-island\") as IslandKind;\n c = createIsland(k, cloneModel(k, (b as IslandEl).__rt as never));\n } else {\n c = b.cloneNode(true) as HTMLElement;\n c.removeAttribute(\"data-hint\");\n }\n b.after(c);\n return c;\n}\n\n/** Moves a block one step, keeping the caret where it was. Returns false at the edge. */\nexport function moveBlock(b: HTMLElement, dir: -1 | 1): boolean {\n const sib = dir < 0 ? b.previousElementSibling : b.nextElementSibling;\n if (!sib) return false;\n const s = window.getSelection();\n // ranges are live (a move collapses them), so keep the raw caret position\n const at = s && s.rangeCount ? { n: s.getRangeAt(0).startContainer, o: s.getRangeAt(0).startOffset } : null;\n const host = document.activeElement as HTMLElement | null;\n if (dir < 0) sib.before(b);\n else sib.after(b);\n if (at && s && b.contains(at.n)) {\n host?.focus({ preventScroll: true });\n const r = document.createRange();\n r.setStart(at.n, Math.min(at.o, at.n.nodeType === 3 ? (at.n as Text).length : at.n.childNodes.length));\n r.collapse(true);\n s.removeAllRanges();\n s.addRange(r);\n }\n return true;\n}\n\n/** Text / Heading / Quote / Code. Returns the new element, or null when nothing changed. */\nexport function turnInto(root: HTMLElement, b: HTMLElement | null, tag: string): HTMLElement | null {\n if (!b || b.parentNode !== root || b.hasAttribute(\"data-rt-island\") || b.tagName === \"HR\") return null;\n if (b.tagName.toLowerCase() === tag) return null;\n const inner =\n b.tagName === \"UL\" || b.tagName === \"OL\"\n ? Array.from(b.children).map((c) => c.innerHTML).join(\"<br>\")\n : b.tagName === \"PRE\"\n ? esc(b.textContent ?? \"\").replace(/\\n/g, \"<br>\")\n : b.innerHTML;\n const n = document.createElement(tag);\n n.innerHTML = tag === \"pre\" ? esc((b.textContent ?? \"\").replace(/​/g, \"\")) || \"<br>\" : inner || \"<br>\";\n b.replaceWith(n);\n root.focus({ preventScroll: true });\n placeCaret(n);\n return n;\n}\n\n/** An empty paragraph at the caret to build a block from (reuses the current one when it is empty). */\nexport function takeEmptyBlock(root: HTMLElement): HTMLElement {\n const s = window.getSelection();\n const anchor = s && s.rangeCount && root.contains(s.anchorNode) ? s.anchorNode : null;\n const li = anchor ? closestBlock(root, anchor) : null;\n let b = topBlock(root, anchor);\n if (li && li.tagName === \"LI\" && !li.textContent?.trim()) {\n const list = li.parentElement!;\n li.remove();\n if (!list.children.length) {\n const p = emptyP();\n list.replaceWith(p);\n b = p;\n } else b = list;\n }\n root.focus({ preventScroll: true });\n if (b && b.tagName === \"P\" && !b.textContent?.replace(/​/g, \"\").trim() && !b.querySelector(\"img\")) {\n b.innerHTML = \"<br>\";\n placeCaret(b);\n return b;\n }\n // an empty heading, quote or code line (e.g. \"/\" typed on one) is reused too, so no empty block is left behind\n if (b && /^(H[1-6]|BLOCKQUOTE|PRE)$/.test(b.tagName) && !b.textContent?.replace(/​/g, \"\").trim()) {\n const p = emptyP();\n b.replaceWith(p);\n placeCaret(p);\n return p;\n }\n const p = emptyP();\n if (b) b.after(p);\n else root.append(p);\n placeCaret(p);\n return p;\n}\n\nexport function insertIsland<K extends IslandKind>(root: HTMLElement, kind: K, model: IslandModels[K] = defaultModel(kind)): IslandEl {\n const target = takeEmptyBlock(root);\n const isl = createIsland(kind, model);\n isl.__rtFocus = true;\n target.before(isl);\n // keep the document tidy: reuse the line that already follows instead of leaving an empty one\n const next = target.nextElementSibling;\n if (isWritable(next)) {\n target.remove();\n placeCaret(next, false);\n } else placeCaret(target);\n return isl;\n}\n\n/** Inserts a basic block at the caret. */\nexport function insertBasic(root: HTMLElement, id: BasicId) {\n switch (id) {\n case \"p\":\n takeEmptyBlock(root);\n return;\n case \"h1\": case \"h2\": case \"h3\": case \"h4\": case \"h5\": case \"h6\":\n turnInto(root, takeEmptyBlock(root), id);\n return;\n case \"quote\":\n turnInto(root, takeEmptyBlock(root), \"blockquote\");\n return;\n case \"code\":\n turnInto(root, takeEmptyBlock(root), \"pre\");\n return;\n case \"ul\": case \"ol\": {\n const t = takeEmptyBlock(root);\n const l = document.createElement(id);\n l.innerHTML = \"<li><br></li>\";\n t.replaceWith(l);\n placeCaret(l.firstElementChild!);\n return;\n }\n case \"hr\": {\n const t = takeEmptyBlock(root);\n t.before(document.createElement(\"hr\"));\n placeCaret(t);\n return;\n }\n case \"image\": case \"table\": case \"pullquote\": case \"takeaways\": case \"callout\": case \"toggle\": case \"video\": case \"file\":\n case \"gallery\": case \"embed\": case \"toc\": case \"columns\": case \"related\": case \"author\":\n insertIsland(root, id);\n return;\n }\n}\n\n/**\n * \"# \", \"> \", \"- \", \"1. \" and \"---\" at the start of a paragraph, each only when the field accepts what it makes.\n * Returns true when it converted.\n */\nexport function liveMarkdown(root: HTMLElement, data: string | null, allows: (f: RichTextFeature) => boolean = () => true): boolean {\n const s = window.getSelection();\n if (!s || !s.rangeCount || !s.isCollapsed) return false;\n const blk = topBlock(root, s.anchorNode);\n if (!blk || blk.tagName !== \"P\") return false;\n const before = textBeforeCaret(blk)?.replace(/ /g, \" \");\n if (before == null) return false;\n if (data === \"-\" && before === \"---\" && (blk.textContent ?? \"\").replace(/ /g, \" \") === \"---\") {\n if (!allows(\"hr\")) return false;\n const p = emptyP();\n blk.replaceWith(document.createElement(\"hr\"), p);\n placeCaret(p);\n return true;\n }\n if (data !== \" \") return false;\n const h = before.match(/^(#{1,6}) $/);\n let tag = h ? `h${h[1].length}` : before === \"> \" ? \"blockquote\" : null;\n let list = before === \"- \" || before === \"* \" ? \"ul\" : /^1[.)] $/.test(before) ? \"ol\" : null;\n if (tag && !allows(tag as RichTextFeature)) tag = null;\n if (list && !allows(list as RichTextFeature)) list = null;\n if (!tag && !list) return false;\n const r = document.createRange();\n r.selectNodeContents(blk);\n r.setEnd(s.anchorNode!, s.anchorOffset);\n r.deleteContents();\n const html = blk.textContent || blk.querySelector(\"img\") ? blk.innerHTML : \"<br>\";\n let target: HTMLElement;\n if (tag) {\n target = document.createElement(tag);\n target.innerHTML = html;\n blk.replaceWith(target);\n } else {\n const l = document.createElement(list!);\n target = document.createElement(\"li\");\n target.innerHTML = html;\n l.append(target);\n blk.replaceWith(l);\n }\n placeCaret(target, false);\n return true;\n}\n\n/** `text` typed between backticks becomes inline code. */\nexport function liveInlineCode(): boolean {\n const s = window.getSelection();\n if (!s || !s.rangeCount || !s.isCollapsed) return false;\n const r = s.getRangeAt(0);\n if (r.startContainer.nodeType !== 3 || r.startContainer.parentElement?.closest(\"code,pre\")) return false;\n const node = r.startContainer as Text;\n const m = node.data.slice(0, r.startOffset).match(/`([^`\\n]+)`$/);\n if (!m) return false;\n const del = document.createRange();\n del.setStart(node, r.startOffset - m[0].length);\n del.setEnd(node, r.startOffset);\n del.deleteContents();\n const code = document.createElement(\"code\");\n code.textContent = m[1];\n del.insertNode(code);\n const next = code.nextSibling;\n if (next && next.nodeType === 3 && next.textContent) {\n const c = document.createRange();\n c.setStart(next, 0);\n c.collapse(true);\n s.removeAllRanges();\n s.addRange(c);\n } else {\n const after = document.createTextNode(\"​\");\n code.after(after);\n placeCaret(after);\n }\n return true;\n}\n\n/* ── What a field accepts, applied to HTML that arrives from outside (paste, Markdown) ── */\nconst MARK_FEATURE: Record<string, RichTextFeature> = {\n STRONG: \"bold\", B: \"bold\", EM: \"italic\", I: \"italic\", U: \"underline\", S: \"strike\", STRIKE: \"strike\", DEL: \"strike\", CODE: \"code\", A: \"link\",\n};\nconst BLOCK_FEATURE: Record<string, RichTextFeature> = {\n H1: \"h1\", H2: \"h2\", H3: \"h3\", H4: \"h4\", H5: \"h5\", H6: \"h6\", BLOCKQUOTE: \"blockquote\", UL: \"ul\", OL: \"ol\", PRE: \"pre\", HR: \"hr\", FIGURE: \"image\", IMG: \"image\",\n};\nconst FEATURE_WORD: Partial<Record<RichTextFeature, string>> = {\n image: \"images\", hr: \"dividers\", pre: \"code blocks\", ul: \"bulleted lists\", ol: \"numbered lists\", blockquote: \"quotes\", link: \"links\",\n};\n\n/**\n * HTML with every element the field does not accept turned into what it does: a heading / quote / code block becomes a\n * paragraph, a list becomes one paragraph per item, a mark or link is unwrapped to its text, a divider or image is\n * removed. `dropped` names what was taken out entirely (said to the author, never silent).\n */\nexport function restrictHtml(html: string, allows: (f: RichTextFeature) => boolean): { html: string; dropped: string[] } {\n if (!html) return { html, dropped: [] };\n const doc = new DOMParser().parseFromString(`<body>${html}</body>`, \"text/html\");\n const dropped = new Set<string>();\n const walk = (el: Element) => {\n for (const ch of Array.from(el.children)) {\n walk(ch);\n const mark = MARK_FEATURE[ch.tagName];\n const block = BLOCK_FEATURE[ch.tagName];\n if (mark && !allows(mark)) ch.replaceWith(...Array.from(ch.childNodes));\n else if (block && !allows(block)) {\n if (block === \"hr\" || block === \"image\") {\n dropped.add(FEATURE_WORD[block]!);\n ch.remove();\n } else if (block === \"ul\" || block === \"ol\") {\n const ps = Array.from(ch.children).map((li) => {\n const p = doc.createElement(\"p\");\n p.innerHTML = li.innerHTML;\n return p;\n });\n ch.replaceWith(...ps);\n } else {\n const p = doc.createElement(\"p\");\n p.innerHTML = block === \"pre\" ? esc(ch.textContent ?? \"\").replace(/\\n/g, \"<br>\") : ch.innerHTML;\n ch.replaceWith(p);\n }\n }\n }\n };\n walk(doc.body);\n return { html: doc.body.innerHTML, dropped: [...dropped] };\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { slugify } from "@bettercms-ai/types";
1
2
  import type { TypeSchema } from "../../schema/types";
2
3
  import type { IslandCtx } from "./blocks";
3
4
  import { type IslandEl } from "./dom";
@@ -14,8 +15,8 @@ export interface OutlineItem {
14
15
  text: string;
15
16
  depth: 2 | 3;
16
17
  }
17
- /** V2 article.ts:250-251. ponytail: richtext's one slugify replaces this when it lands (plan RT5); it must stay equal. */
18
- export declare const slugify: (s: string) => string;
18
+ /** V2 article.ts:250-251, the one heading-id rule the site stamps too (types heading-id.ts, plan RT5). */
19
+ export { slugify };
19
20
  /** The body's H2 and H3, top level only, in order, with stable de-duplicated ids. */
20
21
  export declare function outlineOf(root: ParentNode): OutlineItem[];
21
22
  export declare const tocVisible: (outline: OutlineItem[], depth: TocModel["depth"]) => OutlineItem[];
@@ -1,6 +1,7 @@
1
1
  import { cn } from "../../lib/cn.js";
2
2
  import { ArticlePopoverBlock } from "./article-shell.js";
3
3
  import { placeCaret } from "./dom.js";
4
+ import { slugify } from "@bettercms-ai/types";
4
5
  import * as React from "react";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
6
7
  //#region src/controls/rich-text/toc.tsx
@@ -50,8 +51,6 @@ var TOC_SCHEMA = {
50
51
  }]
51
52
  }]
52
53
  };
53
- /** V2 article.ts:250-251. ponytail: richtext's one slugify replaces this when it lands (plan RT5); it must stay equal. */
54
- var slugify = (s) => s.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "section";
55
54
  /** The body's H2 and H3, top level only, in order, with stable de-duplicated ids. */
56
55
  function outlineOf(root) {
57
56
  const used = /* @__PURE__ */ new Set();
@@ -1 +1 @@
1
- {"version":3,"file":"toc.js","names":[],"sources":["../../../src/controls/rich-text/toc.tsx"],"sourcesContent":["/**\n * RICH TEXT · Table of contents (kit-richtext-ui-spec.md §5): V2's article block, stored as the PT block object\n * `{ _type: \"tableOfContents\", title?, depth: \"2\" | \"3\" }`. The list is derived from the same body's top-level H2/H3,\n * never stored.\n *\n * form V2 article.ts:137-156: Title (max 40, default \"In this article\"), Headings H2 | H2 and H3 (segmented)\n * outline V2 article.ts:248-266: the body's top-level non-empty H2/H3 in order, ids slugified and de-duplicated\n * (x, x-2, …), the same ids the site stamps on its headings\n * preview V2 article-blocks.tsx:537-568: the nav as the reader sees it; each entry a button that scrolls the\n * heading to the centre and puts the caret in it; \"No headings yet.\" when there are none\n * store V2 article-blocks.tsx:45-66: one live outline per editor, so every TOC follows the headings as they are typed\n *\n * The block itself (card, chip, popover at its corner) is the shared article shell (article-shell.tsx, kit-40); it\n * opens on a fresh insert by focusing the card only (V2 article-blocks.tsx:131).\n */\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn\";\nimport type { TypeSchema } from \"../../schema/types\";\nimport { ArticlePopoverBlock } from \"./article-shell\";\nimport type { IslandCtx } from \"./blocks\";\nimport { placeCaret, type IslandEl } from \"./dom\";\n\nexport interface TocModel {\n title: string;\n depth: \"2\" | \"3\";\n}\nexport const TOC_LABEL = \"Table of contents\";\nexport const tocDefault = (): TocModel => ({ title: \"In this article\", depth: \"3\" });\n\n/** The popover form (V2 ARTICLE_SCHEMAS.toc). Two settings → the shell's popover (D54). */\nexport const TOC_SCHEMA: TypeSchema = {\n name: \"tableOfContents\",\n title: TOC_LABEL,\n kind: \"object\",\n icon: \"list-tree\",\n fields: [\n { name: \"title\", title: \"Title\", type: \"string\", validation: { max: 40 } },\n {\n name: \"depth\",\n title: \"Headings\",\n type: \"select\",\n layout: \"segmented\",\n initialValue: \"3\",\n options: [\n { value: \"2\", label: \"H2\" },\n { value: \"3\", label: \"H2 and H3\" },\n ],\n },\n ],\n};\n\n/* ── Outline ───────────────────────────────────────────────────────── */\nexport interface OutlineItem {\n id: string;\n text: string;\n depth: 2 | 3;\n}\n/** V2 article.ts:250-251. ponytail: richtext's one slugify replaces this when it lands (plan RT5); it must stay equal. */\nexport const slugify = (s: string) =>\n s.toLowerCase().normalize(\"NFKD\").replace(/[̀-ͯ]/g, \"\").replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\").slice(0, 60) || \"section\";\n\n/** The body's H2 and H3, top level only, in order, with stable de-duplicated ids. */\nexport function outlineOf(root: ParentNode): OutlineItem[] {\n const used = new Set<string>();\n const out: OutlineItem[] = [];\n root.querySelectorAll(\":scope > h2, :scope > h3\").forEach((h) => {\n const text = (h.textContent ?? \"\").replace(/​/g, \"\").trim();\n if (!text) return;\n let id = slugify(text);\n for (let n = 2; used.has(id); n++) id = `${slugify(text)}-${n}`;\n used.add(id);\n out.push({ id, text, depth: h.tagName === \"H2\" ? 2 : 3 });\n });\n return out;\n}\nexport const tocVisible = (outline: OutlineItem[], depth: TocModel[\"depth\"]) => outline.filter((o) => o.depth === 2 || depth === \"3\");\n\n/** One live outline per editor: the editor sets it after each input, every TOC preview subscribes. */\nexport interface OutlineStore {\n get: () => OutlineItem[];\n subscribe: (fn: () => void) => () => void;\n set: (next: OutlineItem[]) => void;\n}\nexport function createOutlineStore(): OutlineStore {\n let cur: OutlineItem[] = [];\n const subs = new Set<() => void>();\n return {\n get: () => cur,\n subscribe: (fn) => (subs.add(fn), () => void subs.delete(fn)),\n set: (next) => {\n if (JSON.stringify(next) === JSON.stringify(cur)) return;\n cur = next;\n subs.forEach((f) => f());\n },\n };\n}\nexport const NO_OUTLINE: OutlineStore = { get: () => [], subscribe: () => () => {}, set: () => {} };\n\n/* ── Preview (what the reader sees) ────────────────────────────────── */\n/** `host` is the TOC's island element; its parent is the editor surface whose headings it lists. */\nexport function TocPreview({ m, outline, host }: { m: TocModel; outline: OutlineStore; host: HTMLElement }) {\n const all = React.useSyncExternalStore(outline.subscribe, outline.get, outline.get);\n const items = tocVisible(all, m.depth);\n const go = (it: OutlineItem) => {\n const root = host.parentElement;\n if (!root) return;\n const hs = Array.from(root.querySelectorAll<HTMLElement>(\":scope > h2, :scope > h3\")).filter((h) => (h.textContent ?? \"\").replace(/​/g, \"\").trim());\n const h = hs[all.indexOf(it)];\n if (!h) return;\n h.scrollIntoView?.({ block: \"center\", behavior: \"smooth\" });\n root.focus({ preventScroll: true });\n placeCaret(h);\n };\n return (\n <nav aria-label={m.title || TOC_LABEL} className=\"rounded-kit-lg border border-kit-line bg-kit-surface px-4 py-3\">\n {m.title ? (\n <p data-field=\"title\" className=\"mb-1.5 text-kit-sm font-semibold text-kit-fg\">\n {m.title}\n </p>\n ) : null}\n {items.length ? (\n <ol className=\"m-0 flex list-none flex-col gap-0.5 p-0 text-kit-sm\">\n {items.map((it, i) => (\n <li key={`${it.id}-${i}`} className={cn(it.depth === 3 && \"pl-4\")}>\n <button type=\"button\" onClick={(e) => (e.stopPropagation(), go(it))} className=\"max-w-full truncate rounded-kit-sm text-left text-kit-fg-2 hover:text-kit-fg hover:underline\">\n {it.text}\n </button>\n </li>\n ))}\n </ol>\n ) : (\n <p className=\"text-kit-sm text-kit-fg-3\">No headings yet.</p>\n )}\n </nav>\n );\n}\n\n/* ── The block (V2 ArticleBlock, surface \"popover\") ────────────────── */\nexport interface TocBlockProps {\n m: TocModel;\n set: (fn: (m: TocModel) => TocModel, label?: string) => void;\n el: IslandEl;\n /** the editor's live outline rides the island context */\n ctx: IslandCtx & { outline?: OutlineStore };\n autoFocus: boolean;\n}\n\nexport function TocBlock({ m, set, el, ctx, autoFocus }: TocBlockProps) {\n return (\n <ArticlePopoverBlock<TocModel>\n label={TOC_LABEL}\n kind=\"toc\"\n schema={TOC_SCHEMA}\n m={m}\n set={set}\n el={el}\n ctx={ctx}\n autoFocus={autoFocus}\n openOnInsert={false}\n toForm={(x) => ({ title: x.title, depth: x.depth })}\n fromForm={(v) => ({ title: String(v.title ?? \"\"), depth: v.depth === \"2\" ? \"2\" : \"3\" })}\n >\n {() => <TocPreview m={m} outline={ctx.outline ?? NO_OUTLINE} host={el} />}\n </ArticlePopoverBlock>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"toc.js","names":[],"sources":["../../../src/controls/rich-text/toc.tsx"],"sourcesContent":["/**\n * RICH TEXT · Table of contents (kit-richtext-ui-spec.md §5): V2's article block, stored as the PT block object\n * `{ _type: \"tableOfContents\", title?, depth: \"2\" | \"3\" }`. The list is derived from the same body's top-level H2/H3,\n * never stored.\n *\n * form V2 article.ts:137-156: Title (max 40, default \"In this article\"), Headings H2 | H2 and H3 (segmented)\n * outline V2 article.ts:248-266: the body's top-level non-empty H2/H3 in order, ids slugified and de-duplicated\n * (x, x-2, …), the same ids the site stamps on its headings\n * preview V2 article-blocks.tsx:537-568: the nav as the reader sees it; each entry a button that scrolls the\n * heading to the centre and puts the caret in it; \"No headings yet.\" when there are none\n * store V2 article-blocks.tsx:45-66: one live outline per editor, so every TOC follows the headings as they are typed\n *\n * The block itself (card, chip, popover at its corner) is the shared article shell (article-shell.tsx, kit-40); it\n * opens on a fresh insert by focusing the card only (V2 article-blocks.tsx:131).\n */\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn\";\nimport { slugify } from \"@bettercms-ai/types\";\nimport type { TypeSchema } from \"../../schema/types\";\nimport { ArticlePopoverBlock } from \"./article-shell\";\nimport type { IslandCtx } from \"./blocks\";\nimport { placeCaret, type IslandEl } from \"./dom\";\n\nexport interface TocModel {\n title: string;\n depth: \"2\" | \"3\";\n}\nexport const TOC_LABEL = \"Table of contents\";\nexport const tocDefault = (): TocModel => ({ title: \"In this article\", depth: \"3\" });\n\n/** The popover form (V2 ARTICLE_SCHEMAS.toc). Two settings → the shell's popover (D54). */\nexport const TOC_SCHEMA: TypeSchema = {\n name: \"tableOfContents\",\n title: TOC_LABEL,\n kind: \"object\",\n icon: \"list-tree\",\n fields: [\n { name: \"title\", title: \"Title\", type: \"string\", validation: { max: 40 } },\n {\n name: \"depth\",\n title: \"Headings\",\n type: \"select\",\n layout: \"segmented\",\n initialValue: \"3\",\n options: [\n { value: \"2\", label: \"H2\" },\n { value: \"3\", label: \"H2 and H3\" },\n ],\n },\n ],\n};\n\n/* ── Outline ───────────────────────────────────────────────────────── */\nexport interface OutlineItem {\n id: string;\n text: string;\n depth: 2 | 3;\n}\n/** V2 article.ts:250-251, the one heading-id rule the site stamps too (types heading-id.ts, plan RT5). */\nexport { slugify };\n\n/** The body's H2 and H3, top level only, in order, with stable de-duplicated ids. */\nexport function outlineOf(root: ParentNode): OutlineItem[] {\n const used = new Set<string>();\n const out: OutlineItem[] = [];\n root.querySelectorAll(\":scope > h2, :scope > h3\").forEach((h) => {\n const text = (h.textContent ?? \"\").replace(/​/g, \"\").trim();\n if (!text) return;\n let id = slugify(text);\n for (let n = 2; used.has(id); n++) id = `${slugify(text)}-${n}`;\n used.add(id);\n out.push({ id, text, depth: h.tagName === \"H2\" ? 2 : 3 });\n });\n return out;\n}\nexport const tocVisible = (outline: OutlineItem[], depth: TocModel[\"depth\"]) => outline.filter((o) => o.depth === 2 || depth === \"3\");\n\n/** One live outline per editor: the editor sets it after each input, every TOC preview subscribes. */\nexport interface OutlineStore {\n get: () => OutlineItem[];\n subscribe: (fn: () => void) => () => void;\n set: (next: OutlineItem[]) => void;\n}\nexport function createOutlineStore(): OutlineStore {\n let cur: OutlineItem[] = [];\n const subs = new Set<() => void>();\n return {\n get: () => cur,\n subscribe: (fn) => (subs.add(fn), () => void subs.delete(fn)),\n set: (next) => {\n if (JSON.stringify(next) === JSON.stringify(cur)) return;\n cur = next;\n subs.forEach((f) => f());\n },\n };\n}\nexport const NO_OUTLINE: OutlineStore = { get: () => [], subscribe: () => () => {}, set: () => {} };\n\n/* ── Preview (what the reader sees) ────────────────────────────────── */\n/** `host` is the TOC's island element; its parent is the editor surface whose headings it lists. */\nexport function TocPreview({ m, outline, host }: { m: TocModel; outline: OutlineStore; host: HTMLElement }) {\n const all = React.useSyncExternalStore(outline.subscribe, outline.get, outline.get);\n const items = tocVisible(all, m.depth);\n const go = (it: OutlineItem) => {\n const root = host.parentElement;\n if (!root) return;\n const hs = Array.from(root.querySelectorAll<HTMLElement>(\":scope > h2, :scope > h3\")).filter((h) => (h.textContent ?? \"\").replace(/​/g, \"\").trim());\n const h = hs[all.indexOf(it)];\n if (!h) return;\n h.scrollIntoView?.({ block: \"center\", behavior: \"smooth\" });\n root.focus({ preventScroll: true });\n placeCaret(h);\n };\n return (\n <nav aria-label={m.title || TOC_LABEL} className=\"rounded-kit-lg border border-kit-line bg-kit-surface px-4 py-3\">\n {m.title ? (\n <p data-field=\"title\" className=\"mb-1.5 text-kit-sm font-semibold text-kit-fg\">\n {m.title}\n </p>\n ) : null}\n {items.length ? (\n <ol className=\"m-0 flex list-none flex-col gap-0.5 p-0 text-kit-sm\">\n {items.map((it, i) => (\n <li key={`${it.id}-${i}`} className={cn(it.depth === 3 && \"pl-4\")}>\n <button type=\"button\" onClick={(e) => (e.stopPropagation(), go(it))} className=\"max-w-full truncate rounded-kit-sm text-left text-kit-fg-2 hover:text-kit-fg hover:underline\">\n {it.text}\n </button>\n </li>\n ))}\n </ol>\n ) : (\n <p className=\"text-kit-sm text-kit-fg-3\">No headings yet.</p>\n )}\n </nav>\n );\n}\n\n/* ── The block (V2 ArticleBlock, surface \"popover\") ────────────────── */\nexport interface TocBlockProps {\n m: TocModel;\n set: (fn: (m: TocModel) => TocModel, label?: string) => void;\n el: IslandEl;\n /** the editor's live outline rides the island context */\n ctx: IslandCtx & { outline?: OutlineStore };\n autoFocus: boolean;\n}\n\nexport function TocBlock({ m, set, el, ctx, autoFocus }: TocBlockProps) {\n return (\n <ArticlePopoverBlock<TocModel>\n label={TOC_LABEL}\n kind=\"toc\"\n schema={TOC_SCHEMA}\n m={m}\n set={set}\n el={el}\n ctx={ctx}\n autoFocus={autoFocus}\n openOnInsert={false}\n toForm={(x) => ({ title: x.title, depth: x.depth })}\n fromForm={(v) => ({ title: String(v.title ?? \"\"), depth: v.depth === \"2\" ? \"2\" : \"3\" })}\n >\n {() => <TocPreview m={m} outline={ctx.outline ?? NO_OUTLINE} host={el} />}\n </ArticlePopoverBlock>\n );\n}\n"],"mappings":"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"}
@@ -15,6 +15,7 @@
15
15
  * - a list's itemNoun equal to the engine's fallback (lib/schema.ts itemNoun) is the absent one
16
16
  * - a fieldset a field names but the schema does not declare is the declared { name, title: name } (engine/field-list
17
17
  * arrange), appended after the declared ones in first-use order
18
+ * - a rich text's references naming neither posts nor authors are the absent ones
18
19
  * - a rich text's features are read through types normaliseRichTextFeatures (an alias is its names: `headings` is
19
20
  * h1–h6), each name once; naming every RICHTEXT_FEATURES entry, in any order, is the absent list (V2 offers them all)
20
21
  */
package/dist/core/core.js CHANGED
@@ -21,6 +21,7 @@ import { RICHTEXT_FEATURES, canonicalJson, normaliseRichTextFeatures } from "@be
21
21
  * - a list's itemNoun equal to the engine's fallback (lib/schema.ts itemNoun) is the absent one
22
22
  * - a fieldset a field names but the schema does not declare is the declared { name, title: name } (engine/field-list
23
23
  * arrange), appended after the declared ones in first-use order
24
+ * - a rich text's references naming neither posts nor authors are the absent ones
24
25
  * - a rich text's features are read through types normaliseRichTextFeatures (an alias is its names: `headings` is
25
26
  * h1–h6), each name once; naming every RICHTEXT_FEATURES entry, in any order, is the absent list (V2 offers them all)
26
27
  */
@@ -45,6 +46,7 @@ function foldField(f) {
45
46
  ...out,
46
47
  itemNoun: void 0
47
48
  })) delete out.itemNoun;
49
+ if (out.references && out.references.posts === void 0 && out.references.authors === void 0) delete out.references;
48
50
  if (Array.isArray(out.features)) {
49
51
  const { features, unknown } = normaliseRichTextFeatures(out.features.filter((x) => typeof x === "string"));
50
52
  const names = [.../* @__PURE__ */ new Set([...features, ...unknown])];
@@ -1 +1 @@
1
- {"version":3,"file":"core.js","names":[],"sources":["../../src/core/core.ts"],"sourcesContent":["/**\n * THE CORE SCHEMA `C` (P2 C1): one canonical, versioned form of a V2 schema that both directions meet in.\n *\n * normV2: V2 → C canonical JSON (sorted keys, no undefined) of the V2 schema, with V2's own equivalences folded\n * embedV2: C → V2 the schema C holds\n * embedProd: C → prod the prod adapter (fromTypeSchema, the façade) on embedV2(c)\n * normProd: prod → C normV2 of what the prod schema reads back as (toTypeSchema / propsToTypeSchema, the façades)\n *\n * Laws (PBT, src/core/__tests__): norm∘embed = id on C (P1); normProd(embedProd(c)) equals c except under the paths\n * the adapter names (P2); prod zod accepts embedProd(c) (P3); every named loss is excluded, pending or out of its bound\n * (P4); both normalisers are idempotent (P5); the hash is byte-stable (P6); one prod key per facet (P7); out-of-domain\n * prod keys survive a write (P8). The hash is `core1:` + sha256 of types' canonicalJson.\n *\n * V2's equivalences folded by normV2 (what the V2 engine draws the same, so C cannot tell apart):\n * - a list's itemNoun equal to the engine's fallback (lib/schema.ts itemNoun) is the absent one\n * - a fieldset a field names but the schema does not declare is the declared { name, title: name } (engine/field-list\n * arrange), appended after the declared ones in first-use order\n * - a rich text's features are read through types normaliseRichTextFeatures (an alias is its names: `headings` is\n * h1–h6), each name once; naming every RICHTEXT_FEATURES entry, in any order, is the absent list (V2 offers them all)\n */\nimport { fromTypeSchema, propsToTypeSchema, toTypeSchema, type AdapterTarget, type FromTypeSchemaResult, type ToTypeSchemaOptions } from \"../adapters/schema\";\nimport { isHostConfigKey, SERVER_CONFIG_KEYS, type CatalogItem, type LossyItem } from \"../adapters/lossy\";\nimport type { ProdComponent, ProdContentModel } from \"../adapters/prod-types\";\nimport { canonicalJson, normaliseRichTextFeatures, RICHTEXT_FEATURES, type RichTextFeature } from \"@bettercms-ai/types\";\nimport { itemNoun } from \"../lib/schema\";\nimport type { FieldSchema, TypeSchema } from \"../schema/types\";\nimport { facetOf, type FacetRow } from \"./facets\";\n\nexport const CORE_VERSION = 1 as const;\n\nexport interface CoreSchema {\n v: typeof CORE_VERSION;\n schema: TypeSchema;\n}\n\ntype Obj = Record<string, unknown>;\nconst isObj = (v: unknown): v is Obj => !!v && typeof v === \"object\" && !Array.isArray(v);\n\n/** Canonical JSON value: object keys sorted, undefined dropped, arrays in their order (order is meaning). */\nexport function canonical<T>(v: T): T {\n if (Array.isArray(v)) return v.map(canonical) as T;\n if (!isObj(v)) return v;\n const out: Obj = {};\n for (const k of Object.keys(v).sort()) if (v[k] !== undefined) out[k] = canonical(v[k]);\n return out as T;\n}\n\n/** Every feature, canonical: RICHTEXT_FEATURES through normaliseRichTextFeatures, so no alias is ever in it. */\nconst ALL_RICH_FEATURES = normaliseRichTextFeatures(RICHTEXT_FEATURES).features;\n\nfunction foldField(f: FieldSchema): FieldSchema {\n const out: FieldSchema = { ...f };\n if (out.fields) out.fields = out.fields.map(foldField);\n if (Array.isArray(out.of)) out.of = out.of.map(foldField);\n if (out.slotSettings) out.slotSettings = out.slotSettings.map(foldField);\n if (out.itemNoun !== undefined && out.itemNoun === itemNoun({ ...out, itemNoun: undefined })) delete out.itemNoun;\n if (Array.isArray(out.features)) {\n const { features, unknown } = normaliseRichTextFeatures(out.features.filter((x) => typeof x === \"string\"));\n const names = [...new Set<string>([...features, ...unknown])];\n if (unknown.length === 0 && ALL_RICH_FEATURES.every((x) => names.includes(x))) delete out.features;\n // an unknown name stays (a schema read from JSON can hold one): the write names it, it is never dropped silently\n else out.features = names as RichTextFeature[];\n }\n return out;\n}\n\n/** The fieldsets fields name, depth first, the way the adapter visits them (fromTypeSchema propSetsFor / fieldsetsFor). */\nfunction namedSets(fields: FieldSchema[], out: string[] = []): string[] {\n for (const f of fields) {\n if (f.fieldset !== undefined && !out.includes(f.fieldset)) out.push(f.fieldset);\n if (f.fields) namedSets(f.fields, out);\n if (Array.isArray(f.of)) for (const i of f.of) if (i.fields) namedSets(i.fields, out);\n }\n return out;\n}\n\nexport function normV2(v2: TypeSchema): CoreSchema {\n const schema: TypeSchema = { ...v2, fields: v2.fields.map(foldField) };\n const declared = new Set((v2.fieldsets ?? []).map((s) => s.name));\n const undeclared = namedSets(v2.fields).filter((n) => !declared.has(n));\n if (undeclared.length) schema.fieldsets = [...(v2.fieldsets ?? []), ...undeclared.map((name) => ({ name, title: name }))];\n return { v: CORE_VERSION, schema: canonical(schema) };\n}\n\nexport const embedV2 = (c: CoreSchema): TypeSchema => structuredClone(c.schema);\n\nexport function embedProd(c: CoreSchema, lane: \"fields\"): FromTypeSchemaResult<ProdContentModel>;\nexport function embedProd(c: CoreSchema, lane: \"props\"): FromTypeSchemaResult<ProdComponent>;\nexport function embedProd(c: CoreSchema, lane: AdapterTarget): FromTypeSchemaResult<ProdContentModel | ProdComponent>;\nexport function embedProd(c: CoreSchema, lane: AdapterTarget) {\n return lane === \"fields\" ? fromTypeSchema(embedV2(c), \"fields\") : fromTypeSchema(embedV2(c), \"props\");\n}\n\nexport function normProd(prod: ProdContentModel | ProdComponent, lane: AdapterTarget, opts: ToTypeSchemaOptions = {}): CoreSchema {\n return normV2(lane === \"fields\" ? toTypeSchema(prod as ProdContentModel, opts) : propsToTypeSchema(prod as ProdComponent, opts));\n}\n\n/** Byte-stable serialisation of C: types' canonicalJson (the bytes the backend's schema_hash is taken over, same rules). */\nexport const coreJson = (c: CoreSchema): string => canonicalJson(c);\n\n/**\n * A Core hash's prefix: `core<v>:`. Disjoint from the backend's `schema_hash` prefix (`v<n>:`, taken over a stored\n * definition, types' schemaHashInput), so a Core hash can never be read as, or compared with, a stored one.\n */\nexport const CORE_HASH_PREFIX = `core${CORE_VERSION}:` as const;\n\n/** CORE_HASH_PREFIX + sha256 of coreJson (Web Crypto: the same in the browser, Node and Bun). */\nexport async function coreHash(c: CoreSchema): Promise<string> {\n const bytes = new Uint8Array(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(coreJson(c))));\n return `${CORE_HASH_PREFIX}${[...bytes].map((b) => b.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\n/** The leaf paths where two JSON values differ (\"/fields/0/title\"); a missing object or list compares as an empty one. */\nexport function leafDiff(a: unknown, b: unknown, path = \"\", out: string[] = []): string[] {\n const obj = (x: unknown) => x === undefined || isObj(x);\n const arr = (x: unknown) => x === undefined || Array.isArray(x);\n if ((isObj(a) || isObj(b)) && obj(a) && obj(b)) {\n const A = (a ?? {}) as Obj;\n const B = (b ?? {}) as Obj;\n for (const k of new Set([...Object.keys(A), ...Object.keys(B)])) leafDiff(A[k], B[k], `${path}/${k}`, out);\n } else if ((Array.isArray(a) || Array.isArray(b)) && arr(a) && arr(b)) {\n const A = (a ?? []) as unknown[];\n const B = (b ?? []) as unknown[];\n for (let i = 0; i < Math.max(A.length, B.length); i++) leafDiff(A[i], B[i], `${path}/${i}`, out);\n } else if (JSON.stringify(a) !== JSON.stringify(b)) out.push(path);\n return out;\n}\n\nconst under = (path: string, root: string) => path === root || path.startsWith(`${root}/`);\n\nexport interface CoreLossItem {\n path: string;\n feature: string;\n facet: FacetRow | undefined;\n}\n\nexport interface CoreRoundTrip {\n prod: ProdContentModel | ProdComponent;\n back: CoreSchema;\n /** the paths where C and normProd(embedProd(C)) differ */\n diffs: string[];\n /** loss(c): each item the adapter names that has a difference under it (an item C's equivalences absorb is no loss) */\n loss: CoreLossItem[];\n /** a difference no named item covers: a silent drop (P2 fails on any) */\n unreported: string[];\n}\n\n/** C through prod and back, against the lane's read: the oracle P2 and P4 assert on. */\nexport function roundTripCore(c: CoreSchema, lane: AdapterTarget): CoreRoundTrip {\n const r = embedProd(c, lane);\n const back = normProd(r.schema, lane, { blockModels: r.blockModels });\n // the props lane reads each top-level field's default-group placement back explicitly: compare it as such\n const want = lane === \"props\" ? normV2(withDefaultGroups(c.schema)).schema : c.schema;\n const diffs = leafDiff(want, back.schema);\n const named: (LossyItem | CatalogItem)[] = [...r.lossy, ...r.catalog];\n return {\n prod: r.schema,\n back,\n diffs,\n loss: named.filter((l) => diffs.some((d) => under(d, l.path))).map((l) => ({ path: l.path, feature: l.feature, facet: facetOf(l.feature) })),\n unreported: diffs.filter((d) => !named.some((l) => under(d, l.path))),\n };\n}\n\nfunction withDefaultGroups(s: TypeSchema): TypeSchema {\n const d = (s.groups?.find((g) => g.default) ?? s.groups?.[0])?.name;\n return d === undefined ? s : { ...s, fields: s.fields.map((f) => (f.group === undefined ? { ...f, group: d } : f)) };\n}\n\n/* ── P8: out-of-domain prod keys survive a write ── */\n\ntype ProdDef = Obj & { key?: string; type?: string; config?: Obj; fields?: ProdDef[] };\nconst OUT_OF_DOMAIN_KEYS = [\"target\", \"stale\", \"origin\"] as const;\n\n/** One written definition with the out-of-domain keys of the stored one it replaces (same key) carried over. */\nfunction carryDef(written: ProdDef, stored: ProdDef | undefined): ProdDef {\n if (!stored) return written;\n const out: ProdDef = { ...written };\n for (const k of OUT_OF_DOMAIN_KEYS) if (stored[k] !== undefined && out[k] === undefined) out[k] = stored[k];\n const type = String(written.type ?? \"\");\n const host = Object.entries(stored.config ?? {}).filter(([k, v]) => v !== undefined && (isHostConfigKey(type, k) || SERVER_CONFIG_KEYS.has(k)) && out.config?.[k] === undefined);\n if (host.length || out.config) out.config = { ...(out.config ?? {}), ...Object.fromEntries(host) };\n // nested definitions: a model field's fields, a prop's config.fields\n if (Array.isArray(written.fields)) out.fields = carryList(written.fields, stored.fields);\n const cf = out.config?.fields;\n if (Array.isArray(cf)) out.config = { ...out.config, fields: carryList(cf as ProdDef[], stored.config?.fields as ProdDef[] | undefined) };\n return out;\n}\n\nconst carryList = (written: ProdDef[], stored: ProdDef[] | undefined) => written.map((w) => carryDef(w, stored?.find((s) => s.key === w.key)));\n\n/**\n * A prod definition written from C, with every out-of-domain key (facets.ts OUT_OF_DOMAIN: a prop's binding, host and\n * server config) the stored definition held carried over by key. What write_schema stores (C6).\n */\nexport function preserveOutOfDomain<T extends ProdContentModel | ProdComponent>(written: T, stored: T | undefined, lane: AdapterTarget): T {\n if (!stored) return written;\n const list = lane === \"fields\" ? \"fields\" : \"props\";\n const w = written as unknown as Obj;\n const s = stored as unknown as Obj;\n return { ...w, [list]: carryList((w[list] as ProdDef[]) ?? [], s[list] as ProdDef[] | undefined) } as unknown as T;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"core.js","names":[],"sources":["../../src/core/core.ts"],"sourcesContent":["/**\n * THE CORE SCHEMA `C` (P2 C1): one canonical, versioned form of a V2 schema that both directions meet in.\n *\n * normV2: V2 → C canonical JSON (sorted keys, no undefined) of the V2 schema, with V2's own equivalences folded\n * embedV2: C → V2 the schema C holds\n * embedProd: C → prod the prod adapter (fromTypeSchema, the façade) on embedV2(c)\n * normProd: prod → C normV2 of what the prod schema reads back as (toTypeSchema / propsToTypeSchema, the façades)\n *\n * Laws (PBT, src/core/__tests__): norm∘embed = id on C (P1); normProd(embedProd(c)) equals c except under the paths\n * the adapter names (P2); prod zod accepts embedProd(c) (P3); every named loss is excluded, pending or out of its bound\n * (P4); both normalisers are idempotent (P5); the hash is byte-stable (P6); one prod key per facet (P7); out-of-domain\n * prod keys survive a write (P8). The hash is `core1:` + sha256 of types' canonicalJson.\n *\n * V2's equivalences folded by normV2 (what the V2 engine draws the same, so C cannot tell apart):\n * - a list's itemNoun equal to the engine's fallback (lib/schema.ts itemNoun) is the absent one\n * - a fieldset a field names but the schema does not declare is the declared { name, title: name } (engine/field-list\n * arrange), appended after the declared ones in first-use order\n * - a rich text's references naming neither posts nor authors are the absent ones\n * - a rich text's features are read through types normaliseRichTextFeatures (an alias is its names: `headings` is\n * h1–h6), each name once; naming every RICHTEXT_FEATURES entry, in any order, is the absent list (V2 offers them all)\n */\nimport { fromTypeSchema, propsToTypeSchema, toTypeSchema, type AdapterTarget, type FromTypeSchemaResult, type ToTypeSchemaOptions } from \"../adapters/schema\";\nimport { isHostConfigKey, SERVER_CONFIG_KEYS, type CatalogItem, type LossyItem } from \"../adapters/lossy\";\nimport type { ProdComponent, ProdContentModel } from \"../adapters/prod-types\";\nimport { canonicalJson, normaliseRichTextFeatures, RICHTEXT_FEATURES, type RichTextFeature } from \"@bettercms-ai/types\";\nimport { itemNoun } from \"../lib/schema\";\nimport type { FieldSchema, TypeSchema } from \"../schema/types\";\nimport { facetOf, type FacetRow } from \"./facets\";\n\nexport const CORE_VERSION = 1 as const;\n\nexport interface CoreSchema {\n v: typeof CORE_VERSION;\n schema: TypeSchema;\n}\n\ntype Obj = Record<string, unknown>;\nconst isObj = (v: unknown): v is Obj => !!v && typeof v === \"object\" && !Array.isArray(v);\n\n/** Canonical JSON value: object keys sorted, undefined dropped, arrays in their order (order is meaning). */\nexport function canonical<T>(v: T): T {\n if (Array.isArray(v)) return v.map(canonical) as T;\n if (!isObj(v)) return v;\n const out: Obj = {};\n for (const k of Object.keys(v).sort()) if (v[k] !== undefined) out[k] = canonical(v[k]);\n return out as T;\n}\n\n/** Every feature, canonical: RICHTEXT_FEATURES through normaliseRichTextFeatures, so no alias is ever in it. */\nconst ALL_RICH_FEATURES = normaliseRichTextFeatures(RICHTEXT_FEATURES).features;\n\nfunction foldField(f: FieldSchema): FieldSchema {\n const out: FieldSchema = { ...f };\n if (out.fields) out.fields = out.fields.map(foldField);\n if (Array.isArray(out.of)) out.of = out.of.map(foldField);\n if (out.slotSettings) out.slotSettings = out.slotSettings.map(foldField);\n if (out.itemNoun !== undefined && out.itemNoun === itemNoun({ ...out, itemNoun: undefined })) delete out.itemNoun;\n if (out.references && out.references.posts === undefined && out.references.authors === undefined) delete out.references;\n if (Array.isArray(out.features)) {\n const { features, unknown } = normaliseRichTextFeatures(out.features.filter((x) => typeof x === \"string\"));\n const names = [...new Set<string>([...features, ...unknown])];\n if (unknown.length === 0 && ALL_RICH_FEATURES.every((x) => names.includes(x))) delete out.features;\n // an unknown name stays (a schema read from JSON can hold one): the write names it, it is never dropped silently\n else out.features = names as RichTextFeature[];\n }\n return out;\n}\n\n/** The fieldsets fields name, depth first, the way the adapter visits them (fromTypeSchema propSetsFor / fieldsetsFor). */\nfunction namedSets(fields: FieldSchema[], out: string[] = []): string[] {\n for (const f of fields) {\n if (f.fieldset !== undefined && !out.includes(f.fieldset)) out.push(f.fieldset);\n if (f.fields) namedSets(f.fields, out);\n if (Array.isArray(f.of)) for (const i of f.of) if (i.fields) namedSets(i.fields, out);\n }\n return out;\n}\n\nexport function normV2(v2: TypeSchema): CoreSchema {\n const schema: TypeSchema = { ...v2, fields: v2.fields.map(foldField) };\n const declared = new Set((v2.fieldsets ?? []).map((s) => s.name));\n const undeclared = namedSets(v2.fields).filter((n) => !declared.has(n));\n if (undeclared.length) schema.fieldsets = [...(v2.fieldsets ?? []), ...undeclared.map((name) => ({ name, title: name }))];\n return { v: CORE_VERSION, schema: canonical(schema) };\n}\n\nexport const embedV2 = (c: CoreSchema): TypeSchema => structuredClone(c.schema);\n\nexport function embedProd(c: CoreSchema, lane: \"fields\"): FromTypeSchemaResult<ProdContentModel>;\nexport function embedProd(c: CoreSchema, lane: \"props\"): FromTypeSchemaResult<ProdComponent>;\nexport function embedProd(c: CoreSchema, lane: AdapterTarget): FromTypeSchemaResult<ProdContentModel | ProdComponent>;\nexport function embedProd(c: CoreSchema, lane: AdapterTarget) {\n return lane === \"fields\" ? fromTypeSchema(embedV2(c), \"fields\") : fromTypeSchema(embedV2(c), \"props\");\n}\n\nexport function normProd(prod: ProdContentModel | ProdComponent, lane: AdapterTarget, opts: ToTypeSchemaOptions = {}): CoreSchema {\n return normV2(lane === \"fields\" ? toTypeSchema(prod as ProdContentModel, opts) : propsToTypeSchema(prod as ProdComponent, opts));\n}\n\n/** Byte-stable serialisation of C: types' canonicalJson (the bytes the backend's schema_hash is taken over, same rules). */\nexport const coreJson = (c: CoreSchema): string => canonicalJson(c);\n\n/**\n * A Core hash's prefix: `core<v>:`. Disjoint from the backend's `schema_hash` prefix (`v<n>:`, taken over a stored\n * definition, types' schemaHashInput), so a Core hash can never be read as, or compared with, a stored one.\n */\nexport const CORE_HASH_PREFIX = `core${CORE_VERSION}:` as const;\n\n/** CORE_HASH_PREFIX + sha256 of coreJson (Web Crypto: the same in the browser, Node and Bun). */\nexport async function coreHash(c: CoreSchema): Promise<string> {\n const bytes = new Uint8Array(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(coreJson(c))));\n return `${CORE_HASH_PREFIX}${[...bytes].map((b) => b.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\n/** The leaf paths where two JSON values differ (\"/fields/0/title\"); a missing object or list compares as an empty one. */\nexport function leafDiff(a: unknown, b: unknown, path = \"\", out: string[] = []): string[] {\n const obj = (x: unknown) => x === undefined || isObj(x);\n const arr = (x: unknown) => x === undefined || Array.isArray(x);\n if ((isObj(a) || isObj(b)) && obj(a) && obj(b)) {\n const A = (a ?? {}) as Obj;\n const B = (b ?? {}) as Obj;\n for (const k of new Set([...Object.keys(A), ...Object.keys(B)])) leafDiff(A[k], B[k], `${path}/${k}`, out);\n } else if ((Array.isArray(a) || Array.isArray(b)) && arr(a) && arr(b)) {\n const A = (a ?? []) as unknown[];\n const B = (b ?? []) as unknown[];\n for (let i = 0; i < Math.max(A.length, B.length); i++) leafDiff(A[i], B[i], `${path}/${i}`, out);\n } else if (JSON.stringify(a) !== JSON.stringify(b)) out.push(path);\n return out;\n}\n\nconst under = (path: string, root: string) => path === root || path.startsWith(`${root}/`);\n\nexport interface CoreLossItem {\n path: string;\n feature: string;\n facet: FacetRow | undefined;\n}\n\nexport interface CoreRoundTrip {\n prod: ProdContentModel | ProdComponent;\n back: CoreSchema;\n /** the paths where C and normProd(embedProd(C)) differ */\n diffs: string[];\n /** loss(c): each item the adapter names that has a difference under it (an item C's equivalences absorb is no loss) */\n loss: CoreLossItem[];\n /** a difference no named item covers: a silent drop (P2 fails on any) */\n unreported: string[];\n}\n\n/** C through prod and back, against the lane's read: the oracle P2 and P4 assert on. */\nexport function roundTripCore(c: CoreSchema, lane: AdapterTarget): CoreRoundTrip {\n const r = embedProd(c, lane);\n const back = normProd(r.schema, lane, { blockModels: r.blockModels });\n // the props lane reads each top-level field's default-group placement back explicitly: compare it as such\n const want = lane === \"props\" ? normV2(withDefaultGroups(c.schema)).schema : c.schema;\n const diffs = leafDiff(want, back.schema);\n const named: (LossyItem | CatalogItem)[] = [...r.lossy, ...r.catalog];\n return {\n prod: r.schema,\n back,\n diffs,\n loss: named.filter((l) => diffs.some((d) => under(d, l.path))).map((l) => ({ path: l.path, feature: l.feature, facet: facetOf(l.feature) })),\n unreported: diffs.filter((d) => !named.some((l) => under(d, l.path))),\n };\n}\n\nfunction withDefaultGroups(s: TypeSchema): TypeSchema {\n const d = (s.groups?.find((g) => g.default) ?? s.groups?.[0])?.name;\n return d === undefined ? s : { ...s, fields: s.fields.map((f) => (f.group === undefined ? { ...f, group: d } : f)) };\n}\n\n/* ── P8: out-of-domain prod keys survive a write ── */\n\ntype ProdDef = Obj & { key?: string; type?: string; config?: Obj; fields?: ProdDef[] };\nconst OUT_OF_DOMAIN_KEYS = [\"target\", \"stale\", \"origin\"] as const;\n\n/** One written definition with the out-of-domain keys of the stored one it replaces (same key) carried over. */\nfunction carryDef(written: ProdDef, stored: ProdDef | undefined): ProdDef {\n if (!stored) return written;\n const out: ProdDef = { ...written };\n for (const k of OUT_OF_DOMAIN_KEYS) if (stored[k] !== undefined && out[k] === undefined) out[k] = stored[k];\n const type = String(written.type ?? \"\");\n const host = Object.entries(stored.config ?? {}).filter(([k, v]) => v !== undefined && (isHostConfigKey(type, k) || SERVER_CONFIG_KEYS.has(k)) && out.config?.[k] === undefined);\n if (host.length || out.config) out.config = { ...(out.config ?? {}), ...Object.fromEntries(host) };\n // nested definitions: a model field's fields, a prop's config.fields\n if (Array.isArray(written.fields)) out.fields = carryList(written.fields, stored.fields);\n const cf = out.config?.fields;\n if (Array.isArray(cf)) out.config = { ...out.config, fields: carryList(cf as ProdDef[], stored.config?.fields as ProdDef[] | undefined) };\n return out;\n}\n\nconst carryList = (written: ProdDef[], stored: ProdDef[] | undefined) => written.map((w) => carryDef(w, stored?.find((s) => s.key === w.key)));\n\n/**\n * A prod definition written from C, with every out-of-domain key (facets.ts OUT_OF_DOMAIN: a prop's binding, host and\n * server config) the stored definition held carried over by key. What write_schema stores (C6).\n */\nexport function preserveOutOfDomain<T extends ProdContentModel | ProdComponent>(written: T, stored: T | undefined, lane: AdapterTarget): T {\n if (!stored) return written;\n const list = lane === \"fields\" ? \"fields\" : \"props\";\n const w = written as unknown as Obj;\n const s = stored as unknown as Obj;\n return { ...w, [list]: carryList((w[list] as ProdDef[]) ?? [], s[list] as ProdDef[] | undefined) } as unknown as T;\n}\n"],"mappings":"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"}
@@ -341,6 +341,14 @@ export declare const FACETS: {
341
341
  readonly "preview icon": FacetRow;
342
342
  readonly "preview path": FacetRow;
343
343
  readonly "unsupported key": FacetRow;
344
+ readonly "richtext references": {
345
+ readonly row: "B RT8";
346
+ readonly carrier: "value";
347
+ readonly props: readonly ["config.references"];
348
+ readonly fields: readonly ["config.references"];
349
+ readonly bound: "REFERENCES_MODEL_ID";
350
+ readonly status: "landed";
351
+ };
344
352
  readonly "richtext features": {
345
353
  readonly row: "B RT0";
346
354
  readonly carrier: "value";
@@ -324,6 +324,14 @@ var FACETS = {
324
324
  "preview icon": corpusFix("ui.preview.icon", "ui.preview.icon", "DOCK_ICON_NAMES"),
325
325
  "preview path": corpusFix("ui.preview.{title,subtitle,media}", "ui.preview.{title,subtitle,media}", "PREVIEW_PATH"),
326
326
  "unsupported key": corpusFix("—", "—", "V2_SCHEMA_KEYS"),
327
+ "richtext references": {
328
+ row: "B RT8",
329
+ carrier: "value",
330
+ props: ["config.references"],
331
+ fields: ["config.references"],
332
+ bound: "REFERENCES_MODEL_ID",
333
+ status: "landed"
334
+ },
327
335
  "richtext features": {
328
336
  row: "B RT0",
329
337
  carrier: "value",
@@ -1 +1 @@
1
- {"version":3,"file":"facets.js","names":[],"sources":["../../src/core/facets.ts"],"sourcesContent":["/**\n * THE FACET TABLE (kit-facet-table.v3.md, G0b closed 2026-10-23): every V2 schema facet the adapter can name, with the\n * ONE prod key per lane that stores it. `satisfies Record<V2Facet, FacetRow>` makes it total: a feature added to\n * FEATURES or CATALOG (adapters/lossy.ts) without a row here fails tsc.\n *\n * Carriers are `value` (an explicit stored key) and `excluded` (owner-signed). A `bound` names the one shared constant\n * the V2 authoring rule and the prod zod rule both read: a value inside it is carried, one outside it is refused with\n * the facet's name. `status` is \"landed\" when the adapter carries the facet today, else the step that lands it.\n * Prod-only keys V2 has no concept of are not facets: OUT_OF_DOMAIN, preserved on every write (PBT P8).\n */\nimport type { CatalogFeature, LossyFeature } from \"../adapters/lossy\";\n\n/** Prod keys with no V2 concept (facet table §B.1): never written from V2, carried through a write from the stored definition. */\nexport const OUT_OF_DOMAIN = {\n \"prop target\": \"a prop's binding: props[].target (+ stale, origin)\",\n \"host config\": \"HOST_CONFIG_KEYS + SERVER_CONFIG_KEYS in a field's or prop's config (adapters/lossy.ts)\",\n} as const satisfies Partial<Record<CatalogFeature, string>>;\nexport type OutOfDomainFeature = keyof typeof OUT_OF_DOMAIN;\n\nexport type V2Facet = Exclude<LossyFeature | CatalogFeature, OutOfDomainFeature>;\n\nexport interface FacetRow {\n /** facet table v3: an A row number, or the B group */\n row: string;\n carrier: \"value\" | \"excluded\";\n /** the prod key(s) per lane, one per shape the facet takes; [] = the facet does not arise in that lane */\n props: readonly string[];\n fields: readonly string[];\n /** the shared constant: inside it the facet is carried, outside it the facet is refused by name */\n bound?: string;\n status: \"landed\" | `pending:${string}`;\n /** a facet stored at another facet's key (kind:page is the shape facet's value \"page\") */\n foldedInto?: V2Facet;\n}\n\nconst corpusFix = (props: string, fields: string, bound: string): FacetRow => ({ row: \"B corpus-fix\", carrier: \"value\", props: [props], fields: [fields], bound, status: \"landed\" });\n\nexport const FACETS = {\n /* ── A: hit by the corpus ── */\n ai: { row: \"A1\", carrier: \"value\", props: [\"props[].ai\"], fields: [\"fields[].ai\"], status: \"pending:C3\" },\n category: { row: \"A2\", carrier: \"value\", props: [\"components.category\"], fields: [\"content_models.v2.category\"], bound: \"CATEGORY_ORDER\", status: \"pending:C3+C5\" },\n variants: {\n row: \"A3\",\n carrier: \"value\",\n props: [\"component_variant_groups (+ components.variant_group_id, variant_input_mappings; order/icon/description → component_variant_groups.v2.variants)\"],\n fields: [\"fields[_variant] select + showIf {field:_variant}; content_models.v2.variants{slug → {icon, description}}\"],\n status: \"pending:C4\",\n },\n \"block icon\": { row: \"A4\", carrier: \"value\", props: [\"components.icon\"], fields: [\"content_models.icon\"], bound: \"DOCK_ICON_NAMES\", status: \"pending:C1-model-settings\" },\n \"document preview\": { row: \"A5\", carrier: \"value\", props: [\"components.preview\"], fields: [\"content_models.list_preview\"], status: \"pending:C3\" },\n \"document preview path\": { row: \"B catalog\", carrier: \"value\", props: [\"components.preview (path rule widened)\"], fields: [], status: \"pending:C3\" },\n \"kind:global\": { row: \"A6\", carrier: \"value\", props: [\"components.v2.shape\"], fields: [\"content_models.v2.shape\"], status: \"pending:C5\" },\n \"kind:entry (props)\": { row: \"A6\", carrier: \"value\", props: [\"components.v2.shape\"], fields: [], status: \"pending:C5\", foldedInto: \"kind:global\" },\n \"kind:block (fields)\": { row: \"A6\", carrier: \"value\", props: [], fields: [\"content_models.v2.shape\"], status: \"pending:C5\", foldedInto: \"kind:global\" },\n \"kind:page\": { row: \"A22\", carrier: \"value\", props: [], fields: [\"content_models.v2.shape\"], status: \"pending:C5\", foldedInto: \"kind:global\" },\n \"kind:object\": { row: \"A6\", carrier: \"value\", props: [], fields: [\"content_models.v2.shape\"], status: \"pending:C5\", foldedInto: \"kind:global\" },\n \"groups>3\": { row: \"A7\", carrier: \"value\", props: [\"components.tabs\"], fields: [\"content_models.fieldsets[kind:tab]\"], bound: \"TAB_MAX\", status: \"pending:C3+C8\" },\n \"item openMode\": {\n row: \"A8\",\n carrier: \"value\",\n props: [\"config.item.openMode (object item)\", \"config.itemTypes[i].openMode (polymorphic)\"],\n fields: [\"ui.blocks[slug].openMode (modular)\"],\n status: \"pending:C3\",\n },\n \"reference target\": { row: \"A9\", carrier: \"value\", props: [\"config.targets\"], fields: [\"config.targets\"], bound: \"REF_TARGET_KINDS\", status: \"landed\" },\n \"type:slot\": { row: \"A10\", carrier: \"value\", props: [\"props[] type slot (config.allow)\"], fields: [\"fields[] type slot (config.blockSlugs)\"], bound: \"SLOT_BLOCK_SLUGS_REQUIRED\", status: \"landed\" },\n \"block model field\": { row: \"A11\", carrier: \"value\", props: [], fields: [\"kind:'block' model fields[]\"], bound: \"BLOCK_FIELD_TYPES\", status: \"pending:G7\" },\n \"shopify reference\": { row: \"A12\", carrier: \"excluded\", props: [], fields: [], status: \"landed\" },\n \"type:table\": { row: \"A13\", carrier: \"value\", props: [\"props[] type table (config.mode data)\"], fields: [\"fields[] type table\"], status: \"landed\" },\n \"group (nested)\": { row: \"A14\", carrier: \"value\", props: [\"ui.tab {id}\"], fields: [\"ui.tab {id} (nested field)\"], bound: \"TAB_MAX\", status: \"landed\" },\n \"fieldset collision\": { row: \"A15\", carrier: \"value\", props: [\"ui.group {id}\"], fields: [\"fieldsetId\"], bound: \"TAB_FIELDSET_ID_UNIQUE\", status: \"landed\" },\n \"segmented fallback\": { row: \"A16\", carrier: \"value\", props: [\"ui.control segmented\"], fields: [\"ui.control segmented\"], bound: \"SEGMENTED_MAX\", status: \"pending:C3\" },\n \"fieldset (derived)\": { row: \"A17\", carrier: \"value\", props: [\"ui.group (prop group)\"], fields: [\"content_models.fieldsets\"], status: \"landed\" },\n \"itemNoun (derived)\": { row: \"A18\", carrier: \"value\", props: [\"ui.itemLabel\"], fields: [\"ui.itemTitle\"], status: \"landed\" },\n \"reference filter\": { row: \"A19\", carrier: \"value\", props: [\"config.filter\"], fields: [\"config.filter\"], bound: \"REFERENCE_FILTER_MAX\", status: \"landed\" },\n condition: { row: \"A20\", carrier: \"value\", props: [\"showIf (+ scope)\"], fields: [\"showIf (+ scope)\"], bound: \"SHOW_IF_SCOPES\", status: \"pending:C3\" },\n openMode: { row: \"A21\", carrier: \"value\", props: [\"props[].openMode\"], fields: [\"fields[].openMode\"], bound: \"OPEN_MODE_TYPES\", status: \"pending:C9\" },\n \"slot allow\": { row: \"A23\", carrier: \"value\", props: [\"config.allow\"], fields: [\"config.blockSlugs\"], bound: \"SLOT_ALLOW\", status: \"landed\" },\n\n /* ── B: in the code, not hit by the corpus ── */\n \"type:polymorphic-array\": { row: \"B structural\", carrier: \"value\", props: [\"props[] type table (config.mode types)\"], fields: [\"fields[] type modular\"], status: \"pending:C3\" },\n \"item chrome (types)\": { row: \"B structural\", carrier: \"value\", props: [\"config.itemTypes[i] {icon, preview}\"], fields: [\"ui.blocks[slug] {icon, preview}\"], status: \"pending:C3\" },\n \"type:primitive-array\": { row: \"B decided\", carrier: \"value\", props: [\"config.itemType (widened)\"], fields: [\"config.itemType (widened)\"], status: \"pending:C3\" },\n \"depth>10\": { row: \"B decided\", carrier: \"value\", props: [\"nesting\"], fields: [\"nesting\"], bound: \"DEPTH_MAX\", status: \"landed\" },\n slotSettings: { row: \"B decided (BUILD)\", carrier: \"value\", props: [\"config.settings: FieldDef[]\"], fields: [\"config.settings: FieldDef[]\"], bound: \"SLOT_SETTINGS_MAX\", status: \"pending:C3\" },\n \"block placement\": { row: \"B catalog\", carrier: \"value\", props: [\"components.v2.placement\"], fields: [\"content_models.v2.placement\"], status: \"pending:C5\" },\n \"seeded instance\": { row: \"B catalog\", carrier: \"excluded\", props: [], fields: [], status: \"landed\" },\n \"not applicable (props)\": { row: \"B catalog\", carrier: \"excluded\", props: [], fields: [], status: \"landed\" },\n \"type name\": corpusFix(\"components.slug\", \"content_models.slug\", \"SLUG_RE\"),\n \"group name>40\": corpusFix(\"components.tabs[].id\", \"content_models.fieldsets[].id\", \"TAB_NAME_MAX\"),\n \"group title\": corpusFix(\"components.tabs[].name\", \"content_models.fieldsets[].name\", \"TAB_TITLE\"),\n \"group default\": corpusFix(\"components.tabs[0]\", \"content_models.fieldsets[0]\", \"TAB_DEFAULT_FIRST\"),\n \"group icon\": corpusFix(\"components.tabs[].icon\", \"content_models.fieldsets[].icon\", \"DOCK_ICON_NAMES\"),\n \"group name collision\": corpusFix(\"components.tabs[].name\", \"content_models.fieldsets[].name\", \"TAB_NAME_UNIQUE\"),\n \"prop tab limit\": corpusFix(\"components.tabs\", \"content_models.fieldsets\", \"PROP_TAB_MAX\"),\n \"fieldset spans groups\": corpusFix(\"ui.group.tab\", \"fieldsets[].tab\", \"FIELDSET_ONE_TAB\"),\n \"fieldset (props)\": corpusFix(\"ui.group (first prop)\", \"content_models.fieldsets\", \"PROP_GROUP_DEFINED_ON_USE\"),\n \"fieldset name>limit\": corpusFix(\"ui.group.label\", \"content_models.fieldsets[].name\", \"FIELDSET_NAME_MAX\"),\n \"duplicate name\": corpusFix(\"props[].key\", \"fields[].key\", \"KEY_UNIQUE\"),\n \"reserved name\": { row: \"B corpus-fix\", carrier: \"value\", props: [], fields: [\"fields[].key\"], bound: \"KEY_RESERVED + KEY_FORMAT\", status: \"landed\" },\n rows: corpusFix(\"ui.rows\", \"ui.rows\", \"UI_ROWS_MAX\"),\n synced: corpusFix(\"ui.synced\", \"ui.synced\", \"SYNCED_FROM_MAX\"),\n valueShape: corpusFix(\"ui.valueShape\", \"ui.valueShape\", \"VALUE_SHAPE_TYPES\"),\n \"default value\": corpusFix(\"props[].defaultValue\", \"fields[].defaultValue\", \"VALUE_SCHEMAS\"),\n icon: corpusFix(\"ui.icon\", \"ui.icon\", \"DOCK_ICON_NAMES\"),\n layout: corpusFix(\"ui.control / ui.layout\", \"ui.control / ui.layout\", \"CONTROL_TYPES\"),\n \"help>limit\": corpusFix(\"helpText / placeholder / helpMore\", \"helpText / ui.placeholder / helpMore\", \"HELP_CAPS\"),\n \"option icon\": corpusFix(\"options[].icon\", \"options[].icon\", \"DOCK_ICON_NAMES\"),\n \"option description\": corpusFix(\"options[].description\", \"options[].description\", \"OPTION_DESCRIPTION_MAX\"),\n access: corpusFix(\"ui.access\", \"ui.access\", \"ACCESS_CAPS\"),\n timezone: corpusFix(\"config.timezone\", \"config.timezone\", \"TIMEZONE\"),\n \"reference order\": corpusFix(\"config.contentModelId|allowedModelSlugs, config.targets\", \"config.contentModelId|allowedModelSlugs, config.targets\", \"REFERENCE_TO_ORDER\"),\n \"numeric unit\": corpusFix(\"config.unit\", \"config.unit\", \"NUMBER_UNIT_MAX\"),\n \"rating bounds\": corpusFix(\"config.min/max/step (rating)\", \"config.min/max/step (rating)\", \"RATING_BOUNDS\"),\n \"scale rules\": corpusFix(\"config.scale\", \"config.scale\", \"SCALE_RULES\"),\n \"validation.patternMessage\": corpusFix(\"config.patternMessage\", \"config.patternMessage\", \"PATTERN_MESSAGE_MAX\"),\n \"validation.pattern\": corpusFix(\"config.pattern\", \"config.pattern\", \"PATTERN_MAX\"),\n \"validation.length\": corpusFix(\"config.minLength/maxLength\", \"config.minLength/maxLength\", \"LENGTH_TYPES\"),\n \"validation.items\": corpusFix(\"config.minItems/maxItems\", \"config.minItems/maxItems\", \"LIST_ITEMS_CAP\"),\n \"validation.unique\": corpusFix(\"props[].unique\", \"fields[].unique\", \"UNIQUE_TYPES\"),\n \"validation.recommendedMax\": corpusFix(\"props[].recommendedMax\", \"fields[].recommendedMax\", \"RECOMMENDED_MAX\"),\n \"item identity\": corpusFix(\"ui.itemName / ui.itemTitle\", \"ui.itemName / ui.itemTitle\", \"ITEM_NAME_RE\"),\n \"item settings\": corpusFix(\"config.item\", \"config.item\", \"ARRAY_ITEM_SETTINGS\"),\n itemNoun: corpusFix(\"ui.itemLabel\", \"ui.itemLabel\", \"ITEM_LABEL_MAX\"),\n \"item icon\": corpusFix(\"ui.icon (list)\", \"ui.icon (list)\", \"DOCK_ICON_NAMES\"),\n \"preview icon\": corpusFix(\"ui.preview.icon\", \"ui.preview.icon\", \"DOCK_ICON_NAMES\"),\n \"preview path\": corpusFix(\"ui.preview.{title,subtitle,media}\", \"ui.preview.{title,subtitle,media}\", \"PREVIEW_PATH\"),\n \"unsupported key\": corpusFix(\"—\", \"—\", \"V2_SCHEMA_KEYS\"),\n\n /* ── RT0 (kit-richtext-blocks-plan.md): a rich text's features, written as the full V2 set when V2 names none ── */\n \"richtext features\": { row: \"B RT0\", carrier: \"value\", props: [\"config.features\"], fields: [\"config.features\"], bound: \"RICHTEXT_FEATURES\", status: \"landed\" },\n} as const satisfies Record<V2Facet, FacetRow>;\n\nexport const facetOf = (feature: string): FacetRow | undefined => (FACETS as Record<string, FacetRow>)[feature];\n\n/** The owner-signed exclusions: what may be lost by design (shopify until its pipeline, and two deliberate backend rules). */\nexport const EXCLUDED: readonly V2Facet[] = (Object.keys(FACETS) as V2Facet[]).filter((f) => FACETS[f].carrier === \"excluded\");\n"],"mappings":"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"}
1
+ {"version":3,"file":"facets.js","names":[],"sources":["../../src/core/facets.ts"],"sourcesContent":["/**\n * THE FACET TABLE (kit-facet-table.v3.md, G0b closed 2026-10-23): every V2 schema facet the adapter can name, with the\n * ONE prod key per lane that stores it. `satisfies Record<V2Facet, FacetRow>` makes it total: a feature added to\n * FEATURES or CATALOG (adapters/lossy.ts) without a row here fails tsc.\n *\n * Carriers are `value` (an explicit stored key) and `excluded` (owner-signed). A `bound` names the one shared constant\n * the V2 authoring rule and the prod zod rule both read: a value inside it is carried, one outside it is refused with\n * the facet's name. `status` is \"landed\" when the adapter carries the facet today, else the step that lands it.\n * Prod-only keys V2 has no concept of are not facets: OUT_OF_DOMAIN, preserved on every write (PBT P8).\n */\nimport type { CatalogFeature, LossyFeature } from \"../adapters/lossy\";\n\n/** Prod keys with no V2 concept (facet table §B.1): never written from V2, carried through a write from the stored definition. */\nexport const OUT_OF_DOMAIN = {\n \"prop target\": \"a prop's binding: props[].target (+ stale, origin)\",\n \"host config\": \"HOST_CONFIG_KEYS + SERVER_CONFIG_KEYS in a field's or prop's config (adapters/lossy.ts)\",\n} as const satisfies Partial<Record<CatalogFeature, string>>;\nexport type OutOfDomainFeature = keyof typeof OUT_OF_DOMAIN;\n\nexport type V2Facet = Exclude<LossyFeature | CatalogFeature, OutOfDomainFeature>;\n\nexport interface FacetRow {\n /** facet table v3: an A row number, or the B group */\n row: string;\n carrier: \"value\" | \"excluded\";\n /** the prod key(s) per lane, one per shape the facet takes; [] = the facet does not arise in that lane */\n props: readonly string[];\n fields: readonly string[];\n /** the shared constant: inside it the facet is carried, outside it the facet is refused by name */\n bound?: string;\n status: \"landed\" | `pending:${string}`;\n /** a facet stored at another facet's key (kind:page is the shape facet's value \"page\") */\n foldedInto?: V2Facet;\n}\n\nconst corpusFix = (props: string, fields: string, bound: string): FacetRow => ({ row: \"B corpus-fix\", carrier: \"value\", props: [props], fields: [fields], bound, status: \"landed\" });\n\nexport const FACETS = {\n /* ── A: hit by the corpus ── */\n ai: { row: \"A1\", carrier: \"value\", props: [\"props[].ai\"], fields: [\"fields[].ai\"], status: \"pending:C3\" },\n category: { row: \"A2\", carrier: \"value\", props: [\"components.category\"], fields: [\"content_models.v2.category\"], bound: \"CATEGORY_ORDER\", status: \"pending:C3+C5\" },\n variants: {\n row: \"A3\",\n carrier: \"value\",\n props: [\"component_variant_groups (+ components.variant_group_id, variant_input_mappings; order/icon/description → component_variant_groups.v2.variants)\"],\n fields: [\"fields[_variant] select + showIf {field:_variant}; content_models.v2.variants{slug → {icon, description}}\"],\n status: \"pending:C4\",\n },\n \"block icon\": { row: \"A4\", carrier: \"value\", props: [\"components.icon\"], fields: [\"content_models.icon\"], bound: \"DOCK_ICON_NAMES\", status: \"pending:C1-model-settings\" },\n \"document preview\": { row: \"A5\", carrier: \"value\", props: [\"components.preview\"], fields: [\"content_models.list_preview\"], status: \"pending:C3\" },\n \"document preview path\": { row: \"B catalog\", carrier: \"value\", props: [\"components.preview (path rule widened)\"], fields: [], status: \"pending:C3\" },\n \"kind:global\": { row: \"A6\", carrier: \"value\", props: [\"components.v2.shape\"], fields: [\"content_models.v2.shape\"], status: \"pending:C5\" },\n \"kind:entry (props)\": { row: \"A6\", carrier: \"value\", props: [\"components.v2.shape\"], fields: [], status: \"pending:C5\", foldedInto: \"kind:global\" },\n \"kind:block (fields)\": { row: \"A6\", carrier: \"value\", props: [], fields: [\"content_models.v2.shape\"], status: \"pending:C5\", foldedInto: \"kind:global\" },\n \"kind:page\": { row: \"A22\", carrier: \"value\", props: [], fields: [\"content_models.v2.shape\"], status: \"pending:C5\", foldedInto: \"kind:global\" },\n \"kind:object\": { row: \"A6\", carrier: \"value\", props: [], fields: [\"content_models.v2.shape\"], status: \"pending:C5\", foldedInto: \"kind:global\" },\n \"groups>3\": { row: \"A7\", carrier: \"value\", props: [\"components.tabs\"], fields: [\"content_models.fieldsets[kind:tab]\"], bound: \"TAB_MAX\", status: \"pending:C3+C8\" },\n \"item openMode\": {\n row: \"A8\",\n carrier: \"value\",\n props: [\"config.item.openMode (object item)\", \"config.itemTypes[i].openMode (polymorphic)\"],\n fields: [\"ui.blocks[slug].openMode (modular)\"],\n status: \"pending:C3\",\n },\n \"reference target\": { row: \"A9\", carrier: \"value\", props: [\"config.targets\"], fields: [\"config.targets\"], bound: \"REF_TARGET_KINDS\", status: \"landed\" },\n \"type:slot\": { row: \"A10\", carrier: \"value\", props: [\"props[] type slot (config.allow)\"], fields: [\"fields[] type slot (config.blockSlugs)\"], bound: \"SLOT_BLOCK_SLUGS_REQUIRED\", status: \"landed\" },\n \"block model field\": { row: \"A11\", carrier: \"value\", props: [], fields: [\"kind:'block' model fields[]\"], bound: \"BLOCK_FIELD_TYPES\", status: \"pending:G7\" },\n \"shopify reference\": { row: \"A12\", carrier: \"excluded\", props: [], fields: [], status: \"landed\" },\n \"type:table\": { row: \"A13\", carrier: \"value\", props: [\"props[] type table (config.mode data)\"], fields: [\"fields[] type table\"], status: \"landed\" },\n \"group (nested)\": { row: \"A14\", carrier: \"value\", props: [\"ui.tab {id}\"], fields: [\"ui.tab {id} (nested field)\"], bound: \"TAB_MAX\", status: \"landed\" },\n \"fieldset collision\": { row: \"A15\", carrier: \"value\", props: [\"ui.group {id}\"], fields: [\"fieldsetId\"], bound: \"TAB_FIELDSET_ID_UNIQUE\", status: \"landed\" },\n \"segmented fallback\": { row: \"A16\", carrier: \"value\", props: [\"ui.control segmented\"], fields: [\"ui.control segmented\"], bound: \"SEGMENTED_MAX\", status: \"pending:C3\" },\n \"fieldset (derived)\": { row: \"A17\", carrier: \"value\", props: [\"ui.group (prop group)\"], fields: [\"content_models.fieldsets\"], status: \"landed\" },\n \"itemNoun (derived)\": { row: \"A18\", carrier: \"value\", props: [\"ui.itemLabel\"], fields: [\"ui.itemTitle\"], status: \"landed\" },\n \"reference filter\": { row: \"A19\", carrier: \"value\", props: [\"config.filter\"], fields: [\"config.filter\"], bound: \"REFERENCE_FILTER_MAX\", status: \"landed\" },\n condition: { row: \"A20\", carrier: \"value\", props: [\"showIf (+ scope)\"], fields: [\"showIf (+ scope)\"], bound: \"SHOW_IF_SCOPES\", status: \"pending:C3\" },\n openMode: { row: \"A21\", carrier: \"value\", props: [\"props[].openMode\"], fields: [\"fields[].openMode\"], bound: \"OPEN_MODE_TYPES\", status: \"pending:C9\" },\n \"slot allow\": { row: \"A23\", carrier: \"value\", props: [\"config.allow\"], fields: [\"config.blockSlugs\"], bound: \"SLOT_ALLOW\", status: \"landed\" },\n\n /* ── B: in the code, not hit by the corpus ── */\n \"type:polymorphic-array\": { row: \"B structural\", carrier: \"value\", props: [\"props[] type table (config.mode types)\"], fields: [\"fields[] type modular\"], status: \"pending:C3\" },\n \"item chrome (types)\": { row: \"B structural\", carrier: \"value\", props: [\"config.itemTypes[i] {icon, preview}\"], fields: [\"ui.blocks[slug] {icon, preview}\"], status: \"pending:C3\" },\n \"type:primitive-array\": { row: \"B decided\", carrier: \"value\", props: [\"config.itemType (widened)\"], fields: [\"config.itemType (widened)\"], status: \"pending:C3\" },\n \"depth>10\": { row: \"B decided\", carrier: \"value\", props: [\"nesting\"], fields: [\"nesting\"], bound: \"DEPTH_MAX\", status: \"landed\" },\n slotSettings: { row: \"B decided (BUILD)\", carrier: \"value\", props: [\"config.settings: FieldDef[]\"], fields: [\"config.settings: FieldDef[]\"], bound: \"SLOT_SETTINGS_MAX\", status: \"pending:C3\" },\n \"block placement\": { row: \"B catalog\", carrier: \"value\", props: [\"components.v2.placement\"], fields: [\"content_models.v2.placement\"], status: \"pending:C5\" },\n \"seeded instance\": { row: \"B catalog\", carrier: \"excluded\", props: [], fields: [], status: \"landed\" },\n \"not applicable (props)\": { row: \"B catalog\", carrier: \"excluded\", props: [], fields: [], status: \"landed\" },\n \"type name\": corpusFix(\"components.slug\", \"content_models.slug\", \"SLUG_RE\"),\n \"group name>40\": corpusFix(\"components.tabs[].id\", \"content_models.fieldsets[].id\", \"TAB_NAME_MAX\"),\n \"group title\": corpusFix(\"components.tabs[].name\", \"content_models.fieldsets[].name\", \"TAB_TITLE\"),\n \"group default\": corpusFix(\"components.tabs[0]\", \"content_models.fieldsets[0]\", \"TAB_DEFAULT_FIRST\"),\n \"group icon\": corpusFix(\"components.tabs[].icon\", \"content_models.fieldsets[].icon\", \"DOCK_ICON_NAMES\"),\n \"group name collision\": corpusFix(\"components.tabs[].name\", \"content_models.fieldsets[].name\", \"TAB_NAME_UNIQUE\"),\n \"prop tab limit\": corpusFix(\"components.tabs\", \"content_models.fieldsets\", \"PROP_TAB_MAX\"),\n \"fieldset spans groups\": corpusFix(\"ui.group.tab\", \"fieldsets[].tab\", \"FIELDSET_ONE_TAB\"),\n \"fieldset (props)\": corpusFix(\"ui.group (first prop)\", \"content_models.fieldsets\", \"PROP_GROUP_DEFINED_ON_USE\"),\n \"fieldset name>limit\": corpusFix(\"ui.group.label\", \"content_models.fieldsets[].name\", \"FIELDSET_NAME_MAX\"),\n \"duplicate name\": corpusFix(\"props[].key\", \"fields[].key\", \"KEY_UNIQUE\"),\n \"reserved name\": { row: \"B corpus-fix\", carrier: \"value\", props: [], fields: [\"fields[].key\"], bound: \"KEY_RESERVED + KEY_FORMAT\", status: \"landed\" },\n rows: corpusFix(\"ui.rows\", \"ui.rows\", \"UI_ROWS_MAX\"),\n synced: corpusFix(\"ui.synced\", \"ui.synced\", \"SYNCED_FROM_MAX\"),\n valueShape: corpusFix(\"ui.valueShape\", \"ui.valueShape\", \"VALUE_SHAPE_TYPES\"),\n \"default value\": corpusFix(\"props[].defaultValue\", \"fields[].defaultValue\", \"VALUE_SCHEMAS\"),\n icon: corpusFix(\"ui.icon\", \"ui.icon\", \"DOCK_ICON_NAMES\"),\n layout: corpusFix(\"ui.control / ui.layout\", \"ui.control / ui.layout\", \"CONTROL_TYPES\"),\n \"help>limit\": corpusFix(\"helpText / placeholder / helpMore\", \"helpText / ui.placeholder / helpMore\", \"HELP_CAPS\"),\n \"option icon\": corpusFix(\"options[].icon\", \"options[].icon\", \"DOCK_ICON_NAMES\"),\n \"option description\": corpusFix(\"options[].description\", \"options[].description\", \"OPTION_DESCRIPTION_MAX\"),\n access: corpusFix(\"ui.access\", \"ui.access\", \"ACCESS_CAPS\"),\n timezone: corpusFix(\"config.timezone\", \"config.timezone\", \"TIMEZONE\"),\n \"reference order\": corpusFix(\"config.contentModelId|allowedModelSlugs, config.targets\", \"config.contentModelId|allowedModelSlugs, config.targets\", \"REFERENCE_TO_ORDER\"),\n \"numeric unit\": corpusFix(\"config.unit\", \"config.unit\", \"NUMBER_UNIT_MAX\"),\n \"rating bounds\": corpusFix(\"config.min/max/step (rating)\", \"config.min/max/step (rating)\", \"RATING_BOUNDS\"),\n \"scale rules\": corpusFix(\"config.scale\", \"config.scale\", \"SCALE_RULES\"),\n \"validation.patternMessage\": corpusFix(\"config.patternMessage\", \"config.patternMessage\", \"PATTERN_MESSAGE_MAX\"),\n \"validation.pattern\": corpusFix(\"config.pattern\", \"config.pattern\", \"PATTERN_MAX\"),\n \"validation.length\": corpusFix(\"config.minLength/maxLength\", \"config.minLength/maxLength\", \"LENGTH_TYPES\"),\n \"validation.items\": corpusFix(\"config.minItems/maxItems\", \"config.minItems/maxItems\", \"LIST_ITEMS_CAP\"),\n \"validation.unique\": corpusFix(\"props[].unique\", \"fields[].unique\", \"UNIQUE_TYPES\"),\n \"validation.recommendedMax\": corpusFix(\"props[].recommendedMax\", \"fields[].recommendedMax\", \"RECOMMENDED_MAX\"),\n \"item identity\": corpusFix(\"ui.itemName / ui.itemTitle\", \"ui.itemName / ui.itemTitle\", \"ITEM_NAME_RE\"),\n \"item settings\": corpusFix(\"config.item\", \"config.item\", \"ARRAY_ITEM_SETTINGS\"),\n itemNoun: corpusFix(\"ui.itemLabel\", \"ui.itemLabel\", \"ITEM_LABEL_MAX\"),\n \"item icon\": corpusFix(\"ui.icon (list)\", \"ui.icon (list)\", \"DOCK_ICON_NAMES\"),\n \"preview icon\": corpusFix(\"ui.preview.icon\", \"ui.preview.icon\", \"DOCK_ICON_NAMES\"),\n \"preview path\": corpusFix(\"ui.preview.{title,subtitle,media}\", \"ui.preview.{title,subtitle,media}\", \"PREVIEW_PATH\"),\n \"unsupported key\": corpusFix(\"—\", \"—\", \"V2_SCHEMA_KEYS\"),\n\n /* ── RT0 (kit-richtext-blocks-plan.md): a rich text's features, written as the full V2 set when V2 names none ── */\n \"richtext references\": { row: \"B RT8\", carrier: \"value\", props: [\"config.references\"], fields: [\"config.references\"], bound: \"REFERENCES_MODEL_ID\", status: \"landed\" },\n \"richtext features\": { row: \"B RT0\", carrier: \"value\", props: [\"config.features\"], fields: [\"config.features\"], bound: \"RICHTEXT_FEATURES\", status: \"landed\" },\n} as const satisfies Record<V2Facet, FacetRow>;\n\nexport const facetOf = (feature: string): FacetRow | undefined => (FACETS as Record<string, FacetRow>)[feature];\n\n/** The owner-signed exclusions: what may be lost by design (shopify until its pipeline, and two deliberate backend rules). */\nexport const EXCLUDED: readonly V2Facet[] = (Object.keys(FACETS) as V2Facet[]).filter((f) => FACETS[f].carrier === \"excluded\");\n"],"mappings":"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"}
@@ -36,7 +36,8 @@
36
36
  "pattern",
37
37
  "patternMessage",
38
38
  "inputType",
39
- "features"
39
+ "features",
40
+ "references"
40
41
  ],
41
42
  "document": [
42
43
  "minLength",
@@ -44,7 +45,8 @@
44
45
  "pattern",
45
46
  "patternMessage",
46
47
  "inputType",
47
- "features"
48
+ "features",
49
+ "references"
48
50
  ],
49
51
  "number": [
50
52
  "min",
@@ -163,6 +163,14 @@ export interface FieldSchema {
163
163
  * hold an alias (`headings`) or an unknown name: the adapter expands the one and names the other.
164
164
  */
165
165
  features?: RichTextFeature[];
166
+ /**
167
+ * richtext / document: the content models its related posts and author bios draw from (prod config.references,
168
+ * plan RT8), by id. posts absent: the entry's own collection; authors absent: no author bio is offered.
169
+ */
170
+ references?: {
171
+ posts?: string;
172
+ authors?: string;
173
+ };
166
174
  }
167
175
  /** D58: the caps that let someone see / change one field. */
168
176
  export interface FieldAccess {
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../src/schema/types.ts"],"sourcesContent":["/**\n * BETTERCMS SCHEMA — the long-term contract for every editable thing (blocks, pages, entries, globals).\n * One schema language → one form engine (`@/components/kit`) → used by BOTH the Visuals inspector and the\n * Content document editor. Modelled on Sanity's schema (types, fieldsets, groups, previews, validation,\n * conditional fields) plus BetterCMS's block slots for nested layouts.\n *\n * Design principles (CPO D8):\n * - Scales to deep nesting without inline-accordion soup: arrays render compact preview rows; opening an item\n * DRILLS IN (panel slides, breadcrumb grows). Small objects may render inline; lightweight settings open in a POPOVER.\n * - Every field has the same chrome: label · help · required · validation · counter · ⋯ menu · presence · comments.\n * - Blocks can contain blocks (slots) → complex layouts (tabs, columns, bento, mega menus, carousels).\n */\n\nimport type { RichTextFeature } from \"@bettercms-ai/types\";\nimport type { Cap } from \"./sources\";\nimport type { TableFieldOptions } from \"./table\";\n\nexport type PrimitiveType =\n | \"string\" // single line\n | \"text\" // multi line, auto-grow\n | \"richtext\" // portable-text style rich text\n | \"number\"\n | \"boolean\"\n | \"slug\"\n | \"url\"\n | \"email\"\n | \"date\"\n | \"datetime\"\n | \"color\"\n | \"select\" // options: single\n | \"multiselect\"\n | \"tags\"\n | \"image\"\n | \"file\"\n | \"video\"\n | \"icon\" // icon picker (Tabler name)\n | \"link\" // url | page ref + target + rel (one unit)\n | \"button\" // label + link + style (one unit — founder rule)\n | \"reference\" // to a collection entry / page / form\n | \"code\"\n | \"json\"\n | \"geopoint\"\n | \"rating\"\n | \"table\"; // typed columns × keyed rows (CPO D42) — value: TableValue (src/schema/table.ts)\n\nexport type CompoundType =\n | \"object\" // nested fields\n | \"array\" // list of `of` (objects, primitives or references)\n | \"slot\"; // list of nested BLOCKS (block-in-block layouts)\n\nexport type SchemaType = PrimitiveType | CompoundType;\n\nexport interface Option {\n value: string;\n label: string;\n icon?: string;\n description?: string;\n}\n\nexport interface Validation {\n required?: boolean;\n min?: number; // chars for strings, items for arrays, value for numbers\n max?: number;\n /** soft limit: warning (amber) instead of error — e.g. SEO title 60 */\n recommendedMax?: number;\n pattern?: string;\n patternMessage?: string;\n unique?: boolean;\n}\n\n/** How a compound field is edited when opened. */\nexport type OpenMode =\n | \"inline\" // render fields in place (small objects, ≤3 primitives)\n | \"drill\" // slide the panel to a child view; breadcrumb grows (default for array items & objects)\n | \"popover\" // anchored popover (link settings, image crop/alt, small option sets)\n | \"dialog\"; // centered modal for heavy editors (rich text full screen, code, table)\n\nexport interface PreviewConfig {\n /** dotted paths into the item value */\n title?: string;\n subtitle?: string;\n media?: string;\n /** Fallback label, e.g. \"Untitled question\" */\n fallback?: string;\n /** Tabler icon name (kebab-case, e.g. \"help-circle\") shown when there is no media. */\n icon?: string;\n}\n\n/** A condition's operator. Absent: `equals` reads as is / is one of, `notEmpty` as has a value. */\nexport type ConditionOp = \"is\" | \"isNot\" | \"in\" | \"notIn\" | \"empty\" | \"notEmpty\" | \"gt\" | \"lt\";\n\n/**\n * \"Show only when\" (V2 types.ts). The field shows only while the rule holds:\n * - `field` names a key beside it (default), in the object that holds those fields (`scope: \"parent\"`) or at the top of\n * the component or CMS item (`scope: \"root\"`). `_variant` is the component's variant (its first variant when unset).\n * - `op` + `value` (is · is not · is one of · is not one of · is empty · has a value · more than · less than). The legacy\n * `equals` / `notEmpty` forms still read the same.\n * - `also`: more rules, combined by `match` (\"all\", the default, or \"any\").\n */\nexport interface Condition {\n field: string;\n equals?: unknown | unknown[];\n notEmpty?: boolean;\n op?: ConditionOp;\n value?: unknown;\n scope?: \"parent\" | \"root\";\n also?: Condition[];\n match?: \"all\" | \"any\";\n}\n\nexport interface FieldSchema {\n name: string; // key in the value object\n title: string; // label (sentence case)\n type: SchemaType;\n /** Help text under the label (CPO D39). Unset → a house convention from `@/schema/help` may apply; \"\" = none. */\n description?: string;\n /** Longer explanation behind the ⓘ next to the label (tooltip). Use when one line can't carry it. */\n helpMore?: string;\n placeholder?: string;\n initialValue?: unknown;\n validation?: Validation;\n options?: Option[]; // select / multiselect / radio-like\n layout?: \"dropdown\" | \"radio\" | \"segmented\" | \"cards\" | \"switch\" | \"checkbox\" | \"slider\" | \"grid\" | \"list\";\n /** compound */\n fields?: FieldSchema[]; // object\n of?: FieldSchema[] | { type: \"block\"; allow?: string[] }; // array items / slot allowed block types\n openMode?: OpenMode;\n preview?: PreviewConfig;\n /** organisation */\n fieldset?: string; // groups fields into a collapsible fieldset\n group?: string; // which tab (Content / Media / Settings / SEO…)\n /** behaviour */\n hidden?: Condition | boolean;\n /** true = never editable in the form (computed or system values). Who may see or change a field is `access`. */\n readOnly?: boolean;\n /** Archived (V2 types.ts): kept in the schema and its values stay stored, but no form renders it. Schema editors show it. */\n archived?: boolean;\n /**\n * D58 field-level access. `view` missing → everyone who can open the form sees it; `edit` missing → the form's own\n * rule (the host's content.edit). A field the lens can't `view` is not rendered; one it can't `edit` renders as plain\n * text. Fields in the `seo` group default to `{ edit: \"seo.edit\" }`.\n */\n access?: FieldAccess;\n localized?: boolean;\n advanced?: boolean; // tucked into the \"Advanced\" fieldset by default (calm UX)\n ai?: boolean; // show the AI assist sparkle\n reference?: { to: string[]; filter?: string }; // reference targets (collection ids, \"page\", \"form\")\n numeric?: { unit?: string; step?: number; min?: number; max?: number }; // number / slider\n /** number: a developer-controlled SCALE (tokens or a stepped range) with an optional custom escape hatch (CPO D41).\n * Value shape → `ScaleValue`. Takes precedence over `layout: \"slider\"` + `numeric` (which still work, fixed-only). */\n scale?: ScaleSchema;\n /** table: column types allowed, developer-locked columns, row limits, caption rule, JSON-LD offered (CPO D42). */\n table?: TableFieldOptions;\n\n /* ── additive (Block Kit v1) ─────────────────────────────────────────── */\n /** Tabler icon name (kebab-case) used on rows, crumbs and the block picker. */\n icon?: string;\n /** text: minimum visible rows. */\n rows?: number;\n /** Value is synced from a global block (\"Navigation\") — edits apply everywhere it is used. */\n synced?: { from: string };\n /** slot: per-child layout settings stored on each child under `_settings` (e.g. bento span). */\n slotSettings?: FieldSchema[];\n /** array: singular noun for \"Add {noun}\" and empty states (\"question\"). Defaults from the item title. */\n itemNoun?: string;\n /** array: how rows render. \"grid\" = tiles (good for images / logos). */\n arrayLayout?: \"list\" | \"grid\";\n /** Help line shown under a field the lens can see but not change (\"Set by the developer in code.\"). Never a key or a lock. */\n lockReason?: string;\n /**\n * Host-only, never persisted: the list's ORDER is set by the site's code (identity rule 36). The rows stay editable;\n * drag and Move up / down are off and the grip slot shows a lock (V2 content/editor/page-sections.tsx:31,155).\n */\n orderLocked?: boolean;\n /** link / image / button: store a plain string (href or URL) instead of the object — for legacy data. */\n valueShape?: \"string\" | \"object\";\n /** image: an inline-SVG icon, which takes SVG files only (prod `config.media: \"icon\"`). */\n svgOnly?: boolean;\n /**\n * A link LABEL's href: the sibling it links with (prod `config.linkHref`), as the owner answered the editable plan's\n * `link-pair` question where the names do not say (`title` + `to`). Read first by `linkLabelHref`.\n */\n linkHref?: string;\n /**\n * date / datetime: time zone handling (CPO D43). true = the editor picks a zone (defaults to the site's) · \"site\" = always\n * the site's zone · \"fixed:<IANA>\" = always that zone · false = no zone. Default: true for datetime, false for date.\n * Zoned values store ISO with offset + the IANA zone: \"2026-10-14T09:00:00+05:30[Asia/Kolkata]\" (RFC 9557).\n */\n timezone?: boolean | \"site\" | `fixed:${string}`;\n /** code: the field's own language and the only languages its picker offers (prod config.language / languages). */\n language?: string;\n languages?: string[];\n /**\n * richtext / document: the blocks and marks the field offers, from types RICHTEXT_FEATURES (prod config.features).\n * Absent = all of them, as V2's rich text offers (normV2 folds the full set to absent). A schema read from JSON may\n * hold an alias (`headings`) or an unknown name: the adapter expands the one and names the other.\n */\n features?: RichTextFeature[];\n}\n\n/** D58: the caps that let someone see / change one field. */\nexport interface FieldAccess {\n view?: Cap;\n edit?: Cap;\n}\n/** Developer-owned values (anchors, custom CSS, ids): only people who can change the schema edit them. */\nexport const DEV_FIELD: FieldAccess = { edit: \"schema.manage\" };\n/** Developer-only settings: hidden unless you can see the schema. */\nexport const DEV_ONLY_FIELD: FieldAccess = { view: \"schema.view\", edit: \"schema.manage\" };\n\n/* ── Scales (CPO D41) ─────────────────────────────────────────────────── */\nexport type ScaleUnit = \"px\" | \"%\" | \"rem\" | \"em\" | \"vw\" | \"vh\";\n\n/** One allowed value of a token scale. `value` is what is stored; `label` is the short readout (\"XL\"). */\nexport interface ScaleStep {\n value: string;\n label: string;\n /** Long name for screen readers and tooltips (\"Extra large\"). Defaults to `label`. */\n title?: string;\n /** Resolved size in px, shown as \"XL · 48px\" and used to place custom values on the track. */\n px?: number;\n /** Resolved CSS when it isn't px (e.g. \"100vh\", \"var(--space-xl)\"). */\n css?: string;\n}\n\n/** A stepped numeric range (continuous with snapping). The stored value is a bare number in `defaultUnit`. */\nexport interface ScaleRange {\n min: number;\n max: number;\n step?: number;\n}\n\n/** Bounds for the custom escape hatch. min / max may be one number or per unit ({ px: 0, \"%\": 0 }). */\nexport interface ScaleCustom {\n min?: number | Partial<Record<ScaleUnit, number>>;\n max?: number | Partial<Record<ScaleUnit, number>>;\n /** Units the editor may type. Defaults to the scale's `units`. */\n units?: ScaleUnit[];\n /** Arrow-key nudge (Shift ×10, Alt ×0.1). Default 1. */\n step?: number;\n}\n\nexport interface ScaleSchema {\n /** Token steps (S M L XL 2XL) or a numeric range ({ min, max, step }). */\n steps: ScaleStep[] | ScaleRange;\n /** Units of the scale; the first is the default. A range with no units is unitless. */\n units?: ScaleUnit[];\n defaultUnit?: ScaleUnit;\n /** false (default) = fixed only: the editor can pick only the allowed values. true / object = fixed + custom. */\n allowCustom?: boolean | ScaleCustom;\n /** Quick picks in the unit menu when custom is allowed (\"50%\", \"100vh\"). */\n presets?: { label?: string; value: ScaleValue }[];\n}\n\n/**\n * How a scale value is STORED — three shapes, told apart by `typeof`, so tokens and custom values never collide and\n * older data (bare token strings, bare numbers) is already valid:\n * - token → the step's value string (\"xl\")\n * - range → a bare number in the default unit (15 → 15%)\n * - custom → an object ({ value: 200, unit: \"px\" })\n */\nexport type ScaleValue = string | number | ScaleCustomValue;\nexport interface ScaleCustomValue {\n value: number;\n unit: ScaleUnit;\n}\n\nexport interface Fieldset {\n name: string;\n title: string;\n description?: string;\n collapsed?: boolean; // collapsed by default\n columns?: 1 | 2; // short fields side by side\n /** CPO D45: the header toggles the section (default true). false = a static heading for 2–3 fields that are always needed. */\n collapsible?: boolean;\n /**\n * CPO D45: \"breakpoints\" = the members are objects, one per screen size (desktop · tablet · phone), shown as ONE group with a\n * segmented switcher instead of three stacked groups. Sibling objects named desktop / laptop / tablet / mobile / phone get\n * this automatically.\n */\n layout?: \"stack\" | \"breakpoints\";\n}\n\nexport interface FieldGroup {\n name: string;\n title: string;\n icon?: string;\n default?: boolean;\n}\n\n/** A developer-defined layout of a block (swap without losing content). */\nexport interface BlockVariant {\n id: string;\n name: string;\n description?: string;\n}\n\n/** A document/block type. */\nexport interface TypeSchema {\n name: string; // e.g. \"hero\", \"journalStory\"\n title: string;\n kind: \"block\" | \"page\" | \"entry\" | \"global\" | \"object\";\n icon?: string;\n description?: string;\n fields: FieldSchema[];\n fieldsets?: Fieldset[];\n groups?: FieldGroup[];\n preview?: PreviewConfig;\n /* ── additive (Block Kit v1) ── */\n /** Block variants (layouts) — shown as a chip on slot rows and a switcher in the block frame. */\n variants?: BlockVariant[];\n /** Block picker category (\"Layout\", \"Content\", \"Media\"…). */\n category?: string;\n /**\n * kind \"block\" only (CPO D55): where the block fits. \"inline\" = it fits a text column, so a rich-text body lists it in\n * the Insert menu's Article group. \"section\" (default) = page-width, reachable in a body only through More components.\n * \"page\" = a whole page or a feed of other content; never inside a body.\n */\n placement?: \"inline\" | \"section\" | \"page\";\n /** Starting value for a new instance (block added to a slot, new document). */\n initialValue?: Record<string, unknown>;\n}\n\n/** Every array item / slot child carries a stable key. Slot children also carry their block type. */\nexport interface KeyedItem {\n _key?: string;\n /** item type name (arrays with several `of` types) or block type (slots) */\n _type?: string;\n}\n/** A block inside a slot: its own props at the root, plus variant and per-slot layout settings. */\nexport interface SlotChild extends KeyedItem {\n _type: string;\n _variant?: string;\n _settings?: Record<string, unknown>;\n [prop: string]: unknown;\n}\n\n/** Path from the root document/block to the field being edited, e.g. [\"items\", 1, \"answer\"] */\nexport type FieldPath = (string | number)[];\n\n/** One crumb in the inspector/editor breadcrumb (drill-in navigation stack). */\nexport interface Crumb {\n path: FieldPath;\n title: string;\n icon?: string;\n /** tableGroup / tableRow / tableColumn: a table field's rows and columns drill like array items (D50) */\n kind: \"root\" | \"object\" | \"item\" | \"block\" | \"button\" | \"history\" | \"tableGroup\" | \"tableRow\" | \"tableColumn\";\n}\n\n/** Reserved path segment: the field's version history as a stacked panel, e.g. [\"title\", \"@history\"]. */\nexport const HISTORY_SEGMENT = \"@history\";\n"],"mappings":";;AA8MA,IAAa,YAAyB,EAAE,MAAM,gBAAgB;;AAE9D,IAAa,iBAA8B;CAAE,MAAM;CAAe,MAAM;AAAgB;;AA8IxF,IAAa,kBAAkB"}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../src/schema/types.ts"],"sourcesContent":["/**\n * BETTERCMS SCHEMA — the long-term contract for every editable thing (blocks, pages, entries, globals).\n * One schema language → one form engine (`@/components/kit`) → used by BOTH the Visuals inspector and the\n * Content document editor. Modelled on Sanity's schema (types, fieldsets, groups, previews, validation,\n * conditional fields) plus BetterCMS's block slots for nested layouts.\n *\n * Design principles (CPO D8):\n * - Scales to deep nesting without inline-accordion soup: arrays render compact preview rows; opening an item\n * DRILLS IN (panel slides, breadcrumb grows). Small objects may render inline; lightweight settings open in a POPOVER.\n * - Every field has the same chrome: label · help · required · validation · counter · ⋯ menu · presence · comments.\n * - Blocks can contain blocks (slots) → complex layouts (tabs, columns, bento, mega menus, carousels).\n */\n\nimport type { RichTextFeature } from \"@bettercms-ai/types\";\nimport type { Cap } from \"./sources\";\nimport type { TableFieldOptions } from \"./table\";\n\nexport type PrimitiveType =\n | \"string\" // single line\n | \"text\" // multi line, auto-grow\n | \"richtext\" // portable-text style rich text\n | \"number\"\n | \"boolean\"\n | \"slug\"\n | \"url\"\n | \"email\"\n | \"date\"\n | \"datetime\"\n | \"color\"\n | \"select\" // options: single\n | \"multiselect\"\n | \"tags\"\n | \"image\"\n | \"file\"\n | \"video\"\n | \"icon\" // icon picker (Tabler name)\n | \"link\" // url | page ref + target + rel (one unit)\n | \"button\" // label + link + style (one unit — founder rule)\n | \"reference\" // to a collection entry / page / form\n | \"code\"\n | \"json\"\n | \"geopoint\"\n | \"rating\"\n | \"table\"; // typed columns × keyed rows (CPO D42) — value: TableValue (src/schema/table.ts)\n\nexport type CompoundType =\n | \"object\" // nested fields\n | \"array\" // list of `of` (objects, primitives or references)\n | \"slot\"; // list of nested BLOCKS (block-in-block layouts)\n\nexport type SchemaType = PrimitiveType | CompoundType;\n\nexport interface Option {\n value: string;\n label: string;\n icon?: string;\n description?: string;\n}\n\nexport interface Validation {\n required?: boolean;\n min?: number; // chars for strings, items for arrays, value for numbers\n max?: number;\n /** soft limit: warning (amber) instead of error — e.g. SEO title 60 */\n recommendedMax?: number;\n pattern?: string;\n patternMessage?: string;\n unique?: boolean;\n}\n\n/** How a compound field is edited when opened. */\nexport type OpenMode =\n | \"inline\" // render fields in place (small objects, ≤3 primitives)\n | \"drill\" // slide the panel to a child view; breadcrumb grows (default for array items & objects)\n | \"popover\" // anchored popover (link settings, image crop/alt, small option sets)\n | \"dialog\"; // centered modal for heavy editors (rich text full screen, code, table)\n\nexport interface PreviewConfig {\n /** dotted paths into the item value */\n title?: string;\n subtitle?: string;\n media?: string;\n /** Fallback label, e.g. \"Untitled question\" */\n fallback?: string;\n /** Tabler icon name (kebab-case, e.g. \"help-circle\") shown when there is no media. */\n icon?: string;\n}\n\n/** A condition's operator. Absent: `equals` reads as is / is one of, `notEmpty` as has a value. */\nexport type ConditionOp = \"is\" | \"isNot\" | \"in\" | \"notIn\" | \"empty\" | \"notEmpty\" | \"gt\" | \"lt\";\n\n/**\n * \"Show only when\" (V2 types.ts). The field shows only while the rule holds:\n * - `field` names a key beside it (default), in the object that holds those fields (`scope: \"parent\"`) or at the top of\n * the component or CMS item (`scope: \"root\"`). `_variant` is the component's variant (its first variant when unset).\n * - `op` + `value` (is · is not · is one of · is not one of · is empty · has a value · more than · less than). The legacy\n * `equals` / `notEmpty` forms still read the same.\n * - `also`: more rules, combined by `match` (\"all\", the default, or \"any\").\n */\nexport interface Condition {\n field: string;\n equals?: unknown | unknown[];\n notEmpty?: boolean;\n op?: ConditionOp;\n value?: unknown;\n scope?: \"parent\" | \"root\";\n also?: Condition[];\n match?: \"all\" | \"any\";\n}\n\nexport interface FieldSchema {\n name: string; // key in the value object\n title: string; // label (sentence case)\n type: SchemaType;\n /** Help text under the label (CPO D39). Unset → a house convention from `@/schema/help` may apply; \"\" = none. */\n description?: string;\n /** Longer explanation behind the ⓘ next to the label (tooltip). Use when one line can't carry it. */\n helpMore?: string;\n placeholder?: string;\n initialValue?: unknown;\n validation?: Validation;\n options?: Option[]; // select / multiselect / radio-like\n layout?: \"dropdown\" | \"radio\" | \"segmented\" | \"cards\" | \"switch\" | \"checkbox\" | \"slider\" | \"grid\" | \"list\";\n /** compound */\n fields?: FieldSchema[]; // object\n of?: FieldSchema[] | { type: \"block\"; allow?: string[] }; // array items / slot allowed block types\n openMode?: OpenMode;\n preview?: PreviewConfig;\n /** organisation */\n fieldset?: string; // groups fields into a collapsible fieldset\n group?: string; // which tab (Content / Media / Settings / SEO…)\n /** behaviour */\n hidden?: Condition | boolean;\n /** true = never editable in the form (computed or system values). Who may see or change a field is `access`. */\n readOnly?: boolean;\n /** Archived (V2 types.ts): kept in the schema and its values stay stored, but no form renders it. Schema editors show it. */\n archived?: boolean;\n /**\n * D58 field-level access. `view` missing → everyone who can open the form sees it; `edit` missing → the form's own\n * rule (the host's content.edit). A field the lens can't `view` is not rendered; one it can't `edit` renders as plain\n * text. Fields in the `seo` group default to `{ edit: \"seo.edit\" }`.\n */\n access?: FieldAccess;\n localized?: boolean;\n advanced?: boolean; // tucked into the \"Advanced\" fieldset by default (calm UX)\n ai?: boolean; // show the AI assist sparkle\n reference?: { to: string[]; filter?: string }; // reference targets (collection ids, \"page\", \"form\")\n numeric?: { unit?: string; step?: number; min?: number; max?: number }; // number / slider\n /** number: a developer-controlled SCALE (tokens or a stepped range) with an optional custom escape hatch (CPO D41).\n * Value shape → `ScaleValue`. Takes precedence over `layout: \"slider\"` + `numeric` (which still work, fixed-only). */\n scale?: ScaleSchema;\n /** table: column types allowed, developer-locked columns, row limits, caption rule, JSON-LD offered (CPO D42). */\n table?: TableFieldOptions;\n\n /* ── additive (Block Kit v1) ─────────────────────────────────────────── */\n /** Tabler icon name (kebab-case) used on rows, crumbs and the block picker. */\n icon?: string;\n /** text: minimum visible rows. */\n rows?: number;\n /** Value is synced from a global block (\"Navigation\") — edits apply everywhere it is used. */\n synced?: { from: string };\n /** slot: per-child layout settings stored on each child under `_settings` (e.g. bento span). */\n slotSettings?: FieldSchema[];\n /** array: singular noun for \"Add {noun}\" and empty states (\"question\"). Defaults from the item title. */\n itemNoun?: string;\n /** array: how rows render. \"grid\" = tiles (good for images / logos). */\n arrayLayout?: \"list\" | \"grid\";\n /** Help line shown under a field the lens can see but not change (\"Set by the developer in code.\"). Never a key or a lock. */\n lockReason?: string;\n /**\n * Host-only, never persisted: the list's ORDER is set by the site's code (identity rule 36). The rows stay editable;\n * drag and Move up / down are off and the grip slot shows a lock (V2 content/editor/page-sections.tsx:31,155).\n */\n orderLocked?: boolean;\n /** link / image / button: store a plain string (href or URL) instead of the object — for legacy data. */\n valueShape?: \"string\" | \"object\";\n /** image: an inline-SVG icon, which takes SVG files only (prod `config.media: \"icon\"`). */\n svgOnly?: boolean;\n /**\n * A link LABEL's href: the sibling it links with (prod `config.linkHref`), as the owner answered the editable plan's\n * `link-pair` question where the names do not say (`title` + `to`). Read first by `linkLabelHref`.\n */\n linkHref?: string;\n /**\n * date / datetime: time zone handling (CPO D43). true = the editor picks a zone (defaults to the site's) · \"site\" = always\n * the site's zone · \"fixed:<IANA>\" = always that zone · false = no zone. Default: true for datetime, false for date.\n * Zoned values store ISO with offset + the IANA zone: \"2026-10-14T09:00:00+05:30[Asia/Kolkata]\" (RFC 9557).\n */\n timezone?: boolean | \"site\" | `fixed:${string}`;\n /** code: the field's own language and the only languages its picker offers (prod config.language / languages). */\n language?: string;\n languages?: string[];\n /**\n * richtext / document: the blocks and marks the field offers, from types RICHTEXT_FEATURES (prod config.features).\n * Absent = all of them, as V2's rich text offers (normV2 folds the full set to absent). A schema read from JSON may\n * hold an alias (`headings`) or an unknown name: the adapter expands the one and names the other.\n */\n features?: RichTextFeature[];\n /**\n * richtext / document: the content models its related posts and author bios draw from (prod config.references,\n * plan RT8), by id. posts absent: the entry's own collection; authors absent: no author bio is offered.\n */\n references?: { posts?: string; authors?: string };\n}\n\n/** D58: the caps that let someone see / change one field. */\nexport interface FieldAccess {\n view?: Cap;\n edit?: Cap;\n}\n/** Developer-owned values (anchors, custom CSS, ids): only people who can change the schema edit them. */\nexport const DEV_FIELD: FieldAccess = { edit: \"schema.manage\" };\n/** Developer-only settings: hidden unless you can see the schema. */\nexport const DEV_ONLY_FIELD: FieldAccess = { view: \"schema.view\", edit: \"schema.manage\" };\n\n/* ── Scales (CPO D41) ─────────────────────────────────────────────────── */\nexport type ScaleUnit = \"px\" | \"%\" | \"rem\" | \"em\" | \"vw\" | \"vh\";\n\n/** One allowed value of a token scale. `value` is what is stored; `label` is the short readout (\"XL\"). */\nexport interface ScaleStep {\n value: string;\n label: string;\n /** Long name for screen readers and tooltips (\"Extra large\"). Defaults to `label`. */\n title?: string;\n /** Resolved size in px, shown as \"XL · 48px\" and used to place custom values on the track. */\n px?: number;\n /** Resolved CSS when it isn't px (e.g. \"100vh\", \"var(--space-xl)\"). */\n css?: string;\n}\n\n/** A stepped numeric range (continuous with snapping). The stored value is a bare number in `defaultUnit`. */\nexport interface ScaleRange {\n min: number;\n max: number;\n step?: number;\n}\n\n/** Bounds for the custom escape hatch. min / max may be one number or per unit ({ px: 0, \"%\": 0 }). */\nexport interface ScaleCustom {\n min?: number | Partial<Record<ScaleUnit, number>>;\n max?: number | Partial<Record<ScaleUnit, number>>;\n /** Units the editor may type. Defaults to the scale's `units`. */\n units?: ScaleUnit[];\n /** Arrow-key nudge (Shift ×10, Alt ×0.1). Default 1. */\n step?: number;\n}\n\nexport interface ScaleSchema {\n /** Token steps (S M L XL 2XL) or a numeric range ({ min, max, step }). */\n steps: ScaleStep[] | ScaleRange;\n /** Units of the scale; the first is the default. A range with no units is unitless. */\n units?: ScaleUnit[];\n defaultUnit?: ScaleUnit;\n /** false (default) = fixed only: the editor can pick only the allowed values. true / object = fixed + custom. */\n allowCustom?: boolean | ScaleCustom;\n /** Quick picks in the unit menu when custom is allowed (\"50%\", \"100vh\"). */\n presets?: { label?: string; value: ScaleValue }[];\n}\n\n/**\n * How a scale value is STORED — three shapes, told apart by `typeof`, so tokens and custom values never collide and\n * older data (bare token strings, bare numbers) is already valid:\n * - token → the step's value string (\"xl\")\n * - range → a bare number in the default unit (15 → 15%)\n * - custom → an object ({ value: 200, unit: \"px\" })\n */\nexport type ScaleValue = string | number | ScaleCustomValue;\nexport interface ScaleCustomValue {\n value: number;\n unit: ScaleUnit;\n}\n\nexport interface Fieldset {\n name: string;\n title: string;\n description?: string;\n collapsed?: boolean; // collapsed by default\n columns?: 1 | 2; // short fields side by side\n /** CPO D45: the header toggles the section (default true). false = a static heading for 2–3 fields that are always needed. */\n collapsible?: boolean;\n /**\n * CPO D45: \"breakpoints\" = the members are objects, one per screen size (desktop · tablet · phone), shown as ONE group with a\n * segmented switcher instead of three stacked groups. Sibling objects named desktop / laptop / tablet / mobile / phone get\n * this automatically.\n */\n layout?: \"stack\" | \"breakpoints\";\n}\n\nexport interface FieldGroup {\n name: string;\n title: string;\n icon?: string;\n default?: boolean;\n}\n\n/** A developer-defined layout of a block (swap without losing content). */\nexport interface BlockVariant {\n id: string;\n name: string;\n description?: string;\n}\n\n/** A document/block type. */\nexport interface TypeSchema {\n name: string; // e.g. \"hero\", \"journalStory\"\n title: string;\n kind: \"block\" | \"page\" | \"entry\" | \"global\" | \"object\";\n icon?: string;\n description?: string;\n fields: FieldSchema[];\n fieldsets?: Fieldset[];\n groups?: FieldGroup[];\n preview?: PreviewConfig;\n /* ── additive (Block Kit v1) ── */\n /** Block variants (layouts) — shown as a chip on slot rows and a switcher in the block frame. */\n variants?: BlockVariant[];\n /** Block picker category (\"Layout\", \"Content\", \"Media\"…). */\n category?: string;\n /**\n * kind \"block\" only (CPO D55): where the block fits. \"inline\" = it fits a text column, so a rich-text body lists it in\n * the Insert menu's Article group. \"section\" (default) = page-width, reachable in a body only through More components.\n * \"page\" = a whole page or a feed of other content; never inside a body.\n */\n placement?: \"inline\" | \"section\" | \"page\";\n /** Starting value for a new instance (block added to a slot, new document). */\n initialValue?: Record<string, unknown>;\n}\n\n/** Every array item / slot child carries a stable key. Slot children also carry their block type. */\nexport interface KeyedItem {\n _key?: string;\n /** item type name (arrays with several `of` types) or block type (slots) */\n _type?: string;\n}\n/** A block inside a slot: its own props at the root, plus variant and per-slot layout settings. */\nexport interface SlotChild extends KeyedItem {\n _type: string;\n _variant?: string;\n _settings?: Record<string, unknown>;\n [prop: string]: unknown;\n}\n\n/** Path from the root document/block to the field being edited, e.g. [\"items\", 1, \"answer\"] */\nexport type FieldPath = (string | number)[];\n\n/** One crumb in the inspector/editor breadcrumb (drill-in navigation stack). */\nexport interface Crumb {\n path: FieldPath;\n title: string;\n icon?: string;\n /** tableGroup / tableRow / tableColumn: a table field's rows and columns drill like array items (D50) */\n kind: \"root\" | \"object\" | \"item\" | \"block\" | \"button\" | \"history\" | \"tableGroup\" | \"tableRow\" | \"tableColumn\";\n}\n\n/** Reserved path segment: the field's version history as a stacked panel, e.g. [\"title\", \"@history\"]. */\nexport const HISTORY_SEGMENT = \"@history\";\n"],"mappings":";;AAmNA,IAAa,YAAyB,EAAE,MAAM,gBAAgB;;AAE9D,IAAa,iBAA8B;CAAE,MAAM;CAAe,MAAM;AAAgB;;AA8IxF,IAAa,kBAAkB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bettercms-ai/kit",
3
- "version": "2.0.0-next.1",
3
+ "version": "2.0.0-next.2",
4
4
  "description": "The BetterCMS V2 Block Kit: schema language, form engine, controls and design tokens.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -73,7 +73,7 @@
73
73
  },
74
74
  "peerDependencies": {
75
75
  "@base-ui/react": "^1.4.1",
76
- "@bettercms-ai/types": "^1.53.0",
76
+ "@bettercms-ai/types": "^1.56.0",
77
77
  "@tabler/icons-react": "^3.48.0",
78
78
  "framer-motion": "^12.40.0",
79
79
  "react": "^19.2.0",