@abraca/nuxt 2.10.0 → 2.11.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 (70) hide show
  1. package/dist/module.d.mts +14 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +2 -0
  4. package/dist/runtime/assets/editor.css +1 -1
  5. package/dist/runtime/components/AConnectionBadge.d.vue.ts +29 -0
  6. package/dist/runtime/components/AConnectionBadge.vue +79 -0
  7. package/dist/runtime/components/AConnectionBadge.vue.d.ts +29 -0
  8. package/dist/runtime/components/AEditor.d.vue.ts +2 -2
  9. package/dist/runtime/components/AEditor.vue +11 -1
  10. package/dist/runtime/components/AEditor.vue.d.ts +2 -2
  11. package/dist/runtime/components/AEncryptionModePicker.d.vue.ts +33 -0
  12. package/dist/runtime/components/AEncryptionModePicker.vue +211 -0
  13. package/dist/runtime/components/AEncryptionModePicker.vue.d.ts +33 -0
  14. package/dist/runtime/components/AModalShell.d.vue.ts +48 -0
  15. package/dist/runtime/components/AModalShell.vue +105 -0
  16. package/dist/runtime/components/AModalShell.vue.d.ts +48 -0
  17. package/dist/runtime/components/ANodePanel.d.vue.ts +8 -6
  18. package/dist/runtime/components/ANodePanel.vue +25 -0
  19. package/dist/runtime/components/ANodePanel.vue.d.ts +8 -6
  20. package/dist/runtime/components/ANodePanelHeader.d.vue.ts +20 -10
  21. package/dist/runtime/components/ANodePanelHeader.vue +17 -3
  22. package/dist/runtime/components/ANodePanelHeader.vue.d.ts +20 -10
  23. package/dist/runtime/components/ANodeSettingsPanel.d.vue.ts +2 -0
  24. package/dist/runtime/components/ANodeSettingsPanel.vue +21 -1
  25. package/dist/runtime/components/ANodeSettingsPanel.vue.d.ts +2 -0
  26. package/dist/runtime/components/ASnapshotPreviewModal.d.vue.ts +33 -0
  27. package/dist/runtime/components/ASnapshotPreviewModal.vue +430 -0
  28. package/dist/runtime/components/ASnapshotPreviewModal.vue.d.ts +33 -0
  29. package/dist/runtime/components/docs/ADocsSearch.d.vue.ts +2 -2
  30. package/dist/runtime/components/docs/ADocsSearch.vue.d.ts +2 -2
  31. package/dist/runtime/components/editor/ALocationPickerPopover.vue +28 -7
  32. package/dist/runtime/components/registry/APluginDetail.d.vue.ts +2 -2
  33. package/dist/runtime/components/registry/APluginDetail.vue.d.ts +2 -2
  34. package/dist/runtime/components/renderers/AProseRenderer.d.vue.ts +2 -2
  35. package/dist/runtime/components/renderers/AProseRenderer.vue.d.ts +2 -2
  36. package/dist/runtime/components/shell/ABreadcrumbForDoc.d.vue.ts +6 -0
  37. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue +75 -3
  38. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue.d.ts +6 -0
  39. package/dist/runtime/components/shell/ADocPanelServerSettings.d.vue.ts +17 -0
  40. package/dist/runtime/components/shell/ADocPanelServerSettings.vue +253 -0
  41. package/dist/runtime/components/shell/ADocPanelServerSettings.vue.d.ts +17 -0
  42. package/dist/runtime/components/shell/ADocPanelSettings.d.vue.ts +2 -0
  43. package/dist/runtime/components/shell/ADocPanelSettings.vue +15 -4
  44. package/dist/runtime/components/shell/ADocPanelSettings.vue.d.ts +2 -0
  45. package/dist/runtime/components/shell/AUserMenu.d.vue.ts +2 -2
  46. package/dist/runtime/components/shell/AUserMenu.vue.d.ts +2 -2
  47. package/dist/runtime/composables/useDocBreadcrumb.d.ts +17 -2
  48. package/dist/runtime/composables/useDocBreadcrumb.js +17 -3
  49. package/dist/runtime/composables/useDocSnapshots.d.ts +2 -1
  50. package/dist/runtime/composables/useDocSnapshots.js +5 -0
  51. package/dist/runtime/composables/useEditor.d.ts +1 -1
  52. package/dist/runtime/composables/useEditor.js +120 -0
  53. package/dist/runtime/composables/useEditorToolbar.d.ts +12 -4
  54. package/dist/runtime/composables/useEditorToolbar.js +78 -56
  55. package/dist/runtime/composables/useNodeContextMenu.d.ts +10 -0
  56. package/dist/runtime/composables/useNodeContextMenu.js +41 -1
  57. package/dist/runtime/composables/useSwipeGesture.d.ts +48 -0
  58. package/dist/runtime/composables/useSwipeGesture.js +140 -0
  59. package/dist/runtime/extensions/document-header.js +16 -6
  60. package/dist/runtime/extensions/document-meta.js +344 -19
  61. package/dist/runtime/extensions/meta-field.js +42 -0
  62. package/dist/runtime/extensions/views/DocumentMetaView.vue +33 -7
  63. package/dist/runtime/extensions/views/FieldView.vue +51 -19
  64. package/dist/runtime/extensions/views/MetaFieldView.vue +30 -4
  65. package/dist/runtime/middleware/abracadabra-auth.d.ts +1 -1
  66. package/dist/runtime/plugin-abracadabra.client.d.ts +1 -1
  67. package/dist/runtime/plugin-abracadabra.client.js +12 -2
  68. package/dist/runtime/plugin-abracadabra.server.d.ts +1 -1
  69. package/dist/runtime/plugin-shared-globals.client.d.ts +1 -1
  70. package/package.json +1 -4
@@ -1,15 +1,87 @@
1
1
  <script setup>
2
+ import { computed } from "vue";
2
3
  import { useDocBreadcrumb } from "../../composables/useDocBreadcrumb";
3
4
  const props = defineProps({
4
5
  docId: { type: null, required: true },
5
- maxDepth: { type: Number, required: false, default: 8 }
6
+ maxDepth: { type: Number, required: false, default: 8 },
7
+ maxVisible: { type: Number, required: false, default: 0 }
6
8
  });
7
- const { items } = useDocBreadcrumb(() => props.docId, { maxDepth: props.maxDepth });
9
+ const { items, collapsed } = useDocBreadcrumb(() => props.docId, {
10
+ maxDepth: props.maxDepth,
11
+ maxVisible: props.maxVisible
12
+ });
13
+ const overflowItems = computed(
14
+ () => collapsed.value.hidden.map((a) => ({ label: a.label, icon: a.icon, to: a.to }))
15
+ );
8
16
  </script>
9
17
 
10
18
  <template>
19
+ <!-- Collapsed single-line variant (overflow menu for the hidden middle) -->
20
+ <nav
21
+ v-if="collapsed.overflowed"
22
+ aria-label="Breadcrumb"
23
+ class="flex items-center gap-0.5 min-w-0 text-(--ui-text-muted)"
24
+ >
25
+ <template
26
+ v-for="ancestor in collapsed.head"
27
+ :key="ancestor.id"
28
+ >
29
+ <ULink
30
+ :to="ancestor.to"
31
+ class="flex items-center gap-1 px-1.5 py-0.5 rounded text-xs hover:bg-(--ui-bg-elevated)/60 hover:text-(--ui-text) transition-colors min-w-0"
32
+ >
33
+ <UIcon
34
+ :name="ancestor.icon"
35
+ class="size-3.5 shrink-0"
36
+ />
37
+ <span class="truncate max-w-[10ch]">{{ ancestor.label }}</span>
38
+ </ULink>
39
+ <UIcon
40
+ name="i-lucide-chevron-right"
41
+ class="size-3.5 shrink-0 opacity-60"
42
+ />
43
+ </template>
44
+
45
+ <UDropdownMenu :items="overflowItems">
46
+ <button
47
+ type="button"
48
+ class="flex items-center px-1 py-0.5 rounded text-xs hover:bg-(--ui-bg-elevated)/60 hover:text-(--ui-text) transition-colors"
49
+ :aria-label="`Show ${collapsed.hidden.length} hidden`"
50
+ >
51
+
52
+ </button>
53
+ </UDropdownMenu>
54
+ <UIcon
55
+ name="i-lucide-chevron-right"
56
+ class="size-3.5 shrink-0 opacity-60"
57
+ />
58
+
59
+ <template
60
+ v-for="(ancestor, idx) in collapsed.tail"
61
+ :key="ancestor.id"
62
+ >
63
+ <ULink
64
+ :to="ancestor.to"
65
+ class="flex items-center gap-1 px-1.5 py-0.5 rounded text-xs hover:bg-(--ui-bg-elevated)/60 hover:text-(--ui-text) transition-colors min-w-0"
66
+ :class="ancestor.to ? '' : 'pointer-events-none text-(--ui-text)'"
67
+ >
68
+ <UIcon
69
+ :name="ancestor.icon"
70
+ class="size-3.5 shrink-0"
71
+ />
72
+ <span class="truncate max-w-[10ch]">{{ ancestor.label }}</span>
73
+ </ULink>
74
+ <UIcon
75
+ v-if="idx < collapsed.tail.length - 1"
76
+ name="i-lucide-chevron-right"
77
+ class="size-3.5 shrink-0 opacity-60"
78
+ />
79
+ </template>
80
+ </nav>
81
+
82
+ <!-- Full-trail variant -->
11
83
  <UBreadcrumb
12
- v-if="items.length > 0"
84
+ v-else-if="items.length > 0"
13
85
  :items="items"
14
86
  :ui="{ root: 'min-w-0', list: 'min-w-0 flex-nowrap', item: 'truncate', link: 'truncate' }"
15
87
  />
@@ -3,9 +3,15 @@ type __VLS_Props = {
3
3
  docId: string | null | undefined;
4
4
  /** Maximum ancestors to walk (default 8) */
5
5
  maxDepth?: number;
6
+ /**
7
+ * Collapse the middle into a "…" overflow menu once the trail exceeds this
8
+ * many crumbs. 0 (default) renders the full trail with no collapsing.
9
+ */
10
+ maxVisible?: number;
6
11
  };
7
12
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
8
13
  maxDepth: number;
14
+ maxVisible: number;
9
15
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
16
  declare const _default: typeof __VLS_export;
11
17
  export default _default;
@@ -0,0 +1,17 @@
1
+ type __VLS_Props = {
2
+ /** Hub document id (informational — the panel reflects the active server). */
3
+ docId?: string;
4
+ };
5
+ declare var __VLS_96: "storage" | "users" | "members" | "invites", __VLS_97: {};
6
+ type __VLS_Slots = {} & {
7
+ [K in NonNullable<typeof __VLS_96>]?: (props: typeof __VLS_97) => any;
8
+ };
9
+ declare const __VLS_base: 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>;
10
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
11
+ declare const _default: typeof __VLS_export;
12
+ export default _default;
13
+ type __VLS_WithSlots<T, S> = T & {
14
+ new (): {
15
+ $slots: S;
16
+ };
17
+ };
@@ -0,0 +1,253 @@
1
+ <script setup>
2
+ import { computed, onMounted, reactive, ref, useSlots, watch } from "vue";
3
+ import { useAbracadabra } from "../../composables/useAbracadabra";
4
+ defineProps({
5
+ docId: { type: String, required: false }
6
+ });
7
+ const slots = useSlots();
8
+ const { client, status, synced, effectiveRole, currentServerUrl, reconnect } = useAbracadabra();
9
+ const ROLE_LEVELS = {
10
+ observer: 0,
11
+ viewer: 1,
12
+ editor: 2,
13
+ owner: 3,
14
+ admin: 4,
15
+ service: 5
16
+ };
17
+ const roleLevel = computed(() => ROLE_LEVELS[effectiveRole.value ?? ""] ?? 0);
18
+ const canEdit = computed(() => roleLevel.value >= 2);
19
+ const isAdmin = computed(() => roleLevel.value >= 4);
20
+ const roleBadgeColor = computed(() => {
21
+ const r = effectiveRole.value;
22
+ if (r === "admin" || r === "service") return "warning";
23
+ if (r === "owner") return "primary";
24
+ if (r === "editor") return "success";
25
+ return "neutral";
26
+ });
27
+ const statusDotClass = computed(() => {
28
+ if (status.value === "connected") return "bg-(--ui-success)";
29
+ if (status.value === "connecting") return "bg-(--ui-warning)";
30
+ return "bg-(--ui-error)";
31
+ });
32
+ const serverInfo = ref(null);
33
+ async function loadServerInfo() {
34
+ if (!client.value) return;
35
+ try {
36
+ serverInfo.value = await client.value.serverInfo();
37
+ } catch {
38
+ }
39
+ }
40
+ watch(client, loadServerInfo, { immediate: true });
41
+ const healthData = ref(null);
42
+ const healthLoading = ref(false);
43
+ async function checkHealth() {
44
+ if (!client.value) return;
45
+ healthLoading.value = true;
46
+ try {
47
+ healthData.value = await client.value.health();
48
+ } catch {
49
+ } finally {
50
+ healthLoading.value = false;
51
+ }
52
+ }
53
+ const expanded = reactive({ members: false, users: false, invites: false, storage: false });
54
+ const adminSections = computed(() => [
55
+ { key: "members", label: "Members", icon: "i-lucide-users", show: canEdit.value && !!slots.members },
56
+ { key: "users", label: "Users", icon: "i-lucide-shield-alert", show: isAdmin.value && !!slots.users },
57
+ { key: "invites", label: "Invites", icon: "i-lucide-ticket", show: isAdmin.value && !!slots.invites },
58
+ { key: "storage", label: "Storage", icon: "i-lucide-hard-drive", show: isAdmin.value && !!slots.storage }
59
+ ].filter((s) => s.show));
60
+ onMounted(loadServerInfo);
61
+ </script>
62
+
63
+ <template>
64
+ <div class="flex-1 overflow-y-auto px-4 py-3 space-y-3">
65
+ <!-- Connection — read-only URL (single-server) -->
66
+ <div class="rounded-lg bg-(--ui-bg-elevated) overflow-hidden">
67
+ <div class="flex items-center gap-1.5 px-3 py-2 border-b border-(--ui-border)">
68
+ <UIcon
69
+ name="i-lucide-plug"
70
+ class="size-3.5 text-(--ui-text-muted) shrink-0"
71
+ />
72
+ <span class="text-xs font-semibold uppercase tracking-wider text-(--ui-text-muted)">Connection</span>
73
+ </div>
74
+ <div class="px-3 py-3 space-y-1.5">
75
+ <div class="flex items-center gap-2">
76
+ <UIcon
77
+ name="i-lucide-server"
78
+ class="size-3.5 text-(--ui-text-dimmed) shrink-0"
79
+ />
80
+ <span class="text-sm truncate flex-1 min-w-0 text-(--ui-text)">{{ currentServerUrl || "\u2014" }}</span>
81
+ <UButton
82
+ v-if="status !== 'connected'"
83
+ icon="i-lucide-refresh-cw"
84
+ size="xs"
85
+ variant="ghost"
86
+ color="primary"
87
+ @click="reconnect"
88
+ />
89
+ </div>
90
+ </div>
91
+ </div>
92
+
93
+ <!-- Status -->
94
+ <div class="rounded-lg bg-(--ui-bg-elevated) overflow-hidden">
95
+ <div class="flex items-center gap-1.5 px-3 py-2 border-b border-(--ui-border)">
96
+ <UIcon
97
+ name="i-lucide-info"
98
+ class="size-3.5 text-(--ui-text-muted) shrink-0"
99
+ />
100
+ <span class="text-xs font-semibold uppercase tracking-wider text-(--ui-text-muted)">Status</span>
101
+ </div>
102
+ <div class="divide-y divide-(--ui-border)">
103
+ <div class="flex items-center justify-between gap-2 px-3 py-2">
104
+ <div class="flex items-center gap-2 text-sm text-(--ui-text-muted) min-w-0">
105
+ <UIcon
106
+ name="i-lucide-activity"
107
+ class="size-3.5 shrink-0"
108
+ />
109
+ <span class="truncate">Connection</span>
110
+ </div>
111
+ <div class="flex items-center gap-1.5 shrink-0">
112
+ <span :class="['size-1.5 rounded-full', statusDotClass]" />
113
+ <span class="text-sm capitalize">{{ status }}</span>
114
+ </div>
115
+ </div>
116
+ <div class="flex items-center justify-between gap-2 px-3 py-2">
117
+ <div class="flex items-center gap-2 text-sm text-(--ui-text-muted) min-w-0">
118
+ <UIcon
119
+ name="i-lucide-shield"
120
+ class="size-3.5 shrink-0"
121
+ />
122
+ <span class="truncate">Your role</span>
123
+ </div>
124
+ <UBadge
125
+ v-if="effectiveRole"
126
+ :color="roleBadgeColor"
127
+ variant="subtle"
128
+ :label="effectiveRole"
129
+ size="sm"
130
+ class="capitalize shrink-0"
131
+ />
132
+ <span
133
+ v-else
134
+ class="text-sm text-(--ui-text-dimmed)"
135
+ >—</span>
136
+ </div>
137
+ <div class="flex items-center justify-between gap-2 px-3 py-2">
138
+ <div class="flex items-center gap-2 text-sm text-(--ui-text-muted) min-w-0">
139
+ <UIcon
140
+ name="i-lucide-refresh-ccw"
141
+ class="size-3.5 shrink-0"
142
+ />
143
+ <span class="truncate">Synced</span>
144
+ </div>
145
+ <UBadge
146
+ :color="synced ? 'success' : 'neutral'"
147
+ variant="subtle"
148
+ :label="synced ? 'Yes' : 'No'"
149
+ :icon="synced ? 'i-lucide-check' : 'i-lucide-circle'"
150
+ size="sm"
151
+ class="shrink-0"
152
+ />
153
+ </div>
154
+ <div
155
+ v-if="serverInfo?.version"
156
+ class="flex items-center justify-between gap-2 px-3 py-2"
157
+ >
158
+ <div class="flex items-center gap-2 text-sm text-(--ui-text-muted) min-w-0">
159
+ <UIcon
160
+ name="i-lucide-tag"
161
+ class="size-3.5 shrink-0"
162
+ />
163
+ <span class="truncate">Version</span>
164
+ </div>
165
+ <div class="flex items-center gap-1 shrink-0">
166
+ <UBadge
167
+ color="neutral"
168
+ variant="subtle"
169
+ :label="`v${serverInfo.version}`"
170
+ size="sm"
171
+ />
172
+ <UBadge
173
+ v-if="serverInfo.protocol_version"
174
+ color="neutral"
175
+ variant="outline"
176
+ :label="`p${serverInfo.protocol_version}`"
177
+ size="sm"
178
+ />
179
+ </div>
180
+ </div>
181
+ <div
182
+ v-if="serverInfo?.invite_only !== void 0"
183
+ class="flex items-center justify-between gap-2 px-3 py-2"
184
+ >
185
+ <div class="flex items-center gap-2 text-sm text-(--ui-text-muted) min-w-0">
186
+ <UIcon
187
+ :name="serverInfo.invite_only ? 'i-lucide-lock' : 'i-lucide-globe'"
188
+ class="size-3.5 shrink-0"
189
+ />
190
+ <span class="truncate">Invite only</span>
191
+ </div>
192
+ <UBadge
193
+ :color="serverInfo.invite_only ? 'warning' : 'success'"
194
+ variant="subtle"
195
+ :label="serverInfo.invite_only ? 'Yes' : 'No'"
196
+ size="sm"
197
+ class="shrink-0"
198
+ />
199
+ </div>
200
+ <div class="flex items-center justify-between gap-2 px-3 py-2">
201
+ <div class="flex items-center gap-2 text-sm text-(--ui-text-muted) min-w-0">
202
+ <UIcon
203
+ name="i-lucide-heart-pulse"
204
+ class="size-3.5 shrink-0"
205
+ />
206
+ <span class="truncate">Health</span>
207
+ </div>
208
+ <UButton
209
+ :label="healthData ? `v${healthData.version}` : 'Check'"
210
+ color="neutral"
211
+ variant="ghost"
212
+ size="xs"
213
+ class="shrink-0"
214
+ :loading="healthLoading"
215
+ @click="checkHealth"
216
+ />
217
+ </div>
218
+ </div>
219
+ </div>
220
+
221
+ <!-- Admin surfaces — opt-in via slots, gated by role -->
222
+ <div
223
+ v-for="section in adminSections"
224
+ :key="section.key"
225
+ class="rounded-lg bg-(--ui-bg-elevated) overflow-hidden"
226
+ >
227
+ <button
228
+ type="button"
229
+ class="w-full flex items-center gap-1.5 px-3 py-2 border-b border-(--ui-border) hover:bg-(--ui-bg-accented)/40 transition-colors"
230
+ :class="{ 'border-b-0': !expanded[section.key] }"
231
+ @click="expanded[section.key] = !expanded[section.key]"
232
+ >
233
+ <UIcon
234
+ :name="section.icon"
235
+ class="size-3.5 text-(--ui-text-muted) shrink-0"
236
+ />
237
+ <span class="text-xs font-semibold uppercase tracking-wider text-(--ui-text-muted) flex-1 text-left">
238
+ {{ section.label }}
239
+ </span>
240
+ <UIcon
241
+ :name="expanded[section.key] ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
242
+ class="size-3.5 text-(--ui-text-dimmed)"
243
+ />
244
+ </button>
245
+ <div
246
+ v-if="expanded[section.key]"
247
+ class="px-3 py-3"
248
+ >
249
+ <slot :name="section.key" />
250
+ </div>
251
+ </div>
252
+ </div>
253
+ </template>
@@ -0,0 +1,17 @@
1
+ type __VLS_Props = {
2
+ /** Hub document id (informational — the panel reflects the active server). */
3
+ docId?: string;
4
+ };
5
+ declare var __VLS_96: "storage" | "users" | "members" | "invites", __VLS_97: {};
6
+ type __VLS_Slots = {} & {
7
+ [K in NonNullable<typeof __VLS_96>]?: (props: typeof __VLS_97) => any;
8
+ };
9
+ declare const __VLS_base: 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>;
10
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
11
+ declare const _default: typeof __VLS_export;
12
+ export default _default;
13
+ type __VLS_WithSlots<T, S> = T & {
14
+ new (): {
15
+ $slots: S;
16
+ };
17
+ };
@@ -87,6 +87,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
87
87
  "delete-snapshot": (version: number) => any;
88
88
  "restore-snapshot": (version: number) => any;
89
89
  "fork-snapshot": (version: number) => any;
90
+ "preview-snapshot": (version: number) => any;
90
91
  "load-more-snapshots": () => any;
91
92
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
92
93
  "onUpdate-meta"?: ((patch: Partial<DocPageMeta>) => any) | undefined;
@@ -105,6 +106,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
105
106
  "onDelete-snapshot"?: ((version: number) => any) | undefined;
106
107
  "onRestore-snapshot"?: ((version: number) => any) | undefined;
107
108
  "onFork-snapshot"?: ((version: number) => any) | undefined;
109
+ "onPreview-snapshot"?: ((version: number) => any) | undefined;
108
110
  "onLoad-more-snapshots"?: (() => any) | undefined;
109
111
  }>, {
110
112
  snapshots: SnapshotMeta[];
@@ -28,7 +28,7 @@ const props = defineProps({
28
28
  pendingSnapshotVersion: { type: [Number, null], required: false, default: null },
29
29
  creatingSnapshot: { type: Boolean, required: false, default: false }
30
30
  });
31
- const emit = defineEmits(["grant-permission", "change-role", "revoke-permission", "create-invite", "sync-doc", "open-encryption", "user-context-menu", "update-meta", "create-snapshot", "delete-snapshot", "restore-snapshot", "fork-snapshot", "load-more-snapshots"]);
31
+ const emit = defineEmits(["grant-permission", "change-role", "revoke-permission", "create-invite", "sync-doc", "open-encryption", "user-context-menu", "update-meta", "create-snapshot", "delete-snapshot", "restore-snapshot", "fork-snapshot", "preview-snapshot", "load-more-snapshots"]);
32
32
  const grantUserId = ref("");
33
33
  const grantRole = ref("editor");
34
34
  const showManualKeyInput = ref(false);
@@ -114,6 +114,13 @@ function runSnapConfirm() {
114
114
  else emit("restore-snapshot", c.version);
115
115
  }
116
116
  function snapshotMenu(version) {
117
+ const view = [
118
+ {
119
+ label: "View / compare",
120
+ icon: "i-lucide-eye",
121
+ onSelect: () => emit("preview-snapshot", version)
122
+ }
123
+ ];
117
124
  const manage = [
118
125
  {
119
126
  label: "Restore",
@@ -134,7 +141,7 @@ function snapshotMenu(version) {
134
141
  onSelect: () => askSnapDelete(version)
135
142
  }
136
143
  ];
137
- return props.isOwner ? [manage, destructive] : [manage];
144
+ return props.isOwner ? [view, manage, destructive] : [view, manage];
138
145
  }
139
146
  function triggerColor(trigger) {
140
147
  switch (trigger) {
@@ -728,7 +735,11 @@ function patchMeta(key, value) {
728
735
  :key="snap.version"
729
736
  class="flex items-center gap-2.5 p-3 rounded-lg bg-(--ui-bg-elevated)"
730
737
  >
731
- <div class="flex-1 min-w-0">
738
+ <button
739
+ type="button"
740
+ class="flex-1 min-w-0 text-left cursor-pointer"
741
+ @click="emit('preview-snapshot', snap.version)"
742
+ >
732
743
  <div class="flex items-center gap-2">
733
744
  <span class="text-sm font-medium text-(--ui-text-highlighted) tabular-nums">
734
745
  v{{ snap.version }}
@@ -749,7 +760,7 @@ function patchMeta(key, value) {
749
760
  <p class="text-xs text-(--ui-text-dimmed) mt-0.5">
750
761
  {{ formatSnapTime(snap.created_at) }} · {{ formatSnapSize(snap.size_bytes) }}
751
762
  </p>
752
- </div>
763
+ </button>
753
764
  <UDropdownMenu
754
765
  :items="snapshotMenu(snap.version)"
755
766
  :content="{ align: 'end' }"
@@ -87,6 +87,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
87
87
  "delete-snapshot": (version: number) => any;
88
88
  "restore-snapshot": (version: number) => any;
89
89
  "fork-snapshot": (version: number) => any;
90
+ "preview-snapshot": (version: number) => any;
90
91
  "load-more-snapshots": () => any;
91
92
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
92
93
  "onUpdate-meta"?: ((patch: Partial<DocPageMeta>) => any) | undefined;
@@ -105,6 +106,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
105
106
  "onDelete-snapshot"?: ((version: number) => any) | undefined;
106
107
  "onRestore-snapshot"?: ((version: number) => any) | undefined;
107
108
  "onFork-snapshot"?: ((version: number) => any) | undefined;
109
+ "onPreview-snapshot"?: ((version: number) => any) | undefined;
108
110
  "onLoad-more-snapshots"?: (() => any) | undefined;
109
111
  }>, {
110
112
  snapshots: SnapshotMeta[];
@@ -14,13 +14,13 @@ type __VLS_Props = {
14
14
  extraItems?: DropdownMenuItem[][];
15
15
  };
16
16
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
17
+ logout: () => any;
17
18
  "open-settings": () => any;
18
19
  "open-account": () => any;
19
- logout: () => any;
20
20
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
21
+ onLogout?: (() => any) | undefined;
21
22
  "onOpen-settings"?: (() => any) | undefined;
22
23
  "onOpen-account"?: (() => any) | undefined;
23
- onLogout?: (() => any) | undefined;
24
24
  }>, {
25
25
  color: string;
26
26
  collapsed: boolean;
@@ -14,13 +14,13 @@ type __VLS_Props = {
14
14
  extraItems?: DropdownMenuItem[][];
15
15
  };
16
16
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
17
+ logout: () => any;
17
18
  "open-settings": () => any;
18
19
  "open-account": () => any;
19
- logout: () => any;
20
20
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
21
+ onLogout?: (() => any) | undefined;
21
22
  "onOpen-settings"?: (() => any) | undefined;
22
23
  "onOpen-account"?: (() => any) | undefined;
23
- onLogout?: (() => any) | undefined;
24
24
  }>, {
25
25
  color: string;
26
26
  collapsed: boolean;
@@ -6,7 +6,7 @@
6
6
  * Caps depth at `maxDepth` (default 8) to handle malformed cycles
7
7
  * defensively. The last item is the current doc and has no `to`.
8
8
  */
9
- import { type ComputedRef, type MaybeRef } from 'vue';
9
+ import { type ComputedRef, type MaybeRefOrGetter } from 'vue';
10
10
  export interface DocBreadcrumbItem {
11
11
  id: string;
12
12
  label: string;
@@ -14,8 +14,23 @@ export interface DocBreadcrumbItem {
14
14
  /** Navigation target — undefined for the current doc */
15
15
  to?: string;
16
16
  }
17
- export declare function useDocBreadcrumb(docId: MaybeRef<string | null | undefined>, options?: {
17
+ /**
18
+ * The trail split for single-line rendering: when the full trail is longer
19
+ * than `maxVisible`, the middle collapses into `hidden` (shown behind a "…"
20
+ * overflow menu), keeping the first crumb (`head`) and the last
21
+ * `maxVisible - 2` crumbs (`tail`). `overflowed` is false when no collapse is
22
+ * needed — then `head` holds the whole trail and `hidden`/`tail` are empty.
23
+ */
24
+ export interface DocBreadcrumbCollapsed {
25
+ head: DocBreadcrumbItem[];
26
+ hidden: DocBreadcrumbItem[];
27
+ tail: DocBreadcrumbItem[];
28
+ overflowed: boolean;
29
+ }
30
+ export declare function useDocBreadcrumb(docId: MaybeRefOrGetter<string | null | undefined>, options?: {
18
31
  maxDepth?: number;
32
+ maxVisible?: number;
19
33
  }): {
20
34
  items: ComputedRef<DocBreadcrumbItem[]>;
35
+ collapsed: ComputedRef<DocBreadcrumbCollapsed>;
21
36
  };
@@ -1,14 +1,15 @@
1
- import { computed, unref } from "vue";
1
+ import { computed, toValue } from "vue";
2
2
  import { resolveDocType } from "../utils/docTypes.js";
3
3
  import { useRuntimeConfig } from "#imports";
4
4
  import { useDocTree } from "./useDocTree.js";
5
5
  export function useDocBreadcrumb(docId, options = {}) {
6
6
  const maxDepth = options.maxDepth ?? 8;
7
+ const maxVisible = options.maxVisible ?? 0;
7
8
  const config = useRuntimeConfig();
8
9
  const docBasePath = config.public?.abracadabra?.docBasePath ?? "/doc";
9
10
  const tree = useDocTree();
10
11
  const items = computed(() => {
11
- const startId = unref(docId);
12
+ const startId = toValue(docId);
12
13
  if (!startId) return [];
13
14
  const trail = [];
14
15
  let id = startId;
@@ -29,5 +30,18 @@ export function useDocBreadcrumb(docId, options = {}) {
29
30
  }
30
31
  return trail;
31
32
  });
32
- return { items };
33
+ const collapsed = computed(() => {
34
+ const all = items.value;
35
+ if (maxVisible <= 0 || all.length <= maxVisible) {
36
+ return { head: all, hidden: [], tail: [], overflowed: false };
37
+ }
38
+ const tailCount = Math.max(1, maxVisible - 2);
39
+ return {
40
+ head: all.slice(0, 1),
41
+ hidden: all.slice(1, all.length - tailCount),
42
+ tail: all.slice(all.length - tailCount),
43
+ overflowed: true
44
+ };
45
+ });
46
+ return { items, collapsed };
33
47
  }
@@ -19,7 +19,7 @@
19
19
  * onMounted(() => snaps.fetchList())
20
20
  */
21
21
  import { type ComputedRef, type InjectionKey, type Ref } from 'vue';
22
- import type { SnapshotMeta } from '@abraca/dabra';
22
+ import type { SnapshotData, SnapshotMeta } from '@abraca/dabra';
23
23
  import type { AbracadabraLocale } from '../locale.js';
24
24
  export type DocSnapshotsCtx = ReturnType<typeof useDocSnapshots>;
25
25
  export declare const DOC_SNAPSHOTS_KEY: InjectionKey<DocSnapshotsCtx>;
@@ -72,6 +72,7 @@ export declare function useDocSnapshots(docId: Ref<string> | ComputedRef<string>
72
72
  pending: Ref<Record<number, "delete" | "restore" | "fork" | undefined>, Record<number, "delete" | "restore" | "fork" | undefined>>;
73
73
  fetchList: () => Promise<void>;
74
74
  loadMore: () => Promise<void>;
75
+ getSnapshot: (version: number) => Promise<SnapshotData | null>;
75
76
  create: (label?: string) => Promise<void>;
76
77
  remove: (version: number) => Promise<void>;
77
78
  restore: (version: number) => Promise<void>;
@@ -120,6 +120,10 @@ export function useDocSnapshots(docId, overrides) {
120
120
  loading.value = false;
121
121
  }
122
122
  }
123
+ async function getSnapshot(version) {
124
+ if (!client.value || !docId.value) return null;
125
+ return client.value.getSnapshot(docId.value, version, { include: "files" });
126
+ }
123
127
  async function create(label) {
124
128
  if (!client.value || !docId.value) return;
125
129
  creating.value = true;
@@ -226,6 +230,7 @@ export function useDocSnapshots(docId, overrides) {
226
230
  // Actions
227
231
  fetchList,
228
232
  loadMore,
233
+ getSnapshot,
229
234
  create,
230
235
  remove,
231
236
  restore,
@@ -15,7 +15,7 @@
15
15
  * Ported from cou-sh/app/composables/useEditorCollaboration.ts.
16
16
  */
17
17
  import { type Ref, type ShallowRef } from 'vue';
18
- import type { Extensions } from '@tiptap/core';
18
+ import { type Extensions } from '@tiptap/core';
19
19
  import type { CollaborationUser } from '../types.js';
20
20
  export { type CollaborationUser } from '../types.js';
21
21
  export interface UseEditorOptions {