@estiva-app/ui 0.67.0 → 0.68.0

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 (41) hide show
  1. package/dist/BlockNodes.d.ts +12 -1
  2. package/dist/BlockNodes.d.ts.map +1 -1
  3. package/dist/LinkChips.d.ts +82 -0
  4. package/dist/LinkChips.d.ts.map +1 -0
  5. package/dist/ReferenceCards.d.ts +77 -0
  6. package/dist/ReferenceCards.d.ts.map +1 -0
  7. package/dist/ReferenceMenu.d.ts +18 -0
  8. package/dist/ReferenceMenu.d.ts.map +1 -0
  9. package/dist/RichText.d.ts +9 -2
  10. package/dist/RichText.d.ts.map +1 -1
  11. package/dist/SelectionToolbar.d.ts +9 -1
  12. package/dist/SelectionToolbar.d.ts.map +1 -1
  13. package/dist/editor.d.ts +3 -0
  14. package/dist/editor.d.ts.map +1 -1
  15. package/dist/editor.js +589 -20
  16. package/dist/editor.js.map +4 -4
  17. package/dist/index.js +39 -19
  18. package/dist/index.js.map +4 -4
  19. package/package.json +1 -1
  20. package/registry.json +290 -3
  21. package/src/BlockNodes.test.tsx +2 -1
  22. package/src/BlockNodes.tsx +51 -9
  23. package/src/LinkChips.mdx +57 -0
  24. package/src/LinkChips.stories.tsx +67 -0
  25. package/src/LinkChips.test.tsx +178 -0
  26. package/src/LinkChips.ts +243 -0
  27. package/src/ReferenceCards.mdx +79 -0
  28. package/src/ReferenceCards.stories.tsx +64 -0
  29. package/src/ReferenceCards.test.tsx +143 -0
  30. package/src/ReferenceCards.ts +225 -0
  31. package/src/ReferenceMenu.mdx +75 -0
  32. package/src/ReferenceMenu.stories.tsx +59 -0
  33. package/src/ReferenceMenu.test.tsx +289 -0
  34. package/src/ReferenceMenu.tsx +346 -0
  35. package/src/RichText.test.tsx +17 -0
  36. package/src/RichText.tsx +34 -3
  37. package/src/SelectionToolbar.tsx +10 -2
  38. package/src/editor.ts +13 -0
  39. package/src/pages.test.ts +3 -0
  40. package/src/registry/registry.test.ts +6 -6
  41. package/stories/Choosing.mdx +3 -0
package/dist/editor.js CHANGED
@@ -83,7 +83,7 @@ var ICONS = {
83
83
  var SETTLE_MS = 250;
84
84
  var GAP = 8;
85
85
  var POPOVER_GAP = 4;
86
- function SelectionToolbar({ editor, marks = ["bold", "italic", "underline"], link = false }) {
86
+ function SelectionToolbar({ editor, marks = ["bold", "italic", "underline"], link = false, children }) {
87
87
  const [anchor, setAnchor] = useState(null);
88
88
  const [mode, setMode] = useState("marks");
89
89
  const [url, setUrl] = useState("");
@@ -273,7 +273,8 @@ function SelectionToolbar({ editor, marks = ["bold", "italic", "underline"], lin
273
273
  onClick: openField,
274
274
  children: /* @__PURE__ */ jsx(IconLink, { size: 16, stroke: 1.5 })
275
275
  }
276
- )
276
+ ),
277
+ children
277
278
  ] })
278
279
  )
279
280
  }
@@ -1062,14 +1063,540 @@ var SlashCommands = Extension3.create({
1062
1063
  });
1063
1064
 
1064
1065
  // src/BlockNodes.tsx
1065
- import { Extension as Extension4, InputRule, Node as Node2 } from "@tiptap/core";
1066
+ import { Extension as Extension7, InputRule, Node as Node2 } from "@tiptap/core";
1066
1067
  import { DOMSerializer } from "@tiptap/pm/model";
1067
- import { Plugin as Plugin3, PluginKey as PluginKey4 } from "@tiptap/pm/state";
1068
+ import { Plugin as Plugin6, PluginKey as PluginKey7 } from "@tiptap/pm/state";
1068
1069
  import { Mapping, ReplaceAroundStep } from "@tiptap/pm/transform";
1069
1070
  import { NodeViewWrapper as NodeViewWrapper2, ReactNodeViewRenderer as ReactNodeViewRenderer2, ReactRenderer as ReactRenderer2 } from "@tiptap/react";
1070
1071
  import { Checkbox } from "@estiva-app/ui";
1071
1072
  import { calloutLook } from "@estiva-app/ui";
1072
- import { jsx as jsx6 } from "react/jsx-runtime";
1073
+
1074
+ // src/ReferenceMenu.tsx
1075
+ import { useCallback as useCallback3 } from "react";
1076
+ import { Extension as Extension6 } from "@tiptap/core";
1077
+ import { useEditorState } from "@tiptap/react";
1078
+ import { NodeSelection as NodeSelection2, Plugin as Plugin5, PluginKey as PluginKey6, TextSelection as TextSelection2 } from "@tiptap/pm/state";
1079
+ import { ReplaceStep } from "@tiptap/pm/transform";
1080
+ import { IconCheck } from "@tabler/icons-react";
1081
+
1082
+ // src/LinkChips.ts
1083
+ import { Extension as Extension4 } from "@tiptap/core";
1084
+ import { Plugin as Plugin3, PluginKey as PluginKey4 } from "@tiptap/pm/state";
1085
+ var key = new PluginKey4("linkChips");
1086
+ var CHIPS_MADE = "linkChipsMade";
1087
+ var PASTES_KEPT = 8;
1088
+ var LINK_OR_URL = /(\[[^[\]\n]+\]\((?:https?:\/\/|mailto:)[^\s)]+\))|(https?:\/\/[^\s<>"'`]+)/g;
1089
+ var TRAILING = /* @__PURE__ */ new Set([".", ",", ";", ":", "!", "?", ")", "]"]);
1090
+ function urlRuns(text) {
1091
+ if (!text.includes("://")) return [];
1092
+ const out = [];
1093
+ for (const m of text.matchAll(LINK_OR_URL)) {
1094
+ if (m[1]) continue;
1095
+ const start = m.index;
1096
+ if (text.slice(Math.max(0, start - 2), start) === "](" || text[start - 1] === "[") continue;
1097
+ let end = m[0].length;
1098
+ while (end > 0 && TRAILING.has(m[0][end - 1])) end--;
1099
+ out.push({ url: m[0].slice(0, end), from: start, to: start + end, end: start + m[0].length });
1100
+ }
1101
+ return out;
1102
+ }
1103
+ function findLinkChips(doc, selection, referenceOf, pasted = []) {
1104
+ const found = { chips: [], fills: [], open: [] };
1105
+ const open = /* @__PURE__ */ new Set();
1106
+ doc.descendants((node, pos) => {
1107
+ if (node.type.spec.code) return false;
1108
+ if (node.type.name === "reference") {
1109
+ const href = typeof node.attrs.href === "string" ? node.attrs.href : "";
1110
+ if (href && !node.attrs.uri) {
1111
+ const uri = referenceOf?.(href);
1112
+ if (uri) found.fills.push({ pos, uri });
1113
+ else open.add(href);
1114
+ }
1115
+ return false;
1116
+ }
1117
+ if (!node.isText || !node.text) return true;
1118
+ if (node.marks.some((m) => m.type.spec.code || m.type.name === "code" || m.type.name === "link")) return false;
1119
+ for (const run of urlRuns(node.text)) {
1120
+ const from = pos + run.from;
1121
+ const to = pos + run.to;
1122
+ const wasPasted = pasted.some((range) => range.from <= from && pos + run.end <= range.to);
1123
+ const typing = selection && selection.to > from && selection.from <= pos + run.end;
1124
+ if (typing && !(selection.empty && wasPasted)) continue;
1125
+ const uri = referenceOf?.(run.url);
1126
+ if (uri) found.chips.push({ from, to, url: run.url, uri, marks: node.marks.map((m) => m.type.name), pasted: wasPasted });
1127
+ else open.add(run.url);
1128
+ }
1129
+ return false;
1130
+ });
1131
+ found.open = [...open];
1132
+ return found;
1133
+ }
1134
+ function apply(state, found) {
1135
+ if (!found.chips.length && !found.fills.length) return null;
1136
+ const type = state.schema.nodes.reference;
1137
+ if (!type) return null;
1138
+ const tr = state.tr;
1139
+ for (const { pos, uri } of found.fills) {
1140
+ const node = tr.doc.nodeAt(pos);
1141
+ if (node) tr.setNodeMarkup(pos, void 0, { ...node.attrs, uri });
1142
+ }
1143
+ for (const chip of [...found.chips].reverse()) {
1144
+ tr.replaceWith(chip.from, chip.to, type.create({ uri: chip.uri, href: chip.url, marks: chip.marks }));
1145
+ }
1146
+ return tr.setMeta("addToHistory", false).setMeta(CHIPS_MADE, { pasted: found.chips.some((chip) => chip.pasted) });
1147
+ }
1148
+ function refreshLinkChips(editor) {
1149
+ if (editor.isDestroyed) return;
1150
+ const chips = key.getState(editor.state);
1151
+ if (!chips) return;
1152
+ const tr = apply(editor.state, findLinkChips(editor.state.doc, editor.state.selection, chips.options.referenceOf, chips.pasted));
1153
+ if (tr) editor.view.dispatch(tr);
1154
+ }
1155
+ var LinkChips = Extension4.create({
1156
+ name: "linkChips",
1157
+ addOptions() {
1158
+ return { referenceOf: null, ask: null, limit: 24 };
1159
+ },
1160
+ addProseMirrorPlugins() {
1161
+ const options = this.options;
1162
+ const editor = this.editor;
1163
+ return [
1164
+ new Plugin3({
1165
+ key,
1166
+ // The options, where `refreshLinkChips` can read them from the state alone, and where the last pastes landed.
1167
+ state: {
1168
+ init: () => ({ options, pasted: [] }),
1169
+ apply: (tr, value, before, after) => {
1170
+ if (!tr.docChanged) return value;
1171
+ const pasted = value.pasted.map((range) => ({ from: tr.mapping.map(range.from, 1), to: tr.mapping.map(range.to, -1), size: range.to - range.from })).filter((range) => range.from < range.to && range.to - range.from === range.size).map(({ from, to }) => ({ from, to }));
1172
+ if (tr.getMeta("uiEvent") === "paste") {
1173
+ const from = tr.mapping.map(before.selection.from, -1);
1174
+ if (from < after.selection.to) pasted.push({ from, to: after.selection.to });
1175
+ }
1176
+ return { options, pasted: pasted.slice(-PASTES_KEPT) };
1177
+ }
1178
+ },
1179
+ appendTransaction: (trs, _old, state) => {
1180
+ if (!trs.some((tr) => tr.docChanged || tr.selectionSet)) return null;
1181
+ return apply(state, findLinkChips(state.doc, state.selection, options.referenceOf, key.getState(state)?.pasted));
1182
+ },
1183
+ view: (view) => {
1184
+ const asked = /* @__PURE__ */ new Set();
1185
+ const look = (state) => {
1186
+ if (!options.ask) return;
1187
+ const { open } = findLinkChips(state.doc, state.selection, options.referenceOf, key.getState(state)?.pasted);
1188
+ const fresh = open.filter((url) => !asked.has(url)).slice(0, Math.max(0, options.limit - asked.size));
1189
+ if (!fresh.length) return;
1190
+ for (const url of fresh) asked.add(url);
1191
+ const forget = () => {
1192
+ for (const url of fresh) asked.delete(url);
1193
+ };
1194
+ let answer;
1195
+ try {
1196
+ answer = options.ask(fresh);
1197
+ } catch {
1198
+ forget();
1199
+ return;
1200
+ }
1201
+ void Promise.resolve(answer).then(() => refreshLinkChips(editor), forget);
1202
+ };
1203
+ look(view.state);
1204
+ queueMicrotask(() => {
1205
+ try {
1206
+ refreshLinkChips(editor);
1207
+ } catch {
1208
+ }
1209
+ });
1210
+ return {
1211
+ update: (next, prev) => {
1212
+ if (next.state.doc !== prev.doc || !next.state.selection.eq(prev.selection)) look(next.state);
1213
+ }
1214
+ };
1215
+ }
1216
+ })
1217
+ ];
1218
+ }
1219
+ });
1220
+
1221
+ // src/ReferenceMenu.tsx
1222
+ import { MenuItem as MenuItem3, MenuSection, MenuSeparator as MenuSeparator2 } from "@estiva-app/ui";
1223
+ import { Popover as Popover2 } from "@estiva-app/ui";
1224
+
1225
+ // src/ReferenceCards.ts
1226
+ import { Extension as Extension5 } from "@tiptap/core";
1227
+ import { NodeSelection, Plugin as Plugin4, PluginKey as PluginKey5 } from "@tiptap/pm/state";
1228
+ import { Decoration as Decoration2, DecorationSet as DecorationSet2 } from "@tiptap/pm/view";
1229
+ import { useEffect as useEffect3, useState as useState3 } from "react";
1230
+ function layoutOf(raw) {
1231
+ if (typeof raw !== "string" || raw === "" || raw === "none") return void 0;
1232
+ return raw === "detailed" ? "detailed" : "compact";
1233
+ }
1234
+ function referenceCardPlacements(doc) {
1235
+ const out = [];
1236
+ const seen = /* @__PURE__ */ new Map();
1237
+ doc.descendants((node, pos, parent) => {
1238
+ if (!node.isTextblock) return true;
1239
+ const cards = [];
1240
+ node.forEach((child) => {
1241
+ if (child.type.name !== "reference") return;
1242
+ const layout = layoutOf(child.attrs.embed);
1243
+ if (layout && child.attrs.uri) cards.push({ uri: String(child.attrs.uri), layout });
1244
+ });
1245
+ if (cards.length) {
1246
+ const id = node.attrs.blockId ?? parent?.attrs.blockId;
1247
+ const base = id ? `b:${String(id)}` : `p:${pos}`;
1248
+ const n = seen.get(base) ?? 0;
1249
+ seen.set(base, n + 1);
1250
+ out.push({ key: n ? `${base}:${n}` : base, pos: pos + node.nodeSize - 1, cards });
1251
+ }
1252
+ return false;
1253
+ });
1254
+ return out;
1255
+ }
1256
+ var key2 = new PluginKey5("referenceCards");
1257
+ function elementFor(storage, slotKey) {
1258
+ let element = storage.elements.get(slotKey);
1259
+ if (!element) {
1260
+ element = document.createElement("div");
1261
+ element.setAttribute("data-reference-cards", slotKey);
1262
+ element.setAttribute("contenteditable", "false");
1263
+ storage.elements.set(slotKey, element);
1264
+ }
1265
+ return element;
1266
+ }
1267
+ function storageOf(editor) {
1268
+ return editor.storage.referenceCards;
1269
+ }
1270
+ var ReferenceCards = Extension5.create({
1271
+ name: "referenceCards",
1272
+ addStorage() {
1273
+ return { elements: /* @__PURE__ */ new Map() };
1274
+ },
1275
+ addProseMirrorPlugins() {
1276
+ const storage = this.storage;
1277
+ const decorate = (doc) => DecorationSet2.create(
1278
+ doc,
1279
+ referenceCardPlacements(doc).map(
1280
+ (p) => Decoration2.widget(p.pos, () => elementFor(storage, p.key), {
1281
+ key: p.key,
1282
+ side: 1,
1283
+ ignoreSelection: true,
1284
+ // A click on a card is the card's, not a caret placement in the line.
1285
+ stopEvent: () => true
1286
+ })
1287
+ )
1288
+ );
1289
+ return [
1290
+ new Plugin4({
1291
+ key: key2,
1292
+ state: {
1293
+ init: (_, state) => decorate(state.doc),
1294
+ apply: (tr, set) => tr.docChanged ? decorate(tr.doc) : set
1295
+ },
1296
+ props: { decorations: (state) => key2.getState(state) }
1297
+ })
1298
+ ];
1299
+ }
1300
+ });
1301
+ function useReferenceCards(editor) {
1302
+ const [slots, setSlots] = useState3([]);
1303
+ useEffect3(() => {
1304
+ if (!editor) return;
1305
+ const storage = storageOf(editor);
1306
+ if (!storage) return;
1307
+ let last = "";
1308
+ const read = () => {
1309
+ if (editor.isDestroyed) return;
1310
+ const next = referenceCardPlacements(editor.state.doc).map((p) => ({ key: p.key, element: elementFor(storage, p.key), cards: p.cards }));
1311
+ const signature = JSON.stringify(next.map((s) => [s.key, s.cards]));
1312
+ if (signature === last) return;
1313
+ last = signature;
1314
+ const live = new Set(next.map((s) => s.key));
1315
+ for (const k of storage.elements.keys()) if (!live.has(k)) storage.elements.delete(k);
1316
+ setSlots(next);
1317
+ };
1318
+ read();
1319
+ editor.on("transaction", read);
1320
+ return () => {
1321
+ editor.off("transaction", read);
1322
+ };
1323
+ }, [editor]);
1324
+ return slots;
1325
+ }
1326
+ function referenceAtSelection(state) {
1327
+ const { selection } = state;
1328
+ let found = null;
1329
+ if (selection instanceof NodeSelection) {
1330
+ if (selection.node.type.name === "reference") found = { pos: selection.from, node: selection.node };
1331
+ } else if (!selection.empty) {
1332
+ let only = true;
1333
+ state.doc.nodesBetween(selection.from, selection.to, (node2, pos2) => {
1334
+ if (node2.type.name === "reference") {
1335
+ if (found) only = false;
1336
+ found = { pos: pos2, node: node2 };
1337
+ return false;
1338
+ }
1339
+ if (node2.isText) {
1340
+ const from = Math.max(selection.from, pos2) - pos2;
1341
+ const to = Math.min(selection.to, pos2 + node2.nodeSize) - pos2;
1342
+ if ((node2.text ?? "").slice(from, to).trim() !== "") only = false;
1343
+ }
1344
+ return true;
1345
+ });
1346
+ if (!only) found = null;
1347
+ }
1348
+ if (!found) return null;
1349
+ const { pos, node } = found;
1350
+ if (!node.attrs.uri) return null;
1351
+ return { pos, uri: String(node.attrs.uri ?? ""), layout: layoutOf(node.attrs.embed) ?? "none" };
1352
+ }
1353
+ function setReferenceCard(editor, layout) {
1354
+ const at = referenceAtSelection(editor.state);
1355
+ return at ? setReferenceCardAt(editor, at.pos, layout) : false;
1356
+ }
1357
+ function setReferenceCardAt(editor, pos, layout) {
1358
+ const node = editor.state.doc.nodeAt(pos);
1359
+ if (!node || node.type.name !== "reference" || !node.attrs.uri) return false;
1360
+ const wasNode = editor.state.selection instanceof NodeSelection && editor.state.selection.from === pos;
1361
+ const tr = editor.state.tr.setNodeMarkup(pos, void 0, { ...node.attrs, embed: layout === "none" ? null : layout });
1362
+ if (wasNode) tr.setSelection(NodeSelection.create(tr.doc, pos));
1363
+ editor.view.dispatch(tr);
1364
+ return true;
1365
+ }
1366
+
1367
+ // src/ReferenceMenu.tsx
1368
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1369
+ var key3 = new PluginKey6("referenceMenu");
1370
+ var LAYOUTS = [
1371
+ { layout: "none", label: "None" },
1372
+ { layout: "compact", label: "Compact" },
1373
+ { layout: "detailed", label: "Detailed" }
1374
+ ];
1375
+ function chipTarget(view, pos) {
1376
+ const dom = view.nodeDOM(pos);
1377
+ return dom instanceof HTMLElement ? dom.querySelector("a[href], button") : null;
1378
+ }
1379
+ function webHref(view, pos) {
1380
+ const href = view.state.doc.nodeAt(pos)?.attrs.href;
1381
+ return typeof href === "string" && /^https?:\/\//i.test(href) ? href : "";
1382
+ }
1383
+ function rowsOf(view, pos) {
1384
+ const open = chipTarget(view, pos) || webHref(view, pos) ? ["open"] : [];
1385
+ return view.state.doc.nodeAt(pos)?.attrs.uri ? [...open, ...LAYOUTS.map((card) => card.layout)] : open;
1386
+ }
1387
+ function chipAt(state, pos) {
1388
+ const node = state.doc.nodeAt(pos);
1389
+ return node && node.type.name === "reference" ? node : null;
1390
+ }
1391
+ function justPlaced(tr, state) {
1392
+ if (!tr.docChanged || tr.getMeta("history$") || tr.getMeta("preventUpdate")) return null;
1393
+ const whole = tr.steps.some((step) => step instanceof ReplaceStep && step.from === 0 && step.to === tr.before.content.size);
1394
+ if (whole && tr.getMeta("uiEvent") !== "paste") return null;
1395
+ const made = tr.getMeta(CHIPS_MADE);
1396
+ if (made && !made.pasted) return null;
1397
+ const { selection } = state;
1398
+ if (!selection.empty) return null;
1399
+ let at = selection.from;
1400
+ let before = selection.$from.nodeBefore;
1401
+ if (!made && before?.isText && before.text === " ") {
1402
+ at -= 1;
1403
+ before = state.doc.resolve(at).nodeBefore;
1404
+ }
1405
+ if (!before || before.type.name !== "reference" || !before.attrs.uri) return null;
1406
+ const pos = at - before.nodeSize;
1407
+ const old = tr.mapping.invert().map(pos);
1408
+ const was = tr.before.nodeAt(old);
1409
+ if (was && was.type.name === "reference" && was.attrs.uri === before.attrs.uri && tr.mapping.map(old, 1) === pos) return null;
1410
+ return pos;
1411
+ }
1412
+ function dispatchMenu(view, next) {
1413
+ view.dispatch(view.state.tr.setMeta(key3, next));
1414
+ }
1415
+ var passing = false;
1416
+ function openChip(view, pos) {
1417
+ const target = chipTarget(view, pos);
1418
+ if (target) {
1419
+ passing = true;
1420
+ try {
1421
+ target.click();
1422
+ } finally {
1423
+ passing = false;
1424
+ }
1425
+ return;
1426
+ }
1427
+ const href = webHref(view, pos);
1428
+ if (href) window.open(href, "_blank", "noopener,noreferrer");
1429
+ }
1430
+ function choose(editor, pos, row) {
1431
+ dispatchMenu(editor.view, null);
1432
+ if (row === "open") openChip(editor.view, pos);
1433
+ else if (row) setReferenceCardAt(editor, pos, row);
1434
+ }
1435
+ function isReferenceMenuOpen(state) {
1436
+ return !!key3.getState(state);
1437
+ }
1438
+ function referenceMenuClick(editor, pos) {
1439
+ if (passing || editor.isDestroyed || !editor.isEditable || key3.getState(editor.state) === void 0) return false;
1440
+ if (!chipAt(editor.state, pos) || rowsOf(editor.view, pos).length === 0) return false;
1441
+ editor.view.dispatch(caretAfter(editor.state, pos).setMeta(key3, { pos, active: -1 }));
1442
+ return true;
1443
+ }
1444
+ function caretAfter(state, pos) {
1445
+ const node = state.doc.nodeAt(pos);
1446
+ return state.tr.setSelection(TextSelection2.create(state.doc, pos + (node?.nodeSize ?? 1)));
1447
+ }
1448
+ var ReferenceMenu = Extension6.create({
1449
+ name: "referenceMenu",
1450
+ // Before the editor's own keys, so Enter on a highlighted row is the row's, not a new line.
1451
+ priority: 1e3,
1452
+ addProseMirrorPlugins() {
1453
+ const editor = this.editor;
1454
+ return [
1455
+ new Plugin5({
1456
+ key: key3,
1457
+ state: {
1458
+ init: () => null,
1459
+ apply: (tr, value, before, after) => {
1460
+ const meta = tr.getMeta(key3);
1461
+ if (meta !== void 0) return meta;
1462
+ const placed = justPlaced(tr, after);
1463
+ if (placed !== null) return { pos: placed, active: -1 };
1464
+ if (!value) return null;
1465
+ const pos = tr.mapping.map(value.pos);
1466
+ if (!chipAt(after, pos)) return null;
1467
+ if (tr.docChanged && !tr.getMeta("appendedTransaction") && tr.getMeta(CHIPS_MADE) === void 0) return null;
1468
+ const onChip = after.selection instanceof NodeSelection2 && after.selection.from === pos;
1469
+ if (tr.selectionSet && !after.selection.eq(before.selection) && !onChip) return null;
1470
+ return pos === value.pos ? value : { ...value, pos };
1471
+ }
1472
+ },
1473
+ props: {
1474
+ handleDOMEvents: {
1475
+ // Focus gone, the keys no longer reach the menu, and a card chosen now would miss the save on leaving (code review).
1476
+ blur: (view) => {
1477
+ if (key3.getState(view.state)) dispatchMenu(view, null);
1478
+ return false;
1479
+ }
1480
+ },
1481
+ handleKeyDown: (view, event) => {
1482
+ if (event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) return false;
1483
+ const open = key3.getState(view.state);
1484
+ if (!open) {
1485
+ const { selection } = view.state;
1486
+ if (event.key !== "Enter" || !view.editable || !(selection instanceof NodeSelection2) || selection.node.type.name !== "reference") return false;
1487
+ if (rowsOf(view, selection.from).length === 0) return false;
1488
+ view.dispatch(caretAfter(view.state, selection.from).setMeta(key3, { pos: selection.from, active: 0 }));
1489
+ return true;
1490
+ }
1491
+ const rows = rowsOf(view, open.pos);
1492
+ if (event.key === "Escape") {
1493
+ dispatchMenu(view, null);
1494
+ return true;
1495
+ }
1496
+ if (rows.length === 0) return false;
1497
+ if (event.key === "ArrowDown") {
1498
+ dispatchMenu(view, { ...open, active: (open.active + 1) % rows.length });
1499
+ return true;
1500
+ }
1501
+ if (event.key === "ArrowUp") {
1502
+ dispatchMenu(view, { ...open, active: open.active <= 0 ? rows.length - 1 : open.active - 1 });
1503
+ return true;
1504
+ }
1505
+ if (event.key === "Enter" && open.active >= 0) {
1506
+ choose(editor, open.pos, rows[open.active]);
1507
+ return true;
1508
+ }
1509
+ return false;
1510
+ }
1511
+ }
1512
+ })
1513
+ ];
1514
+ }
1515
+ });
1516
+ function ReferenceMenuView({ editor }) {
1517
+ const shown = useEditorState({
1518
+ editor,
1519
+ selector: ({ editor: ed }) => {
1520
+ if (!ed || ed.isDestroyed) return null;
1521
+ const open = key3.getState(ed.state);
1522
+ if (!open) return null;
1523
+ const node = chipAt(ed.state, open.pos);
1524
+ if (!node) return null;
1525
+ const embed = node.attrs.embed;
1526
+ const layout = embed === "detailed" ? "detailed" : typeof embed === "string" && embed !== "" && embed !== "none" ? "compact" : "none";
1527
+ return { pos: open.pos, active: open.active, named: !!node.attrs.uri, layout };
1528
+ }
1529
+ });
1530
+ const keepFocus2 = useCallback3((list) => {
1531
+ const panel = list?.closest('[role="dialog"]');
1532
+ if (!panel) return;
1533
+ const keep = (event) => event.preventDefault();
1534
+ panel.addEventListener("mousedown", keep);
1535
+ return () => panel.removeEventListener("mousedown", keep);
1536
+ }, []);
1537
+ const live = shown && editor && !editor.isDestroyed ? editor : null;
1538
+ const dom = live && shown ? live.view.nodeDOM(shown.pos) : null;
1539
+ const anchor = dom instanceof HTMLElement ? dom : null;
1540
+ const rows = live && shown ? rowsOf(live.view, shown.pos) : [];
1541
+ const opens = rows[0] === "open";
1542
+ const run = (row) => {
1543
+ if (editor && shown) choose(editor, shown.pos, row);
1544
+ };
1545
+ return (
1546
+ // finalFocus={false}: focus never left the text.
1547
+ /* @__PURE__ */ jsx6(
1548
+ Popover2,
1549
+ {
1550
+ anchor,
1551
+ open: !!shown && !!anchor && rows.length > 0,
1552
+ onOpenChange: (next) => {
1553
+ if (!next && editor && !editor.isDestroyed && key3.getState(editor.state)) dispatchMenu(editor.view, null);
1554
+ },
1555
+ side: "bottom",
1556
+ align: "left",
1557
+ ariaLabel: "Link",
1558
+ finalFocus: false,
1559
+ children: shown && /* @__PURE__ */ jsxs5("div", { ref: keepFocus2, role: "menu", "aria-label": "Link", className: "flex flex-col gap-0.5", children: [
1560
+ opens && /* @__PURE__ */ jsx6(
1561
+ MenuItem3,
1562
+ {
1563
+ role: "menuitem",
1564
+ tabIndex: -1,
1565
+ label: "Open",
1566
+ selected: shown.active === 0,
1567
+ onMouseDown: (event) => {
1568
+ event.preventDefault();
1569
+ run("open");
1570
+ }
1571
+ }
1572
+ ),
1573
+ shown.named && /* @__PURE__ */ jsxs5(Fragment3, { children: [
1574
+ opens && /* @__PURE__ */ jsx6(MenuSeparator2, {}),
1575
+ /* @__PURE__ */ jsx6("div", { role: "group", "aria-label": "Card", children: /* @__PURE__ */ jsx6(MenuSection, { label: "Card", children: LAYOUTS.map(({ layout, label }, i) => /* @__PURE__ */ jsx6(
1576
+ MenuItem3,
1577
+ {
1578
+ role: "menuitemradio",
1579
+ "aria-checked": shown.layout === layout,
1580
+ tabIndex: -1,
1581
+ label,
1582
+ selected: shown.active === i + (opens ? 1 : 0),
1583
+ trailing: shown.layout === layout ? /* @__PURE__ */ jsx6(IconCheck, { size: 16, "aria-hidden": true }) : void 0,
1584
+ onMouseDown: (event) => {
1585
+ event.preventDefault();
1586
+ run(layout);
1587
+ }
1588
+ },
1589
+ layout
1590
+ )) }) })
1591
+ ] })
1592
+ ] })
1593
+ }
1594
+ )
1595
+ );
1596
+ }
1597
+
1598
+ // src/BlockNodes.tsx
1599
+ import { jsx as jsx7 } from "react/jsx-runtime";
1073
1600
  var BLOCK_TYPES = [
1074
1601
  "paragraph",
1075
1602
  "heading",
@@ -1227,7 +1754,7 @@ function bringsInBlocks(transactions) {
1227
1754
  })
1228
1755
  );
1229
1756
  }
1230
- var BlockId = Extension4.create({
1757
+ var BlockId = Extension7.create({
1231
1758
  name: "blockId",
1232
1759
  onCreate() {
1233
1760
  const { state, view } = this.editor;
@@ -1239,8 +1766,8 @@ var BlockId = Extension4.create({
1239
1766
  },
1240
1767
  addProseMirrorPlugins() {
1241
1768
  return [
1242
- new Plugin3({
1243
- key: new PluginKey4("blockIdSplit"),
1769
+ new Plugin6({
1770
+ key: new PluginKey7("blockIdSplit"),
1244
1771
  appendTransaction: (transactions, old, state) => {
1245
1772
  if (!bringsInBlocks(transactions)) return null;
1246
1773
  const mapping = new Mapping();
@@ -1274,6 +1801,7 @@ var BlockId = Extension4.create({
1274
1801
  ];
1275
1802
  }
1276
1803
  });
1804
+ var webUrl = (raw) => raw && /^https?:\/\/\S+$/i.test(raw) ? raw : null;
1277
1805
  var ReferenceNode = Node2.create({
1278
1806
  name: "reference",
1279
1807
  group: "inline",
@@ -1286,20 +1814,48 @@ var ReferenceNode = Node2.create({
1286
1814
  },
1287
1815
  addAttributes() {
1288
1816
  return {
1289
- uri: { default: "" },
1290
- marks: { default: [] }
1817
+ // A pasted URL chip keeps only its URL: what it names is the editor's to resolve (`LinkChips`), never
1818
+ // the clipboard's, which could show one object while saving another URL (security review).
1819
+ uri: { default: "", parseHTML: (element) => webUrl(element.getAttribute("data-href")) ? "" : element.getAttribute("data-reference") ?? "" },
1820
+ marks: { default: [] },
1821
+ embed: { default: null, parseHTML: (element) => element.getAttribute("data-embed") },
1822
+ extra: { default: null, parseHTML: () => null },
1823
+ // An http(s) URL only: `LinkChips` makes no other, and a pasted `javascript:` must not reach a saved link.
1824
+ href: { default: null, parseHTML: (element) => webUrl(element.getAttribute("data-href")) }
1291
1825
  };
1292
1826
  },
1293
1827
  parseHTML() {
1294
1828
  return [{ tag: "span[data-reference]" }];
1295
1829
  },
1296
1830
  renderHTML({ node }) {
1297
- return ["span", { "data-reference": String(node.attrs.uri) }, String(node.attrs.uri)];
1831
+ const embed = typeof node.attrs.embed === "string" ? { "data-embed": node.attrs.embed } : {};
1832
+ const href = typeof node.attrs.href === "string" && node.attrs.href ? { "data-href": node.attrs.href } : {};
1833
+ return ["span", { "data-reference": String(node.attrs.uri), ...embed, ...href }, String(node.attrs.href || node.attrs.uri)];
1834
+ },
1835
+ renderText({ node }) {
1836
+ return String(node.attrs.href || node.attrs.uri);
1298
1837
  },
1299
1838
  addNodeView() {
1300
1839
  const View = this.options.view;
1301
1840
  if (!View) return null;
1302
- return ReactNodeViewRenderer2(({ node }) => /* @__PURE__ */ jsx6(NodeViewWrapper2, { as: "span", className: "inline", children: /* @__PURE__ */ jsx6(View, { uri: String(node.attrs.uri ?? "") }) }));
1841
+ return ReactNodeViewRenderer2(({ node, editor, getPos }) => {
1842
+ const uri = String(node.attrs.uri ?? "");
1843
+ const href = typeof node.attrs.href === "string" && node.attrs.href ? node.attrs.href : void 0;
1844
+ return /* @__PURE__ */ jsx7(
1845
+ NodeViewWrapper2,
1846
+ {
1847
+ as: "span",
1848
+ className: "inline",
1849
+ onClickCapture: (event) => {
1850
+ const pos = getPos();
1851
+ if (typeof pos !== "number" || !referenceMenuClick(editor, pos)) return;
1852
+ event.preventDefault();
1853
+ event.stopPropagation();
1854
+ },
1855
+ children: uri ? /* @__PURE__ */ jsx7(View, { uri, ...href ? { href } : {} }) : /* @__PURE__ */ jsx7("span", { children: href })
1856
+ }
1857
+ );
1858
+ });
1303
1859
  }
1304
1860
  });
1305
1861
  var UnknownBlock = Node2.create({
@@ -1327,7 +1883,7 @@ var UnknownBlock = Node2.create({
1327
1883
  function UnknownBlockView({ node }) {
1328
1884
  const source = node.attrs.source;
1329
1885
  const text = String(node.attrs.text ?? "");
1330
- return /* @__PURE__ */ jsx6(
1886
+ return /* @__PURE__ */ jsx7(
1331
1887
  NodeViewWrapper2,
1332
1888
  {
1333
1889
  "data-block-id": source?.id,
@@ -1367,7 +1923,7 @@ var AttachmentNode = Node2.create({
1367
1923
  addNodeView() {
1368
1924
  const View = this.options.view;
1369
1925
  if (!View) return null;
1370
- return ReactNodeViewRenderer2(({ node }) => /* @__PURE__ */ jsx6(NodeViewWrapper2, { className: "my-2", children: /* @__PURE__ */ jsx6(View, { attrs: node.attrs }) }));
1926
+ return ReactNodeViewRenderer2(({ node }) => /* @__PURE__ */ jsx7(NodeViewWrapper2, { className: "my-2", children: /* @__PURE__ */ jsx7(View, { attrs: node.attrs }) }));
1371
1927
  }
1372
1928
  });
1373
1929
  var TICK = "checklistTick";
@@ -1385,7 +1941,7 @@ function itemAtCaret(state) {
1385
1941
  if (item.type.name !== "listItem" || $from.index(-1) !== 0) return null;
1386
1942
  return { node: item, pos: $from.before(-1) };
1387
1943
  }
1388
- var sameAttrs = (a, b) => Object.keys({ ...a.attrs, ...b.attrs }).every((key) => a.attrs[key] === b.attrs[key]);
1944
+ var sameAttrs = (a, b) => Object.keys({ ...a.attrs, ...b.attrs }).every((key4) => a.attrs[key4] === b.attrs[key4]);
1389
1945
  function listItemView(editor, node, view, getPos) {
1390
1946
  const toDOM = node.type.spec.toDOM;
1391
1947
  const { dom, contentDOM } = toDOM ? DOMSerializer.renderSpec(document, toDOM(node)) : { dom: document.createElement("li"), contentDOM: void 0 };
@@ -1436,7 +1992,7 @@ function listItemView(editor, node, view, getPos) {
1436
1992
  }
1437
1993
  };
1438
1994
  }
1439
- var Checklist = Extension4.create({
1995
+ var Checklist = Extension7.create({
1440
1996
  name: "checklist",
1441
1997
  // Before the list item's own Enter and Backspace.
1442
1998
  priority: 101,
@@ -1501,14 +2057,14 @@ var Checklist = Extension4.create({
1501
2057
  addProseMirrorPlugins() {
1502
2058
  const editor = this.editor;
1503
2059
  return [
1504
- new Plugin3({
1505
- key: new PluginKey4("checklist"),
2060
+ new Plugin6({
2061
+ key: new PluginKey7("checklist"),
1506
2062
  props: { nodeViews: { listItem: (node, view, getPos) => listItemView(editor, node, view, getPos) } }
1507
2063
  })
1508
2064
  ];
1509
2065
  }
1510
2066
  });
1511
- var Callout = Extension4.create({
2067
+ var Callout = Extension7.create({
1512
2068
  name: "callout",
1513
2069
  addGlobalAttributes() {
1514
2070
  return [
@@ -1537,8 +2093,12 @@ export {
1537
2093
  EditorTableControls,
1538
2094
  FORMATS,
1539
2095
  KeptSelection,
2096
+ LinkChips,
1540
2097
  PastedImages,
1541
2098
  PersonMention,
2099
+ ReferenceCards,
2100
+ ReferenceMenu,
2101
+ ReferenceMenuView,
1542
2102
  ReferenceNode,
1543
2103
  ReferenceTrigger,
1544
2104
  SelectionToolbar,
@@ -1556,8 +2116,10 @@ export {
1556
2116
  filterPeople,
1557
2117
  filterReferences,
1558
2118
  filterSlashSections,
2119
+ findLinkChips,
1559
2120
  formatSection,
1560
2121
  isChecklistTick,
2122
+ isReferenceMenuOpen,
1561
2123
  isSuggestionActive,
1562
2124
  isSuggestionOpen,
1563
2125
  moveBlock,
@@ -1565,10 +2127,17 @@ export {
1565
2127
  pastedImageName,
1566
2128
  pastedImages,
1567
2129
  peopleMenu,
2130
+ referenceAtSelection,
2131
+ referenceCardPlacements,
1568
2132
  referenceMenu,
2133
+ referenceMenuClick,
2134
+ refreshLinkChips,
2135
+ setReferenceCard,
2136
+ setReferenceCardAt,
1569
2137
  startBlockDrag,
1570
2138
  suggestionPopup,
1571
2139
  typeTrigger,
1572
- urgentPubkeys
2140
+ urgentPubkeys,
2141
+ useReferenceCards
1573
2142
  };
1574
2143
  //# sourceMappingURL=editor.js.map