@abraca/nuxt 2.9.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
@@ -0,0 +1,430 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from "vue";
3
+ import * as Y from "yjs";
4
+ import { yjsToMarkdown, yjsToHtml } from "../utils/yjsConvert";
5
+ import { triggerBadgeColor } from "../composables/useDocSnapshots";
6
+ import AModalShell from "./AModalShell.vue";
7
+ const props = defineProps({
8
+ version: { type: [Number, null], required: true },
9
+ snapshots: { type: Array, required: false, default: () => [] },
10
+ getSnapshot: { type: Function, required: true },
11
+ docLabel: { type: String, required: false },
12
+ docType: { type: String, required: false },
13
+ docMeta: { type: [Object, null], required: false },
14
+ isOwner: { type: Boolean, required: false }
15
+ });
16
+ const emit = defineEmits(["update:version", "restore", "fork", "delete"]);
17
+ const open = computed({
18
+ get: () => props.version != null,
19
+ set: (v) => {
20
+ if (!v) emit("update:version", null);
21
+ }
22
+ });
23
+ const tab = ref("preview");
24
+ const loading = ref(false);
25
+ const error = ref(null);
26
+ const snap = ref(null);
27
+ const mdCache = /* @__PURE__ */ new Map();
28
+ const previewHtml = ref("");
29
+ const selectedMd = ref("");
30
+ function b64ToBytes(b64) {
31
+ const norm = b64.replace(/-/g, "+").replace(/_/g, "/");
32
+ const bin = atob(norm);
33
+ const bytes = new Uint8Array(bin.length);
34
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
35
+ return bytes;
36
+ }
37
+ function renderSnapshot(data) {
38
+ const ydoc = new Y.Doc();
39
+ try {
40
+ Y.applyUpdate(ydoc, b64ToBytes(data));
41
+ const fragment = ydoc.getXmlFragment("default");
42
+ const label = props.docLabel || "Document";
43
+ return {
44
+ html: yjsToHtml(fragment, label),
45
+ md: yjsToMarkdown(fragment, label, props.docMeta ?? void 0, props.docType)
46
+ };
47
+ } finally {
48
+ ydoc.destroy();
49
+ }
50
+ }
51
+ const compareVersions = computed(() => {
52
+ const v = props.version;
53
+ if (v == null) return [];
54
+ return props.snapshots.map((s) => s.version).filter((x) => x !== v).sort((a, b) => b - a);
55
+ });
56
+ const cmpVersion = ref(null);
57
+ const cmpMd = ref("");
58
+ function pickDefaultCompare() {
59
+ const v = props.version;
60
+ if (v == null) {
61
+ cmpVersion.value = null;
62
+ return;
63
+ }
64
+ const older = compareVersions.value.filter((x) => x < v);
65
+ cmpVersion.value = older.length ? older[0] : null;
66
+ }
67
+ const cmpIndex = computed(() => {
68
+ const list = compareVersions.value;
69
+ return cmpVersion.value == null ? -1 : list.indexOf(cmpVersion.value);
70
+ });
71
+ function stepCompare(delta) {
72
+ const list = compareVersions.value;
73
+ if (!list.length) return;
74
+ const i = cmpIndex.value < 0 ? 0 : cmpIndex.value + delta;
75
+ if (i < 0 || i >= list.length) return;
76
+ cmpVersion.value = list[i];
77
+ }
78
+ async function mdFor(version) {
79
+ const cached = mdCache.get(version);
80
+ if (cached != null) return cached;
81
+ const s = await props.getSnapshot(version);
82
+ const md = s?.data ? renderSnapshot(s.data).md : "";
83
+ mdCache.set(version, md);
84
+ return md;
85
+ }
86
+ const DIFF_LINE_CAP = 2e3;
87
+ const DIFF_CELL_CAP = 2e6;
88
+ function lineDiff(oldStr, newStr) {
89
+ const a = oldStr.length ? oldStr.split("\n") : [];
90
+ const b = newStr.length ? newStr.split("\n") : [];
91
+ const n = a.length;
92
+ const m = b.length;
93
+ const dp = Array.from({ length: n + 1 }, () => new Int32Array(m + 1));
94
+ for (let i2 = n - 1; i2 >= 0; i2--) {
95
+ for (let j2 = m - 1; j2 >= 0; j2--) {
96
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
97
+ }
98
+ }
99
+ const rows = [];
100
+ let i = 0;
101
+ let j = 0;
102
+ while (i < n && j < m) {
103
+ if (a[i] === b[j]) {
104
+ rows.push({ t: "ctx", text: a[i] });
105
+ i++;
106
+ j++;
107
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
108
+ rows.push({ t: "del", text: a[i] });
109
+ i++;
110
+ } else {
111
+ rows.push({ t: "add", text: b[j] });
112
+ j++;
113
+ }
114
+ }
115
+ while (i < n) rows.push({ t: "del", text: a[i++] });
116
+ while (j < m) rows.push({ t: "add", text: b[j++] });
117
+ return rows;
118
+ }
119
+ const diffTooLarge = computed(() => {
120
+ const a = cmpMd.value ? cmpMd.value.split("\n").length : 0;
121
+ const b = selectedMd.value ? selectedMd.value.split("\n").length : 0;
122
+ return a > DIFF_LINE_CAP || b > DIFF_LINE_CAP || a * b > DIFF_CELL_CAP;
123
+ });
124
+ const diffRows = computed(() => {
125
+ if (diffTooLarge.value) return [];
126
+ return lineDiff(cmpMd.value, selectedMd.value);
127
+ });
128
+ const diffHasChanges = computed(() => diffRows.value.some((r) => r.t !== "ctx"));
129
+ async function load() {
130
+ const v = props.version;
131
+ if (v == null) return;
132
+ loading.value = true;
133
+ error.value = null;
134
+ snap.value = null;
135
+ previewHtml.value = "";
136
+ selectedMd.value = "";
137
+ cmpMd.value = "";
138
+ tab.value = "preview";
139
+ try {
140
+ const s = await props.getSnapshot(v);
141
+ if (!s) throw new Error("Snapshot not found");
142
+ snap.value = s;
143
+ const { html, md } = s.data ? renderSnapshot(s.data) : { html: "", md: "" };
144
+ previewHtml.value = html;
145
+ selectedMd.value = md;
146
+ mdCache.set(v, md);
147
+ pickDefaultCompare();
148
+ } catch (e) {
149
+ error.value = e instanceof Error ? e.message : String(e);
150
+ } finally {
151
+ loading.value = false;
152
+ }
153
+ }
154
+ watch(cmpVersion, async (v) => {
155
+ cmpMd.value = v == null ? "" : await mdFor(v);
156
+ });
157
+ watch(() => props.version, (v) => {
158
+ mdCache.clear();
159
+ if (v != null) load();
160
+ }, { immediate: true });
161
+ function authorName(publicKey) {
162
+ if (!publicKey) return "Unknown";
163
+ return publicKey.length > 12 ? `${publicKey.slice(0, 8)}\u2026${publicKey.slice(-4)}` : publicKey;
164
+ }
165
+ const absFmt = new Intl.DateTimeFormat(void 0, { dateStyle: "medium", timeStyle: "short" });
166
+ const relFmt = typeof Intl.RelativeTimeFormat === "function" ? new Intl.RelativeTimeFormat(void 0, { numeric: "auto" }) : null;
167
+ function fmtAbs(unixSeconds) {
168
+ return absFmt.format(new Date(unixSeconds * 1e3));
169
+ }
170
+ function fmtRel(unixSeconds) {
171
+ if (!relFmt) return "";
172
+ const diffS = unixSeconds - Date.now() / 1e3;
173
+ const abs = Math.abs(diffS);
174
+ if (abs < 60) return relFmt.format(Math.round(diffS), "second");
175
+ if (abs < 3600) return relFmt.format(Math.round(diffS / 60), "minute");
176
+ if (abs < 86400) return relFmt.format(Math.round(diffS / 3600), "hour");
177
+ return relFmt.format(Math.round(diffS / 86400), "day");
178
+ }
179
+ function fmtSize(bytes) {
180
+ if (bytes == null) return "\u2014";
181
+ if (bytes < 1024) return `${bytes} B`;
182
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
183
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
184
+ }
185
+ function fileIcon(mime) {
186
+ if (!mime) return "i-lucide-file";
187
+ if (mime.startsWith("image/")) return "i-lucide-image";
188
+ if (mime.startsWith("video/")) return "i-lucide-film";
189
+ if (mime.startsWith("audio/")) return "i-lucide-music";
190
+ if (mime === "application/pdf") return "i-lucide-file-text";
191
+ if (/zip|tar|gzip/.test(mime)) return "i-lucide-file-archive";
192
+ return "i-lucide-file";
193
+ }
194
+ function act(kind) {
195
+ const v = props.version;
196
+ if (v == null) return;
197
+ if (kind === "restore") emit("restore", v);
198
+ else if (kind === "fork") emit("fork", v);
199
+ else emit("delete", v);
200
+ emit("update:version", null);
201
+ }
202
+ </script>
203
+
204
+ <template>
205
+ <AModalShell
206
+ :open="open"
207
+ max-width="sm:max-w-3xl"
208
+ @update:open="open = $event"
209
+ >
210
+ <div class="flex flex-col gap-4">
211
+ <h2 class="text-base font-semibold text-(--ui-text-highlighted)">
212
+ {{ snap ? `Snapshot v${snap.version}` : "Snapshot details" }}
213
+ </h2>
214
+
215
+ <!-- Loading -->
216
+ <div
217
+ v-if="loading"
218
+ class="space-y-3"
219
+ >
220
+ <USkeleton class="h-8 w-1/2" />
221
+ <USkeleton class="h-24 w-full" />
222
+ <USkeleton class="h-40 w-full" />
223
+ </div>
224
+
225
+ <!-- Error -->
226
+ <UAlert
227
+ v-else-if="error"
228
+ color="error"
229
+ variant="soft"
230
+ title="Couldn't load snapshot"
231
+ :description="error"
232
+ icon="i-lucide-alert-circle"
233
+ :actions="[{
234
+ label: 'Retry',
235
+ color: 'neutral',
236
+ variant: 'outline',
237
+ onClick: load
238
+ }]"
239
+ />
240
+
241
+ <!-- Detail -->
242
+ <template v-else-if="snap">
243
+ <!-- Metadata header -->
244
+ <div class="flex flex-wrap items-center gap-2 mb-1">
245
+ <UBadge
246
+ :label="snap.trigger"
247
+ :color="triggerBadgeColor(snap.trigger)"
248
+ variant="subtle"
249
+ size="sm"
250
+ />
251
+ <span
252
+ v-if="snap.label"
253
+ class="text-sm text-(--ui-text) truncate"
254
+ >{{ snap.label }}</span>
255
+ <span class="text-xs text-(--ui-text-dimmed)">
256
+ {{ fmtAbs(snap.created_at) }} · {{ fmtRel(snap.created_at) }}
257
+ </span>
258
+ <div class="flex items-center gap-1.5 ms-auto">
259
+ <span class="text-xs text-(--ui-text-muted)">{{ authorName(snap.created_by) }}</span>
260
+ <span class="text-xs text-(--ui-text-dimmed)">· {{ fmtSize(snap.size_bytes) }}</span>
261
+ </div>
262
+ </div>
263
+
264
+ <!-- Preview / Diff toggle -->
265
+ <div class="flex items-center gap-1 mb-1">
266
+ <UButton
267
+ label="Preview"
268
+ icon="i-lucide-eye"
269
+ size="xs"
270
+ :color="tab === 'preview' ? 'primary' : 'neutral'"
271
+ :variant="tab === 'preview' ? 'solid' : 'ghost'"
272
+ @click="tab = 'preview'"
273
+ />
274
+ <UButton
275
+ label="Changes"
276
+ icon="i-lucide-git-compare"
277
+ size="xs"
278
+ :color="tab === 'diff' ? 'primary' : 'neutral'"
279
+ :variant="tab === 'diff' ? 'solid' : 'ghost'"
280
+ @click="tab = 'diff'"
281
+ />
282
+ </div>
283
+
284
+ <!-- Preview tab -->
285
+ <div
286
+ v-show="tab === 'preview'"
287
+ class="snapshot-prose max-h-[55vh] overflow-y-auto rounded-md border border-(--ui-border) bg-(--ui-bg) p-4"
288
+ >
289
+ <!-- eslint-disable-next-line vue/no-v-html -- serialized from our own convert pipeline -->
290
+ <div
291
+ v-if="previewHtml"
292
+ v-html="previewHtml"
293
+ />
294
+ <p
295
+ v-else
296
+ class="text-sm text-(--ui-text-dimmed) text-center py-6"
297
+ >
298
+ This snapshot has no readable content.
299
+ </p>
300
+ </div>
301
+
302
+ <!-- Diff tab -->
303
+ <div v-show="tab === 'diff'">
304
+ <div class="flex items-center gap-2 mb-2 text-xs text-(--ui-text-muted)">
305
+ <span>Compared with</span>
306
+ <template v-if="cmpVersion != null">
307
+ <UButton
308
+ icon="i-lucide-chevron-left"
309
+ size="xs"
310
+ color="neutral"
311
+ variant="ghost"
312
+ :disabled="cmpIndex <= 0"
313
+ @click="stepCompare(-1)"
314
+ />
315
+ <span class="tabular-nums font-medium text-(--ui-text)">v{{ cmpVersion }}</span>
316
+ <UButton
317
+ icon="i-lucide-chevron-right"
318
+ size="xs"
319
+ color="neutral"
320
+ variant="ghost"
321
+ :disabled="cmpIndex < 0 || cmpIndex >= compareVersions.length - 1"
322
+ @click="stepCompare(1)"
323
+ />
324
+ </template>
325
+ <span
326
+ v-else
327
+ class="italic"
328
+ >the earliest version (nothing older to compare)</span>
329
+ </div>
330
+
331
+ <p
332
+ v-if="diffTooLarge"
333
+ class="text-sm text-(--ui-text-dimmed) text-center py-6"
334
+ >
335
+ This document is too large to diff.
336
+ </p>
337
+ <p
338
+ v-else-if="!diffHasChanges"
339
+ class="text-sm text-(--ui-text-dimmed) text-center py-6"
340
+ >
341
+ No textual changes between these versions.
342
+ </p>
343
+ <div
344
+ v-else
345
+ class="max-h-[55vh] overflow-y-auto rounded-md border border-(--ui-border) bg-(--ui-bg) font-mono text-xs"
346
+ >
347
+ <div
348
+ v-for="(row, idx) in diffRows"
349
+ :key="idx"
350
+ class="flex gap-2 px-3 py-0.5 whitespace-pre-wrap break-words"
351
+ :class="{
352
+ 'bg-(--ui-color-success-500)/10 text-(--ui-color-success-600)': row.t === 'add',
353
+ 'bg-(--ui-color-error-500)/10 text-(--ui-color-error-600)': row.t === 'del',
354
+ 'text-(--ui-text-dimmed)': row.t === 'ctx'
355
+ }"
356
+ >
357
+ <span class="select-none w-3 shrink-0 opacity-70">{{ row.t === "add" ? "+" : row.t === "del" ? "\u2212" : "" }}</span>
358
+ <span class="flex-1">{{ row.text || " " }}</span>
359
+ </div>
360
+ </div>
361
+ </div>
362
+
363
+ <!-- Referenced files -->
364
+ <div
365
+ v-if="snap.files && snap.files.length"
366
+ class="mt-1"
367
+ >
368
+ <p class="text-xs font-medium text-(--ui-text-muted) mb-1.5">
369
+ Referenced files
370
+ </p>
371
+ <ul class="rounded-md border border-(--ui-border) divide-y divide-(--ui-border)">
372
+ <li
373
+ v-for="f in snap.files"
374
+ :key="f.id"
375
+ class="flex items-center gap-2.5 px-3 py-2"
376
+ >
377
+ <UIcon
378
+ :name="fileIcon(f.mime_type)"
379
+ class="size-4 text-(--ui-text-muted) shrink-0"
380
+ />
381
+ <span class="text-sm truncate flex-1">{{ f.filename }}</span>
382
+ <span class="text-xs text-(--ui-text-dimmed) shrink-0">{{ fmtSize(f.size) }}</span>
383
+ </li>
384
+ </ul>
385
+ </div>
386
+ </template>
387
+ </div>
388
+
389
+ <template #footer>
390
+ <div class="flex items-center gap-2 w-full">
391
+ <UButton
392
+ label="Cancel"
393
+ color="neutral"
394
+ variant="ghost"
395
+ @click="emit('update:version', null)"
396
+ />
397
+ <div class="ms-auto flex items-center gap-2">
398
+ <UButton
399
+ v-if="isOwner"
400
+ label="Delete"
401
+ icon="i-lucide-trash-2"
402
+ color="error"
403
+ variant="ghost"
404
+ :disabled="!snap"
405
+ @click="act('delete')"
406
+ />
407
+ <UButton
408
+ label="Fork"
409
+ icon="i-lucide-git-branch"
410
+ color="neutral"
411
+ variant="outline"
412
+ :disabled="!snap"
413
+ @click="act('fork')"
414
+ />
415
+ <UButton
416
+ label="Restore"
417
+ icon="i-lucide-history"
418
+ color="primary"
419
+ :disabled="!snap"
420
+ @click="act('restore')"
421
+ />
422
+ </div>
423
+ </div>
424
+ </template>
425
+ </AModalShell>
426
+ </template>
427
+
428
+ <style scoped>
429
+ .snapshot-prose :deep(h1){font-size:1.25rem;font-weight:600;margin:.75rem 0 .5rem}.snapshot-prose :deep(h2){font-size:1.1rem;font-weight:600;margin:.75rem 0 .4rem}.snapshot-prose :deep(h3){font-size:1rem;font-weight:600;margin:.6rem 0 .3rem}.snapshot-prose :deep(p){line-height:1.6;margin:.4rem 0}.snapshot-prose :deep(ul){list-style:disc;margin:.4rem 0;padding-inline-start:1.4rem}.snapshot-prose :deep(ol){list-style:decimal;margin:.4rem 0;padding-inline-start:1.4rem}.snapshot-prose :deep(li){margin:.15rem 0}.snapshot-prose :deep(blockquote){border-inline-start:3px solid var(--ui-border);color:var(--ui-text-muted);margin:.5rem 0;padding-inline-start:.75rem}.snapshot-prose :deep(pre){background:var(--ui-bg-elevated);border-radius:.375rem;font-size:.8rem;margin:.5rem 0;overflow-x:auto;padding:.6rem .8rem}.snapshot-prose :deep(code){font-family:ui-monospace,monospace;font-size:.85em}.snapshot-prose :deep(a){color:var(--ui-primary);text-decoration:underline}.snapshot-prose :deep(img){border-radius:.375rem;height:auto;max-width:100%}.snapshot-prose :deep(table){border-collapse:collapse;margin:.5rem 0}.snapshot-prose :deep(td),.snapshot-prose :deep(th){border:1px solid var(--ui-border);padding:.3rem .5rem}
430
+ </style>
@@ -0,0 +1,33 @@
1
+ import type { SnapshotData, SnapshotMeta } from '@abraca/dabra';
2
+ import type { DocPageMeta } from '../types.js';
3
+ type __VLS_Props = {
4
+ /** Selected version — null closes the modal. The parent owns this. */
5
+ version: number | null;
6
+ /** The loaded snapshot metadata list (compare-baseline candidates). */
7
+ snapshots?: SnapshotMeta[];
8
+ /** Fetch a single snapshot's data blob (e.g. `useDocSnapshots().getSnapshot`). */
9
+ getSnapshot: (version: number) => Promise<SnapshotData | null>;
10
+ /** Document label — used as the title when serializing. */
11
+ docLabel?: string;
12
+ /** Document page type — passed to the markdown serializer. */
13
+ docType?: string;
14
+ /** Document meta — passed to the markdown serializer. */
15
+ docMeta?: DocPageMeta | null;
16
+ /** Whether the current user may delete snapshots. */
17
+ isOwner?: boolean;
18
+ };
19
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
20
+ delete: (version: number) => any;
21
+ restore: (version: number) => any;
22
+ fork: (version: number) => any;
23
+ "update:version": (value: number | null) => any;
24
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
25
+ onDelete?: ((version: number) => any) | undefined;
26
+ onRestore?: ((version: number) => any) | undefined;
27
+ onFork?: ((version: number) => any) | undefined;
28
+ "onUpdate:version"?: ((value: number | null) => any) | undefined;
29
+ }>, {
30
+ snapshots: SnapshotMeta[];
31
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
32
+ declare const _default: typeof __VLS_export;
33
+ export default _default;
@@ -238,9 +238,9 @@ declare const __VLS_export: __VLS_WithSlots<import("vue").DefineComponent<import
238
238
  transition: boolean;
239
239
  autofocus: boolean;
240
240
  loading: boolean;
241
- colorMode: boolean;
242
- overlay: boolean;
243
241
  dismissible: boolean;
242
+ overlay: boolean;
243
+ colorMode: boolean;
244
244
  fullscreen: boolean;
245
245
  modal: boolean;
246
246
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>, __VLS_Slots>;
@@ -238,9 +238,9 @@ declare const __VLS_export: __VLS_WithSlots<import("vue").DefineComponent<import
238
238
  transition: boolean;
239
239
  autofocus: boolean;
240
240
  loading: boolean;
241
- colorMode: boolean;
242
- overlay: boolean;
243
241
  dismissible: boolean;
242
+ overlay: boolean;
243
+ colorMode: boolean;
244
244
  fullscreen: boolean;
245
245
  modal: boolean;
246
246
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>, __VLS_Slots>;
@@ -1,17 +1,28 @@
1
1
  <script setup>
2
2
  import { ref, watch, nextTick, onUnmounted } from "vue";
3
- import mapboxgl from "mapbox-gl";
4
3
  import { useRuntimeConfig } from "#imports";
5
- if (typeof window !== "undefined") {
6
- import("mapbox-gl/dist/mapbox-gl.css");
7
- }
8
4
  const props = defineProps({
9
5
  latValue: { type: Number, required: false },
10
6
  lngValue: { type: Number, required: false },
11
7
  open: { type: Boolean, required: true }
12
8
  });
13
9
  const emit = defineEmits(["update:lat", "update:lng", "update:open", "clear"]);
14
- mapboxgl.accessToken = useRuntimeConfig().public.mapboxAccessToken;
10
+ const mapboxAccessToken = useRuntimeConfig().public?.abracadabra?.mapboxToken;
11
+ let mapboxgl = null;
12
+ const mapboxError = ref(null);
13
+ async function loadMapbox() {
14
+ if (mapboxgl) return true;
15
+ try {
16
+ const mod = await import("mapbox-gl");
17
+ mapboxgl = mod.default || mod;
18
+ await import("mapbox-gl/dist/mapbox-gl.css");
19
+ mapboxgl.accessToken = mapboxAccessToken;
20
+ return true;
21
+ } catch {
22
+ mapboxError.value = "Location picker requires mapbox-gl. Install it with: pnpm add mapbox-gl";
23
+ return false;
24
+ }
25
+ }
15
26
  const mapContainer = ref(null);
16
27
  let map = null;
17
28
  let marker = null;
@@ -37,9 +48,11 @@ function destroyMap() {
37
48
  map = null;
38
49
  marker = null;
39
50
  }
40
- function initMap() {
51
+ async function initMap() {
41
52
  const el = mapContainer.value;
42
53
  if (!el || map) return;
54
+ if (!await loadMapbox()) return;
55
+ if (!mapContainer.value || !props.open || map) return;
43
56
  const center = hasCoords() ? [props.lngValue, props.latValue] : [0, 20];
44
57
  const zoom = hasCoords() ? 10 : 1;
45
58
  map = new mapboxgl.Map({
@@ -110,7 +123,7 @@ async function doSearch(q) {
110
123
  if (!q.trim()) return;
111
124
  searching.value = true;
112
125
  try {
113
- const url = `https://api.mapbox.com/search/geocode/v6/forward?q=${encodeURIComponent(q)}&limit=5&access_token=${mapboxgl.accessToken}`;
126
+ const url = `https://api.mapbox.com/search/geocode/v6/forward?q=${encodeURIComponent(q)}&limit=5&access_token=${mapboxAccessToken}`;
114
127
  const res = await fetch(url);
115
128
  const data = await res.json();
116
129
  searchResults.value = (data.features ?? []).map((f) => ({
@@ -195,10 +208,18 @@ function useMyPosition() {
195
208
 
196
209
  <!-- Map -->
197
210
  <div
211
+ v-show="!mapboxError"
198
212
  ref="mapContainer"
199
213
  class="w-full rounded-md overflow-hidden"
200
214
  style="height: 220px;"
201
215
  />
216
+ <div
217
+ v-if="mapboxError"
218
+ class="w-full rounded-md border border-(--ui-border) bg-(--ui-bg-elevated) flex items-center justify-center p-3 text-center text-xs text-(--ui-text-muted)"
219
+ style="height: 220px;"
220
+ >
221
+ {{ mapboxError }}
222
+ </div>
202
223
 
203
224
  <!-- Footer -->
204
225
  <div class="flex items-center justify-between">
@@ -6,13 +6,13 @@ declare const __VLS_export: import("vue").DefineComponent<{
6
6
  /** See `<APluginBrowser>` — same fallback chain. */
7
7
  serverUrl?: string;
8
8
  }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
9
- installed: (detail: CatalogVersionDetail) => any;
10
9
  back: () => any;
10
+ installed: (detail: CatalogVersionDetail) => any;
11
11
  }, string, import("vue").PublicProps, Readonly<{
12
12
  id: string;
13
13
  /** See `<APluginBrowser>` — same fallback chain. */
14
14
  serverUrl?: string;
15
15
  }> & Readonly<{
16
- onInstalled?: ((detail: CatalogVersionDetail) => any) | undefined;
17
16
  onBack?: (() => any) | undefined;
17
+ onInstalled?: ((detail: CatalogVersionDetail) => any) | undefined;
18
18
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -6,13 +6,13 @@ declare const __VLS_export: import("vue").DefineComponent<{
6
6
  /** See `<APluginBrowser>` — same fallback chain. */
7
7
  serverUrl?: string;
8
8
  }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
9
- installed: (detail: CatalogVersionDetail) => any;
10
9
  back: () => any;
10
+ installed: (detail: CatalogVersionDetail) => any;
11
11
  }, string, import("vue").PublicProps, Readonly<{
12
12
  id: string;
13
13
  /** See `<APluginBrowser>` — same fallback chain. */
14
14
  serverUrl?: string;
15
15
  }> & Readonly<{
16
- onInstalled?: ((detail: CatalogVersionDetail) => any) | undefined;
17
16
  onBack?: (() => any) | undefined;
17
+ onInstalled?: ((detail: CatalogVersionDetail) => any) | undefined;
18
18
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -10,14 +10,14 @@ type __VLS_Props = {
10
10
  };
11
11
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, Record<PropertyKey, unknown>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
12
  rename: (label: string) => any;
13
+ ready: () => any;
13
14
  update: (content: any) => any;
14
15
  updateMeta: (patch: Partial<DocPageMeta>) => any;
15
- ready: () => any;
16
16
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
17
17
  onRename?: ((label: string) => any) | undefined;
18
+ onReady?: (() => any) | undefined;
18
19
  onUpdate?: ((content: any) => any) | undefined;
19
20
  onUpdateMeta?: ((patch: Partial<DocPageMeta>) => any) | undefined;
20
- onReady?: (() => any) | undefined;
21
21
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
22
22
  declare const _default: typeof __VLS_export;
23
23
  export default _default;
@@ -10,14 +10,14 @@ type __VLS_Props = {
10
10
  };
11
11
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, Record<PropertyKey, unknown>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
12
  rename: (label: string) => any;
13
+ ready: () => any;
13
14
  update: (content: any) => any;
14
15
  updateMeta: (patch: Partial<DocPageMeta>) => any;
15
- ready: () => any;
16
16
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
17
17
  onRename?: ((label: string) => any) | undefined;
18
+ onReady?: (() => any) | undefined;
18
19
  onUpdate?: ((content: any) => any) | undefined;
19
20
  onUpdateMeta?: ((patch: Partial<DocPageMeta>) => any) | undefined;
20
- onReady?: (() => any) | undefined;
21
21
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
22
22
  declare const _default: typeof __VLS_export;
23
23
  export default _default;
@@ -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;