@abraca/nuxt 2.0.11 → 2.4.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 (135) hide show
  1. package/dist/module.d.mts +68 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +99 -4
  4. package/dist/runtime/components/ACodeEditor.d.vue.ts +26 -0
  5. package/dist/runtime/components/ACodeEditor.vue +268 -0
  6. package/dist/runtime/components/ACodeEditor.vue.d.ts +26 -0
  7. package/dist/runtime/components/ADocumentTree.vue +52 -20
  8. package/dist/runtime/components/AEditor.d.vue.ts +20 -13
  9. package/dist/runtime/components/AEditor.vue +55 -2
  10. package/dist/runtime/components/AEditor.vue.d.ts +20 -13
  11. package/dist/runtime/components/ANodePanel.vue +64 -60
  12. package/dist/runtime/components/ANotificationBell.d.vue.ts +1 -1
  13. package/dist/runtime/components/ANotificationBell.vue.d.ts +1 -1
  14. package/dist/runtime/components/ASpaceFormModal.d.vue.ts +2 -2
  15. package/dist/runtime/components/ASpaceFormModal.vue.d.ts +2 -2
  16. package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
  17. package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
  18. package/dist/runtime/components/aware/APresenceBlobs.d.vue.ts +29 -1
  19. package/dist/runtime/components/aware/APresenceBlobs.vue +54 -8
  20. package/dist/runtime/components/aware/APresenceBlobs.vue.d.ts +29 -1
  21. package/dist/runtime/components/aware/APresenceCursors.d.vue.ts +11 -0
  22. package/dist/runtime/components/aware/APresenceCursors.vue +74 -9
  23. package/dist/runtime/components/aware/APresenceCursors.vue.d.ts +11 -0
  24. package/dist/runtime/components/aware/AToggleGroup.d.vue.ts +28 -13
  25. package/dist/runtime/components/aware/AToggleGroup.vue +56 -20
  26. package/dist/runtime/components/aware/AToggleGroup.vue.d.ts +28 -13
  27. package/dist/runtime/components/docs/ADocsNavigation.d.vue.ts +1 -1
  28. package/dist/runtime/components/docs/ADocsNavigation.vue.d.ts +1 -1
  29. package/dist/runtime/components/docs/ADocsSearch.d.vue.ts +1 -1
  30. package/dist/runtime/components/docs/ADocsSearch.vue.d.ts +1 -1
  31. package/dist/runtime/components/docs/ADocsSearchButton.d.vue.ts +1 -1
  32. package/dist/runtime/components/docs/ADocsSearchButton.vue.d.ts +1 -1
  33. package/dist/runtime/components/docs/ADocsToc.d.vue.ts +2 -2
  34. package/dist/runtime/components/docs/ADocsToc.vue.d.ts +2 -2
  35. package/dist/runtime/components/editor/AEditorRedoButton.d.vue.ts +1 -1
  36. package/dist/runtime/components/editor/AEditorRedoButton.vue.d.ts +1 -1
  37. package/dist/runtime/components/editor/AEditorUndoButton.d.vue.ts +1 -1
  38. package/dist/runtime/components/editor/AEditorUndoButton.vue.d.ts +1 -1
  39. package/dist/runtime/components/editor/AFileGlbViewer.vue +27 -10
  40. package/dist/runtime/components/editor/ANodeInlineLabel.d.vue.ts +1 -1
  41. package/dist/runtime/components/editor/ANodeInlineLabel.vue.d.ts +1 -1
  42. package/dist/runtime/components/registry/APluginBrowser.d.vue.ts +23 -0
  43. package/dist/runtime/components/registry/APluginBrowser.vue +155 -0
  44. package/dist/runtime/components/registry/APluginBrowser.vue.d.ts +23 -0
  45. package/dist/runtime/components/registry/APluginCapabilityDialog.d.vue.ts +17 -0
  46. package/dist/runtime/components/registry/APluginCapabilityDialog.vue +159 -0
  47. package/dist/runtime/components/registry/APluginCapabilityDialog.vue.d.ts +17 -0
  48. package/dist/runtime/components/registry/APluginCard.d.vue.ts +20 -0
  49. package/dist/runtime/components/registry/APluginCard.vue +91 -0
  50. package/dist/runtime/components/registry/APluginCard.vue.d.ts +20 -0
  51. package/dist/runtime/components/registry/APluginDetail.d.vue.ts +18 -0
  52. package/dist/runtime/components/registry/APluginDetail.vue +252 -0
  53. package/dist/runtime/components/registry/APluginDetail.vue.d.ts +18 -0
  54. package/dist/runtime/components/renderers/ACodeRenderer.d.vue.ts +15 -0
  55. package/dist/runtime/components/renderers/ACodeRenderer.vue +68 -0
  56. package/dist/runtime/components/renderers/ACodeRenderer.vue.d.ts +15 -0
  57. package/dist/runtime/components/renderers/AGraphRenderer.vue +416 -120
  58. package/dist/runtime/components/renderers/AProseRenderer.d.vue.ts +2 -2
  59. package/dist/runtime/components/renderers/AProseRenderer.vue.d.ts +2 -2
  60. package/dist/runtime/components/renderers/sheets/ASheetsToolbar.d.vue.ts +4 -4
  61. package/dist/runtime/components/renderers/sheets/ASheetsToolbar.vue.d.ts +4 -4
  62. package/dist/runtime/components/shell/ABreadcrumbForDoc.d.vue.ts +11 -0
  63. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue +16 -0
  64. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue.d.ts +11 -0
  65. package/dist/runtime/components/shell/ASettingsSection.d.vue.ts +35 -0
  66. package/dist/runtime/components/shell/ASettingsSection.vue +26 -0
  67. package/dist/runtime/components/shell/ASettingsSection.vue.d.ts +35 -0
  68. package/dist/runtime/components/shell/ASidebar.d.vue.ts +1 -1
  69. package/dist/runtime/components/shell/ASidebar.vue.d.ts +1 -1
  70. package/dist/runtime/components/shell/AUserMenu.d.vue.ts +3 -0
  71. package/dist/runtime/components/shell/AUserMenu.vue +4 -0
  72. package/dist/runtime/components/shell/AUserMenu.vue.d.ts +3 -0
  73. package/dist/runtime/composables/useAbracadabraSchema.d.ts +83 -0
  74. package/dist/runtime/composables/useAbracadabraSchema.js +52 -0
  75. package/dist/runtime/composables/useAggregatedPresence.d.ts +1 -6
  76. package/dist/runtime/composables/useCalendarView.d.ts +1 -1
  77. package/dist/runtime/composables/useChat.js +1 -0
  78. package/dist/runtime/composables/useDocBreadcrumb.d.ts +21 -0
  79. package/dist/runtime/composables/useDocBreadcrumb.js +33 -0
  80. package/dist/runtime/composables/useDocEntryTyped.d.ts +60 -0
  81. package/dist/runtime/composables/useDocEntryTyped.js +70 -0
  82. package/dist/runtime/composables/useEditorDragHandle.js +18 -0
  83. package/dist/runtime/composables/useEditorSuggestions.js +2 -1
  84. package/dist/runtime/composables/useInstalledPlugins.d.ts +3 -21
  85. package/dist/runtime/composables/useInstalledPlugins.js +2 -12
  86. package/dist/runtime/composables/useMetaMenuItems.d.ts +21 -0
  87. package/dist/runtime/composables/useMetaMenuItems.js +115 -0
  88. package/dist/runtime/composables/useMetaValidator.d.ts +27 -0
  89. package/dist/runtime/composables/useMetaValidator.js +10 -0
  90. package/dist/runtime/composables/usePluginCatalog.d.ts +161 -0
  91. package/dist/runtime/composables/usePluginCatalog.js +234 -0
  92. package/dist/runtime/composables/useQuery.d.ts +79 -0
  93. package/dist/runtime/composables/useQuery.js +97 -0
  94. package/dist/runtime/composables/useSpaces.js +4 -5
  95. package/dist/runtime/composables/useTableView.d.ts +3 -3
  96. package/dist/runtime/composables/useTypedDoc.d.ts +97 -0
  97. package/dist/runtime/composables/useTypedDoc.js +114 -0
  98. package/dist/runtime/composables/useWebRTC.js +44 -5
  99. package/dist/runtime/extensions/document-meta.js +5 -0
  100. package/dist/runtime/extensions/timeline.d.ts +11 -0
  101. package/dist/runtime/extensions/timeline.js +52 -0
  102. package/dist/runtime/extensions/views/DocumentMetaView.d.vue.ts +4 -0
  103. package/dist/runtime/extensions/views/DocumentMetaView.vue +63 -0
  104. package/dist/runtime/extensions/views/DocumentMetaView.vue.d.ts +4 -0
  105. package/dist/runtime/extensions/views/TimelineItemView.d.vue.ts +4 -0
  106. package/dist/runtime/extensions/views/TimelineItemView.vue +131 -0
  107. package/dist/runtime/extensions/views/TimelineItemView.vue.d.ts +4 -0
  108. package/dist/runtime/extensions/views/TimelineView.d.vue.ts +9 -0
  109. package/dist/runtime/extensions/views/TimelineView.vue +29 -0
  110. package/dist/runtime/extensions/views/TimelineView.vue.d.ts +9 -0
  111. package/dist/runtime/locale.d.ts +2 -0
  112. package/dist/runtime/locale.js +2 -0
  113. package/dist/runtime/plugin-abracadabra.client.js +107 -6
  114. package/dist/runtime/plugin-registry.d.ts +11 -30
  115. package/dist/runtime/plugin-registry.js +2 -82
  116. package/dist/runtime/plugins/core.plugin.js +10 -4
  117. package/dist/runtime/server/api/_abracadabra/spaces.get.d.ts +1 -1
  118. package/dist/runtime/server/plugins/abracadabra-service.js +28 -0
  119. package/dist/runtime/server/utils/docCache.js +24 -3
  120. package/dist/runtime/server/utils/schemaServerSupport.d.ts +52 -0
  121. package/dist/runtime/server/utils/schemaServerSupport.js +51 -0
  122. package/dist/runtime/types.d.ts +63 -46
  123. package/dist/runtime/utils/docTypes.d.ts +15 -0
  124. package/dist/runtime/utils/docTypes.js +20 -0
  125. package/dist/runtime/utils/loadCodeMirror.d.ts +32 -0
  126. package/dist/runtime/utils/loadCodeMirror.js +65 -0
  127. package/dist/runtime/utils/loadThree.d.ts +18 -0
  128. package/dist/runtime/utils/loadThree.js +46 -0
  129. package/dist/runtime/utils/markdownToYjs.d.ts +1 -23
  130. package/dist/runtime/utils/markdownToYjs.js +5 -440
  131. package/dist/runtime/utils/schemaSupport.d.ts +60 -0
  132. package/dist/runtime/utils/schemaSupport.js +40 -0
  133. package/dist/runtime/utils/yjsConvert.d.ts +1 -14
  134. package/dist/runtime/utils/yjsConvert.js +5 -331
  135. package/package.json +86 -21
@@ -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
- function getGraphPos(id) {
54
- const entry = tree.entries.value.find((e) => e.id === id);
55
- if (entry?.meta?.graphX !== void 0 && entry?.meta?.graphY !== void 0) {
56
- return {
57
- x: entry.meta.graphX,
58
- y: entry.meta.graphY,
59
- pinned: entry.meta.graphPinned ?? false
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 void 0;
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
- if (!draggingId.value && activePointers.size === 1) {
136
- setLocalState({ pos: svgCoord(e.clientX, e.clientY) });
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 = vb.value.w / rect.width;
158
- const sy = vb.value.h / rect.height;
159
- const nx = panStart.value.vx - (e.clientX - panStart.value.ex) * sx;
160
- const ny = panStart.value.vy - (e.clientY - panStart.value.ey) * sy;
161
- vb.value = { ...vb.value, x: nx, y: ny };
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
- setLocalState({ pos: null });
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 - 8) / 8));
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
- return simNodes.value.map((n) => ({ ...n }));
430
+ void iconLoadTick.value;
431
+ return simNodes.value;
318
432
  });
319
- const renderedLinks = computed(() => {
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
- for (const node of simNodes.value) {
332
- if (remoteDragByNode.value.has(node.id)) continue;
333
- if (draggingId.value === node.id) continue;
334
- const existing = getGraphPos(node.id);
335
- const pinned = existing?.pinned ?? false;
336
- if (existing && Math.abs(existing.x - node.x) < 1 && Math.abs(existing.y - node.y) < 1 && existing.pinned === pinned) continue;
337
- tree.updateMeta(node.id, { graphX: node.x, graphY: node.y, graphPinned: pinned });
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 = simNodes.value.find((nd) => nd.id === nodeId);
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
- renderTick.value++;
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.12) {
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 r = 40 + (Math.imul(h, 1664525) >>> 0) / 4294967295 * 100;
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
- return allEntries.value.filter((e) => e.parentId !== null && nodeIds.has(e.parentId)).map((e) => ({ source: e.parentId, target: e.id }));
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
- const ids = [...allEntries.value].map((e) => e.id).sort().join(",");
389
- const edges = [...allEdges.value].map((e) => `${e.source}>${e.target}`).sort().join(",");
390
- return ids + "|" + edges;
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 = getGraphPos(entry.id);
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 incoming = allEdges.value.filter((e) => e.target === entry.id).length;
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: tree.childrenOf(entry.id).length,
409
- neighborCount: incoming + outgoing,
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.025).velocityDecay(0.55).stop();
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
- const hasUnpositioned = nodes.some((n) => !getGraphPos(n.id) && !oldPos.has(n.id));
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
- resumeSim();
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
- const oldPos = new Map(simNodes.value.map((n) => [n.id, { x: n.x, y: n.y }]));
435
- buildSimulation(oldPos);
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(allEntries, () => {
718
+ watch(() => tree.entries.value, () => {
442
719
  let changed = false;
443
720
  for (const node of simNodes.value) {
444
- const entry = allEntries.value.find((e) => e.id === node.id);
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 = simNodes.value.find((n) => n.id === id);
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
- restartSim();
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
- const dist = Math.hypot(e.clientX - dragStart.value.ex, e.clientY - dragStart.value.ey);
514
- if (dist > 4) wasDragged.value = true;
515
- const p = svgCoord(e.clientX, e.clientY);
516
- const node = simNodes.value.find((n) => n.id === id);
517
- if (node) {
518
- node.fx = p.x;
519
- node.fy = p.y;
520
- renderTick.value++;
521
- setLocalState({ "graph:dragging": { nodeId: id, x: p.x, y: p.y } });
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 = simNodes.value.find((n) => n.id === id);
818
+ const node = nodeById.get(id);
529
819
  if (node) {
530
- const isPinned = getGraphPos(id)?.pinned ?? false;
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
- setLocalState({ "graph:dragging": null });
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
- setLocalState({ "graph:selection": id ? [id] : [] });
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 = getGraphPos(ctxNode.value.id)?.pinned;
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 = simNodes.value.find((n) => n.id === id);
946
+ const node = nodeById.get(id);
657
947
  if (!node) return;
658
- const isPinned = getGraphPos(id)?.pinned;
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
- <template v-else-if="d3">
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
- <g>
957
- <line
958
- v-for="(link, i) in renderedLinks"
959
- :key="i"
960
- :x1="link.source.x"
961
- :y1="link.source.y"
962
- :x2="link.target.x"
963
- :y2="link.target.y"
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 renderedNodes"
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="getGraphPos(node.id)?.pinned"
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
- <foreignObject
1038
- v-if="node.icon"
1039
- :x="-nodeRadius(node) * 0.45"
1040
- :y="-nodeRadius(node) * 0.45"
1041
- :width="nodeRadius(node) * 0.9"
1042
- :height="nodeRadius(node) * 0.9"
1043
- style="pointer-events: none; overflow: visible"
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"