@abraca/nuxt 2.13.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.
@@ -1,6 +1,6 @@
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";
@@ -10,6 +10,15 @@ import { useEditor } from "../composables/useEditor";
10
10
  import { useDocTree } from "../composables/useDocTree";
11
11
  import { useDocBreadcrumb } from "../composables/useDocBreadcrumb";
12
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";
13
22
  import { useEditorToolbar } from "../composables/useEditorToolbar";
14
23
  import { useEditorSuggestions } from "../composables/useEditorSuggestions";
15
24
  import { useEditorDragHandle } from "../composables/useEditorDragHandle";
@@ -17,6 +26,8 @@ import { useEditorEmojis } from "../composables/useEditorEmojis";
17
26
  import { useEditorMentions } from "../composables/useEditorMentions";
18
27
  import ALinkPopover from "./editor/ALinkPopover.vue";
19
28
  import ADocLinkPopover from "./editor/ADocLinkPopover.vue";
29
+ import ANodeContextMenu from "./shell/ANodeContextMenu.vue";
30
+ import ADocSuggestMenu from "./editor/ADocSuggestMenu.vue";
20
31
  import ACodeEditor from "./ACodeEditor.vue";
21
32
  const props = defineProps({
22
33
  docId: { type: String, required: true },
@@ -52,9 +63,26 @@ watch(provider, async (prov) => {
52
63
  onBeforeUnmount(() => {
53
64
  provider.value?.unpinChild?.(props.docId);
54
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
+ });
55
82
  const { extensions, connectedUsers, ready } = useEditor({
56
83
  childProvider,
57
- docId: props.docId
84
+ docId: props.docId,
85
+ extraExtensions: docSuggestEnabled ? [docSuggestExt] : []
58
86
  });
59
87
  watch(ready, (val) => {
60
88
  if (val) emit("ready");
@@ -81,7 +109,7 @@ const resolvedMetaSchema = computed(() => {
81
109
  });
82
110
  const { items: allSuggestionItems, propertiesOnlyItems } = useEditorSuggestions({ docId: props.docId });
83
111
  const { items: emojiItems } = useEditorEmojis();
84
- const { items: mentionItems } = useEditorMentions({ docId: props.docId });
112
+ const { items: mentionItems } = useEditorMentions({ docId: props.docId, includeDocuments: false });
85
113
  const isInDocumentMeta = ref(false);
86
114
  watchEffect((onCleanup) => {
87
115
  const ed = editorRef.value?.editor;
@@ -263,7 +291,34 @@ function insertNode(editor, type, attrs, content) {
263
291
  if (content) node.content = content;
264
292
  return editor.chain().focus().insertContent(node);
265
293
  }
294
+ const { pickDoc } = useDocLinkPick();
266
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
+ },
267
322
  insertMetaField: {
268
323
  canExecute: () => true,
269
324
  execute: (editor, item) => editor.chain().focus().insertMetaField(item.attrs),
@@ -378,8 +433,101 @@ const _mentionItems = computed(
378
433
  const { items: _breadcrumbItems } = useDocBreadcrumb(() => props.docId);
379
434
  const { getDocUrl } = useDocSlugs();
380
435
  const ancestorChain = computed(
381
- () => _breadcrumbItems.value.slice(0, -1).map((a) => ({ ...a, to: getDocUrl(a.id) }))
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
+ })
382
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
+ }
383
531
  function onPlusClick(e, onClick) {
384
532
  e.stopPropagation();
385
533
  onClick();
@@ -461,16 +609,29 @@ function onPlusClick(e, onClick) {
461
609
  />
462
610
  </li>
463
611
  <li class="flex min-w-0">
464
- <ULink
465
- :to="ancestor.to"
466
- class="flex items-center gap-1.5 px-1.5 py-0.5 rounded-md text-sm text-(--ui-text-muted) font-medium hover:bg-(--ui-bg-elevated)/60 hover:text-(--ui-text) transition-colors min-w-0 select-none"
467
- >
468
- <UIcon
469
- :name="ancestor.icon"
470
- class="size-4 shrink-0"
471
- />
472
- <span class="truncate">{{ ancestor.label }}</span>
473
- </ULink>
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>
474
635
  </li>
475
636
  </template>
476
637
  </ol>
@@ -603,6 +764,13 @@ function onPlusClick(e, onClick) {
603
764
  />
604
765
  </UDropdownMenu>
605
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
+ />
606
774
  </slot>
607
775
  </UEditor>
608
776
  </div>
@@ -621,5 +789,5 @@ function onPlusClick(e, onClick) {
621
789
  </template>
622
790
 
623
791
  <style scoped>
624
- .aeditor-canvas{display:flex;flex-direction:column;min-height:0}.aeditor-breadcrumb{flex-shrink:0;padding:.75rem 1rem 0}@media (min-width:640px){.aeditor-breadcrumb{padding-left:1.5rem;padding-right:1.5rem}}.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}
625
793
  </style>
@@ -68,13 +68,29 @@ function patchType(type) {
68
68
  if (e) treeMap.set(props.nodeId, { ...e, type, updatedAt: Date.now() });
69
69
  }
70
70
  const activeTab = ref(props.initialTab);
71
+ const userPickedTab = ref(false);
71
72
  watch(() => props.nodeId, (id) => {
72
- if (id) activeTab.value = props.initialTab;
73
+ if (id) {
74
+ userPickedTab.value = false;
75
+ activeTab.value = props.initialTab;
76
+ }
73
77
  });
78
+ function setTab(tab) {
79
+ userPickedTab.value = true;
80
+ activeTab.value = tab;
81
+ }
74
82
  const docTypeDef = computed(() => resolveDocType(currentType.value, registry));
75
83
  const usesPageRenderer = computed(
76
84
  () => docTypeDef.value.key !== "doc" && !!docTypeDef.value.component
77
85
  );
86
+ watch([usesPageRenderer, () => props.nodeId], () => {
87
+ if (userPickedTab.value || props.initialTab !== "editor") return;
88
+ activeTab.value = usesPageRenderer.value ? "pageType" : "editor";
89
+ }, { immediate: true });
90
+ const CANONICAL_TABS = ["pageType", "editor", "chat", "settings"];
91
+ const toggleValue = computed(
92
+ () => CANONICAL_TABS.includes(activeTab.value) ? activeTab.value : null
93
+ );
78
94
  const editorRef = ref(null);
79
95
  const tiptapEditor = computed(() => editorRef.value?.editor ?? null);
80
96
  const resize = useResizableWidth({
@@ -256,9 +272,9 @@ const addFieldMenuItems = computed(
256
272
  />
257
273
  </div>
258
274
 
259
- <!-- Right: undo/redo (editor tab) · icon-only tab buttons -->
275
+ <!-- Right: undo/redo (editor tab) · unified view toggle · extras -->
260
276
  <div class="flex items-center gap-0.5">
261
- <template v-if="activeTab === 'editor' && !usesPageRenderer && tiptapEditor">
277
+ <template v-if="activeTab === 'editor' && tiptapEditor">
262
278
  <AEditorUndoButton :editor="tiptapEditor" />
263
279
  <AEditorRedoButton :editor="tiptapEditor" />
264
280
  <USeparator
@@ -267,103 +283,78 @@ const addFieldMenuItems = computed(
267
283
  />
268
284
  </template>
269
285
 
270
- <UTooltip
271
- v-if="showEditorTab"
272
- :text="locale.editorTab"
273
- :content="{ side: 'bottom' }"
274
- >
275
- <UButton
276
- :icon="usesPageRenderer ? docTypeDef.icon : 'i-lucide-file-text'"
277
- size="xs"
278
- :color="activeTab === 'editor' ? 'primary' : 'neutral'"
279
- :variant="activeTab === 'editor' ? 'soft' : 'ghost'"
280
- @click="activeTab = 'editor'"
281
- />
282
- </UTooltip>
283
- <UTooltip
284
- v-if="showPropertiesTab"
285
- :text="locale.propertiesTab"
286
- :content="{ side: 'bottom' }"
287
- >
288
- <UButton
289
- icon="i-lucide-sliders-horizontal"
290
- size="xs"
291
- :color="activeTab === 'properties' ? 'primary' : 'neutral'"
292
- :variant="activeTab === 'properties' ? 'soft' : 'ghost'"
293
- @click="activeTab = 'properties'"
286
+ <!-- Canonical four-slot toggle: pageType · editor · chat · settings -->
287
+ <ADocViewToggle
288
+ :model-value="toggleValue"
289
+ :doc-id="nodeId || ''"
290
+ :doc-type="currentType"
291
+ :chat-unread="chatUnread"
292
+ :editor-disabled="!showEditorTab"
293
+ :page-type-disabled="!childProvider"
294
+ :show-chat="showChatTab"
295
+ :show-settings="showSettingsTab"
296
+ @update:model-value="setTab"
297
+ />
298
+
299
+ <!-- Module extras kept outside the toggle (cou-sh parity: extras
300
+ sit beside the canonical four, never inside them). -->
301
+ <template v-if="showPropertiesTab || showServerSettingsTab || pluginSlots.length">
302
+ <USeparator
303
+ orientation="vertical"
304
+ class="h-4 mx-0.5"
294
305
  />
295
- </UTooltip>
296
- <UChip
297
- v-if="showChatTab"
298
- :text="chatUnread > 0 && activeTab !== 'chat' ? String(chatUnread) : void 0"
299
- color="error"
300
- size="lg"
301
- :show="chatUnread > 0 && activeTab !== 'chat'"
302
- :inset="true"
303
- >
304
306
  <UTooltip
305
- :text="locale.chatTab"
307
+ v-if="showPropertiesTab"
308
+ :text="locale.propertiesTab"
306
309
  :content="{ side: 'bottom' }"
307
310
  >
308
311
  <UButton
309
- icon="i-lucide-message-circle"
312
+ icon="i-lucide-sliders-horizontal"
310
313
  size="xs"
311
- :color="activeTab === 'chat' ? 'primary' : 'neutral'"
312
- :variant="activeTab === 'chat' ? 'soft' : 'ghost'"
313
- @click="activeTab = 'chat'"
314
+ :color="activeTab === 'properties' ? 'primary' : 'neutral'"
315
+ :variant="activeTab === 'properties' ? 'soft' : 'ghost'"
316
+ @click="setTab('properties')"
314
317
  />
315
318
  </UTooltip>
316
- </UChip>
317
- <UTooltip
318
- v-if="showSettingsTab"
319
- :text="locale.settingsTab"
320
- :content="{ side: 'bottom' }"
321
- >
322
- <UButton
323
- icon="i-lucide-settings-2"
324
- size="xs"
325
- :color="activeTab === 'settings' ? 'primary' : 'neutral'"
326
- :variant="activeTab === 'settings' ? 'soft' : 'ghost'"
327
- @click="activeTab = 'settings'"
328
- />
329
- </UTooltip>
330
- <UTooltip
331
- v-if="showServerSettingsTab"
332
- :text="locale.serverTab"
333
- :content="{ side: 'bottom' }"
334
- >
335
- <UButton
336
- icon="i-lucide-bolt"
337
- size="xs"
338
- :color="activeTab === 'serverSettings' ? 'primary' : 'neutral'"
339
- :variant="activeTab === 'serverSettings' ? 'soft' : 'ghost'"
340
- @click="activeTab = 'serverSettings'"
341
- />
342
- </UTooltip>
343
- <UTooltip
344
- v-for="slot in pluginSlots"
345
- :key="slot.id"
346
- :text="slot.label"
347
- :content="{ side: 'bottom' }"
348
- >
349
- <UButton
350
- :icon="slot.icon"
351
- size="xs"
352
- :color="activeTab === slot.id ? 'primary' : 'neutral'"
353
- :variant="activeTab === slot.id ? 'soft' : 'ghost'"
354
- @click="activeTab = slot.id"
355
- />
356
- </UTooltip>
319
+ <UTooltip
320
+ v-if="showServerSettingsTab"
321
+ :text="locale.serverTab"
322
+ :content="{ side: 'bottom' }"
323
+ >
324
+ <UButton
325
+ icon="i-lucide-bolt"
326
+ size="xs"
327
+ :color="activeTab === 'serverSettings' ? 'primary' : 'neutral'"
328
+ :variant="activeTab === 'serverSettings' ? 'soft' : 'ghost'"
329
+ @click="setTab('serverSettings')"
330
+ />
331
+ </UTooltip>
332
+ <UTooltip
333
+ v-for="slot in pluginSlots"
334
+ :key="slot.id"
335
+ :text="slot.label"
336
+ :content="{ side: 'bottom' }"
337
+ >
338
+ <UButton
339
+ :icon="slot.icon"
340
+ size="xs"
341
+ :color="activeTab === slot.id ? 'primary' : 'neutral'"
342
+ :variant="activeTab === slot.id ? 'soft' : 'ghost'"
343
+ @click="setTab(slot.id)"
344
+ />
345
+ </UTooltip>
346
+ </template>
357
347
  </div>
358
348
  </div>
359
349
  </slot>
360
350
 
361
351
  <!-- Content area -->
362
352
  <div class="flex flex-col flex-1 min-h-0 overflow-hidden">
363
- <!-- Editor tab — swaps to the registered page-type renderer when the
364
- doc has a non-'doc' type (kanban / table / calendar / etc.). -->
353
+ <!-- Page-type tab — the registered renderer for non-'doc' types
354
+ (kanban / table / calendar / ). Now its own slot, so the editor
355
+ tab below always exposes the underlying prose. -->
365
356
  <div
366
- v-show="activeTab === 'editor'"
357
+ v-show="activeTab === 'pageType'"
367
358
  class="flex flex-col flex-1 min-h-0"
368
359
  >
369
360
  <component
@@ -375,8 +366,15 @@ const addFieldMenuItems = computed(
375
366
  :child-provider="childProvider"
376
367
  class="flex-1 overflow-hidden"
377
368
  />
369
+ </div>
370
+
371
+ <!-- Editor tab — always the TipTap prose editor for the doc body. -->
372
+ <div
373
+ v-show="activeTab === 'editor'"
374
+ class="flex flex-col flex-1 min-h-0"
375
+ >
378
376
  <AEditor
379
- v-else-if="nodeId"
377
+ v-if="nodeId"
380
378
  ref="editorRef"
381
379
  :doc-id="nodeId"
382
380
  :doc-label="resolvedLabel"
@@ -0,0 +1,7 @@
1
+ import type { DocSuggestPopupState } from '../../composables/useDocSuggest.js';
2
+ type __VLS_Props = {
3
+ state: DocSuggestPopupState;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -0,0 +1,68 @@
1
+ <script setup>
2
+ import { computed, ref, watch, nextTick } from "vue";
3
+ const props = defineProps({
4
+ state: { type: Object, required: true }
5
+ });
6
+ const listRef = ref(null);
7
+ const style = computed(() => {
8
+ const r = props.state.rect;
9
+ if (!r) return {};
10
+ return {
11
+ left: `${Math.round(r.left)}px`,
12
+ top: `${Math.round(r.bottom + 6)}px`
13
+ };
14
+ });
15
+ watch(() => props.state.index, async () => {
16
+ await nextTick();
17
+ listRef.value?.querySelector('[data-active="true"]')?.scrollIntoView({ block: "nearest" });
18
+ });
19
+ function pick(item) {
20
+ props.state.onSelect?.(item);
21
+ }
22
+ </script>
23
+
24
+ <template>
25
+ <Teleport to="body">
26
+ <div
27
+ v-if="state.active && state.rect && state.items.length"
28
+ ref="listRef"
29
+ class="fixed z-[60] w-72 max-h-72 overflow-y-auto overflow-x-hidden rounded-(--ui-radius) border border-(--ui-border) bg-(--ui-bg) shadow-lg p-1"
30
+ :style="style"
31
+ @mousedown.prevent
32
+ >
33
+ <div class="flex items-center gap-1.5 px-2 py-1 text-[11px] font-medium text-(--ui-text-muted)">
34
+ <UIcon
35
+ :name="state.mode === 'embed' ? 'i-lucide-file-box' : 'i-lucide-file-symlink'"
36
+ class="size-3.5 shrink-0"
37
+ />
38
+ <span>{{ state.mode === "embed" ? "Embed document" : "Link document" }}</span>
39
+ </div>
40
+
41
+ <button
42
+ v-for="(item, i) in state.items"
43
+ :key="item.id + ':' + i"
44
+ type="button"
45
+ :data-active="i === state.index"
46
+ class="w-full min-w-0 flex items-center gap-2 px-2 py-1.5 rounded-(--ui-radius) text-sm text-left text-(--ui-text)"
47
+ :class="i === state.index ? 'bg-(--ui-bg-elevated)' : 'hover:bg-(--ui-bg-elevated)/60'"
48
+ @click="pick(item)"
49
+ >
50
+ <UIcon
51
+ :name="item.icon"
52
+ class="size-4 shrink-0"
53
+ :class="item.isCreate ? 'text-(--ui-primary)' : 'text-(--ui-text-dimmed)'"
54
+ />
55
+ <span class="flex-1 min-w-0 flex items-baseline gap-1.5 overflow-hidden">
56
+ <span
57
+ v-if="item.prefix"
58
+ class="text-xs text-(--ui-text-dimmed) truncate shrink-0 max-w-[45%]"
59
+ >{{ item.prefix }} /</span>
60
+ <span
61
+ class="truncate"
62
+ :class="item.isCreate ? 'text-(--ui-primary)' : ''"
63
+ >{{ item.label }}</span>
64
+ </span>
65
+ </button>
66
+ </div>
67
+ </Teleport>
68
+ </template>
@@ -0,0 +1,7 @@
1
+ import type { DocSuggestPopupState } from '../../composables/useDocSuggest.js';
2
+ type __VLS_Props = {
3
+ state: DocSuggestPopupState;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -3,16 +3,17 @@ export interface DocLinkPickResult {
3
3
  label: string;
4
4
  }
5
5
  /**
6
- * Manages the state for the document picker dialog.
7
- * Used by doc-embed extension and inline link insertion.
6
+ * Imperative command-palette document picker.
8
7
  *
9
- * The module does not ship a modal component the consuming app provides
10
- * one and wires it up to the `pending` state. When the app calls `resolve()`
11
- * or `cancel()`, the promise returned by `pickDoc()` settles.
8
+ * Opens `<ADocPickModal>` (a `UCommandPalette` in a `UModal`) via Nuxt UI's
9
+ * overlay manager and resolves with the chosen doc, or `null` if dismissed.
10
+ * Self-contained — callers (the slash doc-embed/doc-link handlers, the
11
+ * `<ADocLinkPopover>` toolbar button, the drag handle) just `await pickDoc()`;
12
+ * no host wiring required. This is the picker used everywhere EXCEPT the inline
13
+ * `[[` / `![[` typing triggers (those commit directly via `<ADocSuggestMenu>`).
14
+ *
15
+ * Mirrors cou-sh/app/composables/useDocLinkPick.ts.
12
16
  */
13
17
  export declare function useDocLinkPick(): {
14
- isOpen: import("vue").Ref<boolean, boolean>;
15
18
  pickDoc: () => Promise<DocLinkPickResult | null>;
16
- resolve: (result: DocLinkPickResult) => void;
17
- cancel: () => void;
18
19
  };
@@ -1,22 +1,11 @@
1
- import { ref } from "vue";
1
+ import { useOverlay } from "#imports";
2
+ import ADocPickModal from "../components/ADocPickModal.vue";
2
3
  export function useDocLinkPick() {
3
- const isOpen = ref(false);
4
- let _resolve = null;
4
+ const overlay = useOverlay();
5
+ const modal = overlay.create(ADocPickModal);
5
6
  function pickDoc() {
6
- return new Promise((resolve2) => {
7
- _resolve = resolve2;
8
- isOpen.value = true;
9
- });
7
+ const { result } = modal.open();
8
+ return result;
10
9
  }
11
- function resolve(result) {
12
- isOpen.value = false;
13
- _resolve?.(result);
14
- _resolve = null;
15
- }
16
- function cancel() {
17
- isOpen.value = false;
18
- _resolve?.(null);
19
- _resolve = null;
20
- }
21
- return { isOpen, pickDoc, resolve, cancel };
10
+ return { pickDoc };
22
11
  }