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

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 (55) 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/link.d.ts +11 -4
  7. package/dist/controls/link.js +8 -6
  8. package/dist/controls/link.js.map +1 -1
  9. package/dist/controls/rich-text/article-shell.js +1 -0
  10. package/dist/controls/rich-text/article-shell.js.map +1 -1
  11. package/dist/controls/rich-text/blocks.d.ts +6 -0
  12. package/dist/controls/rich-text/blocks.js +188 -0
  13. package/dist/controls/rich-text/blocks.js.map +1 -1
  14. package/dist/controls/rich-text/button.d.ts +2 -2
  15. package/dist/controls/rich-text/button.js +10 -14
  16. package/dist/controls/rich-text/button.js.map +1 -1
  17. package/dist/controls/rich-text/catalog.d.ts +2 -5
  18. package/dist/controls/rich-text/catalog.js +105 -7
  19. package/dist/controls/rich-text/catalog.js.map +1 -1
  20. package/dist/controls/rich-text/codec.d.ts +13 -1
  21. package/dist/controls/rich-text/codec.js +7 -0
  22. package/dist/controls/rich-text/codec.js.map +1 -1
  23. package/dist/controls/rich-text/control.js +1 -0
  24. package/dist/controls/rich-text/control.js.map +1 -1
  25. package/dist/controls/rich-text/dom.d.ts +79 -0
  26. package/dist/controls/rich-text/dom.js +159 -7
  27. package/dist/controls/rich-text/dom.js.map +1 -1
  28. package/dist/controls/rich-text/editor.d.ts +4 -2
  29. package/dist/controls/rich-text/editor.js +109 -18
  30. package/dist/controls/rich-text/editor.js.map +1 -1
  31. package/dist/controls/rich-text/link-form.d.ts +10 -0
  32. package/dist/controls/rich-text/link-form.js +39 -5
  33. package/dist/controls/rich-text/link-form.js.map +1 -1
  34. package/dist/controls/rich-text/ops.js +6 -0
  35. package/dist/controls/rich-text/ops.js.map +1 -1
  36. package/dist/controls/rich-text/related.d.ts +12 -4
  37. package/dist/controls/rich-text/related.js +22 -11
  38. package/dist/controls/rich-text/related.js.map +1 -1
  39. package/dist/controls/rich-text/toc.d.ts +3 -2
  40. package/dist/controls/rich-text/toc.js +1 -2
  41. package/dist/controls/rich-text/toc.js.map +1 -1
  42. package/dist/core/core.d.ts +1 -0
  43. package/dist/core/core.js +2 -0
  44. package/dist/core/core.js.map +1 -1
  45. package/dist/core/facets.d.ts +8 -0
  46. package/dist/core/facets.js +8 -0
  47. package/dist/core/facets.js.map +1 -1
  48. package/dist/library/kit-catalog.json +4 -2
  49. package/dist/schema/types.d.ts +8 -0
  50. package/dist/schema/types.js.map +1 -1
  51. package/dist/styles.css +1 -1
  52. package/dist/ui/overlay.d.ts +4 -1
  53. package/dist/ui/overlay.js +2 -1
  54. package/dist/ui/overlay.js.map +1 -1
  55. package/package.json +2 -2
@@ -1,7 +1,17 @@
1
+ import { type LinkRel, type LinkValue } from "../link";
1
2
  export interface TextLink {
2
3
  href: string;
3
4
  newTab?: boolean;
5
+ /** absent = follow (V2 writes no rel for it) */
6
+ rel?: Exclude<LinkRel, "follow">;
4
7
  }
8
+ /** An anchor's link as stored: href, a new tab, and its rel (a new tab's noopener/noreferrer are the renderer's). */
9
+ export declare function textLinkOf(a: HTMLAnchorElement): TextLink;
10
+ /** Writes a link onto its anchor: href, target and rel (none for follow). */
11
+ export declare function applyTextLink(a: HTMLAnchorElement, l: TextLink): void;
12
+ export declare const toLinkValue: (l: TextLink) => LinkValue;
13
+ /** V2 saveLink (RTE:883-899): the resolved href is what is stored; a page or section pick re-reads from it. */
14
+ export declare const fromLinkValue: (v: LinkValue, allowNewTab: boolean) => TextLink;
5
15
  /** An address a link may carry: http(s), mailto, tel, a site path or an in-page anchor. */
6
16
  export declare function linkProblem(href: string): string | null;
7
17
  export declare function LinkForm({ initial, allowNewTab, onSave, onCancel, onDetach, dirty, }: {
@@ -7,11 +7,45 @@ import { jsx, jsxs } from "react/jsx-runtime";
7
7
  import { IconLink } from "@tabler/icons-react";
8
8
  //#region src/controls/rich-text/link-form.tsx
9
9
  /**
10
- * RICH TEXT · the link popover's body for a link inside the text, in V2's link panel chrome (controls/link.tsx
11
- * LinkPopoverBody: what it links to now · the address · Remove link · Cancel · Apply): the address, and "Open in new tab"
12
- * when the field allows it. A text link stores its address and target only (production's link mark), so V2's link type
13
- * select and rel choice, which belong to the link FIELD, are not offered here.
10
+ * RICH TEXT · a link inside the text. A text link and a button are edited in V2's own link panel (controls/link.tsx
11
+ * LinkPopoverBody: Type, the page/section/address, new tab and Rel; RTE:1696) and stored as production's link mark:
12
+ * the resolved href, target and rel (`toLinkValue` / `fromLinkValue`). `LinkForm` below is the address-only form an
13
+ * image block links with.
14
14
  */
15
+ var RELS = [
16
+ "nofollow",
17
+ "sponsored",
18
+ "ugc"
19
+ ];
20
+ /** An anchor's link as stored: href, a new tab, and its rel (a new tab's noopener/noreferrer are the renderer's). */
21
+ function textLinkOf(a) {
22
+ const tokens = (a.getAttribute("rel") ?? "").toLowerCase().split(/\s+/);
23
+ const rel = RELS.find((r) => tokens.includes(r));
24
+ return {
25
+ href: a.getAttribute("href") ?? "",
26
+ newTab: a.getAttribute("target") === "_blank",
27
+ ...rel ? { rel } : {}
28
+ };
29
+ }
30
+ /** Writes a link onto its anchor: href, target and rel (none for follow). */
31
+ function applyTextLink(a, l) {
32
+ a.setAttribute("href", l.href);
33
+ if (l.newTab) a.setAttribute("target", "_blank");
34
+ else a.removeAttribute("target");
35
+ if (l.rel) a.setAttribute("rel", l.rel);
36
+ else a.removeAttribute("rel");
37
+ }
38
+ var toLinkValue = (l) => ({
39
+ href: l.href,
40
+ newTab: !!l.newTab,
41
+ rel: l.rel ?? "follow"
42
+ });
43
+ /** V2 saveLink (RTE:883-899): the resolved href is what is stored; a page or section pick re-reads from it. */
44
+ var fromLinkValue = (v, allowNewTab) => ({
45
+ href: v.href,
46
+ newTab: allowNewTab && !!v.newTab,
47
+ ...v.rel && v.rel !== "follow" ? { rel: v.rel } : {}
48
+ });
15
49
  /** An address a link may carry: http(s), mailto, tel, a site path or an in-page anchor. */
16
50
  function linkProblem(href) {
17
51
  const h = href.trim();
@@ -142,6 +176,6 @@ function LinkForm({ initial, allowNewTab, onSave, onCancel, onDetach, dirty = fa
142
176
  });
143
177
  }
144
178
  //#endregion
145
- export { LinkForm, linkProblem };
179
+ export { LinkForm, applyTextLink, fromLinkValue, linkProblem, textLinkOf, toLinkValue };
146
180
 
147
181
  //# sourceMappingURL=link-form.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"link-form.js","names":[],"sources":["../../../src/controls/rich-text/link-form.tsx"],"sourcesContent":["/**\n * RICH TEXT · the link popover's body for a link inside the text, in V2's link panel chrome (controls/link.tsx\n * LinkPopoverBody: what it links to now · the address · Remove link · Cancel · Apply): the address, and \"Open in new tab\"\n * when the field allows it. A text link stores its address and target only (production's link mark), so V2's link type\n * select and rel choice, which belong to the link FIELD, are not offered here.\n */\nimport * as React from \"react\";\nimport { IconLink } from \"@tabler/icons-react\";\nimport { Button } from \"../../ui/button\";\nimport { Switch } from \"../../ui/controls\";\nimport { Input } from \"../../ui/input\";\nimport { LinkLine, normLink } from \"../link\";\n\nexport interface TextLink {\n href: string;\n newTab?: boolean;\n}\n\n/** An address a link may carry: http(s), mailto, tel, a site path or an in-page anchor. */\nexport function linkProblem(href: string): string | null {\n const h = href.trim();\n if (!h) return \"Add an address first.\";\n if (/^(https?:\\/\\/[^\\s]+|mailto:[^\\s]+|tel:[+\\d][\\d\\s()-]*|\\/(?!\\/)[^\\s]*|#[^\\s]+)$/i.test(h)) return null;\n if (/^[a-z][a-z0-9+.-]*:/i.test(h)) return \"Use an address that starts with https://, mailto: or tel:.\";\n if (/^[^\\s/]+\\.[^\\s]+$/.test(h)) return `Add https:// in front: https://${h}`;\n return \"Use a full address (https://…), a page path (/about) or an anchor (#section).\";\n}\n\nexport function LinkForm({\n initial,\n allowNewTab,\n onSave,\n onCancel,\n onDetach,\n dirty = false,\n}: {\n initial: TextLink;\n allowNewTab: boolean;\n onSave: (v: TextLink) => void;\n onCancel: () => void;\n onDetach?: () => void;\n /** something the host draws beside the form changed (a button's label), so Apply has something to save */\n dirty?: boolean;\n}) {\n const [href, setHref] = React.useState(initial.href);\n const [newTab, setNewTab] = React.useState(!!initial.newTab);\n const [problem, setProblem] = React.useState<string | null>(null);\n const urlId = React.useId();\n const tabId = React.useId();\n const errId = React.useId();\n const start = initial.href.trim();\n // V2 LinkPopoverBody (controls/link.tsx): Apply only when something changed and the link can be saved\n const changed = dirty || href.trim() !== start || (allowNewTab && newTab !== !!initial.newTab);\n const ok = !linkProblem(href);\n return (\n <form\n className=\"flex flex-col gap-3\"\n onSubmit={(e) => {\n e.preventDefault();\n const p = linkProblem(href);\n setProblem(p);\n if (!p) onSave({ href: href.trim(), newTab: allowNewTab && newTab });\n }}\n >\n <div className=\"flex min-w-0 flex-col gap-1\">\n <span className=\"text-kit-sm font-medium text-kit-fg\">Link</span>\n {start ? (\n <span className=\"flex min-w-0 text-kit-fg-2\">\n <LinkLine v={normLink({ href: start, newTab: !!initial.newTab })} />\n </span>\n ) : (\n <span className=\"text-kit-xs text-kit-fg-3\">No link yet</span>\n )}\n </div>\n <div\n className=\"flex flex-col gap-1.5\"\n onBlur={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget as Node) && href.trim()) setProblem(linkProblem(href));\n }}\n >\n <label htmlFor={urlId} className=\"text-kit-xs text-kit-fg-2\">\n Link to\n </label>\n <Input\n id={urlId}\n autoFocus\n size=\"sm\"\n value={href}\n onChange={(e) => {\n setHref(e.target.value);\n if (problem) setProblem(null);\n }}\n onKeyDown={(e) => {\n // Apply is disabled while the address cannot be saved, so Enter says why instead of doing nothing\n if (e.key === \"Enter\" && !ok) {\n e.preventDefault();\n setProblem(linkProblem(href));\n }\n }}\n placeholder=\"https://… or /page\"\n leading={<IconLink stroke={1.5} />}\n aria-invalid={problem ? true : undefined}\n aria-describedby={problem ? errId : undefined}\n />\n {problem ? (\n <p id={errId} role=\"alert\" className=\"text-kit-xs text-kit-danger-fg\">\n {problem}\n </p>\n ) : null}\n </div>\n {allowNewTab ? (\n <label htmlFor={tabId} className=\"flex min-h-7 items-center justify-between gap-3\">\n <span className=\"text-kit-xs text-kit-fg-2\">Open in new tab</span>\n <Switch id={tabId} size=\"sm\" checked={newTab} onCheckedChange={setNewTab} />\n </label>\n ) : null}\n <div className=\"flex items-center gap-2 border-t border-kit-line pt-2.5\">\n {onDetach ? (\n <Button type=\"button\" size=\"xs\" variant=\"danger-ghost\" onClick={onDetach}>\n Remove link\n </Button>\n ) : null}\n <span className=\"flex-1\" />\n <Button type=\"button\" size=\"xs\" variant=\"ghost\" onClick={onCancel}>\n Cancel\n </Button>\n <Button type=\"submit\" size=\"xs\" variant=\"primary\" disabled={!changed || !ok}>\n Apply\n </Button>\n </div>\n </form>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"link-form.js","names":[],"sources":["../../../src/controls/rich-text/link-form.tsx"],"sourcesContent":["/**\n * RICH TEXT · a link inside the text. A text link and a button are edited in V2's own link panel (controls/link.tsx\n * LinkPopoverBody: Type, the page/section/address, new tab and Rel; RTE:1696) and stored as production's link mark:\n * the resolved href, target and rel (`toLinkValue` / `fromLinkValue`). `LinkForm` below is the address-only form an\n * image block links with.\n */\nimport * as React from \"react\";\nimport { IconLink } from \"@tabler/icons-react\";\nimport { Button } from \"../../ui/button\";\nimport { Switch } from \"../../ui/controls\";\nimport { Input } from \"../../ui/input\";\nimport { LinkLine, normLink, type LinkRel, type LinkValue } from \"../link\";\n\nexport interface TextLink {\n href: string;\n newTab?: boolean;\n /** absent = follow (V2 writes no rel for it) */\n rel?: Exclude<LinkRel, \"follow\">;\n}\n\nconst RELS: Exclude<LinkRel, \"follow\">[] = [\"nofollow\", \"sponsored\", \"ugc\"];\n/** An anchor's link as stored: href, a new tab, and its rel (a new tab's noopener/noreferrer are the renderer's). */\nexport function textLinkOf(a: HTMLAnchorElement): TextLink {\n const tokens = (a.getAttribute(\"rel\") ?? \"\").toLowerCase().split(/\\s+/);\n const rel = RELS.find((r) => tokens.includes(r));\n return { href: a.getAttribute(\"href\") ?? \"\", newTab: a.getAttribute(\"target\") === \"_blank\", ...(rel ? { rel } : {}) };\n}\n/** Writes a link onto its anchor: href, target and rel (none for follow). */\nexport function applyTextLink(a: HTMLAnchorElement, l: TextLink) {\n a.setAttribute(\"href\", l.href);\n if (l.newTab) a.setAttribute(\"target\", \"_blank\");\n else a.removeAttribute(\"target\");\n if (l.rel) a.setAttribute(\"rel\", l.rel);\n else a.removeAttribute(\"rel\");\n}\nexport const toLinkValue = (l: TextLink): LinkValue => ({ href: l.href, newTab: !!l.newTab, rel: l.rel ?? \"follow\" });\n/** V2 saveLink (RTE:883-899): the resolved href is what is stored; a page or section pick re-reads from it. */\nexport const fromLinkValue = (v: LinkValue, allowNewTab: boolean): TextLink => ({\n href: v.href,\n newTab: allowNewTab && !!v.newTab,\n ...(v.rel && v.rel !== \"follow\" ? { rel: v.rel } : {}),\n});\n\n/** An address a link may carry: http(s), mailto, tel, a site path or an in-page anchor. */\nexport function linkProblem(href: string): string | null {\n const h = href.trim();\n if (!h) return \"Add an address first.\";\n if (/^(https?:\\/\\/[^\\s]+|mailto:[^\\s]+|tel:[+\\d][\\d\\s()-]*|\\/(?!\\/)[^\\s]*|#[^\\s]+)$/i.test(h)) return null;\n if (/^[a-z][a-z0-9+.-]*:/i.test(h)) return \"Use an address that starts with https://, mailto: or tel:.\";\n if (/^[^\\s/]+\\.[^\\s]+$/.test(h)) return `Add https:// in front: https://${h}`;\n return \"Use a full address (https://…), a page path (/about) or an anchor (#section).\";\n}\n\nexport function LinkForm({\n initial,\n allowNewTab,\n onSave,\n onCancel,\n onDetach,\n dirty = false,\n}: {\n initial: TextLink;\n allowNewTab: boolean;\n onSave: (v: TextLink) => void;\n onCancel: () => void;\n onDetach?: () => void;\n /** something the host draws beside the form changed (a button's label), so Apply has something to save */\n dirty?: boolean;\n}) {\n const [href, setHref] = React.useState(initial.href);\n const [newTab, setNewTab] = React.useState(!!initial.newTab);\n const [problem, setProblem] = React.useState<string | null>(null);\n const urlId = React.useId();\n const tabId = React.useId();\n const errId = React.useId();\n const start = initial.href.trim();\n // V2 LinkPopoverBody (controls/link.tsx): Apply only when something changed and the link can be saved\n const changed = dirty || href.trim() !== start || (allowNewTab && newTab !== !!initial.newTab);\n const ok = !linkProblem(href);\n return (\n <form\n className=\"flex flex-col gap-3\"\n onSubmit={(e) => {\n e.preventDefault();\n const p = linkProblem(href);\n setProblem(p);\n if (!p) onSave({ href: href.trim(), newTab: allowNewTab && newTab });\n }}\n >\n <div className=\"flex min-w-0 flex-col gap-1\">\n <span className=\"text-kit-sm font-medium text-kit-fg\">Link</span>\n {start ? (\n <span className=\"flex min-w-0 text-kit-fg-2\">\n <LinkLine v={normLink({ href: start, newTab: !!initial.newTab })} />\n </span>\n ) : (\n <span className=\"text-kit-xs text-kit-fg-3\">No link yet</span>\n )}\n </div>\n <div\n className=\"flex flex-col gap-1.5\"\n onBlur={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget as Node) && href.trim()) setProblem(linkProblem(href));\n }}\n >\n <label htmlFor={urlId} className=\"text-kit-xs text-kit-fg-2\">\n Link to\n </label>\n <Input\n id={urlId}\n autoFocus\n size=\"sm\"\n value={href}\n onChange={(e) => {\n setHref(e.target.value);\n if (problem) setProblem(null);\n }}\n onKeyDown={(e) => {\n // Apply is disabled while the address cannot be saved, so Enter says why instead of doing nothing\n if (e.key === \"Enter\" && !ok) {\n e.preventDefault();\n setProblem(linkProblem(href));\n }\n }}\n placeholder=\"https://… or /page\"\n leading={<IconLink stroke={1.5} />}\n aria-invalid={problem ? true : undefined}\n aria-describedby={problem ? errId : undefined}\n />\n {problem ? (\n <p id={errId} role=\"alert\" className=\"text-kit-xs text-kit-danger-fg\">\n {problem}\n </p>\n ) : null}\n </div>\n {allowNewTab ? (\n <label htmlFor={tabId} className=\"flex min-h-7 items-center justify-between gap-3\">\n <span className=\"text-kit-xs text-kit-fg-2\">Open in new tab</span>\n <Switch id={tabId} size=\"sm\" checked={newTab} onCheckedChange={setNewTab} />\n </label>\n ) : null}\n <div className=\"flex items-center gap-2 border-t border-kit-line pt-2.5\">\n {onDetach ? (\n <Button type=\"button\" size=\"xs\" variant=\"danger-ghost\" onClick={onDetach}>\n Remove link\n </Button>\n ) : null}\n <span className=\"flex-1\" />\n <Button type=\"button\" size=\"xs\" variant=\"ghost\" onClick={onCancel}>\n Cancel\n </Button>\n <Button type=\"submit\" size=\"xs\" variant=\"primary\" disabled={!changed || !ok}>\n Apply\n </Button>\n </div>\n </form>\n );\n}\n"],"mappings":"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"}
@@ -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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"}
@@ -11,9 +11,15 @@ export interface RelatedModel {
11
11
  posts: string[];
12
12
  }
13
13
  export declare const RELATED_LABEL = "Related posts";
14
- export declare const relatedDefault: () => RelatedModel;
15
- /** The panel form; `to` is the field's collection. "How many" explains the chosen automatic mode. */
16
- export declare const relatedSchema: (to: string[], mode: RelatedMode) => TypeSchema;
14
+ /** `topic: false`: the collection has nothing to rank Same topic by, so a new block is Newest. */
15
+ export declare const relatedDefault: (topic?: boolean) => RelatedModel;
16
+ /** Why Same topic is not offered (RichTextRead.references.topic false; the entry write's relatedPosts.no_topic_field). */
17
+ export declare const NO_TOPIC_REASON = "Same topic needs a tags or category field on this collection.";
18
+ /**
19
+ * The panel form; `to` is the field's collection. "How many" explains the chosen automatic mode. With `topic` false,
20
+ * Same topic is left out and Stories says why (a stored Same topic block keeps the option, so its value still shows).
21
+ */
22
+ export declare const relatedSchema: (to: string[], mode: RelatedMode, topic?: boolean) => TypeSchema;
17
23
  /** What the preview asks the host for: the picked ids, or the automatic mode's query. */
18
24
  export declare function relatedRequest(m: RelatedModel, collection?: string, excludeId?: string): KitRichTextRefRequest | null;
19
25
  export declare function RelatedPreview({ m, onPick, ro, collection, excludeId }: {
@@ -23,7 +29,7 @@ export declare function RelatedPreview({ m, onPick, ro, collection, excludeId }:
23
29
  collection?: string;
24
30
  excludeId?: string;
25
31
  }): import("react/jsx-runtime").JSX.Element;
26
- export declare function RelatedBlock({ m, set, el, ctx, autoFocus, collection, excludeId, }: {
32
+ export declare function RelatedBlock({ m, set, el, ctx, autoFocus, collection, excludeId, topic, }: {
27
33
  m: RelatedModel;
28
34
  set: (fn: (m: RelatedModel) => RelatedModel, label?: string) => void;
29
35
  el: IslandEl;
@@ -33,4 +39,6 @@ export declare function RelatedBlock({ m, set, el, ctx, autoFocus, collection, e
33
39
  collection: string;
34
40
  /** the entry being edited, left out of the automatic modes */
35
41
  excludeId?: string;
42
+ /** false: the collection has no topic field, so Same topic is not offered */
43
+ topic?: boolean;
36
44
  }): import("react/jsx-runtime").JSX.Element;
@@ -20,20 +20,26 @@ import { IconPlus } from "@tabler/icons-react";
20
20
  * A fresh insert focuses the card (V2 article-blocks.tsx:131).
21
21
  */
22
22
  var RELATED_LABEL = "Related posts";
23
- var relatedDefault = () => ({
23
+ /** `topic: false`: the collection has nothing to rank Same topic by, so a new block is Newest. */
24
+ var relatedDefault = (topic = true) => ({
24
25
  title: "Related stories",
25
- mode: "topic",
26
+ mode: topic ? "topic" : "newest",
26
27
  count: 3,
27
28
  posts: []
28
29
  });
30
+ /** Why Same topic is not offered (RichTextRead.references.topic false; the entry write's relatedPosts.no_topic_field). */
31
+ var NO_TOPIC_REASON = "Same topic needs a tags or category field on this collection.";
29
32
  var MODE_TAG = {
30
33
  topic: "Same topic",
31
34
  newest: "Newest"
32
35
  };
33
36
  var asMode = (v) => v === "newest" || v === "manual" ? v : "topic";
34
37
  var clampCount = (v) => Math.min(6, Math.max(1, Math.round(Number(v) || 3)));
35
- /** The panel form; `to` is the field's collection. "How many" explains the chosen automatic mode. */
36
- var relatedSchema = (to, mode) => ({
38
+ /**
39
+ * The panel form; `to` is the field's collection. "How many" explains the chosen automatic mode. With `topic` false,
40
+ * Same topic is left out and Stories says why (a stored Same topic block keeps the option, so its value still shows).
41
+ */
42
+ var relatedSchema = (to, mode, topic = true) => ({
37
43
  name: "relatedPosts",
38
44
  title: RELATED_LABEL,
39
45
  kind: "object",
@@ -50,12 +56,12 @@ var relatedSchema = (to, mode) => ({
50
56
  title: "Stories",
51
57
  type: "select",
52
58
  layout: "segmented",
53
- initialValue: "topic",
59
+ initialValue: topic ? "topic" : "newest",
54
60
  options: [
55
- {
61
+ ...topic || mode === "topic" ? [{
56
62
  value: "topic",
57
63
  label: "Same topic"
58
- },
64
+ }] : [],
59
65
  {
60
66
  value: "newest",
61
67
  label: "Newest"
@@ -64,7 +70,8 @@ var relatedSchema = (to, mode) => ({
64
70
  value: "manual",
65
71
  label: "Hand-picked"
66
72
  }
67
- ]
73
+ ],
74
+ ...topic ? {} : { description: NO_TOPIC_REASON }
68
75
  },
69
76
  {
70
77
  name: "count",
@@ -174,8 +181,12 @@ function RelatedPreview({ m, onPick, ro, collection, excludeId }) {
174
181
  })]
175
182
  });
176
183
  }
177
- function RelatedBlock({ m, set, el, ctx, autoFocus, collection, excludeId }) {
178
- const schema = React.useMemo(() => relatedSchema([collection], m.mode), [collection, m.mode]);
184
+ function RelatedBlock({ m, set, el, ctx, autoFocus, collection, excludeId, topic = true }) {
185
+ const schema = React.useMemo(() => relatedSchema([collection], m.mode, topic), [
186
+ collection,
187
+ m.mode,
188
+ topic
189
+ ]);
179
190
  return /* @__PURE__ */ jsx(ArticlePanelBlock, {
180
191
  label: RELATED_LABEL,
181
192
  kind: "related",
@@ -208,6 +219,6 @@ function RelatedBlock({ m, set, el, ctx, autoFocus, collection, excludeId }) {
208
219
  });
209
220
  }
210
221
  //#endregion
211
- export { RELATED_LABEL, RelatedBlock, RelatedPreview, relatedDefault, relatedRequest, relatedSchema };
222
+ export { NO_TOPIC_REASON, RELATED_LABEL, RelatedBlock, RelatedPreview, relatedDefault, relatedRequest, relatedSchema };
212
223
 
213
224
  //# sourceMappingURL=related.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"related.js","names":[],"sources":["../../../src/controls/rich-text/related.tsx"],"sourcesContent":["/**\n * RICH TEXT · Related posts (kit-richtext-ui-spec.md §6): V2's article block, stored as the PT block object\n * `{ _type: \"relatedPosts\", title?, mode, count, posts: { _ref }[] }`. Each post's title, cover, category and URL are\n * derived from the referenced or queried entries, never stored.\n *\n * modes e1 2026-10-24 (signed deviation from V2's Automatic | Hand-picked): Same topic (topic) · Newest (newest) ·\n * Hand-picked (manual). \"How many\" shows for the two automatic modes, Posts for Hand-picked. The schema's\n * `hidden` is \"show only when\" (schema/types.ts), as V2's was.\n * form V2 article.ts:167-195: Title (≤40, default \"Related stories\"), Stories (segmented), How many (1–6),\n * Posts (1–6 unique references to the field's collection). Four settings → the floating panel (D54).\n * preview V2 article-blocks.tsx:572-597: the title with the automatic mode's tag, three post cards (4:3 cover,\n * two-line title, category); \"Pick stories\" when Hand-picked holds nothing that resolves.\n * data KitServices.resolveRichTextRefs (useRichTextRefs): ids for Hand-picked, a query for the automatic modes.\n * A fresh insert focuses the card (V2 article-blocks.tsx:131).\n */\nimport * as React from \"react\";\nimport { IconPlus } from \"@tabler/icons-react\";\nimport { useRichTextRefs, type KitRichTextRefRequest } from \"../../context\";\nimport type { TypeSchema } from \"../../schema/types\";\nimport { ArticleEmpty, ArticlePanelBlock } from \"./article-shell\";\nimport type { IslandCtx } from \"./blocks\";\nimport type { IslandEl } from \"./dom\";\n\nexport type RelatedMode = \"topic\" | \"newest\" | \"manual\";\nexport interface RelatedModel {\n title: string;\n mode: RelatedMode;\n count: number;\n /** entry ids, Hand-picked only; the PT form wraps each as `{ _key, _ref }` */\n posts: string[];\n}\nexport const RELATED_LABEL = \"Related posts\";\nexport const relatedDefault = (): RelatedModel => ({ title: \"Related stories\", mode: \"topic\", count: 3, posts: [] });\nconst MODE_TAG: Record<Exclude<RelatedMode, \"manual\">, string> = { topic: \"Same topic\", newest: \"Newest\" };\nconst asMode = (v: unknown): RelatedMode => (v === \"newest\" || v === \"manual\" ? v : \"topic\");\nconst clampCount = (v: unknown) => Math.min(6, Math.max(1, Math.round(Number(v) || 3)));\n\n/** The panel form; `to` is the field's collection. \"How many\" explains the chosen automatic mode. */\nexport const relatedSchema = (to: string[], mode: RelatedMode): TypeSchema => ({\n name: \"relatedPosts\",\n title: RELATED_LABEL,\n kind: \"object\",\n icon: \"news\",\n fields: [\n { name: \"title\", title: \"Title\", type: \"string\", validation: { max: 40 } },\n {\n name: \"mode\",\n title: \"Stories\",\n type: \"select\",\n layout: \"segmented\",\n initialValue: \"topic\",\n options: [\n { value: \"topic\", label: \"Same topic\" },\n { value: \"newest\", label: \"Newest\" },\n { value: \"manual\", label: \"Hand-picked\" },\n ],\n },\n {\n name: \"count\",\n title: \"How many\",\n type: \"number\",\n hidden: { field: \"mode\", op: \"in\", value: [\"topic\", \"newest\"] },\n numeric: { min: 1, max: 6, step: 1 },\n description: mode === \"newest\" ? \"Newest first.\" : \"Same topic, newest first.\",\n },\n {\n name: \"posts\",\n title: \"Posts\",\n type: \"array\",\n hidden: { field: \"mode\", equals: \"manual\" },\n validation: { min: 1, max: 6, unique: true },\n of: [{ name: \"post\", title: \"Post\", type: \"reference\", reference: { to } }],\n },\n ],\n});\n\n/** What the preview asks the host for: the picked ids, or the automatic mode's query. */\nexport function relatedRequest(m: RelatedModel, collection?: string, excludeId?: string): KitRichTextRefRequest | null {\n if (m.mode === \"manual\") return m.posts.length ? { kind: \"relatedPosts\", ids: m.posts } : null;\n return { kind: \"relatedPosts\", query: { collection, mode: m.mode, count: m.count, excludeId } };\n}\n\nexport function RelatedPreview({ m, onPick, ro, collection, excludeId }: { m: RelatedModel; onPick: () => void; ro: boolean; collection?: string; excludeId?: string }) {\n const { items, loading, error } = useRichTextRefs(relatedRequest(m, collection, excludeId));\n // Hand-picked keeps the picked order; a post that no longer resolves is skipped (V2 .filter)\n const posts = m.mode === \"manual\" ? m.posts.map((id) => items.find((x) => x.id === id)).filter((x): x is NonNullable<typeof x> => !!x) : items;\n if (m.mode === \"manual\" && !loading && !error && !posts.length)\n return <ArticleEmpty icon={<IconPlus stroke={1.5} />} title={m.title || RELATED_LABEL} action=\"Pick stories\" onAction={onPick} ro={ro} />;\n return (\n <aside className=\"rounded-kit-lg border border-kit-line bg-kit-surface px-4 py-3\" aria-label={m.title || RELATED_LABEL} aria-busy={loading || undefined}>\n <div className=\"mb-2 flex items-baseline gap-2\">\n {m.title ? (\n <p data-field=\"title\" className=\"text-kit-sm font-semibold text-kit-fg\">\n {m.title}\n </p>\n ) : null}\n {m.mode !== \"manual\" ? (\n <span data-field=\"mode\" className=\"text-kit-2xs text-kit-fg-3\">\n {MODE_TAG[m.mode]}\n </span>\n ) : null}\n </div>\n {error ? (\n <p role=\"status\" className=\"text-kit-sm text-kit-warning-fg\">\n The stories couldn't load: {error}\n </p>\n ) : (\n <ul className=\"m-0 grid grid-cols-3 gap-3 p-0\" data-field={m.mode === \"manual\" ? \"posts\" : \"count\"}>\n {posts.map((e) => (\n <li key={e.id} className=\"min-w-0 list-none\">\n {e.cover ? (\n <img src={e.cover} alt=\"\" draggable={false} className=\"block aspect-[4/3] w-full rounded-kit-md border border-kit-line object-cover\" />\n ) : (\n <div className=\"aspect-[4/3] rounded-kit-md bg-kit-muted\" />\n )}\n <p className=\"mt-1.5 line-clamp-2 text-kit-xs font-medium leading-snug text-kit-fg\">{e.title}</p>\n {e.category ? <p className=\"mt-0.5 truncate text-kit-2xs text-kit-fg-3\">{e.category}</p> : null}\n </li>\n ))}\n </ul>\n )}\n </aside>\n );\n}\n\nexport function RelatedBlock({\n m, set, el, ctx, autoFocus, collection, excludeId,\n}: {\n m: RelatedModel;\n set: (fn: (m: RelatedModel) => RelatedModel, label?: string) => void;\n el: IslandEl;\n ctx: IslandCtx;\n autoFocus: boolean;\n /** the posts collection (the field's config) */\n collection: string;\n /** the entry being edited, left out of the automatic modes */\n excludeId?: string;\n}) {\n const schema = React.useMemo(() => relatedSchema([collection], m.mode), [collection, m.mode]);\n return (\n <ArticlePanelBlock<RelatedModel>\n label={RELATED_LABEL}\n kind=\"related\"\n schema={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, mode: x.mode, count: x.count, posts: x.posts })}\n fromForm={(v) => ({\n title: String(v.title ?? \"\"),\n mode: asMode(v.mode),\n count: clampCount(v.count),\n posts: Array.isArray(v.posts) ? v.posts.filter((x): x is string => typeof x === \"string\") : [],\n })}\n >\n {(open, ro) => <RelatedPreview m={m} onPick={() => open(\"posts\")} ro={ro} collection={collection} excludeId={excludeId} />}\n </ArticlePanelBlock>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"related.js","names":[],"sources":["../../../src/controls/rich-text/related.tsx"],"sourcesContent":["/**\n * RICH TEXT · Related posts (kit-richtext-ui-spec.md §6): V2's article block, stored as the PT block object\n * `{ _type: \"relatedPosts\", title?, mode, count, posts: { _ref }[] }`. Each post's title, cover, category and URL are\n * derived from the referenced or queried entries, never stored.\n *\n * modes e1 2026-10-24 (signed deviation from V2's Automatic | Hand-picked): Same topic (topic) · Newest (newest) ·\n * Hand-picked (manual). \"How many\" shows for the two automatic modes, Posts for Hand-picked. The schema's\n * `hidden` is \"show only when\" (schema/types.ts), as V2's was.\n * form V2 article.ts:167-195: Title (≤40, default \"Related stories\"), Stories (segmented), How many (1–6),\n * Posts (1–6 unique references to the field's collection). Four settings → the floating panel (D54).\n * preview V2 article-blocks.tsx:572-597: the title with the automatic mode's tag, three post cards (4:3 cover,\n * two-line title, category); \"Pick stories\" when Hand-picked holds nothing that resolves.\n * data KitServices.resolveRichTextRefs (useRichTextRefs): ids for Hand-picked, a query for the automatic modes.\n * A fresh insert focuses the card (V2 article-blocks.tsx:131).\n */\nimport * as React from \"react\";\nimport { IconPlus } from \"@tabler/icons-react\";\nimport { useRichTextRefs, type KitRichTextRefRequest } from \"../../context\";\nimport type { TypeSchema } from \"../../schema/types\";\nimport { ArticleEmpty, ArticlePanelBlock } from \"./article-shell\";\nimport type { IslandCtx } from \"./blocks\";\nimport type { IslandEl } from \"./dom\";\n\nexport type RelatedMode = \"topic\" | \"newest\" | \"manual\";\nexport interface RelatedModel {\n title: string;\n mode: RelatedMode;\n count: number;\n /** entry ids, Hand-picked only; the PT form wraps each as `{ _key, _ref }` */\n posts: string[];\n}\nexport const RELATED_LABEL = \"Related posts\";\n/** `topic: false`: the collection has nothing to rank Same topic by, so a new block is Newest. */\nexport const relatedDefault = (topic = true): RelatedModel => ({ title: \"Related stories\", mode: topic ? \"topic\" : \"newest\", count: 3, posts: [] });\n/** Why Same topic is not offered (RichTextRead.references.topic false; the entry write's relatedPosts.no_topic_field). */\nexport const NO_TOPIC_REASON = \"Same topic needs a tags or category field on this collection.\";\nconst MODE_TAG: Record<Exclude<RelatedMode, \"manual\">, string> = { topic: \"Same topic\", newest: \"Newest\" };\nconst asMode = (v: unknown): RelatedMode => (v === \"newest\" || v === \"manual\" ? v : \"topic\");\nconst clampCount = (v: unknown) => Math.min(6, Math.max(1, Math.round(Number(v) || 3)));\n\n/**\n * The panel form; `to` is the field's collection. \"How many\" explains the chosen automatic mode. With `topic` false,\n * Same topic is left out and Stories says why (a stored Same topic block keeps the option, so its value still shows).\n */\nexport const relatedSchema = (to: string[], mode: RelatedMode, topic = true): TypeSchema => ({\n name: \"relatedPosts\",\n title: RELATED_LABEL,\n kind: \"object\",\n icon: \"news\",\n fields: [\n { name: \"title\", title: \"Title\", type: \"string\", validation: { max: 40 } },\n {\n name: \"mode\",\n title: \"Stories\",\n type: \"select\",\n layout: \"segmented\",\n initialValue: topic ? \"topic\" : \"newest\",\n options: [\n ...(topic || mode === \"topic\" ? [{ value: \"topic\", label: \"Same topic\" }] : []),\n { value: \"newest\", label: \"Newest\" },\n { value: \"manual\", label: \"Hand-picked\" },\n ],\n ...(topic ? {} : { description: NO_TOPIC_REASON }),\n },\n {\n name: \"count\",\n title: \"How many\",\n type: \"number\",\n hidden: { field: \"mode\", op: \"in\", value: [\"topic\", \"newest\"] },\n numeric: { min: 1, max: 6, step: 1 },\n description: mode === \"newest\" ? \"Newest first.\" : \"Same topic, newest first.\",\n },\n {\n name: \"posts\",\n title: \"Posts\",\n type: \"array\",\n hidden: { field: \"mode\", equals: \"manual\" },\n validation: { min: 1, max: 6, unique: true },\n of: [{ name: \"post\", title: \"Post\", type: \"reference\", reference: { to } }],\n },\n ],\n});\n\n/** What the preview asks the host for: the picked ids, or the automatic mode's query. */\nexport function relatedRequest(m: RelatedModel, collection?: string, excludeId?: string): KitRichTextRefRequest | null {\n if (m.mode === \"manual\") return m.posts.length ? { kind: \"relatedPosts\", ids: m.posts } : null;\n return { kind: \"relatedPosts\", query: { collection, mode: m.mode, count: m.count, excludeId } };\n}\n\nexport function RelatedPreview({ m, onPick, ro, collection, excludeId }: { m: RelatedModel; onPick: () => void; ro: boolean; collection?: string; excludeId?: string }) {\n const { items, loading, error } = useRichTextRefs(relatedRequest(m, collection, excludeId));\n // Hand-picked keeps the picked order; a post that no longer resolves is skipped (V2 .filter)\n const posts = m.mode === \"manual\" ? m.posts.map((id) => items.find((x) => x.id === id)).filter((x): x is NonNullable<typeof x> => !!x) : items;\n if (m.mode === \"manual\" && !loading && !error && !posts.length)\n return <ArticleEmpty icon={<IconPlus stroke={1.5} />} title={m.title || RELATED_LABEL} action=\"Pick stories\" onAction={onPick} ro={ro} />;\n return (\n <aside className=\"rounded-kit-lg border border-kit-line bg-kit-surface px-4 py-3\" aria-label={m.title || RELATED_LABEL} aria-busy={loading || undefined}>\n <div className=\"mb-2 flex items-baseline gap-2\">\n {m.title ? (\n <p data-field=\"title\" className=\"text-kit-sm font-semibold text-kit-fg\">\n {m.title}\n </p>\n ) : null}\n {m.mode !== \"manual\" ? (\n <span data-field=\"mode\" className=\"text-kit-2xs text-kit-fg-3\">\n {MODE_TAG[m.mode]}\n </span>\n ) : null}\n </div>\n {error ? (\n <p role=\"status\" className=\"text-kit-sm text-kit-warning-fg\">\n The stories couldn't load: {error}\n </p>\n ) : (\n <ul className=\"m-0 grid grid-cols-3 gap-3 p-0\" data-field={m.mode === \"manual\" ? \"posts\" : \"count\"}>\n {posts.map((e) => (\n <li key={e.id} className=\"min-w-0 list-none\">\n {e.cover ? (\n <img src={e.cover} alt=\"\" draggable={false} className=\"block aspect-[4/3] w-full rounded-kit-md border border-kit-line object-cover\" />\n ) : (\n <div className=\"aspect-[4/3] rounded-kit-md bg-kit-muted\" />\n )}\n <p className=\"mt-1.5 line-clamp-2 text-kit-xs font-medium leading-snug text-kit-fg\">{e.title}</p>\n {e.category ? <p className=\"mt-0.5 truncate text-kit-2xs text-kit-fg-3\">{e.category}</p> : null}\n </li>\n ))}\n </ul>\n )}\n </aside>\n );\n}\n\nexport function RelatedBlock({\n m, set, el, ctx, autoFocus, collection, excludeId, topic = true,\n}: {\n m: RelatedModel;\n set: (fn: (m: RelatedModel) => RelatedModel, label?: string) => void;\n el: IslandEl;\n ctx: IslandCtx;\n autoFocus: boolean;\n /** the posts collection (the field's config) */\n collection: string;\n /** the entry being edited, left out of the automatic modes */\n excludeId?: string;\n /** false: the collection has no topic field, so Same topic is not offered */\n topic?: boolean;\n}) {\n const schema = React.useMemo(() => relatedSchema([collection], m.mode, topic), [collection, m.mode, topic]);\n return (\n <ArticlePanelBlock<RelatedModel>\n label={RELATED_LABEL}\n kind=\"related\"\n schema={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, mode: x.mode, count: x.count, posts: x.posts })}\n fromForm={(v) => ({\n title: String(v.title ?? \"\"),\n mode: asMode(v.mode),\n count: clampCount(v.count),\n posts: Array.isArray(v.posts) ? v.posts.filter((x): x is string => typeof x === \"string\") : [],\n })}\n >\n {(open, ro) => <RelatedPreview m={m} onPick={() => open(\"posts\")} ro={ro} collection={collection} excludeId={excludeId} />}\n </ArticlePanelBlock>\n );\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])];