@bettercms-ai/kit 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/adapters/lossy.d.ts +12 -4
  2. package/dist/adapters/lossy.js +19 -8
  3. package/dist/adapters/lossy.js.map +1 -1
  4. package/dist/adapters/prod-types.d.ts +46 -14
  5. package/dist/adapters/prod-types.js +37 -35
  6. package/dist/adapters/prod-types.js.map +1 -1
  7. package/dist/adapters/schema.d.ts +39 -0
  8. package/dist/adapters/schema.js +281 -82
  9. package/dist/adapters/schema.js.map +1 -1
  10. package/dist/context.d.ts +39 -0
  11. package/dist/context.js +40 -1
  12. package/dist/context.js.map +1 -1
  13. package/dist/controls/form-picker-register.d.ts +7 -1
  14. package/dist/controls/form-picker-register.js +1 -5
  15. package/dist/controls/form-picker-register.js.map +1 -1
  16. package/dist/controls/icon.d.ts +1 -1
  17. package/dist/controls/icon.js +1 -1
  18. package/dist/controls/icon.js.map +1 -1
  19. package/dist/controls/link.d.ts +11 -4
  20. package/dist/controls/link.js +120 -202
  21. package/dist/controls/link.js.map +1 -1
  22. package/dist/controls/location.js +3 -8
  23. package/dist/controls/location.js.map +1 -1
  24. package/dist/controls/media.d.ts +0 -14
  25. package/dist/controls/media.js +52 -95
  26. package/dist/controls/media.js.map +1 -1
  27. package/dist/controls/reference-services.d.ts +23 -2
  28. package/dist/controls/reference.d.ts +2 -2
  29. package/dist/controls/reference.js +79 -28
  30. package/dist/controls/reference.js.map +1 -1
  31. package/dist/controls/rich-text/article-shell.d.ts +145 -0
  32. package/dist/controls/rich-text/article-shell.js +514 -0
  33. package/dist/controls/rich-text/article-shell.js.map +1 -0
  34. package/dist/controls/rich-text/author.d.ts +25 -0
  35. package/dist/controls/rich-text/author.js +107 -0
  36. package/dist/controls/rich-text/author.js.map +1 -0
  37. package/dist/controls/rich-text/blocks.d.ts +19 -2
  38. package/dist/controls/rich-text/blocks.js +275 -638
  39. package/dist/controls/rich-text/blocks.js.map +1 -1
  40. package/dist/controls/rich-text/button.d.ts +23 -0
  41. package/dist/controls/rich-text/button.js +81 -0
  42. package/dist/controls/rich-text/button.js.map +1 -0
  43. package/dist/controls/rich-text/catalog.d.ts +2 -5
  44. package/dist/controls/rich-text/catalog.js +121 -7
  45. package/dist/controls/rich-text/catalog.js.map +1 -1
  46. package/dist/controls/rich-text/checklist.d.ts +24 -0
  47. package/dist/controls/rich-text/checklist.js +115 -0
  48. package/dist/controls/rich-text/checklist.js.map +1 -0
  49. package/dist/controls/rich-text/codec.d.ts +15 -1
  50. package/dist/controls/rich-text/codec.js +11 -1
  51. package/dist/controls/rich-text/codec.js.map +1 -1
  52. package/dist/controls/rich-text/columns.d.ts +41 -0
  53. package/dist/controls/rich-text/columns.js +181 -0
  54. package/dist/controls/rich-text/columns.js.map +1 -0
  55. package/dist/controls/rich-text/control.js +1 -0
  56. package/dist/controls/rich-text/control.js.map +1 -1
  57. package/dist/controls/rich-text/dom.d.ts +84 -4
  58. package/dist/controls/rich-text/dom.js +171 -10
  59. package/dist/controls/rich-text/dom.js.map +1 -1
  60. package/dist/controls/rich-text/editor.d.ts +4 -2
  61. package/dist/controls/rich-text/editor.js +199 -26
  62. package/dist/controls/rich-text/editor.js.map +1 -1
  63. package/dist/controls/rich-text/embed.d.ts +28 -0
  64. package/dist/controls/rich-text/embed.js +210 -0
  65. package/dist/controls/rich-text/embed.js.map +1 -0
  66. package/dist/controls/rich-text/footnote.d.ts +41 -0
  67. package/dist/controls/rich-text/footnote.js +185 -0
  68. package/dist/controls/rich-text/footnote.js.map +1 -0
  69. package/dist/controls/rich-text/gallery.d.ts +28 -0
  70. package/dist/controls/rich-text/gallery.js +167 -0
  71. package/dist/controls/rich-text/gallery.js.map +1 -0
  72. package/dist/controls/rich-text/link-form.d.ts +13 -1
  73. package/dist/controls/rich-text/link-form.js +41 -7
  74. package/dist/controls/rich-text/link-form.js.map +1 -1
  75. package/dist/controls/rich-text/ops.js +6 -0
  76. package/dist/controls/rich-text/ops.js.map +1 -1
  77. package/dist/controls/rich-text/related.d.ts +49 -0
  78. package/dist/controls/rich-text/related.js +233 -0
  79. package/dist/controls/rich-text/related.js.map +1 -0
  80. package/dist/controls/rich-text/toc.d.ts +47 -0
  81. package/dist/controls/rich-text/toc.js +160 -0
  82. package/dist/controls/rich-text/toc.js.map +1 -0
  83. package/dist/controls/slot-register.d.ts +6 -1
  84. package/dist/controls/slot-register.js +1 -4
  85. package/dist/controls/slot-register.js.map +1 -1
  86. package/dist/core/core.d.ts +11 -3
  87. package/dist/core/core.js +24 -6
  88. package/dist/core/core.js.map +1 -1
  89. package/dist/core/facets.d.ts +16 -0
  90. package/dist/core/facets.js +17 -1
  91. package/dist/core/facets.js.map +1 -1
  92. package/dist/core/index.js +2 -2
  93. package/dist/index.js +16 -5
  94. package/dist/index.js.map +1 -1
  95. package/dist/ledgers/exclusions.v1.js +141 -0
  96. package/dist/ledgers/exclusions.v1.js.map +1 -0
  97. package/dist/library/coverage.d.ts +13 -1
  98. package/dist/library/coverage.js +12 -1
  99. package/dist/library/coverage.js.map +1 -1
  100. package/dist/library/kit-catalog.json +156 -153
  101. package/dist/schema/types.d.ts +17 -0
  102. package/dist/schema/types.js.map +1 -1
  103. package/dist/styles.css +1 -1
  104. package/dist/testing/coverage-extras.d.ts +8 -0
  105. package/dist/testing/coverage-extras.js +44 -0
  106. package/dist/testing/coverage-extras.js.map +1 -0
  107. package/dist/testing/exclusions.d.ts +30 -0
  108. package/dist/testing/exclusions.js +13 -0
  109. package/dist/testing/exclusions.js.map +1 -0
  110. package/dist/ui/autocomplete.d.ts +29 -1
  111. package/dist/ui/autocomplete.js +102 -47
  112. package/dist/ui/autocomplete.js.map +1 -1
  113. package/dist/ui/button.js +3 -5
  114. package/dist/ui/button.js.map +1 -1
  115. package/dist/ui/index.js +2 -2
  116. package/dist/ui/overlay.d.ts +4 -1
  117. package/dist/ui/overlay.js +2 -1
  118. package/dist/ui/overlay.js.map +1 -1
  119. package/dist/ui/toast.js +1 -1
  120. package/package.json +8 -2
@@ -4,19 +4,24 @@ 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";
17
+ import { duplicateBlock, ensureLineAfter, insertBasic, insertIsland, liveInlineCode, liveMarkdown, moveBlock, restrictHtml, turnInto } from "./ops.js";
18
+ import { ButtonLinkPanel, applyButton, buttonAt, buttonInitial, insertButton, removeButton } from "./button.js";
19
+ import { checklistShortcut, insertChecklist, syncTaskA11y, toggleTaskAtPointer, toggleTaskFromKey } from "./checklist.js";
15
20
  import { ALL_FEATURES } from "./codec.js";
16
21
  import { htmlToMarkdown, looksLikeMarkdown, markdownToHtml } from "./markdown.js";
17
- import { duplicateBlock, ensureLineAfter, insertBasic, insertIsland, liveInlineCode, liveMarkdown, moveBlock, restrictHtml, turnInto } from "./ops.js";
18
22
  import { EMPTY_FMT, FormatToolbar, MARK_FEATURE, stylesFor } from "./toolbar.js";
19
23
  import { SelectionBubble } from "./floating.js";
24
+ import { FN_MARK, FOOTNOTE_POPOVER_CLASS, FOOTNOTE_REFUSED, FootnoteEditor, FootnotesList, backToMarker, caretAfterMarker, insertFootnote, renumberFootnotes } from "./footnote.js";
20
25
  import * as React from "react";
21
26
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
22
27
  import { IconArrowBarToDown, IconArrowBarToUp, IconCopy, IconGripVertical, IconPlus, IconTransform, IconTrash } from "@tabler/icons-react";
@@ -112,11 +117,23 @@ function insertHtmlAtCaret(html) {
112
117
  s.addRange(r);
113
118
  }
114
119
  }
115
- var NOT_COUNTED = "[data-rt=component],[data-rt=pullquote],[data-rt=takeaways],[data-rt=embed],[data-rt=video],[data-rt=file]";
120
+ var NOT_COUNTED = [
121
+ "[data-rt=component]",
122
+ "[data-rt=pullquote]",
123
+ "[data-rt=takeaways]",
124
+ "[data-rt=toc]",
125
+ "[data-rt=related]",
126
+ "[data-rt=author]",
127
+ "[data-rt=embed]",
128
+ "[data-rt=video]",
129
+ "[data-rt=file]",
130
+ "[data-rt=footnotes]",
131
+ "sup[data-rt=fn]"
132
+ ].join(",");
116
133
  var WORDS = (s) => s.replace(/\u200b/g, "").trim().split(/\s+/).filter(Boolean).length;
117
134
  function countWords(html) {
118
135
  if (!html) return 0;
119
- if (!/data-rt="(?:component|pullquote|takeaways|embed|video|file)"/.test(html) || typeof DOMParser === "undefined") return WORDS(html.replace(/<\/(p|li|h[1-6]|blockquote|pre|figcaption|td|th|summary|div)>|<br\s*\/?>/g, " ").replace(/<[^>]+>/g, "").replace(/&nbsp;|&[a-z]+;/g, " "));
136
+ if (!/data-rt="(?:component|pullquote|takeaways|toc|related|author|embed|video|file|footnotes|fn)"/.test(html) || typeof DOMParser === "undefined") return WORDS(html.replace(/<[^>]+>/g, " ").replace(/&nbsp;|&[a-z]+;/g, " "));
120
137
  const doc = new DOMParser().parseFromString(`<body>${html}</body>`, "text/html");
121
138
  doc.body.querySelectorAll(NOT_COUNTED).forEach((n) => n.remove());
122
139
  doc.body.querySelectorAll("p,li,h1,h2,h3,h4,h5,h6,td,th,figcaption,summary,pre,blockquote,div").forEach((n) => n.append(" "));
@@ -128,7 +145,7 @@ var countComponents = (html) => (html.match(/data-rt="component"/g) ?? []).lengt
128
145
  var bodyCountLabel = (html) => {
129
146
  const w = countWords(html);
130
147
  const c = countComponents(html);
131
- return `${w.toLocaleString("en-US")} ${w === 1 ? "word" : "words"}${c ? ` · ${c} ${c === 1 ? "block" : "blocks"}` : ""}`;
148
+ return `${w.toLocaleString("en-US")} ${w === 1 ? "word" : "words"}${c ? ` · ${c} ${c === 1 ? "component" : "components"}` : ""}`;
132
149
  };
133
150
  function insertTextAtCaret(text) {
134
151
  if (exec("insertText", text)) return;
@@ -161,7 +178,7 @@ function slashPlacement(caret, viewportH) {
161
178
  maxHeight: Math.max(SLASH_MIN_H, Math.min(SLASH_MENU_H, flipUp ? above : below))
162
179
  };
163
180
  }
164
- 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 }) {
165
182
  const hints = shortcutHintsOn();
166
183
  const ph = placeholder ?? (hints ? "Type '/' for commands" : "Write something…");
167
184
  const dense = variant === "panel";
@@ -249,6 +266,11 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
249
266
  const t = window.setTimeout(() => setNotice(null), 8e3);
250
267
  return () => window.clearTimeout(t);
251
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);
252
274
  const syncIslands = React.useCallback(() => {
253
275
  const el = ref.current;
254
276
  if (!el) return setIslands((p) => p.length ? [] : p);
@@ -272,7 +294,12 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
272
294
  const el = ref.current;
273
295
  if (!el) return;
274
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"));
275
299
  hydrate(el);
300
+ syncTaskA11y(el);
301
+ setFootnotes(renumberFootnotes(el));
302
+ outline.current.set(outlineOf(el));
276
303
  known.current = html;
277
304
  setEmpty(isEmptyHtml(html));
278
305
  setWords(countWords(html));
@@ -283,6 +310,9 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
283
310
  const el = ref.current;
284
311
  if (!el) return;
285
312
  syncIslands();
313
+ syncTaskA11y(el);
314
+ outline.current.set(outlineOf(el));
315
+ setFootnotes(renumberFootnotes(el));
286
316
  const html = serialize(el);
287
317
  setEmpty(isEmptyHtml(html) && !el.querySelector(":scope > [data-rt-island], :scope > hr"));
288
318
  setWords(countWords(html));
@@ -568,7 +598,25 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
568
598
  }
569
599
  if (!it.id || !blockAllowed(it.id, allows)) return;
570
600
  noteRecent(it);
571
- insertBasic(el, it.id);
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
+ }
608
+ if (it.id === "task") insertChecklist(el);
609
+ else if (it.id === "button") {
610
+ const a = insertButton(el);
611
+ emit();
612
+ refresh();
613
+ openButton(a);
614
+ return;
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);
572
620
  emit();
573
621
  refresh();
574
622
  };
@@ -645,6 +693,11 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
645
693
  }
646
694
  const mod = e.metaKey || e.ctrlKey;
647
695
  const k = eventKey(e);
696
+ if (!mod && !e.altKey && !readOnly && ref.current && toggleTaskFromKey(ref.current, e.key)) {
697
+ e.preventDefault();
698
+ emit();
699
+ return;
700
+ }
648
701
  if (mod && !e.altKey && k === "k") {
649
702
  e.preventDefault();
650
703
  saveSel();
@@ -750,6 +803,11 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
750
803
  hist.current.textAt = Date.now();
751
804
  hist.current.future = [];
752
805
  }
806
+ if (!single && ne.inputType === "insertText" && allows("checklist") && checklistShortcut(ref.current, ne.data)) {
807
+ setSlash(null);
808
+ emit();
809
+ return;
810
+ }
753
811
  if (!single && ne.inputType === "insertText" && (ne.data === " " || ne.data === "-") && liveMarkdown(ref.current, ne.data, allows)) {
754
812
  setSlash(null);
755
813
  emit();
@@ -789,6 +847,7 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
789
847
  restoreSel();
790
848
  const s = window.getSelection();
791
849
  const anchor = (s?.anchorNode?.nodeType === 1 ? s.anchorNode : s?.anchorNode?.parentElement)?.closest("a") ?? null;
850
+ if (anchor && ref.current && buttonAt(ref.current, anchor)) return openButton(anchor);
792
851
  if (!anchor && (!s || s.isCollapsed || !s.toString().trim())) {
793
852
  notify("Select some text first", { description: hints ? "Then press ⌘K to link it." : "Then link it." });
794
853
  return;
@@ -798,26 +857,66 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
798
857
  setLink({
799
858
  rect,
800
859
  existing: anchor,
801
- initial: anchor ? {
802
- href: anchor.getAttribute("href") ?? "",
803
- newTab: anchor.getAttribute("target") === "_blank"
804
- } : { 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
870
+ });
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
+ };
885
+ /** V2 rich-text-editor.tsx:865-873: a button's popover, its label above the link form */
886
+ const openButton = (a) => {
887
+ if (readOnly) return;
888
+ const { link: initial, label } = buttonInitial(a);
889
+ setBubble(null);
890
+ setLink({
891
+ rect: a.getBoundingClientRect(),
892
+ existing: a,
893
+ initial,
894
+ button: {
895
+ a,
896
+ label
897
+ }
805
898
  });
806
899
  };
900
+ const saveButton = (v, label) => {
901
+ const b = link?.button;
902
+ setLink(null);
903
+ if (!b) return;
904
+ applyButton(b.a, v, label);
905
+ emit();
906
+ };
807
907
  const saveLink = (v) => {
808
908
  const l = link;
809
909
  setLink(null);
810
910
  if (!l) return;
811
911
  if (l.existing) {
812
- l.existing.setAttribute("href", v.href);
813
- if (v.newTab) l.existing.setAttribute("target", "_blank");
814
- else l.existing.removeAttribute("target");
912
+ applyTextLink(l.existing, v);
815
913
  emit();
816
914
  return;
817
915
  }
818
916
  run(() => {
819
917
  const text = window.getSelection()?.toString() || v.href;
820
- 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>`);
821
920
  });
822
921
  };
823
922
  const aiRewrite = (preset) => {
@@ -1072,7 +1171,8 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
1072
1171
  moveBlock: moveBlock$1,
1073
1172
  islandUndo,
1074
1173
  islandRedo,
1075
- startDrag
1174
+ startDrag,
1175
+ allows
1076
1176
  });
1077
1177
  api.current = {
1078
1178
  emit,
@@ -1085,7 +1185,8 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
1085
1185
  moveBlock: moveBlock$1,
1086
1186
  islandUndo,
1087
1187
  islandRedo,
1088
- startDrag
1188
+ startDrag,
1189
+ allows
1089
1190
  };
1090
1191
  const islandCtx = React.useMemo(() => ({
1091
1192
  readOnly,
@@ -1100,8 +1201,15 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
1100
1201
  move: (x, dir) => api.current.moveBlock(x, dir),
1101
1202
  undo: () => api.current.islandUndo(),
1102
1203
  redo: () => api.current.islandRedo(),
1103
- drag: (x, e) => api.current.startDrag(x, e)
1104
- }), [readOnly, dense]);
1204
+ drag: (x, e) => api.current.startDrag(x, e),
1205
+ allows: (f) => api.current.allows(f),
1206
+ outline: outline.current,
1207
+ references
1208
+ }), [
1209
+ readOnly,
1210
+ dense,
1211
+ references
1212
+ ]);
1105
1213
  const commentSel = onComment ? () => {
1106
1214
  restoreSel();
1107
1215
  const q = window.getSelection()?.toString().trim();
@@ -1305,6 +1413,22 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
1305
1413
  onKeyDown,
1306
1414
  onKeyUp: refresh,
1307
1415
  onMouseUp: refresh,
1416
+ onMouseDown: (e) => {
1417
+ if (readOnly || !ref.current || !toggleTaskAtPointer(ref.current, e.target, e.clientX)) return;
1418
+ e.preventDefault();
1419
+ emit();
1420
+ },
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
+ }
1427
+ const a = ref.current ? buttonAt(ref.current, e.target) : null;
1428
+ if (!a || readOnly) return;
1429
+ e.preventDefault();
1430
+ openButton(a);
1431
+ },
1308
1432
  onPaste: (e) => {
1309
1433
  if (readOnly) return;
1310
1434
  if (Array.from(e.clipboardData.files).filter((f) => f.type.startsWith("image/")).length) {
@@ -1352,7 +1476,7 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
1352
1476
  notify("That drop is not an image from the media library");
1353
1477
  }
1354
1478
  },
1355
- 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)
1356
1480
  }),
1357
1481
  empty ? /* @__PURE__ */ jsx("div", {
1358
1482
  "aria-hidden": true,
@@ -1481,6 +1605,13 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
1481
1605
  }), el, key))
1482
1606
  ]
1483
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
+ }),
1484
1615
  /* @__PURE__ */ jsxs("div", {
1485
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",
1486
1617
  children: [notice ? /* @__PURE__ */ jsxs("span", {
@@ -1506,7 +1637,7 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
1506
1637
  words.toLocaleString("en-US"),
1507
1638
  " ",
1508
1639
  words === 1 ? "word" : "words",
1509
- comps ? ` · ${comps} ${comps === 1 ? "block" : "blocks"}` : "",
1640
+ comps ? ` · ${comps} ${comps === 1 ? "component" : "components"}` : "",
1510
1641
  " · ",
1511
1642
  Math.max(1, Math.round(words / 220)),
1512
1643
  " min read"
@@ -1562,6 +1693,35 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
1562
1693
  maxHeight: slashMaxH
1563
1694
  })
1564
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
+ }),
1565
1725
  /* @__PURE__ */ jsxs(Popover, {
1566
1726
  open: !!link,
1567
1727
  onOpenChange: (o) => !o && setLink(null),
@@ -1579,12 +1739,24 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
1579
1739
  }) }), /* @__PURE__ */ jsx(PopoverContent, {
1580
1740
  className: "w-[340px] rounded-kit-lg p-3",
1581
1741
  finalFocus: false,
1582
- children: link ? /* @__PURE__ */ jsx("div", {
1742
+ children: link?.button ? /* @__PURE__ */ jsx(ButtonLinkPanel, {
1743
+ initial: link.initial,
1744
+ label: link.button.label,
1745
+ allowNewTab,
1746
+ onSave: saveButton,
1747
+ onCancel: () => setLink(null),
1748
+ onDetach: () => {
1749
+ const a = link.button.a;
1750
+ setLink(null);
1751
+ if (ref.current) removeButton(ref.current, a);
1752
+ emit();
1753
+ }
1754
+ }) : link ? /* @__PURE__ */ jsx("div", {
1583
1755
  className: "flex flex-col gap-2",
1584
- children: /* @__PURE__ */ jsx(LinkForm, {
1585
- initial: link.initial,
1586
- allowNewTab,
1587
- onSave: saveLink,
1756
+ children: /* @__PURE__ */ jsx(LinkPopoverBody, {
1757
+ initial: toLinkValue(link.initial),
1758
+ hideNewTab: !allowNewTab,
1759
+ onSave: (v) => saveLink(fromLinkValue(v, allowNewTab)),
1588
1760
  onCancel: () => setLink(null),
1589
1761
  onDetach: link.existing ? () => {
1590
1762
  const a = link.existing;
@@ -1609,6 +1781,7 @@ function RichTextEditor({ value, onChange, readOnly, placeholder, docKey, featur
1609
1781
  placeholder,
1610
1782
  docKey: `${docKey}:expanded`,
1611
1783
  features,
1784
+ references,
1612
1785
  single,
1613
1786
  allowNewTab,
1614
1787
  expandCrumbs,