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