@abraca/nuxt 2.11.0 → 2.14.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 (93) hide show
  1. package/dist/module.d.mts +15 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +9 -0
  4. package/dist/runtime/components/ACodeEditor.vue +123 -22
  5. package/dist/runtime/components/ADocPickerModal.d.vue.ts +31 -0
  6. package/dist/runtime/components/ADocPickerModal.vue +191 -0
  7. package/dist/runtime/components/ADocPickerModal.vue.d.ts +31 -0
  8. package/dist/runtime/components/ADocViewToggle.d.vue.ts +40 -0
  9. package/dist/runtime/components/ADocViewToggle.vue +234 -0
  10. package/dist/runtime/components/ADocViewToggle.vue.d.ts +40 -0
  11. package/dist/runtime/components/ADocumentTree.vue +66 -1
  12. package/dist/runtime/components/AEditor.d.vue.ts +17 -10
  13. package/dist/runtime/components/AEditor.vue +403 -167
  14. package/dist/runtime/components/AEditor.vue.d.ts +17 -10
  15. package/dist/runtime/components/ANodePanel.d.vue.ts +9 -1
  16. package/dist/runtime/components/ANodePanel.vue +553 -481
  17. package/dist/runtime/components/ANodePanel.vue.d.ts +9 -1
  18. package/dist/runtime/components/ATagsEditor.d.vue.ts +19 -0
  19. package/dist/runtime/components/ATagsEditor.vue +60 -0
  20. package/dist/runtime/components/ATagsEditor.vue.d.ts +19 -0
  21. package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
  22. package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
  23. package/dist/runtime/components/chat/AChatInput.d.vue.ts +11 -6
  24. package/dist/runtime/components/chat/AChatInput.vue +33 -2
  25. package/dist/runtime/components/chat/AChatInput.vue.d.ts +11 -6
  26. package/dist/runtime/components/chat/AChatList.d.vue.ts +12 -0
  27. package/dist/runtime/components/chat/AChatList.vue +76 -32
  28. package/dist/runtime/components/chat/AChatList.vue.d.ts +12 -0
  29. package/dist/runtime/components/chat/AChatMessages.d.vue.ts +4 -0
  30. package/dist/runtime/components/chat/AChatMessages.vue +57 -4
  31. package/dist/runtime/components/chat/AChatMessages.vue.d.ts +4 -0
  32. package/dist/runtime/components/chat/AChatPanel.d.vue.ts +6 -2
  33. package/dist/runtime/components/chat/AChatPanel.vue +17 -1
  34. package/dist/runtime/components/chat/AChatPanel.vue.d.ts +6 -2
  35. package/dist/runtime/components/chat/ANodeChatPanel.vue +1 -1
  36. package/dist/runtime/components/docs/ADocsSearch.d.vue.ts +1 -1
  37. package/dist/runtime/components/docs/ADocsSearch.vue.d.ts +1 -1
  38. package/dist/runtime/components/editor/ADocSuggestMenu.d.vue.ts +7 -0
  39. package/dist/runtime/components/editor/ADocSuggestMenu.vue +68 -0
  40. package/dist/runtime/components/editor/ADocSuggestMenu.vue.d.ts +7 -0
  41. package/dist/runtime/components/renderers/AChartRenderer.client.d.vue.ts +17 -0
  42. package/dist/runtime/components/renderers/AChartRenderer.client.vue +622 -0
  43. package/dist/runtime/components/renderers/AChartRenderer.client.vue.d.ts +17 -0
  44. package/dist/runtime/components/renderers/AGraphRenderer.vue +64 -15
  45. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.d.vue.ts +2 -2
  46. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.vue.d.ts +2 -2
  47. package/dist/runtime/components/renderers/media/MediaTransportBar.d.vue.ts +2 -2
  48. package/dist/runtime/components/renderers/media/MediaTransportBar.vue.d.ts +2 -2
  49. package/dist/runtime/components/renderers/sheets/ASheetsGrid.d.vue.ts +2 -2
  50. package/dist/runtime/components/renderers/sheets/ASheetsGrid.vue.d.ts +2 -2
  51. package/dist/runtime/components/settings/ASettingsAppearancePanel.d.vue.ts +3 -0
  52. package/dist/runtime/components/settings/ASettingsAppearancePanel.vue +67 -0
  53. package/dist/runtime/components/settings/ASettingsAppearancePanel.vue.d.ts +3 -0
  54. package/dist/runtime/components/settings/ASettingsGroup.d.vue.ts +24 -0
  55. package/dist/runtime/components/settings/ASettingsGroup.vue +31 -0
  56. package/dist/runtime/components/settings/ASettingsGroup.vue.d.ts +24 -0
  57. package/dist/runtime/components/settings/ASettingsModal.vue +84 -53
  58. package/dist/runtime/components/settings/ASettingsPlaceholder.d.vue.ts +20 -0
  59. package/dist/runtime/components/settings/ASettingsPlaceholder.vue +32 -0
  60. package/dist/runtime/components/settings/ASettingsPlaceholder.vue.d.ts +20 -0
  61. package/dist/runtime/components/settings/ASettingsRow.d.vue.ts +34 -0
  62. package/dist/runtime/components/settings/ASettingsRow.vue +34 -0
  63. package/dist/runtime/components/settings/ASettingsRow.vue.d.ts +34 -0
  64. package/dist/runtime/components/settings/sections.d.ts +37 -0
  65. package/dist/runtime/components/settings/sections.js +45 -0
  66. package/dist/runtime/components/shell/AUserMenu.d.vue.ts +2 -2
  67. package/dist/runtime/components/shell/AUserMenu.vue.d.ts +2 -2
  68. package/dist/runtime/components/shell/AUserProfilePopover.d.vue.ts +1 -1
  69. package/dist/runtime/components/shell/AUserProfilePopover.vue.d.ts +1 -1
  70. package/dist/runtime/composables/useChat.d.ts +22 -1
  71. package/dist/runtime/composables/useChat.js +79 -8
  72. package/dist/runtime/composables/useDocLinkPick.d.ts +9 -8
  73. package/dist/runtime/composables/useDocLinkPick.js +7 -18
  74. package/dist/runtime/composables/useDocSuggest.d.ts +34 -0
  75. package/dist/runtime/composables/useDocSuggest.js +56 -0
  76. package/dist/runtime/composables/useNodeContextMenu.d.ts +4 -0
  77. package/dist/runtime/composables/useNodeContextMenu.js +18 -0
  78. package/dist/runtime/composables/useSettingsModal.d.ts +1 -1
  79. package/dist/runtime/extensions/doc-link-drop.js +2 -2
  80. package/dist/runtime/extensions/doc-suggest.d.ts +28 -0
  81. package/dist/runtime/extensions/doc-suggest.js +85 -0
  82. package/dist/runtime/locale.d.ts +8 -0
  83. package/dist/runtime/locale.js +9 -1
  84. package/dist/runtime/utils/chatContent.d.ts +20 -2
  85. package/dist/runtime/utils/chatContent.js +20 -1
  86. package/dist/runtime/utils/codeHighlightStyle.d.ts +15 -0
  87. package/dist/runtime/utils/codeHighlightStyle.js +34 -0
  88. package/dist/runtime/utils/docTypes.js +43 -0
  89. package/dist/runtime/utils/loadCodeMirror.d.ts +1 -0
  90. package/dist/runtime/utils/loadCodeMirror.js +6 -3
  91. package/dist/runtime/utils/titleSync.d.ts +130 -0
  92. package/dist/runtime/utils/titleSync.js +53 -0
  93. package/package.json +12 -1
@@ -1,12 +1,24 @@
1
1
  <script setup>
2
2
  import { shallowRef, ref, watch, watchEffect, computed, nextTick, onBeforeUnmount } from "vue";
3
- import { useNuxtApp } from "#imports";
3
+ import { useNuxtApp, navigateTo, useRuntimeConfig } from "#imports";
4
4
  import { Fragment } from "@tiptap/pm/model";
5
5
  import { useSyncedMap } from "../composables/useYDoc";
6
6
  import { resolveDocType, GEO_TYPE_META_SCHEMAS } from "../utils/docTypes";
7
7
  import { schemaFieldToAttrs, userFieldToAttrs } from "../extensions/meta-field";
8
+ import { decideTitleSync, setHeaderText as applyHeaderText, UNTITLED } from "../utils/titleSync";
8
9
  import { useEditor } from "../composables/useEditor";
9
10
  import { useDocTree } from "../composables/useDocTree";
11
+ import { useDocBreadcrumb } from "../composables/useDocBreadcrumb";
12
+ import { useDocSlugs } from "../composables/useDocSlugs";
13
+ import { useChildTree } from "../composables/useChildTree";
14
+ import { useFavorites } from "../composables/useFavorites";
15
+ import { useWindowManager } from "../composables/useWindowManager";
16
+ import { useDocDragBus } from "../composables/useDocDragBus";
17
+ import { useDocLinkPick } from "../composables/useDocLinkPick";
18
+ import { makeDocSearch } from "../composables/useDocSuggest";
19
+ import { DocSuggest } from "../extensions/doc-suggest";
20
+ import { useNodeContextMenu } from "../composables/useNodeContextMenu";
21
+ import { DOC_DRAG_MIME, isDocDrag, parseDocDragPayload, isDescendantInMap } from "../utils/docDragDrop";
10
22
  import { useEditorToolbar } from "../composables/useEditorToolbar";
11
23
  import { useEditorSuggestions } from "../composables/useEditorSuggestions";
12
24
  import { useEditorDragHandle } from "../composables/useEditorDragHandle";
@@ -14,6 +26,8 @@ import { useEditorEmojis } from "../composables/useEditorEmojis";
14
26
  import { useEditorMentions } from "../composables/useEditorMentions";
15
27
  import ALinkPopover from "./editor/ALinkPopover.vue";
16
28
  import ADocLinkPopover from "./editor/ADocLinkPopover.vue";
29
+ import ANodeContextMenu from "./shell/ANodeContextMenu.vue";
30
+ import ADocSuggestMenu from "./editor/ADocSuggestMenu.vue";
17
31
  import ACodeEditor from "./ACodeEditor.vue";
18
32
  const props = defineProps({
19
33
  docId: { type: String, required: true },
@@ -28,7 +42,8 @@ const props = defineProps({
28
42
  parentType: { type: String, required: false },
29
43
  metaSchema: { type: Array, required: false },
30
44
  variant: { type: String, required: false, default: "doc" },
31
- showSourceToggle: { type: Boolean, required: false, default: false }
45
+ showSourceToggle: { type: Boolean, required: false, default: false },
46
+ showBreadcrumb: { type: Boolean, required: false, default: true }
32
47
  });
33
48
  const viewMode = ref("rich");
34
49
  const emit = defineEmits(["ready", "update", "rename", "updateMeta"]);
@@ -48,9 +63,26 @@ watch(provider, async (prov) => {
48
63
  onBeforeUnmount(() => {
49
64
  provider.value?.unpinChild?.(props.docId);
50
65
  });
66
+ const docSuggestEnabled = (useRuntimeConfig().public?.abracadabra?.docPicker ?? "inline") === "inline";
67
+ const _suggestTree = useChildTree(doc, props.docId);
68
+ const docSuggestExt = DocSuggest.configure({
69
+ search: makeDocSearch(_suggestTree, props.docId),
70
+ onCommit: (item, mode, range, query) => {
71
+ const ed = editorRef.value?.editor;
72
+ if (!ed || !props.editable) return;
73
+ let docId = item.id;
74
+ if (item.isCreate) docId = _suggestTree.createChild(props.docId, query.trim() || "Untitled", "doc");
75
+ if (!docId) return;
76
+ const chain = ed.chain().focus().deleteRange(range);
77
+ if (mode === "embed") chain.insertDocEmbed({ docId });
78
+ else chain.insertDocLink({ docId });
79
+ chain.run();
80
+ }
81
+ });
51
82
  const { extensions, connectedUsers, ready } = useEditor({
52
83
  childProvider,
53
- docId: props.docId
84
+ docId: props.docId,
85
+ extraExtensions: docSuggestEnabled ? [docSuggestExt] : []
54
86
  });
55
87
  watch(ready, (val) => {
56
88
  if (val) emit("ready");
@@ -77,7 +109,7 @@ const resolvedMetaSchema = computed(() => {
77
109
  });
78
110
  const { items: allSuggestionItems, propertiesOnlyItems } = useEditorSuggestions({ docId: props.docId });
79
111
  const { items: emojiItems } = useEditorEmojis();
80
- const { items: mentionItems } = useEditorMentions({ docId: props.docId });
112
+ const { items: mentionItems } = useEditorMentions({ docId: props.docId, includeDocuments: false });
81
113
  const isInDocumentMeta = ref(false);
82
114
  watchEffect((onCleanup) => {
83
115
  const ed = editorRef.value?.editor;
@@ -167,85 +199,126 @@ function initDocumentMeta(ed) {
167
199
  metaInitDone = true;
168
200
  }
169
201
  let syncingFromEditor = false;
170
- const _lastEmittedHeader = ref("");
202
+ let lastSyncedLabel = null;
171
203
  function getHeaderText(ed) {
172
204
  const first = ed.state.doc.firstChild;
173
205
  if (!first || first.type.name !== "documentHeader") return "";
174
206
  return first.textContent;
175
207
  }
176
- function setHeaderText(ed, text) {
177
- const first = ed.state.doc.firstChild;
178
- if (!first || first.type.name !== "documentHeader") return;
179
- if (first.textContent === text) return;
180
- const { tr, schema } = ed.state;
181
- const to = 1 + first.content.size;
182
- if (text) {
183
- tr.replaceWith(1, to, schema.text(text));
184
- } else {
185
- tr.delete(1, to);
186
- }
187
- ed.view.dispatch(tr);
208
+ function currentTreeLabel() {
209
+ return _treeMap.data[props.docId]?.label ?? props.docLabel ?? "";
188
210
  }
189
- function syncHeaderToTree(ed) {
190
- const text = getHeaderText(ed);
191
- if (text === _lastEmittedHeader.value) return;
192
- _lastEmittedHeader.value = text;
211
+ function writeTreeLabel(label) {
212
+ const e = _treeMap.data[props.docId];
213
+ const next = label || UNTITLED;
214
+ if (!e || e.label === next) return;
193
215
  syncingFromEditor = true;
194
- emit("rename", text || "Untitled");
216
+ lastSyncedLabel = next;
217
+ _treeMap.set(props.docId, { ...e, label: next, updatedAt: Date.now() });
218
+ emit("rename", next);
195
219
  nextTick(() => {
196
220
  syncingFromEditor = false;
197
221
  });
198
222
  }
223
+ function syncHeader(ed, isRemoteUpdate, initialSyncDone) {
224
+ const action = decideTitleSync({
225
+ headerText: getHeaderText(ed),
226
+ treeLabel: currentTreeLabel(),
227
+ isRemoteUpdate,
228
+ initialSyncDone
229
+ });
230
+ if (action.kind === "noop") return;
231
+ if (action.kind === "header-from-tree") {
232
+ applyHeaderText(ed, action.text);
233
+ return;
234
+ }
235
+ writeTreeLabel(action.label);
236
+ }
199
237
  watchEffect((onCleanup) => {
200
238
  const ed = editorRef.value?.editor;
201
239
  if (!ed || !ready.value) return;
202
240
  let initialSyncDone = false;
203
- function doInitialSync() {
204
- if (initialSyncDone) return;
205
- initialSyncDone = true;
206
- const headerText = getHeaderText(ed);
207
- const treeLabel = props.docLabel ?? "";
208
- if (headerText !== treeLabel) {
209
- const treeMeansEmpty = !treeLabel || treeLabel === "Untitled";
210
- const headerMeansEmpty = !headerText;
211
- if (treeMeansEmpty && headerMeansEmpty) return;
212
- if (!treeMeansEmpty && headerMeansEmpty) {
213
- setHeaderText(ed, treeLabel);
214
- return;
241
+ let headerTimer = null;
242
+ const runSync = (isRemote) => {
243
+ const wasInitial = !initialSyncDone;
244
+ if (wasInitial) initialSyncDone = true;
245
+ syncHeader(ed, isRemote, !wasInitial);
246
+ };
247
+ function onUpdate({ transaction }) {
248
+ const isRemote = !!transaction?.getMeta?.("y-sync$")?.isChangeOrigin;
249
+ initDocumentMeta(ed);
250
+ if (isRemote || !initialSyncDone) {
251
+ if (headerTimer) {
252
+ clearTimeout(headerTimer);
253
+ headerTimer = null;
215
254
  }
216
- _lastEmittedHeader.value = headerText;
217
- syncingFromEditor = true;
218
- emit("rename", headerText);
219
- nextTick(() => {
220
- syncingFromEditor = false;
221
- });
255
+ runSync(isRemote);
256
+ } else {
257
+ if (headerTimer) clearTimeout(headerTimer);
258
+ headerTimer = setTimeout(() => {
259
+ headerTimer = null;
260
+ runSync(false);
261
+ }, 200);
222
262
  }
223
263
  }
224
- function onUpdate() {
225
- syncHeaderToTree(ed);
226
- initDocumentMeta(ed);
227
- }
228
264
  nextTick(() => {
229
- doInitialSync();
265
+ runSync(false);
230
266
  initDocumentMeta(ed);
231
267
  });
232
268
  ed.on("update", onUpdate);
233
- onCleanup(() => ed.off("update", onUpdate));
234
- });
235
- watch(() => props.docLabel, (newLabel) => {
236
- if (syncingFromEditor) return;
237
- if (newLabel === _lastEmittedHeader.value) return;
238
- if (!_treeMap.lastUpdateLocal.value) return;
239
- const ed = editorRef.value?.editor;
240
- if (!ed || !ready.value) return;
241
- setHeaderText(ed, newLabel || "");
269
+ onCleanup(() => {
270
+ if (headerTimer) clearTimeout(headerTimer);
271
+ ed.off("update", onUpdate);
272
+ });
242
273
  });
274
+ watch(
275
+ () => _treeMap.data[props.docId]?.label ?? props.docLabel,
276
+ (newLabel) => {
277
+ if (syncingFromEditor) return;
278
+ if (lastSyncedLabel !== null && newLabel === lastSyncedLabel) {
279
+ lastSyncedLabel = null;
280
+ return;
281
+ }
282
+ lastSyncedLabel = null;
283
+ if (newLabel === void 0 || newLabel === "" || newLabel === UNTITLED) return;
284
+ const ed = editorRef.value?.editor;
285
+ if (!ed || !ready.value) return;
286
+ applyHeaderText(ed, newLabel);
287
+ }
288
+ );
243
289
  function insertNode(editor, type, attrs, content) {
244
290
  const node = { type, attrs };
245
291
  if (content) node.content = content;
246
292
  return editor.chain().focus().insertContent(node);
247
293
  }
294
+ const { pickDoc } = useDocLinkPick();
248
295
  const editorHandlers = {
296
+ // Slash → pick a document → embed it as a block. `execute` returns a chain
297
+ // synchronously (the dispatcher .run()s it); the actual insert happens async
298
+ // once the picker resolves.
299
+ docEmbed: {
300
+ canExecute: () => true,
301
+ execute: (editor) => {
302
+ pickDoc().then((result) => {
303
+ if (!result) return;
304
+ editor.chain().focus().insertDocEmbed({ docId: result.id }).run();
305
+ });
306
+ return editor.chain().focus();
307
+ },
308
+ isActive: (editor) => editor.isActive("docEmbed")
309
+ },
310
+ // Slash/drag-handle → pick a document → insert an inline link to it.
311
+ docLink: {
312
+ canExecute: () => true,
313
+ execute: (editor) => {
314
+ pickDoc().then((result) => {
315
+ if (!result) return;
316
+ editor.chain().focus().insertDocLink({ docId: result.id }).run();
317
+ });
318
+ return editor.chain().focus();
319
+ },
320
+ isActive: (editor) => editor.isActive("docLink")
321
+ },
249
322
  insertMetaField: {
250
323
  canExecute: () => true,
251
324
  execute: (editor, item) => editor.chain().focus().insertMetaField(item.attrs),
@@ -357,6 +430,104 @@ const editorHandlers = {
357
430
  const _mentionItems = computed(
358
431
  () => registry.getAllMentionProviders().flatMap((p) => p.label ? [p] : [])
359
432
  );
433
+ const { items: _breadcrumbItems } = useDocBreadcrumb(() => props.docId);
434
+ const { getDocUrl } = useDocSlugs();
435
+ const ancestorChain = computed(
436
+ () => _breadcrumbItems.value.slice(0, -1).map((a) => {
437
+ const entry = docTree.getEntry(a.id);
438
+ return { ...a, type: entry?.type, to: getDocUrl(a.id) };
439
+ })
440
+ );
441
+ const bcTree = useChildTree(doc, props.docId);
442
+ const { isFavorite, toggleFavorite } = useFavorites(doc);
443
+ const docDragBus = useDocDragBus();
444
+ const breadcrumbDragId = ref(null);
445
+ const breadcrumbDropTargetId = ref(null);
446
+ function onBcDragStart(e, a) {
447
+ if (!props.editable) return;
448
+ breadcrumbDragId.value = a.id;
449
+ if (e.dataTransfer) {
450
+ e.dataTransfer.effectAllowed = "move";
451
+ e.dataTransfer.setData("text/plain", a.id);
452
+ e.dataTransfer.setData(DOC_DRAG_MIME, JSON.stringify({ id: a.id, label: a.label }));
453
+ }
454
+ docDragBus.startDrag({
455
+ docIds: [a.id],
456
+ labels: [a.label],
457
+ icons: a.icon ? [a.icon] : void 0,
458
+ source: "renderer",
459
+ sourceDocId: props.docId,
460
+ pointer: { x: e.clientX, y: e.clientY }
461
+ });
462
+ }
463
+ function onBcDragEnd() {
464
+ breadcrumbDragId.value = null;
465
+ breadcrumbDropTargetId.value = null;
466
+ docDragBus.endDrag();
467
+ }
468
+ function onBcDragOver(e, ancestorId) {
469
+ if (!isDocDrag(e)) return;
470
+ if (breadcrumbDragId.value === ancestorId) return;
471
+ e.preventDefault();
472
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
473
+ breadcrumbDropTargetId.value = ancestorId;
474
+ }
475
+ function onBcDragLeave(e) {
476
+ const related = e.relatedTarget;
477
+ if (related && e.currentTarget.contains(related)) return;
478
+ breadcrumbDropTargetId.value = null;
479
+ }
480
+ function onBcDrop(e, ancestorId) {
481
+ e.preventDefault();
482
+ e.stopPropagation();
483
+ breadcrumbDropTargetId.value = null;
484
+ if (!props.editable) return;
485
+ const payload = parseDocDragPayload(e);
486
+ if (!payload) return;
487
+ if (payload.id === ancestorId) return;
488
+ if (isDescendantInMap(bcTree.treeMap.data, payload.id, ancestorId)) return;
489
+ bcTree.moveEntry(payload.id, ancestorId, Date.now());
490
+ breadcrumbDragId.value = null;
491
+ docDragBus.endDrag();
492
+ }
493
+ async function bcOpenAsWindow(id, label) {
494
+ const wm = useWindowManager();
495
+ if (wm.windows.has(id)) {
496
+ wm.focusWindow(id);
497
+ return;
498
+ }
499
+ const childProv = await provider.value?.loadChild(id);
500
+ if (!childProv) return;
501
+ if (!childProv.isSynced) {
502
+ await new Promise((resolve) => {
503
+ const done = () => {
504
+ childProv.off("synced", done);
505
+ resolve();
506
+ };
507
+ childProv.on("synced", done);
508
+ setTimeout(resolve, 6e3);
509
+ });
510
+ }
511
+ const entry = bcTree.treeMap.get(id);
512
+ wm.openWindow({ id, title: label, docId: id, docType: entry?.type, provider: childProv, initialTab: "editor" });
513
+ }
514
+ function bcMenuItems(a) {
515
+ return useNodeContextMenu({
516
+ nodeId: a.id,
517
+ label: a.label,
518
+ type: a.type,
519
+ tree: bcTree,
520
+ registry,
521
+ favorites: { isFavorite, toggleFavorite },
522
+ onOpen: (id) => navigateTo(getDocUrl(id)),
523
+ onOpenInWindow: () => bcOpenAsWindow(a.id, a.label),
524
+ onCopyLink: (id) => {
525
+ if (!import.meta.client || !navigator?.clipboard) return;
526
+ navigator.clipboard.writeText(new URL(getDocUrl(id), location.origin).href).catch(() => {
527
+ });
528
+ }
529
+ }).items;
530
+ }
360
531
  function onPlusClick(e, onClick) {
361
532
  e.stopPropagation();
362
533
  onClick();
@@ -387,7 +558,10 @@ function onPlusClick(e, onClick) {
387
558
  >
388
559
  <div class="aeditor-source-toolbar">
389
560
  <span class="aeditor-source-toolbar__label">
390
- <UIcon name="i-lucide-code-xml" class="size-3.5" />
561
+ <UIcon
562
+ name="i-lucide-code-xml"
563
+ class="size-3.5"
564
+ />
391
565
  XML source — read-only mirror of <code>getXmlFragment('default')</code>
392
566
  </span>
393
567
  <UButton
@@ -408,136 +582,198 @@ function onPlusClick(e, onClick) {
408
582
  />
409
583
  </div>
410
584
 
411
- <UEditor
585
+ <!-- Editor canvas: ancestor breadcrumb (cou-sh parity) + the TipTap editor,
586
+ stacked in one scroll container (the parent's overflow class falls
587
+ through to this wrapper). -->
588
+ <div
412
589
  v-else
413
- :class="{ 'prose-variant': variant === 'prose' }"
414
- ref="editorRef"
415
- v-slot="{ editor }"
416
- v-model="model"
417
- :content-type="contentType"
418
- :editable="editable"
419
- :extensions="extensions"
420
- :starter-kit="{ undoRedo: false, codeBlock: false, document: false }"
421
- :handlers="editorHandlers"
422
- :placeholder="placeholder"
423
- @update:model-value="emit('update', $event)"
590
+ class="aeditor-canvas"
424
591
  >
425
- <!-- Floating source-view toggle (opt-in via :show-source-toggle="true") -->
426
- <UButton
427
- v-if="showSourceToggle"
428
- class="aeditor-source-toggle"
429
- icon="i-lucide-code-xml"
430
- size="xs"
431
- variant="ghost"
432
- color="neutral"
433
- :title="'View XML source'"
434
- @click="viewMode = 'source'"
435
- />
436
- <!-- Default slot: app can override entire editor content -->
437
- <slot
438
- :editor="editor"
439
- :connected-users="connectedUsers"
440
- :ready="ready"
592
+ <nav
593
+ v-if="showBreadcrumb && ancestorChain.length"
594
+ aria-label="Breadcrumb"
595
+ class="aeditor-breadcrumb"
441
596
  >
442
- <!-- ── Bubble toolbar (appears when text is selected) ─────────────── -->
443
- <!-- 4 named slots are forwarded to consumers:
444
- #link / #doc-link defaulted to ALinkPopover / ADocLinkPopover
445
- #create-child-doc / #send-to-chat — empty by default; app-defined -->
446
- <UEditorToolbar
447
- v-if="showToolbar"
448
- :editor="editor"
449
- :items="toolbarItems"
450
- layout="bubble"
451
- :should-show="({ view, state }) => view.hasFocus() && !state.selection.empty"
452
- >
453
- <template #link>
454
- <slot
455
- name="link"
456
- :editor="editor"
457
- >
458
- <ALinkPopover :editor="editor" />
459
- </slot>
460
- </template>
461
- <template #doc-link>
462
- <slot
463
- name="doc-link"
464
- :editor="editor"
597
+ <ol class="flex items-center gap-0.5 min-w-0">
598
+ <template
599
+ v-for="(ancestor, idx) in ancestorChain"
600
+ :key="ancestor.id"
601
+ >
602
+ <li
603
+ v-if="idx > 0"
604
+ class="flex shrink-0"
465
605
  >
466
- <ADocLinkPopover :editor="editor" />
467
- </slot>
468
- </template>
469
- <template #create-child-doc>
470
- <slot
471
- name="create-child-doc"
472
- :editor="editor"
473
- />
474
- </template>
475
- <template #send-to-chat>
476
- <slot
477
- name="send-to-chat"
478
- :editor="editor"
479
- />
606
+ <UIcon
607
+ name="i-lucide-chevron-right"
608
+ class="size-4 text-(--ui-text-dimmed)"
609
+ />
610
+ </li>
611
+ <li class="flex min-w-0">
612
+ <ANodeContextMenu :items="bcMenuItems(ancestor)">
613
+ <button
614
+ type="button"
615
+ draggable="true"
616
+ class="flex items-center gap-1.5 px-1.5 py-0.5 rounded-md text-sm font-medium transition-colors min-w-0 select-none cursor-pointer"
617
+ :class="[
618
+ breadcrumbDragId === ancestor.id ? 'opacity-30' : '',
619
+ breadcrumbDropTargetId === ancestor.id ? 'bg-(--ui-primary)/10 ring-2 ring-(--ui-primary)/50 text-(--ui-text)' : 'text-(--ui-text-muted) hover:bg-(--ui-bg-elevated)/60 hover:text-(--ui-text)'
620
+ ]"
621
+ @click="navigateTo(ancestor.to)"
622
+ @dragstart="onBcDragStart($event, ancestor)"
623
+ @dragend="onBcDragEnd"
624
+ @dragover="onBcDragOver($event, ancestor.id)"
625
+ @dragleave="onBcDragLeave"
626
+ @drop="onBcDrop($event, ancestor.id)"
627
+ >
628
+ <UIcon
629
+ :name="ancestor.icon"
630
+ class="size-4 shrink-0"
631
+ />
632
+ <span class="truncate">{{ ancestor.label }}</span>
633
+ </button>
634
+ </ANodeContextMenu>
635
+ </li>
480
636
  </template>
481
- </UEditorToolbar>
637
+ </ol>
638
+ </nav>
482
639
 
483
- <!-- ── Slash command menu ──────────────────────────────────────────── -->
484
- <UEditorSuggestionMenu
485
- v-if="showSuggestionMenu"
486
- :editor="editor"
487
- :items="suggestionItems"
640
+ <UEditor
641
+ ref="editorRef"
642
+ v-slot="{ editor }"
643
+ v-model="model"
644
+ :class="{ 'prose-variant': variant === 'prose' }"
645
+ :content-type="contentType"
646
+ :editable="editable"
647
+ :extensions="extensions"
648
+ :starter-kit="{ undoRedo: false, codeBlock: false, document: false }"
649
+ :handlers="editorHandlers"
650
+ :placeholder="placeholder"
651
+ @update:model-value="emit('update', $event)"
652
+ >
653
+ <!-- Floating source-view toggle (opt-in via :show-source-toggle="true") -->
654
+ <UButton
655
+ v-if="showSourceToggle"
656
+ class="aeditor-source-toggle"
657
+ icon="i-lucide-code-xml"
658
+ size="xs"
659
+ variant="ghost"
660
+ color="neutral"
661
+ :title="'View XML source'"
662
+ @click="viewMode = 'source'"
488
663
  />
489
-
490
- <!-- ── Emoji menu (`:` trigger) ───────────────────────────────────── -->
491
- <UEditorEmojiMenu
664
+ <!-- Default slot: app can override entire editor content -->
665
+ <slot
492
666
  :editor="editor"
493
- :items="emojiItems"
494
- />
667
+ :connected-users="connectedUsers"
668
+ :ready="ready"
669
+ >
670
+ <!-- ── Bubble toolbar (appears when text is selected) ─────────────── -->
671
+ <!-- 4 named slots are forwarded to consumers:
672
+ #link / #doc-link — defaulted to ALinkPopover / ADocLinkPopover
673
+ #create-child-doc / #send-to-chat — empty by default; app-defined -->
674
+ <UEditorToolbar
675
+ v-if="showToolbar"
676
+ :editor="editor"
677
+ :items="toolbarItems"
678
+ layout="bubble"
679
+ :should-show="({ view, state }) => view.hasFocus() && !state.selection.empty"
680
+ >
681
+ <template #link>
682
+ <slot
683
+ name="link"
684
+ :editor="editor"
685
+ >
686
+ <ALinkPopover :editor="editor" />
687
+ </slot>
688
+ </template>
689
+ <template #doc-link>
690
+ <slot
691
+ name="doc-link"
692
+ :editor="editor"
693
+ >
694
+ <ADocLinkPopover :editor="editor" />
695
+ </slot>
696
+ </template>
697
+ <template #create-child-doc>
698
+ <slot
699
+ name="create-child-doc"
700
+ :editor="editor"
701
+ />
702
+ </template>
703
+ <template #send-to-chat>
704
+ <slot
705
+ name="send-to-chat"
706
+ :editor="editor"
707
+ />
708
+ </template>
709
+ </UEditorToolbar>
495
710
 
496
- <!-- ── Mention menu (`@` trigger) — users + docs + plugin providers ── -->
497
- <UEditorMentionMenu
498
- :editor="editor"
499
- :items="mentionItems"
500
- />
711
+ <!-- ── Slash command menu ──────────────────────────────────────────── -->
712
+ <UEditorSuggestionMenu
713
+ v-if="showSuggestionMenu"
714
+ :editor="editor"
715
+ :items="suggestionItems"
716
+ />
501
717
 
502
- <!-- ── Drag handle plus button + grip dropdown ───────────────────── -->
503
- <UEditorDragHandle
504
- v-if="showDragHandle"
505
- v-slot="{ ui, onClick }"
506
- :editor="editor"
507
- @node-change="dragHandle.onNodeChange"
508
- >
509
- <!-- Plus: insert block via slash menu -->
510
- <UButton
511
- icon="i-lucide-plus"
512
- color="neutral"
513
- variant="ghost"
514
- size="sm"
515
- :class="ui.handle()"
516
- @click="(e) => onPlusClick(e, onClick)"
718
+ <!-- ── Emoji menu (`:` trigger) ───────────────────────────────────── -->
719
+ <UEditorEmojiMenu
720
+ :editor="editor"
721
+ :items="emojiItems"
722
+ />
723
+
724
+ <!-- ── Mention menu (`@` trigger) — users + docs + plugin providers ── -->
725
+ <UEditorMentionMenu
726
+ :editor="editor"
727
+ :items="mentionItems"
517
728
  />
518
729
 
519
- <!-- Grip: block context menu -->
520
- <UDropdownMenu
521
- v-slot="{ open }"
522
- :modal="false"
523
- :items="dragHandle.getItems(editor)"
524
- :content="{ side: 'left' }"
525
- :ui="{ content: 'w-48', label: 'text-xs' }"
526
- @update:open="(v) => editor.chain().setMeta('lockDragHandle', v).run()"
730
+ <!-- ── Drag handle plus button + grip dropdown ───────────────────── -->
731
+ <UEditorDragHandle
732
+ v-if="showDragHandle"
733
+ v-slot="{ ui, onClick }"
734
+ :editor="editor"
735
+ @node-change="dragHandle.onNodeChange"
527
736
  >
737
+ <!-- Plus: insert block via slash menu -->
528
738
  <UButton
739
+ icon="i-lucide-plus"
529
740
  color="neutral"
530
741
  variant="ghost"
531
- :active-variant="'soft'"
532
742
  size="sm"
533
- icon="i-lucide-grip-vertical"
534
- :active="open"
535
743
  :class="ui.handle()"
744
+ @click="(e) => onPlusClick(e, onClick)"
536
745
  />
537
- </UDropdownMenu>
538
- </UEditorDragHandle>
539
- </slot>
540
- </UEditor>
746
+
747
+ <!-- Grip: block context menu -->
748
+ <UDropdownMenu
749
+ v-slot="{ open }"
750
+ :modal="false"
751
+ :items="dragHandle.getItems(editor)"
752
+ :content="{ side: 'left' }"
753
+ :ui="{ content: 'w-48', label: 'text-xs' }"
754
+ @update:open="(v) => editor.chain().setMeta('lockDragHandle', v).run()"
755
+ >
756
+ <UButton
757
+ color="neutral"
758
+ variant="ghost"
759
+ :active-variant="'soft'"
760
+ size="sm"
761
+ icon="i-lucide-grip-vertical"
762
+ :active="open"
763
+ :class="ui.handle()"
764
+ />
765
+ </UDropdownMenu>
766
+ </UEditorDragHandle>
767
+
768
+ <!-- Inline `[[` doc-link / `![[` doc-embed mention-style popup
769
+ (only mounted when docPicker === 'inline'). -->
770
+ <ADocSuggestMenu
771
+ v-if="docSuggestEnabled && editor?.storage?.docSuggest"
772
+ :state="editor.storage.docSuggest.popup"
773
+ />
774
+ </slot>
775
+ </UEditor>
776
+ </div>
541
777
 
542
778
  <template #fallback>
543
779
  <div
@@ -553,5 +789,5 @@ function onPlusClick(e, onClick) {
553
789
  </template>
554
790
 
555
791
  <style scoped>
556
- .prose-variant :deep(.tiptap){color:var(--ui-text);font-family:ui-serif,Georgia,Cambria,Times New Roman,Times,serif;font-size:1.0625rem;line-height:1.75}.prose-variant :deep(.tiptap p){margin-bottom:1em;margin-top:0}.prose-variant :deep(.tiptap h1),.prose-variant :deep(.tiptap h2),.prose-variant :deep(.tiptap h3),.prose-variant :deep(.tiptap h4){font-family:ui-serif,Georgia,Cambria,Times New Roman,Times,serif;letter-spacing:-.01em;line-height:1.25;margin-bottom:.5em;margin-top:1.75em}.prose-variant :deep(.tiptap h1){font-size:2rem;font-weight:700}.prose-variant :deep(.tiptap h2){font-size:1.5rem;font-weight:700}.prose-variant :deep(.tiptap h3){font-size:1.25rem;font-weight:600}.prose-variant :deep(.tiptap blockquote){border-left:3px solid var(--ui-border);color:var(--ui-text-muted);font-style:italic;margin-left:0;padding-left:1em}.prose-variant :deep(.tiptap ol),.prose-variant :deep(.tiptap ul){margin-bottom:1em;padding-left:1.5em}.prose-variant :deep(.tiptap .document-header){font-family:ui-serif,Georgia,Cambria,Times New Roman,Times,serif;font-size:2.75rem;font-weight:700;letter-spacing:-.02em;line-height:1.15;margin-bottom:1.5rem}.aeditor-source-wrap{display:flex;flex:1 1 0;flex-direction:column;min-height:0}.aeditor-source-toolbar{align-items:center;background:var(--ui-bg);border-bottom:1px solid var(--ui-border);display:flex;flex-shrink:0;justify-content:space-between;padding:.375rem .75rem}.aeditor-source-toolbar__label{align-items:center;color:var(--ui-text-muted);display:inline-flex;font-size:.75rem;gap:.375rem}.aeditor-source-toolbar__label code{background:var(--ui-bg-elevated);border:1px solid var(--ui-border);border-radius:var(--ui-radius);font-family:ui-monospace,SF Mono,Menlo,Monaco,Consolas,monospace;font-size:.7rem;padding:.05rem .3rem}.aeditor-source-toggle{opacity:.6;position:absolute;right:.375rem;top:.375rem;transition:opacity .12s ease;z-index:5}.aeditor-source-toggle:hover{opacity:1}
792
+ .aeditor-canvas{display:flex;flex-direction:column;min-height:0}.aeditor-breadcrumb{flex-shrink:0;padding:1rem 0 0}@media (min-width:640px){.aeditor-breadcrumb{padding-left:2rem;padding-right:2rem}}.prose-variant :deep(.tiptap){color:var(--ui-text);font-family:ui-serif,Georgia,Cambria,Times New Roman,Times,serif;font-size:1.0625rem;line-height:1.75}.prose-variant :deep(.tiptap p){margin-bottom:1em;margin-top:0}.prose-variant :deep(.tiptap h1),.prose-variant :deep(.tiptap h2),.prose-variant :deep(.tiptap h3),.prose-variant :deep(.tiptap h4){font-family:ui-serif,Georgia,Cambria,Times New Roman,Times,serif;letter-spacing:-.01em;line-height:1.25;margin-bottom:.5em;margin-top:1.75em}.prose-variant :deep(.tiptap h1){font-size:2rem;font-weight:700}.prose-variant :deep(.tiptap h2){font-size:1.5rem;font-weight:700}.prose-variant :deep(.tiptap h3){font-size:1.25rem;font-weight:600}.prose-variant :deep(.tiptap blockquote){border-left:3px solid var(--ui-border);color:var(--ui-text-muted);font-style:italic;margin-left:0;padding-left:1em}.prose-variant :deep(.tiptap ol),.prose-variant :deep(.tiptap ul){margin-bottom:1em;padding-left:1.5em}.prose-variant :deep(.tiptap .document-header){font-family:ui-serif,Georgia,Cambria,Times New Roman,Times,serif;font-size:2.75rem;font-weight:700;letter-spacing:-.02em;line-height:1.15;margin-bottom:1.5rem}.aeditor-source-wrap{display:flex;flex:1 1 0;flex-direction:column;min-height:0}.aeditor-source-toolbar{align-items:center;background:var(--ui-bg);border-bottom:1px solid var(--ui-border);display:flex;flex-shrink:0;justify-content:space-between;padding:.375rem .75rem}.aeditor-source-toolbar__label{align-items:center;color:var(--ui-text-muted);display:inline-flex;font-size:.75rem;gap:.375rem}.aeditor-source-toolbar__label code{background:var(--ui-bg-elevated);border:1px solid var(--ui-border);border-radius:var(--ui-radius);font-family:ui-monospace,SF Mono,Menlo,Monaco,Consolas,monospace;font-size:.7rem;padding:.05rem .3rem}.aeditor-source-toggle{opacity:.6;position:absolute;right:.375rem;top:.375rem;transition:opacity .12s ease;z-index:5}.aeditor-source-toggle:hover{opacity:1}
557
793
  </style>