@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
@@ -4,11 +4,13 @@ import { Button } from "../../ui/button.js";
4
4
  import { shortcutHintsOn } from "../../ui/shortcut-hint.js";
5
5
  import { Menu, MenuContent, MenuItem, MenuSeparator, MenuSub, MenuSubContent, MenuSubTrigger, MenuTrigger, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Tooltip } from "../../ui/overlay.js";
6
6
  import { Textarea } from "../../ui/input.js";
7
+ import { LinkPopoverBody } from "../link.js";
7
8
  import { ExpandedEditorModal } from "../../ui/expanded-editor-modal.js";
8
9
  import { COMPONENT_INSERT_TOAST } from "../component-services.js";
9
10
  import { eventKey } from "../../lib/key-event.js";
10
11
  import { closestBlock, createIsland, defaultModel, editingHost, esc, hydrate, isEmptyHtml, placeCaret, rangeRect, rid, sanitize, serialize, textBeforeCaret, topBlock } from "./dom.js";
11
- import { LinkForm } from "./link-form.js";
12
+ import { applyTextLink, fromLinkValue, textLinkOf, toLinkValue } from "./link-form.js";
13
+ import { createOutlineStore, outlineOf } from "./toc.js";
12
14
  import { CatalogComponentsProvider, InsertMenuContent, SlashMenuPanel, blockAllowed, catalogKey, noteRecent, useCatalog } from "./catalog.js";
13
15
  import { openComponentPanel } from "./component-panel.js";
14
16
  import { Island } from "./blocks.js";
@@ -19,6 +21,7 @@ import { ALL_FEATURES } from "./codec.js";
19
21
  import { htmlToMarkdown, looksLikeMarkdown, markdownToHtml } from "./markdown.js";
20
22
  import { EMPTY_FMT, FormatToolbar, MARK_FEATURE, stylesFor } from "./toolbar.js";
21
23
  import { SelectionBubble } from "./floating.js";
24
+ import { FN_MARK, FOOTNOTE_POPOVER_CLASS, FOOTNOTE_REFUSED, FootnoteEditor, FootnotesList, backToMarker, caretAfterMarker, insertFootnote, renumberFootnotes } from "./footnote.js";
22
25
  import * as React from "react";
23
26
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
24
27
  import { IconArrowBarToDown, IconArrowBarToUp, IconCopy, IconGripVertical, IconPlus, IconTransform, IconTrash } from "@tabler/icons-react";
@@ -175,7 +178,7 @@ function slashPlacement(caret, viewportH) {
175
178
  maxHeight: Math.max(SLASH_MIN_H, Math.min(SLASH_MENU_H, flipUp ? above : below))
176
179
  };
177
180
  }
178
- function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, features = ALL_FEATURES, single, allowNewTab = true, id, invalid, variant = "document", expandCrumbs, expandedOpen, onExpandedOpenChange, inModal, hideFullscreen, maxHeight, autoFocus, onComment, components }) {
181
+ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, features = ALL_FEATURES, references, single, allowNewTab = true, id, invalid, variant = "document", expandCrumbs, expandedOpen, onExpandedOpenChange, inModal, hideFullscreen, maxHeight, autoFocus, onComment, components }) {
179
182
  const hints = shortcutHintsOn();
180
183
  const ph = placeholder ?? (hints ? "Type '/' for commands" : "Write something…");
181
184
  const dense = variant === "panel";
@@ -263,6 +266,11 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
263
266
  const t = window.setTimeout(() => setNotice(null), 8e3);
264
267
  return () => window.clearTimeout(t);
265
268
  }, [notice]);
269
+ /** V2 article-blocks.tsx:45-66: one live outline per editor, so every table of contents follows the headings */
270
+ const outline = React.useRef(createOutlineStore());
271
+ /** V2 rich-text-editor.tsx:271-276: the body's markers in text order, and the one whose note is open */
272
+ const [footnotes, setFootnotes] = React.useState([]);
273
+ const [fnEdit, setFnEdit] = React.useState(null);
266
274
  const syncIslands = React.useCallback(() => {
267
275
  const el = ref.current;
268
276
  if (!el) return setIslands((p) => p.length ? [] : p);
@@ -286,8 +294,12 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
286
294
  const el = ref.current;
287
295
  if (!el) return;
288
296
  el.innerHTML = sanitize(html || "") || "<p><br></p>";
297
+ el.querySelectorAll(":scope > ol[data-rt=\"footnotes\"]").forEach((n) => n.remove());
298
+ el.querySelectorAll(FN_MARK).forEach((m) => m.setAttribute("contenteditable", "false"));
289
299
  hydrate(el);
290
300
  syncTaskA11y(el);
301
+ setFootnotes(renumberFootnotes(el));
302
+ outline.current.set(outlineOf(el));
291
303
  known.current = html;
292
304
  setEmpty(isEmptyHtml(html));
293
305
  setWords(countWords(html));
@@ -299,6 +311,8 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
299
311
  if (!el) return;
300
312
  syncIslands();
301
313
  syncTaskA11y(el);
314
+ outline.current.set(outlineOf(el));
315
+ setFootnotes(renumberFootnotes(el));
302
316
  const html = serialize(el);
303
317
  setEmpty(isEmptyHtml(html) && !el.querySelector(":scope > [data-rt-island], :scope > hr"));
304
318
  setWords(countWords(html));
@@ -584,6 +598,13 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
584
598
  }
585
599
  if (!it.id || !blockAllowed(it.id, allows)) return;
586
600
  noteRecent(it);
601
+ if (it.id === "footnote") {
602
+ const m = insertFootnote(el);
603
+ if (!m) return void notify(FOOTNOTE_REFUSED.title, { description: FOOTNOTE_REFUSED.description });
604
+ emit();
605
+ openFootnote(m, true);
606
+ return;
607
+ }
587
608
  if (it.id === "task") insertChecklist(el);
588
609
  else if (it.id === "button") {
589
610
  const a = insertButton(el);
@@ -591,7 +612,11 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
591
612
  refresh();
592
613
  openButton(a);
593
614
  return;
594
- } else insertBasic(el, it.id);
615
+ } else if (it.id === "related" && references?.topic === false) insertIsland(el, "related", {
616
+ ...defaultModel("related"),
617
+ mode: "newest"
618
+ });
619
+ else insertBasic(el, it.id);
595
620
  emit();
596
621
  refresh();
597
622
  };
@@ -832,12 +857,31 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
832
857
  setLink({
833
858
  rect,
834
859
  existing: anchor,
835
- initial: anchor ? {
836
- href: anchor.getAttribute("href") ?? "",
837
- newTab: anchor.getAttribute("target") === "_blank"
838
- } : { href: "" }
860
+ initial: anchor ? textLinkOf(anchor) : { href: "" }
861
+ });
862
+ };
863
+ const openFootnote = (m, fresh = false) => {
864
+ if (readOnly) return;
865
+ setBubble(null);
866
+ setFnEdit({
867
+ el: m,
868
+ rect: m.getBoundingClientRect(),
869
+ fresh
839
870
  });
840
871
  };
872
+ const removeFootnote = (m) => {
873
+ setFnEdit(null);
874
+ m.remove();
875
+ emit();
876
+ };
877
+ /** closed: a new footnote with no note is taken back out (V2), else the caret goes back after its marker */
878
+ const closeFootnote = () => {
879
+ const f = fnEdit;
880
+ setFnEdit(null);
881
+ if (!f || !ref.current) return;
882
+ if (f.fresh && !(f.el.getAttribute("data-note") ?? "").trim()) return removeFootnote(f.el);
883
+ caretAfterMarker(ref.current, f.el);
884
+ };
841
885
  /** V2 rich-text-editor.tsx:865-873: a button's popover, its label above the link form */
842
886
  const openButton = (a) => {
843
887
  if (readOnly) return;
@@ -865,15 +909,14 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
865
909
  setLink(null);
866
910
  if (!l) return;
867
911
  if (l.existing) {
868
- l.existing.setAttribute("href", v.href);
869
- if (v.newTab) l.existing.setAttribute("target", "_blank");
870
- else l.existing.removeAttribute("target");
912
+ applyTextLink(l.existing, v);
871
913
  emit();
872
914
  return;
873
915
  }
874
916
  run(() => {
875
917
  const text = window.getSelection()?.toString() || v.href;
876
- insertHtmlAtCaret(`<a href="${esc(v.href).replace(/"/g, "&quot;")}"${v.newTab ? ` target="_blank"` : ""}>${esc(text)}</a>`);
918
+ const attr = (s) => esc(s).replace(/"/g, "&quot;");
919
+ insertHtmlAtCaret(`<a href="${attr(v.href)}"${v.newTab ? ` target="_blank"` : ""}${v.rel ? ` rel="${attr(v.rel)}"` : ""}>${esc(text)}</a>`);
877
920
  });
878
921
  };
879
922
  const aiRewrite = (preset) => {
@@ -1159,8 +1202,14 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
1159
1202
  undo: () => api.current.islandUndo(),
1160
1203
  redo: () => api.current.islandRedo(),
1161
1204
  drag: (x, e) => api.current.startDrag(x, e),
1162
- allows: (f) => api.current.allows(f)
1163
- }), [readOnly, dense]);
1205
+ allows: (f) => api.current.allows(f),
1206
+ outline: outline.current,
1207
+ references
1208
+ }), [
1209
+ readOnly,
1210
+ dense,
1211
+ references
1212
+ ]);
1164
1213
  const commentSel = onComment ? () => {
1165
1214
  restoreSel();
1166
1215
  const q = window.getSelection()?.toString().trim();
@@ -1370,6 +1419,11 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
1370
1419
  emit();
1371
1420
  },
1372
1421
  onClick: (e) => {
1422
+ const fn = e.target.closest?.(FN_MARK);
1423
+ if (fn && ref.current?.contains(fn) && !fn.closest("[data-rt-island]") && !readOnly) {
1424
+ e.preventDefault();
1425
+ return openFootnote(fn);
1426
+ }
1373
1427
  const a = ref.current ? buttonAt(ref.current, e.target) : null;
1374
1428
  if (!a || readOnly) return;
1375
1429
  e.preventDefault();
@@ -1422,7 +1476,7 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
1422
1476
  notify("That drop is not an image from the media library");
1423
1477
  }
1424
1478
  },
1425
- className: cn(RT_PROSE, dense && "text-kit-sm leading-[1.6] [&_h1:not([data-rt-isolate]_*)]:text-kit-xl [&_h1:not([data-rt-isolate]_*)]:leading-7 [&_h2:not([data-rt-isolate]_*)]:text-kit-lg [&_h2:not([data-rt-isolate]_*)]:leading-7 [&_h3:not([data-rt-isolate]_*)]:text-kit-md [&_h4:not([data-rt-isolate]_*)]:text-kit-base [&_h5:not([data-rt-isolate]_*)]:text-kit-sm [&_h6:not([data-rt-isolate]_*)]:text-kit-xs", "peer relative outline-none focus-visible:shadow-none [&_*:not([data-rt-isolate]_*)]:outline-none", "[&>p[data-hint]]:before:pointer-events-none [&>p[data-hint]]:before:float-left [&>p[data-hint]]:before:h-0 [&>p[data-hint]]:before:text-kit-fg-4 [&>p[data-hint]]:before:content-[attr(data-hint)]", "[&_a[data-button]:not([data-rt-isolate]_*)]:cursor-pointer [&_ul[data-task]>li:not([data-rt-isolate]_*)]:before:cursor-pointer", "[&_sup[data-rt=fn]]:mx-px [&_sup[data-rt=fn]]:cursor-pointer [&_sup[data-rt=fn]]:rounded-kit-sm [&_sup[data-rt=fn]]:px-0.5 [&_sup[data-rt=fn]]:text-[0.72em] [&_sup[data-rt=fn]]:font-semibold [&_sup[data-rt=fn]]:text-kit-accent-fg [&_sup[data-rt=fn]:hover]:bg-kit-accent-soft", dense ? "px-2.5 py-2" : "px-4 py-3.5", minH)
1479
+ className: cn(RT_PROSE, dense && "text-kit-sm leading-[1.6] [&_h1:not([data-rt-isolate]_*)]:text-kit-xl [&_h1:not([data-rt-isolate]_*)]:leading-7 [&_h2:not([data-rt-isolate]_*)]:text-kit-lg [&_h2:not([data-rt-isolate]_*)]:leading-7 [&_h3:not([data-rt-isolate]_*)]:text-kit-md [&_h4:not([data-rt-isolate]_*)]:text-kit-base [&_h5:not([data-rt-isolate]_*)]:text-kit-sm [&_h6:not([data-rt-isolate]_*)]:text-kit-xs", "peer relative outline-none focus-visible:shadow-none [&_*:not([data-rt-isolate]_*)]:outline-none", "[&>p[data-hint]]:before:pointer-events-none [&>p[data-hint]]:before:float-left [&>p[data-hint]]:before:h-0 [&>p[data-hint]]:before:text-kit-fg-4 [&>p[data-hint]]:before:content-[attr(data-hint)]", "[&_a[data-button]:not([data-rt-isolate]_*)]:cursor-pointer [&_ul[data-task]>li:not([data-rt-isolate]_*)]:before:cursor-pointer", "[&_sup[data-rt=fn]]:mx-px [&_sup[data-rt=fn]]:cursor-pointer [&_sup[data-rt=fn]]:rounded-kit-sm [&_sup[data-rt=fn]]:px-0.5 [&_sup[data-rt=fn]]:text-[0.72em] [&_sup[data-rt=fn]]:leading-[0] [&_sup[data-rt=fn]]:font-semibold [&_sup[data-rt=fn]]:text-kit-accent-fg [&_sup[data-rt=fn]:hover]:bg-kit-accent-soft", dense ? "px-2.5 py-2" : "px-4 py-3.5", minH)
1426
1480
  }),
1427
1481
  empty ? /* @__PURE__ */ jsx("div", {
1428
1482
  "aria-hidden": true,
@@ -1551,6 +1605,13 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
1551
1605
  }), el, key))
1552
1606
  ]
1553
1607
  }),
1608
+ /* @__PURE__ */ jsx(FootnotesList, {
1609
+ notes: footnotes,
1610
+ readOnly,
1611
+ density: dense ? "dense" : "default",
1612
+ onEdit: (m) => openFootnote(m),
1613
+ onBack: (m) => ref.current && backToMarker(ref.current, m)
1614
+ }),
1554
1615
  /* @__PURE__ */ jsxs("div", {
1555
1616
  className: "flex h-7 items-center justify-end gap-3 rounded-b-kit-lg border-t border-kit-line-subtle px-3 text-kit-2xs text-kit-fg-3",
1556
1617
  children: [notice ? /* @__PURE__ */ jsxs("span", {
@@ -1632,6 +1693,35 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
1632
1693
  maxHeight: slashMaxH
1633
1694
  })
1634
1695
  })] }), document.body) : null,
1696
+ /* @__PURE__ */ jsxs(Popover, {
1697
+ open: !!fnEdit,
1698
+ onOpenChange: (o) => !o && closeFootnote(),
1699
+ children: [/* @__PURE__ */ jsx(PopoverAnchor, { render: /* @__PURE__ */ jsx("span", {
1700
+ className: "pointer-events-none fixed",
1701
+ style: fnEdit ? {
1702
+ top: fnEdit.rect.bottom,
1703
+ left: fnEdit.rect.left,
1704
+ width: Math.max(1, fnEdit.rect.width),
1705
+ height: 1
1706
+ } : {
1707
+ top: 0,
1708
+ left: 0
1709
+ }
1710
+ }) }), /* @__PURE__ */ jsx(PopoverContent, {
1711
+ className: FOOTNOTE_POPOVER_CLASS,
1712
+ finalFocus: false,
1713
+ children: fnEdit ? /* @__PURE__ */ jsx(FootnoteEditor, {
1714
+ n: footnotes.findIndex((f) => f.el === fnEdit.el) + 1,
1715
+ note: fnEdit.el.getAttribute("data-note") ?? "",
1716
+ onChange: (v) => {
1717
+ fnEdit.el.setAttribute("data-note", v);
1718
+ emit();
1719
+ },
1720
+ onRemove: () => removeFootnote(fnEdit.el),
1721
+ onDone: closeFootnote
1722
+ }, footnotes.findIndex((f) => f.el === fnEdit.el)) : null
1723
+ })]
1724
+ }),
1635
1725
  /* @__PURE__ */ jsxs(Popover, {
1636
1726
  open: !!link,
1637
1727
  onOpenChange: (o) => !o && setLink(null),
@@ -1663,10 +1753,10 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
1663
1753
  }
1664
1754
  }) : link ? /* @__PURE__ */ jsx("div", {
1665
1755
  className: "flex flex-col gap-2",
1666
- children: /* @__PURE__ */ jsx(LinkForm, {
1667
- initial: link.initial,
1668
- allowNewTab,
1669
- onSave: saveLink,
1756
+ children: /* @__PURE__ */ jsx(LinkPopoverBody, {
1757
+ initial: toLinkValue(link.initial),
1758
+ hideNewTab: !allowNewTab,
1759
+ onSave: (v) => saveLink(fromLinkValue(v, allowNewTab)),
1670
1760
  onCancel: () => setLink(null),
1671
1761
  onDetach: link.existing ? () => {
1672
1762
  const a = link.existing;
@@ -1691,6 +1781,7 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
1691
1781
  placeholder,
1692
1782
  docKey: `${docKey}:expanded`,
1693
1783
  features,
1784
+ references,
1694
1785
  single,
1695
1786
  allowNewTab,
1696
1787
  expandCrumbs,