@abraca/nuxt 2.0.10 → 2.3.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.
- package/dist/module.d.mts +68 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +99 -4
- package/dist/runtime/components/ACodeEditor.d.vue.ts +26 -0
- package/dist/runtime/components/ACodeEditor.vue +268 -0
- package/dist/runtime/components/ACodeEditor.vue.d.ts +26 -0
- package/dist/runtime/components/ADocumentTree.vue +52 -20
- package/dist/runtime/components/AEditor.d.vue.ts +20 -13
- package/dist/runtime/components/AEditor.vue +55 -2
- package/dist/runtime/components/AEditor.vue.d.ts +20 -13
- package/dist/runtime/components/ANodePanel.vue +64 -60
- package/dist/runtime/components/ANotificationBell.d.vue.ts +1 -1
- package/dist/runtime/components/ANotificationBell.vue.d.ts +1 -1
- package/dist/runtime/components/ASpaceFormModal.d.vue.ts +2 -2
- package/dist/runtime/components/ASpaceFormModal.vue.d.ts +2 -2
- package/dist/runtime/components/aware/APresenceBlobs.d.vue.ts +29 -1
- package/dist/runtime/components/aware/APresenceBlobs.vue +54 -8
- package/dist/runtime/components/aware/APresenceBlobs.vue.d.ts +29 -1
- package/dist/runtime/components/aware/APresenceCursors.d.vue.ts +11 -0
- package/dist/runtime/components/aware/APresenceCursors.vue +74 -9
- package/dist/runtime/components/aware/APresenceCursors.vue.d.ts +11 -0
- package/dist/runtime/components/aware/AToggleGroup.d.vue.ts +28 -13
- package/dist/runtime/components/aware/AToggleGroup.vue +56 -20
- package/dist/runtime/components/aware/AToggleGroup.vue.d.ts +28 -13
- package/dist/runtime/components/docs/ADocsNavigation.d.vue.ts +1 -1
- package/dist/runtime/components/docs/ADocsNavigation.vue.d.ts +1 -1
- package/dist/runtime/components/docs/ADocsSearchButton.d.vue.ts +1 -1
- package/dist/runtime/components/docs/ADocsSearchButton.vue.d.ts +1 -1
- package/dist/runtime/components/docs/ADocsToc.d.vue.ts +2 -2
- package/dist/runtime/components/docs/ADocsToc.vue.d.ts +2 -2
- package/dist/runtime/components/editor/AEditorRedoButton.d.vue.ts +1 -1
- package/dist/runtime/components/editor/AEditorRedoButton.vue.d.ts +1 -1
- package/dist/runtime/components/editor/AEditorUndoButton.d.vue.ts +1 -1
- package/dist/runtime/components/editor/AEditorUndoButton.vue.d.ts +1 -1
- package/dist/runtime/components/editor/ANodeInlineLabel.d.vue.ts +1 -1
- package/dist/runtime/components/editor/ANodeInlineLabel.vue.d.ts +1 -1
- package/dist/runtime/components/registry/APluginBrowser.d.vue.ts +23 -0
- package/dist/runtime/components/registry/APluginBrowser.vue +155 -0
- package/dist/runtime/components/registry/APluginBrowser.vue.d.ts +23 -0
- package/dist/runtime/components/registry/APluginCapabilityDialog.d.vue.ts +17 -0
- package/dist/runtime/components/registry/APluginCapabilityDialog.vue +159 -0
- package/dist/runtime/components/registry/APluginCapabilityDialog.vue.d.ts +17 -0
- package/dist/runtime/components/registry/APluginCard.d.vue.ts +20 -0
- package/dist/runtime/components/registry/APluginCard.vue +91 -0
- package/dist/runtime/components/registry/APluginCard.vue.d.ts +20 -0
- package/dist/runtime/components/registry/APluginDetail.d.vue.ts +18 -0
- package/dist/runtime/components/registry/APluginDetail.vue +252 -0
- package/dist/runtime/components/registry/APluginDetail.vue.d.ts +18 -0
- package/dist/runtime/components/renderers/ACodeRenderer.d.vue.ts +15 -0
- package/dist/runtime/components/renderers/ACodeRenderer.vue +68 -0
- package/dist/runtime/components/renderers/ACodeRenderer.vue.d.ts +15 -0
- package/dist/runtime/components/renderers/AGraphRenderer.vue +416 -120
- package/dist/runtime/components/renderers/AProseRenderer.d.vue.ts +2 -2
- package/dist/runtime/components/renderers/AProseRenderer.vue.d.ts +2 -2
- package/dist/runtime/components/shell/ABreadcrumbForDoc.d.vue.ts +11 -0
- package/dist/runtime/components/shell/ABreadcrumbForDoc.vue +16 -0
- package/dist/runtime/components/shell/ABreadcrumbForDoc.vue.d.ts +11 -0
- package/dist/runtime/components/shell/ASettingsSection.d.vue.ts +35 -0
- package/dist/runtime/components/shell/ASettingsSection.vue +26 -0
- package/dist/runtime/components/shell/ASettingsSection.vue.d.ts +35 -0
- package/dist/runtime/components/shell/ASidebar.d.vue.ts +1 -1
- package/dist/runtime/components/shell/ASidebar.vue.d.ts +1 -1
- package/dist/runtime/components/shell/AUserMenu.d.vue.ts +3 -0
- package/dist/runtime/components/shell/AUserMenu.vue +4 -0
- package/dist/runtime/components/shell/AUserMenu.vue.d.ts +3 -0
- package/dist/runtime/composables/useAbracadabraSchema.d.ts +83 -0
- package/dist/runtime/composables/useAbracadabraSchema.js +52 -0
- package/dist/runtime/composables/useAggregatedPresence.d.ts +1 -6
- package/dist/runtime/composables/useCalendarView.d.ts +1 -1
- package/dist/runtime/composables/useChat.js +1 -0
- package/dist/runtime/composables/useDocBreadcrumb.d.ts +21 -0
- package/dist/runtime/composables/useDocBreadcrumb.js +33 -0
- package/dist/runtime/composables/useDocEntryTyped.d.ts +60 -0
- package/dist/runtime/composables/useDocEntryTyped.js +70 -0
- package/dist/runtime/composables/useEditorDragHandle.js +18 -0
- package/dist/runtime/composables/useEditorSuggestions.js +2 -1
- package/dist/runtime/composables/useInstalledPlugins.d.ts +3 -21
- package/dist/runtime/composables/useInstalledPlugins.js +2 -12
- package/dist/runtime/composables/useMetaMenuItems.d.ts +21 -0
- package/dist/runtime/composables/useMetaMenuItems.js +115 -0
- package/dist/runtime/composables/useMetaValidator.d.ts +27 -0
- package/dist/runtime/composables/useMetaValidator.js +10 -0
- package/dist/runtime/composables/usePluginCatalog.d.ts +161 -0
- package/dist/runtime/composables/usePluginCatalog.js +234 -0
- package/dist/runtime/composables/useQuery.d.ts +79 -0
- package/dist/runtime/composables/useQuery.js +97 -0
- package/dist/runtime/composables/useSpaces.js +4 -5
- package/dist/runtime/composables/useTableView.d.ts +3 -3
- package/dist/runtime/composables/useTypedDoc.d.ts +97 -0
- package/dist/runtime/composables/useTypedDoc.js +114 -0
- package/dist/runtime/composables/useWebRTC.js +44 -5
- package/dist/runtime/extensions/document-meta.js +5 -0
- package/dist/runtime/extensions/timeline.d.ts +11 -0
- package/dist/runtime/extensions/timeline.js +52 -0
- package/dist/runtime/extensions/views/DocumentMetaView.d.vue.ts +4 -0
- package/dist/runtime/extensions/views/DocumentMetaView.vue +63 -0
- package/dist/runtime/extensions/views/DocumentMetaView.vue.d.ts +4 -0
- package/dist/runtime/extensions/views/TimelineItemView.d.vue.ts +4 -0
- package/dist/runtime/extensions/views/TimelineItemView.vue +131 -0
- package/dist/runtime/extensions/views/TimelineItemView.vue.d.ts +4 -0
- package/dist/runtime/extensions/views/TimelineView.d.vue.ts +9 -0
- package/dist/runtime/extensions/views/TimelineView.vue +29 -0
- package/dist/runtime/extensions/views/TimelineView.vue.d.ts +9 -0
- package/dist/runtime/locale.d.ts +2 -0
- package/dist/runtime/locale.js +2 -0
- package/dist/runtime/plugin-abracadabra.client.js +107 -6
- package/dist/runtime/plugin-registry.d.ts +11 -30
- package/dist/runtime/plugin-registry.js +2 -82
- package/dist/runtime/plugins/core.plugin.js +10 -4
- package/dist/runtime/server/api/_abracadabra/spaces.get.d.ts +1 -1
- package/dist/runtime/server/plugins/abracadabra-service.js +28 -0
- package/dist/runtime/server/utils/docCache.js +24 -3
- package/dist/runtime/server/utils/schemaServerSupport.d.ts +52 -0
- package/dist/runtime/server/utils/schemaServerSupport.js +51 -0
- package/dist/runtime/types.d.ts +63 -46
- package/dist/runtime/utils/docTypes.d.ts +15 -0
- package/dist/runtime/utils/docTypes.js +20 -0
- package/dist/runtime/utils/loadCodeMirror.d.ts +32 -0
- package/dist/runtime/utils/loadCodeMirror.js +65 -0
- package/dist/runtime/utils/markdownToYjs.d.ts +1 -23
- package/dist/runtime/utils/markdownToYjs.js +5 -440
- package/dist/runtime/utils/schemaSupport.d.ts +60 -0
- package/dist/runtime/utils/schemaSupport.js +40 -0
- package/dist/runtime/utils/yjsConvert.d.ts +1 -14
- package/dist/runtime/utils/yjsConvert.js +5 -331
- package/package.json +84 -23
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from "vue";
|
|
3
3
|
import { useRuntimeConfig } from "#imports";
|
|
4
4
|
import { useRafFn, useEventListener } from "@vueuse/core";
|
|
5
|
+
import { getIcon, loadIcon } from "@iconify/vue";
|
|
5
6
|
import { useRendererBase } from "../../composables/useRendererBase";
|
|
6
7
|
import { useNodePanel } from "../../composables/useNodePanel";
|
|
7
8
|
import { DEFAULT_LOCALE } from "../../locale";
|
|
@@ -36,6 +37,67 @@ onMounted(async () => {
|
|
|
36
37
|
d3Error.value = true;
|
|
37
38
|
}
|
|
38
39
|
});
|
|
40
|
+
const providerSynced = ref(false);
|
|
41
|
+
let detachSynced = null;
|
|
42
|
+
let syncTimer = null;
|
|
43
|
+
watch(childProviderRef, (p) => {
|
|
44
|
+
detachSynced?.();
|
|
45
|
+
detachSynced = null;
|
|
46
|
+
if (syncTimer) {
|
|
47
|
+
clearTimeout(syncTimer);
|
|
48
|
+
syncTimer = null;
|
|
49
|
+
}
|
|
50
|
+
if (!p) {
|
|
51
|
+
providerSynced.value = false;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (p.isSynced) {
|
|
55
|
+
providerSynced.value = true;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
providerSynced.value = false;
|
|
59
|
+
const onSync = () => {
|
|
60
|
+
providerSynced.value = true;
|
|
61
|
+
};
|
|
62
|
+
p.on?.("synced", onSync);
|
|
63
|
+
detachSynced = () => p.off?.("synced", onSync);
|
|
64
|
+
syncTimer = setTimeout(() => {
|
|
65
|
+
providerSynced.value = true;
|
|
66
|
+
}, 3e3);
|
|
67
|
+
}, { immediate: true });
|
|
68
|
+
const iconSVGCache = /* @__PURE__ */ new Map();
|
|
69
|
+
const iconMissing = /* @__PURE__ */ new Set();
|
|
70
|
+
const iconInFlight = /* @__PURE__ */ new Set();
|
|
71
|
+
const iconLoadTick = ref(0);
|
|
72
|
+
function getIconSVG(name) {
|
|
73
|
+
if (iconSVGCache.has(name)) return iconSVGCache.get(name);
|
|
74
|
+
if (iconMissing.has(name)) return null;
|
|
75
|
+
const data = getIcon(`lucide:${name}`);
|
|
76
|
+
if (data) {
|
|
77
|
+
const body = data.body.replace(/stroke="currentColor"/g, 'stroke="var(--ui-bg)"');
|
|
78
|
+
iconSVGCache.set(name, body);
|
|
79
|
+
return body;
|
|
80
|
+
}
|
|
81
|
+
if (iconInFlight.has(name)) return null;
|
|
82
|
+
iconInFlight.add(name);
|
|
83
|
+
loadIcon(`lucide:${name}`).then(
|
|
84
|
+
() => {
|
|
85
|
+
iconInFlight.delete(name);
|
|
86
|
+
const d = getIcon(`lucide:${name}`);
|
|
87
|
+
if (d) {
|
|
88
|
+
iconSVGCache.set(name, d.body.replace(/stroke="currentColor"/g, 'stroke="var(--ui-bg)"'));
|
|
89
|
+
iconLoadTick.value++;
|
|
90
|
+
} else {
|
|
91
|
+
iconMissing.add(name);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
() => {
|
|
95
|
+
iconInFlight.delete(name);
|
|
96
|
+
iconMissing.add(name);
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
39
101
|
watch(childDoc, (doc) => {
|
|
40
102
|
if (!doc) return;
|
|
41
103
|
const legacyMap = doc.getMap("graph-positions");
|
|
@@ -50,16 +112,26 @@ watch(childDoc, (doc) => {
|
|
|
50
112
|
}
|
|
51
113
|
});
|
|
52
114
|
}, { immediate: true });
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
115
|
+
const entryById = computed(() => {
|
|
116
|
+
const m = /* @__PURE__ */ new Map();
|
|
117
|
+
for (const e of tree.entries.value) m.set(e.id, e);
|
|
118
|
+
return m;
|
|
119
|
+
});
|
|
120
|
+
const posByEntry = computed(() => {
|
|
121
|
+
const m = /* @__PURE__ */ new Map();
|
|
122
|
+
for (const e of tree.entries.value) {
|
|
123
|
+
if (e.meta?.graphX !== void 0 && e.meta?.graphY !== void 0) {
|
|
124
|
+
m.set(e.id, {
|
|
125
|
+
x: e.meta.graphX,
|
|
126
|
+
y: e.meta.graphY,
|
|
127
|
+
pinned: e.meta.graphPinned ?? false
|
|
128
|
+
});
|
|
129
|
+
}
|
|
61
130
|
}
|
|
62
|
-
return
|
|
131
|
+
return m;
|
|
132
|
+
});
|
|
133
|
+
function getGraphPos(id) {
|
|
134
|
+
return posByEntry.value.get(id);
|
|
63
135
|
}
|
|
64
136
|
const svgRef = ref(null);
|
|
65
137
|
const vb = ref({ x: -500, y: -350, w: 1e3, h: 700 });
|
|
@@ -109,6 +181,37 @@ let isPinching = false;
|
|
|
109
181
|
let pinchStart = null;
|
|
110
182
|
const isPanning = ref(false);
|
|
111
183
|
const panStart = ref({ ex: 0, ey: 0, vx: 0, vy: 0 });
|
|
184
|
+
let pendingSvgMove = null;
|
|
185
|
+
let pendingNodeMove = null;
|
|
186
|
+
let pointerRafId = null;
|
|
187
|
+
function schedulePointerFlush() {
|
|
188
|
+
if (pointerRafId !== null) return;
|
|
189
|
+
pointerRafId = requestAnimationFrame(() => {
|
|
190
|
+
pointerRafId = null;
|
|
191
|
+
if (pendingSvgMove) {
|
|
192
|
+
const m = pendingSvgMove;
|
|
193
|
+
pendingSvgMove = null;
|
|
194
|
+
flushSvgPointerMove(m);
|
|
195
|
+
}
|
|
196
|
+
if (pendingNodeMove) {
|
|
197
|
+
const m = pendingNodeMove;
|
|
198
|
+
pendingNodeMove = null;
|
|
199
|
+
flushNodePointerMove(m);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
let pendingLocalState = null;
|
|
204
|
+
let awarenessRafId = null;
|
|
205
|
+
function queueLocalState(patch) {
|
|
206
|
+
pendingLocalState = pendingLocalState ? { ...pendingLocalState, ...patch } : { ...patch };
|
|
207
|
+
if (awarenessRafId !== null) return;
|
|
208
|
+
awarenessRafId = requestAnimationFrame(() => {
|
|
209
|
+
awarenessRafId = null;
|
|
210
|
+
const patchOut = pendingLocalState;
|
|
211
|
+
pendingLocalState = null;
|
|
212
|
+
if (patchOut) setLocalState(patchOut);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
112
215
|
function onSvgPointerDown(e) {
|
|
113
216
|
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
114
217
|
activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
@@ -116,6 +219,11 @@ function onSvgPointerDown(e) {
|
|
|
116
219
|
if (activePointers.size === 1) {
|
|
117
220
|
isPanning.value = true;
|
|
118
221
|
isPinching = false;
|
|
222
|
+
if (zoomRafId) {
|
|
223
|
+
cancelAnimationFrame(zoomRafId);
|
|
224
|
+
zoomRafId = null;
|
|
225
|
+
}
|
|
226
|
+
vb.value = { ...targetVb.value };
|
|
119
227
|
panStart.value = { ex: e.clientX, ey: e.clientY, vx: targetVb.value.x, vy: targetVb.value.y };
|
|
120
228
|
} else if (activePointers.size === 2) {
|
|
121
229
|
isPanning.value = false;
|
|
@@ -132,8 +240,12 @@ function onSvgPointerDown(e) {
|
|
|
132
240
|
function onSvgPointerMove(e) {
|
|
133
241
|
if (!activePointers.has(e.pointerId)) return;
|
|
134
242
|
activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
135
|
-
|
|
136
|
-
|
|
243
|
+
pendingSvgMove = { clientX: e.clientX, clientY: e.clientY, pointerId: e.pointerId };
|
|
244
|
+
schedulePointerFlush();
|
|
245
|
+
}
|
|
246
|
+
function flushSvgPointerMove(m) {
|
|
247
|
+
if (!draggingId.value && !isPanning.value && activePointers.size === 1) {
|
|
248
|
+
queueLocalState({ pos: svgCoord(m.clientX, m.clientY) });
|
|
137
249
|
}
|
|
138
250
|
if (isPinching && activePointers.size >= 2 && pinchStart) {
|
|
139
251
|
const pts = [...activePointers.values()];
|
|
@@ -154,11 +266,11 @@ function onSvgPointerMove(e) {
|
|
|
154
266
|
}
|
|
155
267
|
if (!isPanning.value || activePointers.size !== 1) return;
|
|
156
268
|
const rect = svgRef.value.getBoundingClientRect();
|
|
157
|
-
const sx =
|
|
158
|
-
const sy =
|
|
159
|
-
const nx = panStart.value.vx - (
|
|
160
|
-
const ny = panStart.value.vy - (
|
|
161
|
-
vb.value = { ...
|
|
269
|
+
const sx = targetVb.value.w / rect.width;
|
|
270
|
+
const sy = targetVb.value.h / rect.height;
|
|
271
|
+
const nx = panStart.value.vx - (m.clientX - panStart.value.ex) * sx;
|
|
272
|
+
const ny = panStart.value.vy - (m.clientY - panStart.value.ey) * sy;
|
|
273
|
+
vb.value = { ...targetVb.value, x: nx, y: ny };
|
|
162
274
|
targetVb.value = { ...targetVb.value, x: nx, y: ny };
|
|
163
275
|
}
|
|
164
276
|
function onSvgPointerUp(e) {
|
|
@@ -183,7 +295,7 @@ function onSvgPointerLeave(e) {
|
|
|
183
295
|
isPanning.value = false;
|
|
184
296
|
isPinching = false;
|
|
185
297
|
pinchStart = null;
|
|
186
|
-
|
|
298
|
+
queueLocalState({ pos: null });
|
|
187
299
|
}
|
|
188
300
|
}
|
|
189
301
|
function fitView() {
|
|
@@ -232,7 +344,7 @@ const labelOpacity = computed(() => {
|
|
|
232
344
|
if (!showLabels.value) return 0;
|
|
233
345
|
const pxPerUnit = (svgRef.value?.clientWidth ?? 1e3) / vb.value.w;
|
|
234
346
|
const apparentR = 16 * pxPerUnit;
|
|
235
|
-
return Math.max(0, Math.min(1, (apparentR -
|
|
347
|
+
return Math.max(0, Math.min(1, (apparentR - 3) / 5));
|
|
236
348
|
});
|
|
237
349
|
const SPACING_PRESETS = {
|
|
238
350
|
compact: { charge: -70, linkDist: 50, centerStrength: 0.1, collideExtra: 2 },
|
|
@@ -310,52 +422,106 @@ const viewSettingsActive = computed(
|
|
|
310
422
|
() => !showLabels.value || !showRefEdges.value || edgeThickness.value !== "normal"
|
|
311
423
|
);
|
|
312
424
|
const simNodes = ref([]);
|
|
425
|
+
const nodeById = /* @__PURE__ */ new Map();
|
|
313
426
|
const renderTick = ref(0);
|
|
314
427
|
let sim = null;
|
|
315
428
|
const renderedNodes = computed(() => {
|
|
316
429
|
void renderTick.value;
|
|
317
|
-
|
|
430
|
+
void iconLoadTick.value;
|
|
431
|
+
return simNodes.value;
|
|
318
432
|
});
|
|
319
|
-
const
|
|
433
|
+
const visibleNodes = computed(() => {
|
|
434
|
+
void renderTick.value;
|
|
435
|
+
void iconLoadTick.value;
|
|
436
|
+
const v = vb.value;
|
|
437
|
+
const mx = v.w * 0.3;
|
|
438
|
+
const my = v.h * 0.3;
|
|
439
|
+
const x0 = v.x - mx;
|
|
440
|
+
const x1 = v.x + v.w + mx;
|
|
441
|
+
const y0 = v.y - my;
|
|
442
|
+
const y1 = v.y + v.h + my;
|
|
443
|
+
const ls = labelScale.value;
|
|
444
|
+
return simNodes.value.filter((n) => {
|
|
445
|
+
const r = nodeRadius(n) + ls * 60;
|
|
446
|
+
return n.x + r > x0 && n.x - r < x1 && n.y + r > y0 && n.y - r < y1;
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
const allSimLinks = computed(() => {
|
|
320
450
|
void renderTick.value;
|
|
321
451
|
if (!sim || !d3.value) return [];
|
|
322
452
|
const linkForce = sim.force("link");
|
|
323
453
|
if (!linkForce) return [];
|
|
324
454
|
return linkForce.links().filter((l) => l.source?.x !== void 0 && l.target?.x !== void 0);
|
|
325
455
|
});
|
|
456
|
+
const visibleLinks = computed(() => {
|
|
457
|
+
const v = vb.value;
|
|
458
|
+
const mx = v.w * 0.3;
|
|
459
|
+
const my = v.h * 0.3;
|
|
460
|
+
const x0 = v.x - mx;
|
|
461
|
+
const x1 = v.x + v.w + mx;
|
|
462
|
+
const y0 = v.y - my;
|
|
463
|
+
const y1 = v.y + v.h + my;
|
|
464
|
+
return allSimLinks.value.filter((l) => {
|
|
465
|
+
const s = l.source, t = l.target;
|
|
466
|
+
return s.x > x0 && s.x < x1 && s.y > y0 && s.y < y1 || t.x > x0 && t.x < x1 && t.y > y0 && t.y < y1;
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
const batchedEdgePathD = computed(() => {
|
|
470
|
+
let d = "";
|
|
471
|
+
for (const link of visibleLinks.value) {
|
|
472
|
+
d += `M${link.source.x},${link.source.y}L${link.target.x},${link.target.y}`;
|
|
473
|
+
}
|
|
474
|
+
return d;
|
|
475
|
+
});
|
|
326
476
|
let savePosTimer = null;
|
|
327
477
|
function savePositionsDebounced() {
|
|
328
478
|
if (savePosTimer) clearTimeout(savePosTimer);
|
|
329
479
|
savePosTimer = setTimeout(() => {
|
|
330
480
|
savePosTimer = null;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
481
|
+
const doc = childDoc.value;
|
|
482
|
+
const work = () => {
|
|
483
|
+
for (const node of simNodes.value) {
|
|
484
|
+
if (remoteDragByNode.value.has(node.id)) continue;
|
|
485
|
+
if (draggingId.value === node.id) continue;
|
|
486
|
+
const existing = posByEntry.value.get(node.id);
|
|
487
|
+
const pinned = existing?.pinned ?? false;
|
|
488
|
+
if (existing && Math.abs(existing.x - node.x) < 1 && Math.abs(existing.y - node.y) < 1 && existing.pinned === pinned) continue;
|
|
489
|
+
tree.updateMeta(node.id, { graphX: node.x, graphY: node.y, graphPinned: pinned });
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
if (doc && typeof doc.transact === "function") doc.transact(work);
|
|
493
|
+
else work();
|
|
339
494
|
}, 500);
|
|
340
495
|
}
|
|
496
|
+
let lastRenderTime = 0;
|
|
497
|
+
const RENDER_INTERVAL = 16;
|
|
341
498
|
const { pause: pauseSim, resume: resumeSim } = useRafFn(() => {
|
|
342
499
|
if (!sim || sim.alpha() < sim.alphaMin()) {
|
|
343
500
|
pauseSim();
|
|
344
501
|
if (simNodes.value.length > 0) savePositionsDebounced();
|
|
502
|
+
renderTick.value++;
|
|
345
503
|
return;
|
|
346
504
|
}
|
|
347
505
|
sim.tick();
|
|
348
506
|
for (const [nodeId, drag] of remoteDragByNode.value) {
|
|
349
|
-
const n =
|
|
507
|
+
const n = nodeById.get(nodeId);
|
|
350
508
|
if (n) {
|
|
351
509
|
n.fx = drag.x;
|
|
352
510
|
n.fy = drag.y;
|
|
353
511
|
}
|
|
354
512
|
}
|
|
355
|
-
|
|
513
|
+
const now = performance.now();
|
|
514
|
+
if (now - lastRenderTime >= RENDER_INTERVAL) {
|
|
515
|
+
lastRenderTime = now;
|
|
516
|
+
renderTick.value++;
|
|
517
|
+
}
|
|
356
518
|
}, { immediate: false });
|
|
357
|
-
function restartSim(alpha = 0.
|
|
519
|
+
function restartSim(alpha = 0.05) {
|
|
358
520
|
if (!sim) return;
|
|
521
|
+
for (const n of simNodes.value) {
|
|
522
|
+
n.vx = 0;
|
|
523
|
+
n.vy = 0;
|
|
524
|
+
}
|
|
359
525
|
sim.alpha(Math.max(sim.alpha(), alpha));
|
|
360
526
|
resumeSim();
|
|
361
527
|
}
|
|
@@ -366,9 +532,23 @@ function seedPos(id) {
|
|
|
366
532
|
let h = 0;
|
|
367
533
|
for (const c of id) h = Math.imul(31, h) + c.charCodeAt(0) | 0;
|
|
368
534
|
const angle = (h >>> 0) / 4294967295 * 2 * Math.PI;
|
|
369
|
-
const
|
|
535
|
+
const count = allEntries.value.length;
|
|
536
|
+
const sp = SPACING_PRESETS[spacingLevel.value];
|
|
537
|
+
const baseR = Math.max(sp.linkDist, count * sp.linkDist * 0.3);
|
|
538
|
+
const r = baseR + (Math.imul(h, 1664525) >>> 0) / 4294967295 * baseR * 0.5;
|
|
370
539
|
return { x: Math.cos(angle) * r, y: Math.sin(angle) * r };
|
|
371
540
|
}
|
|
541
|
+
function spawnPosNearParent(entry) {
|
|
542
|
+
const saved = posByEntry.value.get(entry.id);
|
|
543
|
+
if (saved) return { x: saved.x, y: saved.y };
|
|
544
|
+
const parent = entry.parentId ? nodeById.get(entry.parentId) : null;
|
|
545
|
+
if (parent) {
|
|
546
|
+
const angle = Math.random() * 2 * Math.PI;
|
|
547
|
+
const dist = SPACING_PRESETS[spacingLevel.value].linkDist * 0.5;
|
|
548
|
+
return { x: parent.x + Math.cos(angle) * dist, y: parent.y + Math.sin(angle) * dist };
|
|
549
|
+
}
|
|
550
|
+
return seedPos(entry.id);
|
|
551
|
+
}
|
|
372
552
|
const allEntries = computed(() => {
|
|
373
553
|
const result = [];
|
|
374
554
|
function collect(parentId) {
|
|
@@ -382,31 +562,54 @@ const allEntries = computed(() => {
|
|
|
382
562
|
});
|
|
383
563
|
const allEdges = computed(() => {
|
|
384
564
|
const nodeIds = new Set(allEntries.value.map((e) => e.id));
|
|
385
|
-
|
|
565
|
+
const out = [];
|
|
566
|
+
for (const e of allEntries.value) {
|
|
567
|
+
if (e.parentId !== null && nodeIds.has(e.parentId)) {
|
|
568
|
+
out.push({ source: e.parentId, target: e.id });
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return out;
|
|
572
|
+
});
|
|
573
|
+
const degreeById = computed(() => {
|
|
574
|
+
const m = /* @__PURE__ */ new Map();
|
|
575
|
+
for (const e of allEdges.value) {
|
|
576
|
+
m.set(e.source, (m.get(e.source) ?? 0) + 1);
|
|
577
|
+
m.set(e.target, (m.get(e.target) ?? 0) + 1);
|
|
578
|
+
}
|
|
579
|
+
return m;
|
|
386
580
|
});
|
|
387
581
|
const graphKey = computed(() => {
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
582
|
+
let h = 0;
|
|
583
|
+
const djb2 = (s) => {
|
|
584
|
+
for (let i = 0; i < s.length; i++) h = (h << 5) + h + s.charCodeAt(i) | 0;
|
|
585
|
+
};
|
|
586
|
+
for (const e of allEntries.value) djb2(e.id);
|
|
587
|
+
h = h * 33 + allEntries.value.length | 0;
|
|
588
|
+
for (const e of allEdges.value) {
|
|
589
|
+
djb2(e.source);
|
|
590
|
+
djb2(e.target);
|
|
591
|
+
}
|
|
592
|
+
h = h * 33 + allEdges.value.length | 0;
|
|
593
|
+
return h;
|
|
391
594
|
});
|
|
392
595
|
function buildSimulation(oldPos = /* @__PURE__ */ new Map()) {
|
|
393
596
|
if (!d3.value) return;
|
|
394
597
|
const { forceSimulation, forceManyBody, forceCollide, forceLink, forceX, forceY } = d3.value;
|
|
395
598
|
const entries = allEntries.value;
|
|
599
|
+
const degree = degreeById.value;
|
|
396
600
|
const nodes = entries.map((entry) => {
|
|
397
|
-
const saved =
|
|
601
|
+
const saved = posByEntry.value.get(entry.id);
|
|
398
602
|
const old = oldPos.get(entry.id);
|
|
399
603
|
const seed = seedPos(entry.id);
|
|
400
|
-
const
|
|
401
|
-
const outgoing = allEdges.value.filter((e) => e.source === entry.id).length;
|
|
604
|
+
const neighborCount = degree.get(entry.id) ?? 0;
|
|
402
605
|
return {
|
|
403
606
|
id: entry.id,
|
|
404
607
|
label: entry.label,
|
|
405
608
|
type: entry.type,
|
|
406
609
|
color: entry.meta?.color,
|
|
407
610
|
icon: entry.meta?.icon,
|
|
408
|
-
childCount:
|
|
409
|
-
neighborCount
|
|
611
|
+
childCount: 0,
|
|
612
|
+
neighborCount,
|
|
410
613
|
x: saved?.x ?? old?.x ?? seed.x,
|
|
411
614
|
y: saved?.y ?? old?.y ?? seed.y,
|
|
412
615
|
vx: 0,
|
|
@@ -420,28 +623,102 @@ function buildSimulation(oldPos = /* @__PURE__ */ new Map()) {
|
|
|
420
623
|
pauseSim();
|
|
421
624
|
sim?.stop();
|
|
422
625
|
const p = SPACING_PRESETS[spacingLevel.value];
|
|
423
|
-
sim = forceSimulation(nodes).force("charge", forceManyBody().strength(p.charge)).force("x", forceX(0).strength(p.centerStrength)).force("y", forceY(0).strength(p.centerStrength)).force("link", forceLink(edges).id((dd) => dd.id).distance(p.linkDist).strength(0.5)).force("collide", forceCollide().radius((n) => nodeRadius(n) + p.collideExtra).strength(0.85)).alphaDecay(0.
|
|
626
|
+
sim = forceSimulation(nodes).force("charge", forceManyBody().strength(p.charge)).force("x", forceX(0).strength(p.centerStrength)).force("y", forceY(0).strength(p.centerStrength)).force("link", forceLink(edges).id((dd) => dd.id).distance(p.linkDist).strength(0.5)).force("collide", forceCollide().radius((n) => nodeRadius(n) + p.collideExtra).strength(0.85)).alphaDecay(0.04).velocityDecay(0.4).stop();
|
|
424
627
|
simNodes.value = nodes;
|
|
425
|
-
|
|
628
|
+
nodeById.clear();
|
|
629
|
+
for (const n of nodes) nodeById.set(n.id, n);
|
|
630
|
+
const hasUnpositioned = nodes.some((n) => !posByEntry.value.has(n.id) && !oldPos.has(n.id));
|
|
426
631
|
if (hasUnpositioned) {
|
|
427
|
-
|
|
632
|
+
for (const n of nodes) {
|
|
633
|
+
if (posByEntry.value.has(n.id) || oldPos.has(n.id)) {
|
|
634
|
+
n.fx = n.x;
|
|
635
|
+
n.fy = n.y;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
sim.alpha(1);
|
|
639
|
+
const ticks = Math.ceil(Math.log(sim.alphaMin()) / Math.log(1 - sim.alphaDecay()));
|
|
640
|
+
for (let i = 0; i < ticks; i++) sim.tick();
|
|
641
|
+
for (const n of nodes) {
|
|
642
|
+
if (!posByEntry.value.get(n.id)?.pinned) {
|
|
643
|
+
n.fx = null;
|
|
644
|
+
n.fy = null;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
renderTick.value++;
|
|
428
648
|
} else {
|
|
429
649
|
renderTick.value++;
|
|
430
650
|
}
|
|
431
651
|
}
|
|
432
652
|
let hasFitOnce = false;
|
|
433
653
|
watch(graphKey, () => {
|
|
434
|
-
|
|
435
|
-
|
|
654
|
+
if (!d3.value) return;
|
|
655
|
+
if (!sim) {
|
|
656
|
+
buildSimulation();
|
|
657
|
+
if (!hasFitOnce && simNodes.value.length > 0) {
|
|
658
|
+
hasFitOnce = true;
|
|
659
|
+
nextTick(fitView);
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const entries = allEntries.value;
|
|
664
|
+
const currentIds = new Set(simNodes.value.map((n) => n.id));
|
|
665
|
+
const newIds = new Set(entries.map((e) => e.id));
|
|
666
|
+
const entryMap = new Map(entries.map((e) => [e.id, e]));
|
|
667
|
+
const degree = degreeById.value;
|
|
668
|
+
const kept = simNodes.value.filter((n) => newIds.has(n.id));
|
|
669
|
+
for (const n of simNodes.value) {
|
|
670
|
+
if (!newIds.has(n.id)) nodeById.delete(n.id);
|
|
671
|
+
}
|
|
672
|
+
const added = [];
|
|
673
|
+
for (const entry of entries) {
|
|
674
|
+
if (!currentIds.has(entry.id)) {
|
|
675
|
+
const pos = spawnPosNearParent(entry);
|
|
676
|
+
const saved = posByEntry.value.get(entry.id);
|
|
677
|
+
const node = {
|
|
678
|
+
id: entry.id,
|
|
679
|
+
label: entry.label,
|
|
680
|
+
type: entry.type,
|
|
681
|
+
color: entry.meta?.color,
|
|
682
|
+
icon: entry.meta?.icon,
|
|
683
|
+
childCount: 0,
|
|
684
|
+
neighborCount: degree.get(entry.id) ?? 0,
|
|
685
|
+
x: pos.x,
|
|
686
|
+
y: pos.y,
|
|
687
|
+
vx: 0,
|
|
688
|
+
vy: 0,
|
|
689
|
+
fx: saved?.pinned ? saved.x : null,
|
|
690
|
+
fy: saved?.pinned ? saved.y : null
|
|
691
|
+
};
|
|
692
|
+
added.push(node);
|
|
693
|
+
nodeById.set(node.id, node);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
for (const node of kept) {
|
|
697
|
+
const entry = entryMap.get(node.id);
|
|
698
|
+
if (entry) {
|
|
699
|
+
node.label = entry.label;
|
|
700
|
+
node.type = entry.type;
|
|
701
|
+
node.color = entry.meta?.color;
|
|
702
|
+
node.icon = entry.meta?.icon;
|
|
703
|
+
node.neighborCount = degree.get(entry.id) ?? 0;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const allNodes = [...kept, ...added];
|
|
707
|
+
simNodes.value = allNodes;
|
|
708
|
+
sim.nodes(allNodes);
|
|
709
|
+
const nodeIdSet = new Set(allNodes.map((n) => n.id));
|
|
710
|
+
const edges = allEdges.value.filter((e) => nodeIdSet.has(e.source) && nodeIdSet.has(e.target)).map((e) => ({ source: e.source, target: e.target }));
|
|
711
|
+
sim.force("link").links(edges);
|
|
712
|
+
restartSim(added.length > 0 ? 0.3 : 0.1);
|
|
436
713
|
if (!hasFitOnce && simNodes.value.length > 0) {
|
|
437
714
|
hasFitOnce = true;
|
|
438
715
|
nextTick(fitView);
|
|
439
716
|
}
|
|
440
717
|
});
|
|
441
|
-
watch(
|
|
718
|
+
watch(() => tree.entries.value, () => {
|
|
442
719
|
let changed = false;
|
|
443
720
|
for (const node of simNodes.value) {
|
|
444
|
-
const entry =
|
|
721
|
+
const entry = entryById.value.get(node.id);
|
|
445
722
|
if (entry && (entry.label !== node.label || entry.type !== node.type || entry.meta?.color !== node.color || entry.meta?.icon !== node.icon)) {
|
|
446
723
|
node.label = entry.label;
|
|
447
724
|
node.type = entry.type;
|
|
@@ -451,6 +728,16 @@ watch(allEntries, () => {
|
|
|
451
728
|
}
|
|
452
729
|
}
|
|
453
730
|
if (changed) renderTick.value++;
|
|
731
|
+
for (const node of simNodes.value) {
|
|
732
|
+
const s = posByEntry.value.get(node.id);
|
|
733
|
+
if (s?.pinned && draggingId.value !== node.id) {
|
|
734
|
+
node.fx = s.x;
|
|
735
|
+
node.fy = s.y;
|
|
736
|
+
} else if (!s?.pinned && draggingId.value !== node.id && !remoteDragByNode.value.has(node.id)) {
|
|
737
|
+
node.fx = null;
|
|
738
|
+
node.fy = null;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
454
741
|
});
|
|
455
742
|
watch(childDoc, (doc) => {
|
|
456
743
|
if (doc && d3.value) {
|
|
@@ -472,76 +759,79 @@ watch(d3, (val) => {
|
|
|
472
759
|
}
|
|
473
760
|
}
|
|
474
761
|
});
|
|
475
|
-
watch(tree.entries, () => {
|
|
476
|
-
for (const node of simNodes.value) {
|
|
477
|
-
const s = getGraphPos(node.id);
|
|
478
|
-
if (s?.pinned && draggingId.value !== node.id) {
|
|
479
|
-
node.fx = s.x;
|
|
480
|
-
node.fy = s.y;
|
|
481
|
-
} else if (!s?.pinned && draggingId.value !== node.id && !remoteDragByNode.value.has(node.id)) {
|
|
482
|
-
node.fx = null;
|
|
483
|
-
node.fy = null;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
});
|
|
487
762
|
onBeforeUnmount(() => {
|
|
488
763
|
pauseSim();
|
|
489
764
|
sim?.stop();
|
|
490
765
|
if (savePosTimer) clearTimeout(savePosTimer);
|
|
491
766
|
if (zoomRafId) cancelAnimationFrame(zoomRafId);
|
|
767
|
+
if (pointerRafId !== null) cancelAnimationFrame(pointerRafId);
|
|
768
|
+
if (awarenessRafId !== null) cancelAnimationFrame(awarenessRafId);
|
|
769
|
+
if (syncTimer) clearTimeout(syncTimer);
|
|
770
|
+
detachSynced?.();
|
|
492
771
|
setLocalState({ "pos": null, "graph:dragging": null, "graph:selection": [] });
|
|
493
772
|
});
|
|
494
773
|
const draggingId = ref(null);
|
|
495
774
|
const dragStart = ref({ ex: 0, ey: 0 });
|
|
775
|
+
const dragOffset = ref({ dx: 0, dy: 0 });
|
|
496
776
|
const wasDragged = ref(false);
|
|
497
777
|
function onNodePointerDown(e, id) {
|
|
498
778
|
if (!props.editable) return;
|
|
499
779
|
e.stopPropagation();
|
|
500
|
-
const node =
|
|
780
|
+
const node = nodeById.get(id);
|
|
501
781
|
if (!node) return;
|
|
502
782
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
503
783
|
draggingId.value = id;
|
|
504
784
|
wasDragged.value = false;
|
|
505
785
|
dragStart.value = { ex: e.clientX, ey: e.clientY };
|
|
786
|
+
const p = svgCoord(e.clientX, e.clientY);
|
|
787
|
+
dragOffset.value = { dx: node.x - p.x, dy: node.y - p.y };
|
|
506
788
|
node.fx = node.x;
|
|
507
789
|
node.fy = node.y;
|
|
508
|
-
|
|
509
|
-
setLocalState({ "graph:dragging": { nodeId: id, x: node.x, y: node.y } });
|
|
790
|
+
queueLocalState({ "graph:dragging": { nodeId: id, x: node.x, y: node.y } });
|
|
510
791
|
}
|
|
511
792
|
function onNodePointerMove(e, id) {
|
|
793
|
+
if (!props.editable) return;
|
|
512
794
|
if (draggingId.value !== id) return;
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
795
|
+
pendingNodeMove = { clientX: e.clientX, clientY: e.clientY, pointerId: e.pointerId };
|
|
796
|
+
schedulePointerFlush();
|
|
797
|
+
}
|
|
798
|
+
function flushNodePointerMove(m) {
|
|
799
|
+
const id = draggingId.value;
|
|
800
|
+
if (!id) return;
|
|
801
|
+
const dist = Math.hypot(m.clientX - dragStart.value.ex, m.clientY - dragStart.value.ey);
|
|
802
|
+
if (dist > 4 && !wasDragged.value) {
|
|
803
|
+
wasDragged.value = true;
|
|
804
|
+
restartSim(0.03);
|
|
522
805
|
}
|
|
806
|
+
const p = svgCoord(m.clientX, m.clientY);
|
|
807
|
+
const node = nodeById.get(id);
|
|
808
|
+
if (!node) return;
|
|
809
|
+
node.fx = p.x + dragOffset.value.dx;
|
|
810
|
+
node.fy = p.y + dragOffset.value.dy;
|
|
811
|
+
renderTick.value++;
|
|
812
|
+
queueLocalState({ "graph:dragging": { nodeId: id, x: node.fx, y: node.fy } });
|
|
523
813
|
}
|
|
524
814
|
function onNodePointerUp(_e, id) {
|
|
525
815
|
onDragEnd(id);
|
|
526
816
|
}
|
|
527
817
|
function onDragEnd(id) {
|
|
528
|
-
const node =
|
|
818
|
+
const node = nodeById.get(id);
|
|
529
819
|
if (node) {
|
|
530
|
-
const isPinned =
|
|
820
|
+
const isPinned = posByEntry.value.get(id)?.pinned ?? false;
|
|
531
821
|
tree.updateMeta(id, { graphX: node.x, graphY: node.y, graphPinned: isPinned });
|
|
532
822
|
if (!isPinned) {
|
|
533
823
|
node.fx = null;
|
|
534
824
|
node.fy = null;
|
|
535
825
|
}
|
|
536
826
|
}
|
|
537
|
-
|
|
827
|
+
queueLocalState({ "graph:dragging": null });
|
|
538
828
|
draggingId.value = null;
|
|
539
|
-
restartSim();
|
|
829
|
+
if (wasDragged.value) restartSim(0.03);
|
|
540
830
|
}
|
|
541
831
|
const selectedNodeId = ref(null);
|
|
542
832
|
function selectNode(id) {
|
|
543
833
|
selectedNodeId.value = id;
|
|
544
|
-
|
|
834
|
+
queueLocalState({ "graph:selection": id ? [id] : [] });
|
|
545
835
|
}
|
|
546
836
|
function onSvgClick() {
|
|
547
837
|
selectNode(null);
|
|
@@ -605,7 +895,7 @@ useEventListener(window, "pointerdown", () => {
|
|
|
605
895
|
});
|
|
606
896
|
const ctxItems = computed(() => {
|
|
607
897
|
if (ctxNode.value) {
|
|
608
|
-
const isPinned =
|
|
898
|
+
const isPinned = posByEntry.value.get(ctxNode.value.id)?.pinned;
|
|
609
899
|
return [
|
|
610
900
|
[
|
|
611
901
|
{
|
|
@@ -653,9 +943,9 @@ const ctxItems = computed(() => {
|
|
|
653
943
|
});
|
|
654
944
|
function togglePin(id) {
|
|
655
945
|
if (!props.editable) return;
|
|
656
|
-
const node =
|
|
946
|
+
const node = nodeById.get(id);
|
|
657
947
|
if (!node) return;
|
|
658
|
-
const isPinned =
|
|
948
|
+
const isPinned = posByEntry.value.get(id)?.pinned;
|
|
659
949
|
tree.updateMeta(id, { graphX: node.x, graphY: node.y, graphPinned: !isPinned });
|
|
660
950
|
node.fx = isPinned ? null : node.x;
|
|
661
951
|
node.fy = isPinned ? null : node.y;
|
|
@@ -723,6 +1013,13 @@ const DOC_TYPE_INITIALS = {
|
|
|
723
1013
|
function docTypeInitial(type) {
|
|
724
1014
|
return DOC_TYPE_INITIALS[type ?? "doc"] ?? "?";
|
|
725
1015
|
}
|
|
1016
|
+
const isLoading = computed(() => {
|
|
1017
|
+
if (d3Error.value) return false;
|
|
1018
|
+
if (!d3.value) return true;
|
|
1019
|
+
if (!childDoc.value) return true;
|
|
1020
|
+
if (!providerSynced.value && simNodes.value.length === 0) return true;
|
|
1021
|
+
return false;
|
|
1022
|
+
});
|
|
726
1023
|
defineExpose({ connectedUsers });
|
|
727
1024
|
</script>
|
|
728
1025
|
|
|
@@ -745,7 +1042,21 @@ defineExpose({ connectedUsers });
|
|
|
745
1042
|
</p>
|
|
746
1043
|
</div>
|
|
747
1044
|
|
|
748
|
-
|
|
1045
|
+
<!-- Loading skeleton — shown while d3/provider/initial sync are pending -->
|
|
1046
|
+
<div
|
|
1047
|
+
v-else-if="isLoading"
|
|
1048
|
+
class="flex flex-col items-center justify-center h-full gap-3 text-center"
|
|
1049
|
+
>
|
|
1050
|
+
<UIcon
|
|
1051
|
+
name="i-lucide-loader-circle"
|
|
1052
|
+
class="size-8 text-(--ui-text-dimmed) animate-spin"
|
|
1053
|
+
/>
|
|
1054
|
+
<p class="text-xs text-(--ui-text-dimmed)">
|
|
1055
|
+
{{ locale.loading ?? "Loading graph\u2026" }}
|
|
1056
|
+
</p>
|
|
1057
|
+
</div>
|
|
1058
|
+
|
|
1059
|
+
<template v-else>
|
|
749
1060
|
<!-- Floating toolbar (bottom-center) -->
|
|
750
1061
|
<div class="graph-toolbar-bar">
|
|
751
1062
|
<!-- Create -->
|
|
@@ -873,7 +1184,7 @@ defineExpose({ connectedUsers });
|
|
|
873
1184
|
</UPopover>
|
|
874
1185
|
</div>
|
|
875
1186
|
|
|
876
|
-
<!-- Empty state -->
|
|
1187
|
+
<!-- Empty state — only reached once d3+provider+initial sync are all settled -->
|
|
877
1188
|
<div
|
|
878
1189
|
v-if="renderedNodes.length === 0"
|
|
879
1190
|
class="flex flex-col items-center justify-center h-full gap-3 text-center"
|
|
@@ -899,7 +1210,8 @@ defineExpose({ connectedUsers });
|
|
|
899
1210
|
ref="svgRef"
|
|
900
1211
|
class="w-full h-full select-none"
|
|
901
1212
|
:class="isPanning ? 'cursor-grabbing' : 'cursor-default'"
|
|
902
|
-
style="touch-action: none"
|
|
1213
|
+
style="touch-action: none; contain: layout style paint"
|
|
1214
|
+
overflow="visible"
|
|
903
1215
|
:viewBox="viewBoxStr"
|
|
904
1216
|
@wheel.prevent="onWheel"
|
|
905
1217
|
@pointerdown="onSvgPointerDown"
|
|
@@ -952,24 +1264,19 @@ defineExpose({ connectedUsers });
|
|
|
952
1264
|
fill="url(#gdots)"
|
|
953
1265
|
/>
|
|
954
1266
|
|
|
955
|
-
<!-- Edges layer -->
|
|
956
|
-
<
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
stroke="var(--ui-border)"
|
|
965
|
-
:stroke-width="1.2 * edgeScale"
|
|
966
|
-
opacity="0.6"
|
|
967
|
-
/>
|
|
968
|
-
</g>
|
|
1267
|
+
<!-- Edges layer — one batched <path> for all visible structural edges -->
|
|
1268
|
+
<path
|
|
1269
|
+
v-if="batchedEdgePathD"
|
|
1270
|
+
:d="batchedEdgePathD"
|
|
1271
|
+
fill="none"
|
|
1272
|
+
stroke="var(--ui-text-muted)"
|
|
1273
|
+
:stroke-width="1.8 * edgeScale"
|
|
1274
|
+
opacity="0.9"
|
|
1275
|
+
/>
|
|
969
1276
|
|
|
970
|
-
<!-- Nodes layer -->
|
|
1277
|
+
<!-- Nodes layer (viewport-culled) -->
|
|
971
1278
|
<g
|
|
972
|
-
v-for="node in
|
|
1279
|
+
v-for="node in visibleNodes"
|
|
973
1280
|
:key="node.id"
|
|
974
1281
|
data-node
|
|
975
1282
|
:transform="`translate(${node.x}, ${node.y})`"
|
|
@@ -1024,7 +1331,7 @@ defineExpose({ connectedUsers });
|
|
|
1024
1331
|
|
|
1025
1332
|
<!-- Pinned indicator -->
|
|
1026
1333
|
<circle
|
|
1027
|
-
v-if="
|
|
1334
|
+
v-if="posByEntry.get(node.id)?.pinned"
|
|
1028
1335
|
:cx="nodeRadius(node) - 5"
|
|
1029
1336
|
:cy="-(nodeRadius(node) - 5)"
|
|
1030
1337
|
r="4"
|
|
@@ -1033,29 +1340,18 @@ defineExpose({ connectedUsers });
|
|
|
1033
1340
|
stroke-width="1"
|
|
1034
1341
|
/>
|
|
1035
1342
|
|
|
1036
|
-
<!-- Custom icon -->
|
|
1037
|
-
<
|
|
1038
|
-
v-if="node.icon"
|
|
1039
|
-
:
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
>
|
|
1045
|
-
<div
|
|
1046
|
-
xmlns="http://www.w3.org/1999/xhtml"
|
|
1047
|
-
style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center"
|
|
1048
|
-
>
|
|
1049
|
-
<UIcon
|
|
1050
|
-
:name="`i-lucide-${node.icon}`"
|
|
1051
|
-
style="color: white; opacity: 0.9; width: 100%; height: 100%"
|
|
1052
|
-
/>
|
|
1053
|
-
</div>
|
|
1054
|
-
</foreignObject>
|
|
1343
|
+
<!-- Custom icon — native SVG (no foreignObject) -->
|
|
1344
|
+
<g
|
|
1345
|
+
v-if="node.icon && getIconSVG(node.icon)"
|
|
1346
|
+
:transform="`translate(${-nodeRadius(node) * 0.45}, ${-nodeRadius(node) * 0.45}) scale(${nodeRadius(node) * 0.9 / 24})`"
|
|
1347
|
+
opacity="0.9"
|
|
1348
|
+
style="pointer-events: none"
|
|
1349
|
+
v-html="getIconSVG(node.icon)"
|
|
1350
|
+
/>
|
|
1055
1351
|
|
|
1056
1352
|
<!-- Fallback: doc-type initial -->
|
|
1057
1353
|
<text
|
|
1058
|
-
v-else
|
|
1354
|
+
v-else-if="!node.icon"
|
|
1059
1355
|
text-anchor="middle"
|
|
1060
1356
|
dominant-baseline="central"
|
|
1061
1357
|
:font-size="nodeRadius(node) * 0.5"
|