@abraca/nuxt 0.2.0 → 0.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.
Files changed (152) hide show
  1. package/dist/module.d.mts +46 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +95 -2
  4. package/dist/runtime/assets/editor.css +1 -0
  5. package/dist/runtime/components/ACommandPalette.vue +4 -1
  6. package/dist/runtime/components/ADocRenderer.d.vue.ts +29 -0
  7. package/dist/runtime/components/ADocRenderer.vue +99 -0
  8. package/dist/runtime/components/ADocRenderer.vue.d.ts +29 -0
  9. package/dist/runtime/components/ADocTypeSelect.vue +4 -1
  10. package/dist/runtime/components/ADocumentTree.vue +78 -19
  11. package/dist/runtime/components/AEditor.d.vue.ts +9 -4
  12. package/dist/runtime/components/AEditor.vue +102 -7
  13. package/dist/runtime/components/AEditor.vue.d.ts +9 -4
  14. package/dist/runtime/components/AIconPicker.vue +8 -2
  15. package/dist/runtime/components/ANodePanel.vue +100 -61
  16. package/dist/runtime/components/ANotifications.vue +35 -8
  17. package/dist/runtime/components/APermissionGuard.vue +3 -1
  18. package/dist/runtime/components/APresence.vue +14 -3
  19. package/dist/runtime/components/AProvider.vue +7 -1
  20. package/dist/runtime/components/AVoiceBar.vue +57 -15
  21. package/dist/runtime/components/AVoiceTile.vue +4 -1
  22. package/dist/runtime/components/aware/AArea.vue +1 -1
  23. package/dist/runtime/components/aware/AAvatar.vue +85 -16
  24. package/dist/runtime/components/aware/AButton.vue +5 -1
  25. package/dist/runtime/components/aware/ACursorLabel.vue +5 -1
  26. package/dist/runtime/components/aware/ADocBadge.vue +4 -1
  27. package/dist/runtime/components/aware/AFacepile.vue +13 -3
  28. package/dist/runtime/components/aware/AInput.vue +5 -1
  29. package/dist/runtime/components/aware/ATextarea.vue +5 -1
  30. package/dist/runtime/components/aware/AUserList.vue +8 -2
  31. package/dist/runtime/components/renderers/ACalendarRenderer.d.vue.ts +12 -1
  32. package/dist/runtime/components/renderers/ACalendarRenderer.vue +388 -114
  33. package/dist/runtime/components/renderers/ACalendarRenderer.vue.d.ts +12 -1
  34. package/dist/runtime/components/renderers/ACallRenderer.d.vue.ts +13 -0
  35. package/dist/runtime/components/renderers/ACallRenderer.vue +169 -0
  36. package/dist/runtime/components/renderers/ACallRenderer.vue.d.ts +13 -0
  37. package/dist/runtime/components/renderers/AChecklistRenderer.d.vue.ts +19 -0
  38. package/dist/runtime/components/renderers/AChecklistRenderer.vue +581 -0
  39. package/dist/runtime/components/renderers/AChecklistRenderer.vue.d.ts +19 -0
  40. package/dist/runtime/components/renderers/ADashboardRenderer.d.vue.ts +19 -0
  41. package/dist/runtime/components/renderers/ADashboardRenderer.vue +1372 -0
  42. package/dist/runtime/components/renderers/ADashboardRenderer.vue.d.ts +19 -0
  43. package/dist/runtime/components/renderers/AGalleryCoverImage.d.vue.ts +8 -0
  44. package/dist/runtime/components/renderers/AGalleryCoverImage.vue +60 -0
  45. package/dist/runtime/components/renderers/AGalleryCoverImage.vue.d.ts +8 -0
  46. package/dist/runtime/components/renderers/AGalleryRenderer.d.vue.ts +12 -1
  47. package/dist/runtime/components/renderers/AGalleryRenderer.vue +221 -55
  48. package/dist/runtime/components/renderers/AGalleryRenderer.vue.d.ts +12 -1
  49. package/dist/runtime/components/renderers/AGraphRenderer.d.vue.ts +19 -0
  50. package/dist/runtime/components/renderers/AGraphRenderer.vue +1027 -0
  51. package/dist/runtime/components/renderers/AGraphRenderer.vue.d.ts +19 -0
  52. package/dist/runtime/components/renderers/AKanbanRenderer.d.vue.ts +13 -1
  53. package/dist/runtime/components/renderers/AKanbanRenderer.vue +474 -140
  54. package/dist/runtime/components/renderers/AKanbanRenderer.vue.d.ts +13 -1
  55. package/dist/runtime/components/renderers/AMapRenderer.d.vue.ts +19 -0
  56. package/dist/runtime/components/renderers/AMapRenderer.vue +1622 -0
  57. package/dist/runtime/components/renderers/AMapRenderer.vue.d.ts +19 -0
  58. package/dist/runtime/components/renderers/AOutlineRenderer.d.vue.ts +12 -1
  59. package/dist/runtime/components/renderers/AOutlineRenderer.vue +294 -134
  60. package/dist/runtime/components/renderers/AOutlineRenderer.vue.d.ts +12 -1
  61. package/dist/runtime/components/renderers/ATableRenderer.d.vue.ts +12 -1
  62. package/dist/runtime/components/renderers/ATableRenderer.vue +437 -145
  63. package/dist/runtime/components/renderers/ATableRenderer.vue.d.ts +12 -1
  64. package/dist/runtime/components/renderers/ATimelineRenderer.d.vue.ts +19 -0
  65. package/dist/runtime/components/renderers/ATimelineRenderer.vue +446 -0
  66. package/dist/runtime/components/renderers/ATimelineRenderer.vue.d.ts +19 -0
  67. package/dist/runtime/composables/useAwareness.js +5 -0
  68. package/dist/runtime/composables/useBroadcastSync.d.ts +18 -0
  69. package/dist/runtime/composables/useBroadcastSync.js +26 -0
  70. package/dist/runtime/composables/useChat.js +4 -2
  71. package/dist/runtime/composables/useChatUsers.js +2 -1
  72. package/dist/runtime/composables/useCommandPalette.js +62 -3
  73. package/dist/runtime/composables/useConnectionStatus.js +7 -0
  74. package/dist/runtime/composables/useDevicePairing.d.ts +58 -0
  75. package/dist/runtime/composables/useDevicePairing.js +108 -0
  76. package/dist/runtime/composables/useDocExport.d.ts +5 -0
  77. package/dist/runtime/composables/useDocExport.js +2 -2
  78. package/dist/runtime/composables/useDocImport.js +4 -3
  79. package/dist/runtime/composables/useDocSeo.d.ts +20 -0
  80. package/dist/runtime/composables/useDocSeo.js +44 -0
  81. package/dist/runtime/composables/useDocSlugs.d.ts +7 -0
  82. package/dist/runtime/composables/useDocSlugs.js +20 -0
  83. package/dist/runtime/composables/useDocTree.d.ts +34 -0
  84. package/dist/runtime/composables/useDocTree.js +35 -0
  85. package/dist/runtime/composables/useEditorDragHandle.js +2 -1
  86. package/dist/runtime/composables/useEditorMentions.js +4 -2
  87. package/dist/runtime/composables/useEditorSuggestions.d.ts +1 -0
  88. package/dist/runtime/composables/useEditorSuggestions.js +9 -2
  89. package/dist/runtime/composables/useEditorToolbar.js +2 -1
  90. package/dist/runtime/composables/useFileIndex.js +2 -1
  91. package/dist/runtime/composables/useFileTransfer.d.ts +112 -0
  92. package/dist/runtime/composables/useFileTransfer.js +171 -0
  93. package/dist/runtime/composables/useFollowUser.js +2 -1
  94. package/dist/runtime/composables/useInvites.d.ts +56 -0
  95. package/dist/runtime/composables/useInvites.js +77 -0
  96. package/dist/runtime/composables/useNodePanel.d.ts +14 -0
  97. package/dist/runtime/composables/useNodePanel.js +52 -0
  98. package/dist/runtime/composables/useNotifications.js +4 -2
  99. package/dist/runtime/composables/usePasskeyAccounts.js +4 -2
  100. package/dist/runtime/composables/useSearchIndex.d.ts +1 -0
  101. package/dist/runtime/composables/useSearchIndex.js +13 -5
  102. package/dist/runtime/composables/useServerInfo.d.ts +31 -0
  103. package/dist/runtime/composables/useServerInfo.js +80 -0
  104. package/dist/runtime/composables/useSlugRoute.d.ts +6 -0
  105. package/dist/runtime/composables/useSlugRoute.js +19 -0
  106. package/dist/runtime/composables/useSpaces.d.ts +37 -0
  107. package/dist/runtime/composables/useSpaces.js +83 -0
  108. package/dist/runtime/composables/useTouchDrag.d.ts +34 -0
  109. package/dist/runtime/composables/useTouchDrag.js +191 -0
  110. package/dist/runtime/composables/useTrash.d.ts +1 -1
  111. package/dist/runtime/composables/useTrash.js +6 -3
  112. package/dist/runtime/composables/useWebRTC.d.ts +50 -0
  113. package/dist/runtime/composables/useWebRTC.js +177 -0
  114. package/dist/runtime/extensions/meta-field.d.ts +4 -1
  115. package/dist/runtime/extensions/steps.js +1 -1
  116. package/dist/runtime/extensions/views/AccordionItemView.vue +13 -3
  117. package/dist/runtime/extensions/views/AccordionView.vue +4 -1
  118. package/dist/runtime/extensions/views/BadgeView.vue +11 -2
  119. package/dist/runtime/extensions/views/CalloutView.vue +4 -1
  120. package/dist/runtime/extensions/views/CardGroupView.vue +4 -1
  121. package/dist/runtime/extensions/views/CardView.vue +17 -3
  122. package/dist/runtime/extensions/views/CodeGroupView.vue +4 -1
  123. package/dist/runtime/extensions/views/CollapsibleView.vue +8 -2
  124. package/dist/runtime/extensions/views/FileNodeView.vue +32 -8
  125. package/dist/runtime/extensions/views/KbdView.vue +8 -2
  126. package/dist/runtime/extensions/views/MetaFieldView.vue +208 -46
  127. package/dist/runtime/extensions/views/ProseIconView.vue +8 -2
  128. package/dist/runtime/extensions/views/TabsView.vue +17 -4
  129. package/dist/runtime/locale.d.ts +71 -0
  130. package/dist/runtime/locale.js +71 -0
  131. package/dist/runtime/plugin-abracadabra.client.js +29 -3
  132. package/dist/runtime/plugin-abracadabra.server.js +2 -0
  133. package/dist/runtime/server/api/_abracadabra/render/[docId].get.d.ts +1 -1
  134. package/dist/runtime/server/api/_abracadabra/render/[docId].get.js +29 -4
  135. package/dist/runtime/server/api/_abracadabra/resolve/[...slug].get.d.ts +2 -0
  136. package/dist/runtime/server/api/_abracadabra/resolve/[...slug].get.js +43 -0
  137. package/dist/runtime/server/api/_abracadabra/slugs.get.d.ts +2 -0
  138. package/dist/runtime/server/api/_abracadabra/slugs.get.js +7 -0
  139. package/dist/runtime/server/plugins/abracadabra-service.js +10 -5
  140. package/dist/runtime/server/runners/doc-tree-cache.js +4 -0
  141. package/dist/runtime/server/utils/slugMap.d.ts +32 -0
  142. package/dist/runtime/server/utils/slugMap.js +58 -0
  143. package/dist/runtime/types.d.ts +1 -0
  144. package/dist/runtime/utils/docTypes.d.ts +29 -1
  145. package/dist/runtime/utils/docTypes.js +129 -1
  146. package/dist/runtime/utils/markdownToYjs.js +2 -2
  147. package/dist/runtime/utils/sdkRef.d.ts +2 -0
  148. package/dist/runtime/utils/sdkRef.js +7 -0
  149. package/dist/runtime/utils/slugify.d.ts +40 -0
  150. package/dist/runtime/utils/slugify.js +36 -0
  151. package/dist/types.d.mts +6 -0
  152. package/package.json +32 -19
@@ -0,0 +1,1622 @@
1
+ <script setup>
2
+ import { ref, computed, watch, shallowRef, onMounted, onBeforeUnmount, onUnmounted, defineAsyncComponent } from "vue";
3
+ import { useRendererBase } from "../../composables/useRendererBase";
4
+ import { useNodePanel } from "../../composables/useNodePanel";
5
+ import { DEFAULT_LOCALE } from "../../locale";
6
+ const AEditor = defineAsyncComponent(
7
+ () => import("../AEditor.vue")
8
+ );
9
+ const props = defineProps({
10
+ docId: { type: String, required: true },
11
+ childProvider: { type: null, required: true },
12
+ docLabel: { type: String, required: true },
13
+ pageTypes: { type: Array, required: false },
14
+ labels: { type: Object, required: false },
15
+ editable: { type: Boolean, required: false, default: true }
16
+ });
17
+ const config = useRuntimeConfig();
18
+ const locale = computed(() => ({
19
+ ...DEFAULT_LOCALE.renderers.map,
20
+ ...config.public?.abracadabra?.locale?.renderers?.map ?? {},
21
+ ...props.labels ?? {}
22
+ }));
23
+ const { childProviderRef, tree, connectedUsers, states, setLocalState } = useRendererBase(props);
24
+ const { userName, userColor: userColorName } = useAbracadabra();
25
+ const localClientId = computed(
26
+ () => childProviderRef.value?.awareness?.clientID ?? 0
27
+ );
28
+ const {
29
+ openNodeId,
30
+ openNodeLabel,
31
+ openNodeProvider,
32
+ isLoading: _nodePanelLoading,
33
+ openNode,
34
+ closePanel
35
+ } = useNodePanel(childProviderRef);
36
+ let mapboxgl = null;
37
+ const mapboxLoaded = ref(false);
38
+ const mapboxError = ref(null);
39
+ const allEntries = computed(() => tree.childrenOf(null));
40
+ function getEntryLocation(entry) {
41
+ if (entry.meta?.geoLat !== void 0 && entry.meta?.geoLng !== void 0) {
42
+ return { lat: entry.meta.geoLat, lng: entry.meta.geoLng };
43
+ }
44
+ const meta = entry.meta;
45
+ if (meta) {
46
+ for (const key of Object.keys(meta)) {
47
+ if (key.startsWith("_lat_")) {
48
+ const lat = meta[key];
49
+ const lng = meta["_lng_" + key.slice(5)];
50
+ if (typeof lat === "number" && typeof lng === "number") {
51
+ return { lat, lng };
52
+ }
53
+ }
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+ const markers = computed(
59
+ () => allEntries.value.filter((e) => {
60
+ if (e.meta?.geoType === "line" || e.meta?.geoType === "measure")
61
+ return false;
62
+ return e.meta?.geoType === "marker" || getEntryLocation(e) !== null;
63
+ })
64
+ );
65
+ const lines = computed(
66
+ () => allEntries.value.filter((e) => e.meta?.geoType === "line")
67
+ );
68
+ const measures = computed(
69
+ () => allEntries.value.filter((e) => e.meta?.geoType === "measure")
70
+ );
71
+ const markerInstances = /* @__PURE__ */ new Map();
72
+ const linePointInstances = /* @__PURE__ */ new Map();
73
+ function entryToMarkerData(entry) {
74
+ const loc = getEntryLocation(entry);
75
+ return {
76
+ lng: loc?.lng ?? 0,
77
+ lat: loc?.lat ?? 0,
78
+ label: entry.label,
79
+ icon: entry.meta?.icon ?? "map-pin",
80
+ color: entry.meta?.color ?? "#3b82f6",
81
+ addedByName: "",
82
+ createdAt: entry.order
83
+ };
84
+ }
85
+ const MARKER_ICON_SVG = {
86
+ "map-pin": `<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/>`,
87
+ "star": `<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>`,
88
+ "flag": `<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/>`,
89
+ "home": `<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>`,
90
+ "building-2": `<path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z"/><path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2"/><path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2"/><path d="M10 6h4"/><path d="M10 10h4"/><path d="M10 14h4"/><path d="M10 18h4"/>`,
91
+ "coffee": `<path d="M17 8h1a4 4 0 1 1 0 8h-1"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/><line x1="6" y1="2" x2="6" y2="4"/><line x1="10" y1="2" x2="10" y2="4"/><line x1="14" y1="2" x2="14" y2="4"/>`,
92
+ "utensils": `<path d="M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2"/><path d="M7 2v20"/><path d="M21 15V2a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3Zm0 0v7"/>`,
93
+ "camera": `<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/>`,
94
+ "heart": `<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>`,
95
+ "zap": `<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>`,
96
+ "triangle-alert": `<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>`,
97
+ "car": `<path d="M19 17H5a2 2 0 0 1-2-2V9l2-4h14l2 4v6a2 2 0 0 1-2 2Z"/><path d="M9 17v2"/><path d="M15 17v2"/><circle cx="7.5" cy="13.5" r="1.5"/><circle cx="16.5" cy="13.5" r="1.5"/>`,
98
+ "plane": `<path d="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z"/>`,
99
+ "anchor": `<circle cx="12" cy="5" r="3"/><line x1="12" y1="22" x2="12" y2="8"/><path d="M5 12H2a10 10 0 0 0 20 0h-3"/>`,
100
+ "tree-pine": `<path d="m17 14 3 3.3a1 1 0 0 1-.7 1.7H4.7a1 1 0 0 1-.7-1.7L7 14"/><path d="m14 10 3 3.3a1 1 0 0 1-.7 1.7H7.7a1 1 0 0 1-.7-1.7L10 10"/><path d="M12 2 7 7.7a1 1 0 0 0 .7 1.7h8.6a1 1 0 0 0 .7-1.7L12 2Z"/><path d="M12 22v-3"/>`,
101
+ "mountain": `<path d="m8 3 4 8 5-5 5 15H2L8 3z"/>`,
102
+ "waves": `<path d="M2 6c.6.5 1.2 1 2.5 1C7 7 7 5 9.5 5c2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"/><path d="M2 12c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"/><path d="M2 18c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"/>`,
103
+ "shield": `<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>`,
104
+ "crosshair": `<circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/>`,
105
+ "circle-dot": `<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="1"/>`,
106
+ "bookmark": `<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/>`,
107
+ "gem": `<path d="M6 3h12l4 6-10 13L2 9Z"/><path d="M11 3 8 9l4 13 4-13-3-6"/><path d="M2 9h20"/>`,
108
+ "radio": `<circle cx="12" cy="12" r="2"/><path d="M4.93 4.93a10 10 0 0 0 0 14.14"/><path d="M7.76 7.76a6 6 0 0 0 0 8.48"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M16.24 7.76a6 6 0 0 1 0 8.48"/>`,
109
+ "compass": `<circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/>`
110
+ };
111
+ const MARKER_COLORS = [
112
+ { key: "blue", hex: "#3b82f6", border: "#1d4ed8" },
113
+ { key: "orange", hex: "#f97316", border: "#c2410c" },
114
+ { key: "green", hex: "#22c55e", border: "#15803d" },
115
+ { key: "red", hex: "#ef4444", border: "#b91c1c" },
116
+ { key: "purple", hex: "#a855f7", border: "#7e22ce" }
117
+ ];
118
+ const COLOR_HEX = {
119
+ primary: "#6366f1",
120
+ blue: "#3b82f6",
121
+ sky: "#0ea5e9",
122
+ indigo: "#6366f1",
123
+ cyan: "#06b6d4",
124
+ green: "#22c55e",
125
+ emerald: "#10b981",
126
+ teal: "#14b8a6",
127
+ orange: "#f97316",
128
+ amber: "#f59e0b",
129
+ yellow: "#eab308",
130
+ red: "#ef4444",
131
+ rose: "#f43f5e",
132
+ pink: "#ec4899",
133
+ purple: "#a855f7",
134
+ violet: "#8b5cf6",
135
+ neutral: "#6b7280",
136
+ slate: "#64748b"
137
+ };
138
+ const mapContainer = ref();
139
+ let map = null;
140
+ let mapResizeObserver = null;
141
+ const mapReady = ref(false);
142
+ const mapFadedIn = ref(false);
143
+ const colorMode = useColorMode();
144
+ const isDark = computed(() => colorMode.value === "dark");
145
+ const activeMode = ref("pan");
146
+ const showLabels = ref(true);
147
+ const popupProvider = shallowRef(null);
148
+ const popupDocId = ref(null);
149
+ const popupDocLabel = ref("");
150
+ async function loadPopupProvider(nodeId, label) {
151
+ popupProvider.value = null;
152
+ popupDocId.value = nodeId;
153
+ popupDocLabel.value = label;
154
+ if (!childProviderRef.value) return;
155
+ try {
156
+ const childProv = await childProviderRef.value.loadChild(nodeId);
157
+ if (!childProv.isSynced) {
158
+ await new Promise((resolve) => {
159
+ const done = () => {
160
+ childProv.off("synced", done);
161
+ resolve();
162
+ };
163
+ childProv.on("synced", done);
164
+ setTimeout(resolve, 4e3);
165
+ });
166
+ }
167
+ const doc = childProv.document;
168
+ const frag = doc.getXmlFragment("default");
169
+ if (!frag._item) {
170
+ doc.transact(() => {
171
+ void frag.length;
172
+ });
173
+ }
174
+ if (popupDocId.value === nodeId) {
175
+ popupProvider.value = childProv;
176
+ }
177
+ } catch (e) {
178
+ console.error("Failed to load popup provider:", e);
179
+ }
180
+ }
181
+ function clearPopupProvider() {
182
+ popupProvider.value = null;
183
+ popupDocId.value = null;
184
+ popupDocLabel.value = "";
185
+ }
186
+ const selectedMarkerKey = ref(null);
187
+ const markerPopupScreenPos = ref({ x: 0, y: 0 });
188
+ const selectedMarker = computed(
189
+ () => selectedMarkerKey.value ? markers.value.find((e) => e.id === selectedMarkerKey.value) ?? null : null
190
+ );
191
+ const selectedLineKey = ref(null);
192
+ const linePopupScreenPos = ref({ x: 0, y: 0 });
193
+ const linePopupGeoPos = ref(null);
194
+ const selectedLine = computed(
195
+ () => selectedLineKey.value ? lines.value.find((e) => e.id === selectedLineKey.value) ?? null : null
196
+ );
197
+ watch([selectedMarkerKey, selectedLineKey], ([mk, lk]) => {
198
+ if (!mk && !lk) clearPopupProvider();
199
+ });
200
+ const selectedMeasureKey = ref(null);
201
+ const measurePopupScreenPos = ref({ x: 0, y: 0 });
202
+ const measurePopupGeoPos = ref(null);
203
+ const selectedMeasureInfo = computed(() => {
204
+ if (!selectedMeasureKey.value) return null;
205
+ const entry = measures.value.find((e) => e.id === selectedMeasureKey.value);
206
+ if (!entry) return null;
207
+ const pts = tree.childrenOf(entry.id).filter(
208
+ (p) => p.meta?.geoLat !== void 0 && p.meta?.geoLng !== void 0
209
+ );
210
+ if (pts.length < 2) return null;
211
+ let dist = 0;
212
+ for (let i = 1; i < pts.length; i++) {
213
+ dist += haversine(
214
+ [pts[i - 1].meta.geoLng, pts[i - 1].meta.geoLat],
215
+ [pts[i].meta.geoLng, pts[i].meta.geoLat]
216
+ );
217
+ }
218
+ return { entry, dist };
219
+ });
220
+ const drawPoints = ref([]);
221
+ const drawMouseLngLat = ref(null);
222
+ const measurePoints = ref([]);
223
+ const measurePreviewScreenPos = ref({ x: 0, y: 0 });
224
+ const measurePreviewTotal = computed(() => {
225
+ let d = 0;
226
+ for (let i = 1; i < measurePoints.value.length; i++)
227
+ d += haversine(measurePoints.value[i - 1], measurePoints.value[i]);
228
+ return d;
229
+ });
230
+ function haversine(a, b) {
231
+ const R = 6371e3;
232
+ const \u03C61 = a[1] * Math.PI / 180, \u03C62 = b[1] * Math.PI / 180;
233
+ const \u0394\u03C6 = (b[1] - a[1]) * Math.PI / 180, \u0394\u03BB = (b[0] - a[0]) * Math.PI / 180;
234
+ const x = Math.sin(\u0394\u03C6 / 2) ** 2 + Math.cos(\u03C61) * Math.cos(\u03C62) * Math.sin(\u0394\u03BB / 2) ** 2;
235
+ return R * 2 * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));
236
+ }
237
+ function formatDist(m) {
238
+ return m >= 1e3 ? `${(m / 1e3).toFixed(1)} km` : `${Math.round(m)} m`;
239
+ }
240
+ const pingElements = /* @__PURE__ */ new Map();
241
+ const measureLabelEls = /* @__PURE__ */ new Map();
242
+ const followingClientId = ref(null);
243
+ let lastAppliedFollowTime = 0;
244
+ let followEaseInProgress = false;
245
+ let rafId = 0;
246
+ let broadcastFrame = 0;
247
+ const CURSOR_LERP = 0.25;
248
+ const CAMERA_LERP = 0.08;
249
+ const myColorHex = computed(() => COLOR_HEX[userColorName.value] ?? "#6366f1");
250
+ const mapCursorClass = computed(
251
+ () => activeMode.value !== "pan" && followingClientId.value === null ? "cursor-crosshair" : ""
252
+ );
253
+ const modeHint = computed(() => {
254
+ switch (activeMode.value) {
255
+ case "marker":
256
+ return locale.value.hintMarker ?? "Click to place marker";
257
+ case "ping":
258
+ return locale.value.hintPing ?? "Click to send ping";
259
+ case "line":
260
+ return drawPoints.value.length >= 2 ? `${drawPoints.value.length} ${locale.value.points ?? "points"}` : locale.value.hintLine ?? "Click to start drawing a line";
261
+ case "measure":
262
+ return measurePoints.value.length >= 1 ? measurePoints.value.length >= 2 ? `${formatDist(measurePreviewTotal.value)}` : locale.value.hintMeasureAdd ?? "Click to add points" : locale.value.hintMeasure ?? "Click to start measuring";
263
+ default:
264
+ return null;
265
+ }
266
+ });
267
+ const modeHintIcon = computed(() => {
268
+ switch (activeMode.value) {
269
+ case "marker":
270
+ return "i-lucide-map-pin";
271
+ case "ping":
272
+ return "i-lucide-radio";
273
+ case "line":
274
+ return "i-lucide-route";
275
+ case "measure":
276
+ return "i-lucide-ruler";
277
+ default:
278
+ return "i-lucide-hand";
279
+ }
280
+ });
281
+ function toolBtn(mode) {
282
+ const active = activeMode.value === mode;
283
+ return {
284
+ color: active ? "primary" : "neutral",
285
+ variant: active ? "solid" : "ghost",
286
+ size: "sm"
287
+ };
288
+ }
289
+ function setMode(mode) {
290
+ if (!props.editable) return;
291
+ if (activeMode.value === mode) {
292
+ activeMode.value = "pan";
293
+ return;
294
+ }
295
+ activeMode.value = mode;
296
+ if (mode !== "line") {
297
+ drawPoints.value = [];
298
+ drawMouseLngLat.value = null;
299
+ updateDrawPreview();
300
+ }
301
+ if (mode !== "measure") {
302
+ measurePoints.value = [];
303
+ clearMeasurePreview();
304
+ }
305
+ selectedMarkerKey.value = null;
306
+ selectedLineKey.value = null;
307
+ selectedMeasureKey.value = null;
308
+ }
309
+ const remoteUsers = computed(
310
+ () => states.value.filter((s) => s.clientId !== localClientId.value).map((s) => ({
311
+ clientId: s.clientId,
312
+ name: s.user?.name ?? "User",
313
+ color: s.user?.color ?? "blue",
314
+ colorHex: s.user?.color || "#6366f1",
315
+ camera: s.mapCamera,
316
+ cursor: s.mapCursor,
317
+ mapPing: s.mapPing
318
+ }))
319
+ );
320
+ const allPingStates = computed(
321
+ () => states.value.map((s) => ({
322
+ clientId: s.clientId,
323
+ colorHex: s.user?.color || "#6366f1",
324
+ mapPing: s.mapPing
325
+ })).filter((s) => s.mapPing && Date.now() - s.mapPing.t < 3e3)
326
+ );
327
+ const followingUser = computed(
328
+ () => remoteUsers.value.find((u) => u.clientId === followingClientId.value) ?? null
329
+ );
330
+ const getLightPreset = () => isDark.value ? "night" : "day";
331
+ function broadcastCamera() {
332
+ if (!map || followingClientId.value !== null) return;
333
+ const c = map.getCenter();
334
+ setLocalState({
335
+ mapCamera: {
336
+ center: [c.lng, c.lat],
337
+ zoom: map.getZoom(),
338
+ pitch: map.getPitch(),
339
+ bearing: map.getBearing(),
340
+ t: Date.now()
341
+ }
342
+ });
343
+ }
344
+ function breakFollow() {
345
+ if (followEaseInProgress) return;
346
+ if (followingClientId.value !== null) followingClientId.value = null;
347
+ }
348
+ function followUser(clientId) {
349
+ if (clientId === localClientId.value) return;
350
+ followingClientId.value = clientId;
351
+ lastAppliedFollowTime = 0;
352
+ }
353
+ function emptyFC() {
354
+ return { type: "FeatureCollection", features: [] };
355
+ }
356
+ const presenceLerpMap = /* @__PURE__ */ new Map();
357
+ function createPresenceEl(name, colorHex) {
358
+ const el = document.createElement("div");
359
+ el.className = "presence-marker";
360
+ const initials = name.split(" ").map((w) => w[0] ?? "").join("").substring(0, 2).toUpperCase();
361
+ el.innerHTML = `<div class="presence-avatar" style="background:${colorHex}">${initials}</div><div class="presence-label">${name}</div>`;
362
+ return el;
363
+ }
364
+ function tickPresenceMarkers() {
365
+ if (!map || !mapReady.value || !mapboxgl) return;
366
+ const seen = /* @__PURE__ */ new Set();
367
+ for (const user of remoteUsers.value) {
368
+ if (!user.camera) continue;
369
+ seen.add(user.clientId);
370
+ const [tLng, tLat] = user.camera.center;
371
+ const p = presenceLerpMap.get(user.clientId);
372
+ if (p) {
373
+ p.tgtLng = tLng;
374
+ p.tgtLat = tLat;
375
+ p.curLng += (p.tgtLng - p.curLng) * CAMERA_LERP;
376
+ p.curLat += (p.tgtLat - p.curLat) * CAMERA_LERP;
377
+ p.marker.setLngLat([p.curLng, p.curLat]);
378
+ } else {
379
+ const m = new mapboxgl.Marker({
380
+ element: createPresenceEl(user.name, user.colorHex),
381
+ anchor: "bottom"
382
+ }).setLngLat([tLng, tLat]).addTo(map);
383
+ presenceLerpMap.set(user.clientId, {
384
+ marker: m,
385
+ curLng: tLng,
386
+ curLat: tLat,
387
+ tgtLng: tLng,
388
+ tgtLat: tLat
389
+ });
390
+ }
391
+ }
392
+ for (const [id, p] of presenceLerpMap) {
393
+ if (!seen.has(id)) {
394
+ p.marker.remove();
395
+ presenceLerpMap.delete(id);
396
+ }
397
+ }
398
+ }
399
+ const cursorLerpMap = /* @__PURE__ */ new Map();
400
+ function createCursorEl(name, colorHex) {
401
+ const el = document.createElement("div");
402
+ el.className = "map-cursor-el";
403
+ el.innerHTML = `
404
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="display:block">
405
+ <path d="M2 2L7 14L9 9L14 7L2 2Z" fill="${colorHex}" stroke="rgba(0,0,0,.25)" stroke-width="1" stroke-linejoin="round"/>
406
+ </svg>
407
+ <span class="map-cursor-name" style="background:${colorHex}">${name}</span>
408
+ `;
409
+ return el;
410
+ }
411
+ function tickCursors() {
412
+ if (!map || !mapContainer.value) return;
413
+ const seen = /* @__PURE__ */ new Set();
414
+ for (const user of remoteUsers.value) {
415
+ if (!user.cursor) continue;
416
+ seen.add(user.clientId);
417
+ const proj = map.project([user.cursor.lng, user.cursor.lat]);
418
+ const c = cursorLerpMap.get(user.clientId);
419
+ if (c) {
420
+ c.screenX += (proj.x - c.screenX) * CURSOR_LERP;
421
+ c.screenY += (proj.y - c.screenY) * CURSOR_LERP;
422
+ c.el.style.transform = `translate(${c.screenX}px,${c.screenY}px)`;
423
+ } else {
424
+ const el = createCursorEl(user.name, user.colorHex);
425
+ mapContainer.value.appendChild(el);
426
+ el.style.transform = `translate(${proj.x}px,${proj.y}px)`;
427
+ cursorLerpMap.set(user.clientId, {
428
+ el,
429
+ screenX: proj.x,
430
+ screenY: proj.y
431
+ });
432
+ }
433
+ }
434
+ for (const [id, c] of cursorLerpMap) {
435
+ if (!seen.has(id)) {
436
+ c.el.remove();
437
+ cursorLerpMap.delete(id);
438
+ }
439
+ }
440
+ }
441
+ let cursorBroadcastFrame = 0;
442
+ function onMapPointerMove(e) {
443
+ if (!map || !mapContainer.value) return;
444
+ cursorBroadcastFrame++;
445
+ if (cursorBroadcastFrame % 2 !== 0) return;
446
+ const rect = mapContainer.value.getBoundingClientRect();
447
+ const x = e.clientX - rect.left, y = e.clientY - rect.top;
448
+ try {
449
+ const { lng, lat } = map.unproject([x, y]);
450
+ setLocalState({ mapCursor: { lng, lat } });
451
+ if (activeMode.value === "line" && drawPoints.value.length > 0) {
452
+ drawMouseLngLat.value = [lng, lat];
453
+ updateDrawPreview();
454
+ }
455
+ } catch {
456
+ }
457
+ }
458
+ function onMapPointerLeave() {
459
+ setLocalState({ mapCursor: null });
460
+ drawMouseLngLat.value = null;
461
+ if (activeMode.value === "line") updateDrawPreview();
462
+ }
463
+ function getIconSVG(name) {
464
+ if (MARKER_ICON_SVG[name]) return MARKER_ICON_SVG[name];
465
+ return MARKER_ICON_SVG["map-pin"];
466
+ }
467
+ function buildPinHTML(data) {
468
+ const colorEntry = MARKER_COLORS.find((c) => c.hex === data.color);
469
+ const hex = data.color || "#3b82f6";
470
+ const border = colorEntry?.border ?? "#333";
471
+ const svg = getIconSVG(data.icon ?? "map-pin");
472
+ return { hex, border, svg };
473
+ }
474
+ function createMarkerEl(data, key) {
475
+ const { hex, border, svg } = buildPinHTML(data);
476
+ const container = document.createElement("div");
477
+ container.className = "collab-marker-container";
478
+ const pin = document.createElement("div");
479
+ pin.className = "collab-marker-pin";
480
+ pin.style.cssText = `background:${hex};box-shadow:0 2px 8px rgba(0,0,0,.4),0 0 0 2px ${border};`;
481
+ pin.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="13" height="13">${svg}</svg>`;
482
+ const labelEl = document.createElement("div");
483
+ labelEl.className = "collab-marker-label";
484
+ labelEl.textContent = data.label;
485
+ pin.addEventListener("click", (ev) => {
486
+ ev.stopPropagation();
487
+ openMarkerPopup(key);
488
+ });
489
+ if (props.editable) {
490
+ container.addEventListener("contextmenu", (ev) => {
491
+ ev.preventDefault();
492
+ ev.stopPropagation();
493
+ if (selectedMarkerKey.value === key) closeMarkerPopup();
494
+ tree.deleteEntry(key);
495
+ });
496
+ }
497
+ container.appendChild(pin);
498
+ container.appendChild(labelEl);
499
+ return container;
500
+ }
501
+ function updateMarkerEl(el, data) {
502
+ const pin = el.querySelector(".collab-marker-pin");
503
+ const label = el.querySelector(".collab-marker-label");
504
+ if (pin) {
505
+ const { hex, border, svg } = buildPinHTML(data);
506
+ pin.style.cssText = `background:${hex};box-shadow:0 2px 8px rgba(0,0,0,.4),0 0 0 2px ${border};`;
507
+ pin.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="13" height="13">${svg}</svg>`;
508
+ }
509
+ if (label) label.textContent = data.label;
510
+ }
511
+ function syncMarkers() {
512
+ if (!map || !mapReady.value || !mapboxgl) return;
513
+ const currentKeys = new Set(markers.value.map((e) => e.id));
514
+ for (const [key, m] of markerInstances) {
515
+ if (!currentKeys.has(key)) {
516
+ m.remove();
517
+ markerInstances.delete(key);
518
+ }
519
+ }
520
+ for (const entry of markers.value) {
521
+ const data = entryToMarkerData(entry);
522
+ if (markerInstances.has(entry.id)) {
523
+ updateMarkerEl(markerInstances.get(entry.id).getElement(), data);
524
+ } else {
525
+ const el = createMarkerEl(data, entry.id);
526
+ const m = new mapboxgl.Marker({ element: el, anchor: "bottom", draggable: props.editable }).setLngLat([data.lng, data.lat]).addTo(map);
527
+ if (props.editable) {
528
+ m.on("dragend", () => {
529
+ const lngLat = m.getLngLat();
530
+ tree.updateMeta(entry.id, { geoLng: lngLat.lng, geoLat: lngLat.lat });
531
+ });
532
+ }
533
+ markerInstances.set(entry.id, m);
534
+ }
535
+ const inst = markerInstances.get(entry.id);
536
+ const cur = inst.getLngLat();
537
+ if (cur.lng !== data.lng || cur.lat !== data.lat) {
538
+ inst.setLngLat([data.lng, data.lat]);
539
+ }
540
+ }
541
+ }
542
+ watch(markers, () => syncMarkers(), { deep: true });
543
+ watch(
544
+ () => tree.treeMap.yMap.value,
545
+ (ymap, oldYmap) => {
546
+ if (oldYmap) oldYmap.unobserve(onTreeYMapChange);
547
+ if (ymap) ymap.observe(onTreeYMapChange);
548
+ },
549
+ { immediate: true }
550
+ );
551
+ function onTreeYMapChange() {
552
+ syncMarkers();
553
+ syncLineSource();
554
+ syncLinePoints();
555
+ }
556
+ onBeforeUnmount(() => {
557
+ tree.treeMap.yMap.value?.unobserve(onTreeYMapChange);
558
+ });
559
+ function openMarkerPopup(key) {
560
+ const entry = markers.value.find((e) => e.id === key);
561
+ if (!entry) return;
562
+ selectedMarkerKey.value = key;
563
+ selectedLineKey.value = null;
564
+ selectedMeasureKey.value = null;
565
+ updateAllPopupPositions();
566
+ loadPopupProvider(key, entry.label);
567
+ }
568
+ function closeMarkerPopup() {
569
+ selectedMarkerKey.value = null;
570
+ }
571
+ function deleteSelectedMarker() {
572
+ if (!selectedMarkerKey.value || !props.editable) return;
573
+ tree.deleteEntry(selectedMarkerKey.value);
574
+ closeMarkerPopup();
575
+ }
576
+ function openLinePopup(key, geoPos) {
577
+ selectedLineKey.value = key;
578
+ linePopupGeoPos.value = geoPos;
579
+ selectedMarkerKey.value = null;
580
+ selectedMeasureKey.value = null;
581
+ updateAllPopupPositions();
582
+ const entry = lines.value.find((e) => e.id === key);
583
+ if (entry) loadPopupProvider(key, entry.label);
584
+ }
585
+ function deleteSelectedLine() {
586
+ if (!selectedLineKey.value || !props.editable) return;
587
+ tree.deleteEntry(selectedLineKey.value);
588
+ selectedLineKey.value = null;
589
+ }
590
+ function openMeasurePopup(key, geoPos) {
591
+ selectedMeasureKey.value = key;
592
+ measurePopupGeoPos.value = geoPos;
593
+ selectedMarkerKey.value = null;
594
+ selectedLineKey.value = null;
595
+ updateAllPopupPositions();
596
+ }
597
+ function deleteSelectedMeasure() {
598
+ if (!selectedMeasureKey.value || !props.editable) return;
599
+ tree.deleteEntry(selectedMeasureKey.value);
600
+ selectedMeasureKey.value = null;
601
+ }
602
+ function sendPing(lng, lat) {
603
+ setLocalState({ mapPing: { lng, lat, t: Date.now() } });
604
+ setTimeout(() => setLocalState({ mapPing: null }), 3500);
605
+ }
606
+ function createPingEl(colorHex) {
607
+ const root = document.createElement("div");
608
+ root.className = "map-ping-root";
609
+ root.style.setProperty("--ping-color", colorHex);
610
+ root.innerHTML = `
611
+ <div class="map-ping-ring" style="animation-delay:0s"></div>
612
+ <div class="map-ping-ring" style="animation-delay:0.5s"></div>
613
+ <div class="map-ping-ring" style="animation-delay:1s"></div>
614
+ <div class="map-ping-dot"></div>
615
+ `;
616
+ return root;
617
+ }
618
+ function tickPings() {
619
+ if (!map || !mapContainer.value) return;
620
+ const seenIds = /* @__PURE__ */ new Set();
621
+ for (const ps of allPingStates.value) {
622
+ if (!ps.mapPing) continue;
623
+ if (Date.now() - ps.mapPing.t >= 3e3) continue;
624
+ seenIds.add(ps.clientId);
625
+ const proj = map.project([ps.mapPing.lng, ps.mapPing.lat]);
626
+ let el = pingElements.get(ps.clientId);
627
+ if (!el) {
628
+ el = createPingEl(ps.colorHex);
629
+ mapContainer.value.appendChild(el);
630
+ pingElements.set(ps.clientId, el);
631
+ }
632
+ el.style.left = `${proj.x}px`;
633
+ el.style.top = `${proj.y}px`;
634
+ }
635
+ for (const [id, el] of pingElements) {
636
+ if (!seenIds.has(id)) {
637
+ el.remove();
638
+ pingElements.delete(id);
639
+ }
640
+ }
641
+ }
642
+ function updateDrawPreview() {
643
+ if (!map || !map.getSource("draw-preview")) return;
644
+ const pts = drawPoints.value;
645
+ const mouse = drawMouseLngLat.value;
646
+ const coords = mouse && pts.length > 0 ? [...pts, mouse] : [...pts];
647
+ map.getSource("draw-preview")?.setData({
648
+ type: "FeatureCollection",
649
+ features: coords.length >= 2 ? [
650
+ {
651
+ type: "Feature",
652
+ geometry: { type: "LineString", coordinates: coords },
653
+ properties: {}
654
+ }
655
+ ] : []
656
+ });
657
+ map.getSource("draw-points")?.setData({
658
+ type: "FeatureCollection",
659
+ features: pts.map((p) => ({
660
+ type: "Feature",
661
+ geometry: { type: "Point", coordinates: p },
662
+ properties: {}
663
+ }))
664
+ });
665
+ }
666
+ function finishLine() {
667
+ if (drawPoints.value.length < 2 || !props.editable) return;
668
+ const lineId = tree.createChild(null, "Line");
669
+ tree.updateMeta(lineId, { geoType: "line", color: myColorHex.value });
670
+ drawPoints.value.forEach((pt, i) => {
671
+ const ptId = tree.createChild(lineId, `Point ${i + 1}`);
672
+ tree.updateMeta(ptId, { geoType: "marker", geoLng: pt[0], geoLat: pt[1] });
673
+ });
674
+ drawPoints.value = [];
675
+ drawMouseLngLat.value = null;
676
+ activeMode.value = "pan";
677
+ updateDrawPreview();
678
+ }
679
+ function cancelDrawing() {
680
+ drawPoints.value = [];
681
+ drawMouseLngLat.value = null;
682
+ updateDrawPreview();
683
+ }
684
+ function syncLineSource() {
685
+ if (!map || !map.getSource("collab-lines")) return;
686
+ const features = lines.value.map((line) => {
687
+ const pts = tree.childrenOf(line.id).filter(
688
+ (p) => p.meta?.geoLat !== void 0 && p.meta?.geoLng !== void 0
689
+ );
690
+ const coords = pts.map(
691
+ (p) => [p.meta.geoLng, p.meta.geoLat]
692
+ );
693
+ return {
694
+ type: "Feature",
695
+ geometry: { type: "LineString", coordinates: coords },
696
+ properties: {
697
+ color: line.meta?.color ?? "#3b82f6",
698
+ label: line.label ?? "",
699
+ key: line.id
700
+ }
701
+ };
702
+ });
703
+ map.getSource("collab-lines")?.setData({
704
+ type: "FeatureCollection",
705
+ features
706
+ });
707
+ }
708
+ function createLinePointEl() {
709
+ const el = document.createElement("div");
710
+ el.className = "collab-line-point";
711
+ return el;
712
+ }
713
+ function syncLinePoints() {
714
+ if (!map || !mapboxgl) return;
715
+ const activePointKeys = /* @__PURE__ */ new Set();
716
+ for (const line of lines.value) {
717
+ const lineColor = line.meta?.color ?? "#3b82f6";
718
+ const pts = tree.childrenOf(line.id).filter(
719
+ (p) => p.meta?.geoLat !== void 0 && p.meta?.geoLng !== void 0
720
+ );
721
+ for (const pt of pts) {
722
+ activePointKeys.add(pt.id);
723
+ const lng = pt.meta.geoLng;
724
+ const lat = pt.meta.geoLat;
725
+ if (linePointInstances.has(pt.id)) {
726
+ const inst = linePointInstances.get(pt.id);
727
+ const cur = inst.getLngLat();
728
+ if (cur.lng !== lng || cur.lat !== lat) inst.setLngLat([lng, lat]);
729
+ const el = inst.getElement();
730
+ el.style.background = lineColor;
731
+ } else {
732
+ const el = createLinePointEl();
733
+ el.style.background = lineColor;
734
+ el.addEventListener("click", (ev) => {
735
+ ev.stopPropagation();
736
+ openNode(pt.id, pt.label);
737
+ });
738
+ const m = new mapboxgl.Marker({ element: el, anchor: "center", draggable: props.editable }).setLngLat([lng, lat]).addTo(map);
739
+ if (props.editable) {
740
+ m.on("dragend", () => {
741
+ const lngLat = m.getLngLat();
742
+ tree.updateMeta(pt.id, { geoLng: lngLat.lng, geoLat: lngLat.lat });
743
+ });
744
+ }
745
+ linePointInstances.set(pt.id, m);
746
+ }
747
+ }
748
+ }
749
+ for (const [key, m] of linePointInstances) {
750
+ if (!activePointKeys.has(key)) {
751
+ m.remove();
752
+ linePointInstances.delete(key);
753
+ }
754
+ }
755
+ }
756
+ watch(
757
+ tree.entries,
758
+ () => {
759
+ if (selectedLineKey.value && !lines.value.find((e) => e.id === selectedLineKey.value))
760
+ selectedLineKey.value = null;
761
+ syncMeasureSource();
762
+ if (selectedMeasureKey.value && !measures.value.find((e) => e.id === selectedMeasureKey.value))
763
+ selectedMeasureKey.value = null;
764
+ },
765
+ { deep: true }
766
+ );
767
+ function updateMeasurePreview() {
768
+ if (!map || !map.getSource("measure-preview-lines")) return;
769
+ const pts = measurePoints.value;
770
+ map.getSource("measure-preview-lines")?.setData({
771
+ type: "FeatureCollection",
772
+ features: pts.length >= 2 ? [
773
+ {
774
+ type: "Feature",
775
+ geometry: { type: "LineString", coordinates: pts },
776
+ properties: {}
777
+ }
778
+ ] : []
779
+ });
780
+ map.getSource("measure-preview-points")?.setData({
781
+ type: "FeatureCollection",
782
+ features: pts.map((p) => ({
783
+ type: "Feature",
784
+ geometry: { type: "Point", coordinates: p },
785
+ properties: {}
786
+ }))
787
+ });
788
+ if (pts.length >= 1 && map) {
789
+ const last = pts[pts.length - 1];
790
+ const proj = map.project(last);
791
+ measurePreviewScreenPos.value = { x: proj.x + 10, y: proj.y - 10 };
792
+ }
793
+ }
794
+ function clearMeasurePreview() {
795
+ if (!map) return;
796
+ map.getSource("measure-preview-lines")?.setData(emptyFC());
797
+ map.getSource("measure-preview-points")?.setData(emptyFC());
798
+ }
799
+ function finishMeasure() {
800
+ if (measurePoints.value.length < 2 || !props.editable) return;
801
+ const measureId = tree.createChild(null, "Measure");
802
+ tree.updateMeta(measureId, { geoType: "measure" });
803
+ measurePoints.value.forEach((pt, i) => {
804
+ const ptId = tree.createChild(measureId, `Point ${i + 1}`);
805
+ tree.updateMeta(ptId, { geoType: "marker", geoLng: pt[0], geoLat: pt[1] });
806
+ });
807
+ measurePoints.value = [];
808
+ clearMeasurePreview();
809
+ }
810
+ function syncMeasureSource() {
811
+ if (!map || !map.getSource("measure-collab-lines")) return;
812
+ const lineFeatures = measures.value.map((entry) => {
813
+ const pts = tree.childrenOf(entry.id).filter((p) => p.meta?.geoLat !== void 0);
814
+ const coords = pts.map(
815
+ (p) => [p.meta.geoLng, p.meta.geoLat]
816
+ );
817
+ return {
818
+ type: "Feature",
819
+ geometry: { type: "LineString", coordinates: coords },
820
+ properties: { key: entry.id }
821
+ };
822
+ });
823
+ map.getSource("measure-collab-lines")?.setData({
824
+ type: "FeatureCollection",
825
+ features: lineFeatures
826
+ });
827
+ const pointFeatures = measures.value.flatMap(
828
+ (entry) => tree.childrenOf(entry.id).filter((p) => p.meta?.geoLat !== void 0).map((p, i) => ({
829
+ type: "Feature",
830
+ id: `${entry.id}-${i}`,
831
+ geometry: {
832
+ type: "Point",
833
+ coordinates: [p.meta.geoLng, p.meta.geoLat]
834
+ },
835
+ properties: { measureId: entry.id }
836
+ }))
837
+ );
838
+ map.getSource("measure-collab-points")?.setData({
839
+ type: "FeatureCollection",
840
+ features: pointFeatures
841
+ });
842
+ }
843
+ function tickMeasureLabels() {
844
+ if (!map || !mapContainer.value || !mapReady.value) return;
845
+ const seen = /* @__PURE__ */ new Set();
846
+ for (const entry of measures.value) {
847
+ const pts = tree.childrenOf(entry.id).filter((p) => p.meta?.geoLat !== void 0);
848
+ if (pts.length < 2) continue;
849
+ seen.add(entry.id);
850
+ const midPt = pts[Math.floor((pts.length - 1) / 2)];
851
+ const proj = map.project([midPt.meta.geoLng, midPt.meta.geoLat]);
852
+ let dist = 0;
853
+ for (let i = 1; i < pts.length; i++) {
854
+ dist += haversine(
855
+ [pts[i - 1].meta.geoLng, pts[i - 1].meta.geoLat],
856
+ [pts[i].meta.geoLng, pts[i].meta.geoLat]
857
+ );
858
+ }
859
+ let el = measureLabelEls.get(entry.id);
860
+ if (!el) {
861
+ el = document.createElement("div");
862
+ el.className = "measure-label-el";
863
+ mapContainer.value.appendChild(el);
864
+ measureLabelEls.set(entry.id, el);
865
+ }
866
+ el.textContent = formatDist(dist);
867
+ el.style.left = `${proj.x}px`;
868
+ el.style.top = `${proj.y}px`;
869
+ }
870
+ for (const [id, el] of measureLabelEls) {
871
+ if (!seen.has(id)) {
872
+ el.remove();
873
+ measureLabelEls.delete(id);
874
+ }
875
+ }
876
+ }
877
+ function updateAllPopupPositions() {
878
+ if (!map) return;
879
+ if (selectedMarkerKey.value) {
880
+ const entry = markers.value.find((e) => e.id === selectedMarkerKey.value);
881
+ if (entry) {
882
+ const loc = getEntryLocation(entry);
883
+ const pt = map.project([loc?.lng ?? 0, loc?.lat ?? 0]);
884
+ markerPopupScreenPos.value = { x: pt.x + 16, y: pt.y - 80 };
885
+ }
886
+ }
887
+ if (linePopupGeoPos.value) {
888
+ const pt = map.project(linePopupGeoPos.value);
889
+ linePopupScreenPos.value = { x: pt.x + 8, y: pt.y - 50 };
890
+ }
891
+ if (measurePopupGeoPos.value) {
892
+ const pt = map.project(measurePopupGeoPos.value);
893
+ measurePopupScreenPos.value = { x: pt.x + 8, y: pt.y - 50 };
894
+ }
895
+ if (measurePoints.value.length >= 1) {
896
+ const last = measurePoints.value[measurePoints.value.length - 1];
897
+ const proj = map.project(last);
898
+ measurePreviewScreenPos.value = { x: proj.x + 10, y: proj.y - 10 };
899
+ }
900
+ }
901
+ function handleMapClick(e) {
902
+ if (followingClientId.value !== null) return;
903
+ const target = e.originalEvent.target;
904
+ if (target.closest(".collab-marker-pin") || target.closest(".collab-line-point")) return;
905
+ if (selectedMarkerKey.value) {
906
+ closeMarkerPopup();
907
+ return;
908
+ }
909
+ switch (activeMode.value) {
910
+ case "pan": {
911
+ const bbox = [
912
+ [e.point.x - 8, e.point.y - 8],
913
+ [e.point.x + 8, e.point.y + 8]
914
+ ];
915
+ const lineFeats = map.queryRenderedFeatures(bbox, {
916
+ layers: ["collab-lines-layer"]
917
+ });
918
+ if (lineFeats.length > 0) {
919
+ const id = lineFeats[0].properties?.key;
920
+ if (id && lines.value.find((e2) => e2.id === id)) {
921
+ openLinePopup(id, [e.lngLat.lng, e.lngLat.lat]);
922
+ return;
923
+ }
924
+ }
925
+ const measureFeats = map.queryRenderedFeatures(bbox, {
926
+ layers: ["measure-collab-lines-layer"]
927
+ });
928
+ if (measureFeats.length > 0) {
929
+ const id = measureFeats[0].properties?.key;
930
+ if (id && measures.value.find((e2) => e2.id === id)) {
931
+ openMeasurePopup(id, [e.lngLat.lng, e.lngLat.lat]);
932
+ return;
933
+ }
934
+ }
935
+ selectedLineKey.value = null;
936
+ selectedMeasureKey.value = null;
937
+ break;
938
+ }
939
+ case "marker":
940
+ if (!props.editable) break;
941
+ if (target.closest(".map-ping-root,.presence-marker,.map-cursor-el"))
942
+ return;
943
+ {
944
+ const id = tree.createChild(null, userName.value);
945
+ tree.updateMeta(id, {
946
+ geoType: "marker",
947
+ geoLat: e.lngLat.lat,
948
+ geoLng: e.lngLat.lng,
949
+ icon: "map-pin",
950
+ color: myColorHex.value
951
+ });
952
+ }
953
+ break;
954
+ case "ping":
955
+ sendPing(e.lngLat.lng, e.lngLat.lat);
956
+ activeMode.value = "pan";
957
+ break;
958
+ case "line":
959
+ if (!props.editable) break;
960
+ drawPoints.value.push([e.lngLat.lng, e.lngLat.lat]);
961
+ updateDrawPreview();
962
+ break;
963
+ case "measure": {
964
+ if (!props.editable) break;
965
+ if (measurePoints.value.length >= 2) {
966
+ const last = measurePoints.value[measurePoints.value.length - 1];
967
+ const proj = map.project(last);
968
+ const dx = e.point.x - proj.x, dy = e.point.y - proj.y;
969
+ if (Math.sqrt(dx * dx + dy * dy) < 15) {
970
+ finishMeasure();
971
+ break;
972
+ }
973
+ }
974
+ measurePoints.value.push([e.lngLat.lng, e.lngLat.lat]);
975
+ updateMeasurePreview();
976
+ break;
977
+ }
978
+ }
979
+ }
980
+ function handleMapDblClick(e) {
981
+ if (activeMode.value === "line" && drawPoints.value.length >= 2) {
982
+ e.preventDefault();
983
+ finishLine();
984
+ }
985
+ }
986
+ function handleMapContextMenu(e) {
987
+ if (!map || !props.editable) return;
988
+ e.originalEvent.preventDefault();
989
+ const bbox = [
990
+ [e.point.x - 8, e.point.y - 8],
991
+ [e.point.x + 8, e.point.y + 8]
992
+ ];
993
+ const lineFeats = map.queryRenderedFeatures(bbox, {
994
+ layers: ["collab-lines-layer"]
995
+ });
996
+ if (lineFeats.length > 0) {
997
+ const id = lineFeats[0].properties?.key;
998
+ if (id) {
999
+ tree.deleteEntry(id);
1000
+ if (selectedLineKey.value === id) selectedLineKey.value = null;
1001
+ }
1002
+ return;
1003
+ }
1004
+ const measureFeats = map.queryRenderedFeatures(bbox, {
1005
+ layers: ["measure-collab-lines-layer"]
1006
+ });
1007
+ if (measureFeats.length > 0) {
1008
+ const id = measureFeats[0].properties?.key;
1009
+ if (id) {
1010
+ tree.deleteEntry(id);
1011
+ if (selectedMeasureKey.value === id) selectedMeasureKey.value = null;
1012
+ }
1013
+ return;
1014
+ }
1015
+ }
1016
+ function onKeyDown(e) {
1017
+ if (e.key === "Escape") {
1018
+ if (activeMode.value === "line") cancelDrawing();
1019
+ if (activeMode.value === "measure") {
1020
+ measurePoints.value = [];
1021
+ clearMeasurePreview();
1022
+ }
1023
+ closeMarkerPopup();
1024
+ selectedLineKey.value = null;
1025
+ selectedMeasureKey.value = null;
1026
+ activeMode.value = "pan";
1027
+ }
1028
+ }
1029
+ function applyFollowCamera() {
1030
+ const user = followingUser.value;
1031
+ if (!user?.camera || !map) return;
1032
+ if (user.camera.t <= lastAppliedFollowTime) return;
1033
+ lastAppliedFollowTime = user.camera.t;
1034
+ followEaseInProgress = true;
1035
+ map.easeTo({
1036
+ center: user.camera.center,
1037
+ zoom: user.camera.zoom,
1038
+ pitch: user.camera.pitch,
1039
+ bearing: user.camera.bearing,
1040
+ duration: 200,
1041
+ easing: (t) => t
1042
+ });
1043
+ setTimeout(() => {
1044
+ followEaseInProgress = false;
1045
+ }, 250);
1046
+ }
1047
+ function startLoop() {
1048
+ const loop = () => {
1049
+ broadcastFrame++;
1050
+ if (followingClientId.value !== null) applyFollowCamera();
1051
+ if (mapReady.value) {
1052
+ tickCursors();
1053
+ tickPings();
1054
+ tickMeasureLabels();
1055
+ updateAllPopupPositions();
1056
+ }
1057
+ if (broadcastFrame % 2 === 0 && mapReady.value) tickPresenceMarkers();
1058
+ if (followingClientId.value === null && broadcastFrame % 4 === 0 && map)
1059
+ broadcastCamera();
1060
+ rafId = requestAnimationFrame(loop);
1061
+ };
1062
+ rafId = requestAnimationFrame(loop);
1063
+ }
1064
+ function applyLabels() {
1065
+ if (!map?.isStyleLoaded()) return;
1066
+ const v = showLabels.value;
1067
+ map.setConfigProperty("basemap", "showPlaceLabels", v);
1068
+ map.setConfigProperty("basemap", "showPointOfInterestLabels", v);
1069
+ map.setConfigProperty("basemap", "showRoadLabels", v);
1070
+ map.setConfigProperty("basemap", "showTransitLabels", v);
1071
+ }
1072
+ watch(showLabels, applyLabels);
1073
+ watch(activeMode, (mode) => {
1074
+ if (!map) return;
1075
+ if (mode === "line") map.doubleClickZoom.disable();
1076
+ else map.doubleClickZoom.enable();
1077
+ });
1078
+ onMounted(async () => {
1079
+ try {
1080
+ const mod = await import("mapbox-gl");
1081
+ mapboxgl = mod.default || mod;
1082
+ await import("mapbox-gl/dist/mapbox-gl.css");
1083
+ mapboxLoaded.value = true;
1084
+ } catch {
1085
+ mapboxError.value = "Map renderer requires mapbox-gl. Install it with: pnpm add mapbox-gl";
1086
+ return;
1087
+ }
1088
+ if (!mapContainer.value) return;
1089
+ window.addEventListener("keydown", onKeyDown);
1090
+ const token = config.public?.abracadabra?.mapboxToken;
1091
+ if (!token) {
1092
+ mapboxError.value = "Mapbox access token not configured. Set abracadabra.mapboxToken in runtimeConfig.public.";
1093
+ return;
1094
+ }
1095
+ mapboxgl.accessToken = token;
1096
+ map = new mapboxgl.Map({
1097
+ container: mapContainer.value,
1098
+ style: "mapbox://styles/mapbox/standard",
1099
+ projection: "globe",
1100
+ center: [0, 20],
1101
+ zoom: 2,
1102
+ pitch: 0,
1103
+ bearing: 0,
1104
+ antialias: true,
1105
+ attributionControl: false
1106
+ });
1107
+ map.on("style.load", () => {
1108
+ map.setConfigProperty("basemap", "lightPreset", getLightPreset());
1109
+ applyLabels();
1110
+ });
1111
+ map.on("load", () => {
1112
+ try {
1113
+ map.addSource("mapbox-dem", {
1114
+ type: "raster-dem",
1115
+ url: "mapbox://mapbox.mapbox-terrain-dem-v1",
1116
+ tileSize: 512,
1117
+ maxzoom: 14
1118
+ });
1119
+ map.setTerrain({ source: "mapbox-dem", exaggeration: 1 });
1120
+ } catch {
1121
+ }
1122
+ map.addControl(
1123
+ new mapboxgl.AttributionControl({ compact: true }),
1124
+ "bottom-right"
1125
+ );
1126
+ map.addSource("collab-lines", { type: "geojson", data: emptyFC() });
1127
+ map.addLayer({
1128
+ id: "collab-lines-layer",
1129
+ type: "line",
1130
+ source: "collab-lines",
1131
+ paint: {
1132
+ "line-color": ["get", "color"],
1133
+ "line-width": 4,
1134
+ "line-opacity": 0.85
1135
+ }
1136
+ });
1137
+ map.addSource("draw-preview", { type: "geojson", data: emptyFC() });
1138
+ map.addLayer({
1139
+ id: "draw-preview-layer",
1140
+ type: "line",
1141
+ source: "draw-preview",
1142
+ paint: {
1143
+ "line-color": myColorHex.value,
1144
+ "line-width": 2,
1145
+ "line-dasharray": [4, 3],
1146
+ "line-opacity": 0.7
1147
+ }
1148
+ });
1149
+ map.addSource("draw-points", { type: "geojson", data: emptyFC() });
1150
+ map.addLayer({
1151
+ id: "draw-points-layer",
1152
+ type: "circle",
1153
+ source: "draw-points",
1154
+ paint: {
1155
+ "circle-radius": 5,
1156
+ "circle-color": myColorHex.value,
1157
+ "circle-stroke-color": "white",
1158
+ "circle-stroke-width": 2
1159
+ }
1160
+ });
1161
+ map.addSource("measure-preview-lines", {
1162
+ type: "geojson",
1163
+ data: emptyFC()
1164
+ });
1165
+ map.addLayer({
1166
+ id: "measure-preview-lines-layer",
1167
+ type: "line",
1168
+ source: "measure-preview-lines",
1169
+ paint: {
1170
+ "line-color": "#facc15",
1171
+ "line-width": 2,
1172
+ "line-dasharray": [3, 2]
1173
+ }
1174
+ });
1175
+ map.addSource("measure-preview-points", {
1176
+ type: "geojson",
1177
+ data: emptyFC()
1178
+ });
1179
+ map.addLayer({
1180
+ id: "measure-preview-points-layer",
1181
+ type: "circle",
1182
+ source: "measure-preview-points",
1183
+ paint: {
1184
+ "circle-radius": 6,
1185
+ "circle-color": "#facc15",
1186
+ "circle-stroke-color": "white",
1187
+ "circle-stroke-width": 2
1188
+ }
1189
+ });
1190
+ map.addSource("measure-collab-lines", {
1191
+ type: "geojson",
1192
+ data: emptyFC()
1193
+ });
1194
+ map.addLayer({
1195
+ id: "measure-collab-lines-layer",
1196
+ type: "line",
1197
+ source: "measure-collab-lines",
1198
+ paint: {
1199
+ "line-color": "#facc15",
1200
+ "line-width": 3,
1201
+ "line-dasharray": [3, 2],
1202
+ "line-opacity": 0.85
1203
+ }
1204
+ });
1205
+ map.addSource("measure-collab-points", {
1206
+ type: "geojson",
1207
+ data: emptyFC()
1208
+ });
1209
+ map.addLayer({
1210
+ id: "measure-collab-points-layer",
1211
+ type: "circle",
1212
+ source: "measure-collab-points",
1213
+ paint: {
1214
+ "circle-radius": 4,
1215
+ "circle-color": "#facc15",
1216
+ "circle-stroke-color": "white",
1217
+ "circle-stroke-width": 1.5,
1218
+ "circle-opacity": 0.85
1219
+ }
1220
+ });
1221
+ mapReady.value = true;
1222
+ setTimeout(() => mapFadedIn.value = true, 100);
1223
+ map.on("moveend", () => {
1224
+ if (followingClientId.value === null) broadcastCamera();
1225
+ });
1226
+ for (const ev of ["dragstart", "zoomstart", "pitchstart", "rotatestart"]) {
1227
+ map.on(ev, breakFollow);
1228
+ }
1229
+ map.on("click", handleMapClick);
1230
+ map.on("dblclick", handleMapDblClick);
1231
+ map.on("contextmenu", handleMapContextMenu);
1232
+ syncMarkers();
1233
+ syncLineSource();
1234
+ syncLinePoints();
1235
+ syncMeasureSource();
1236
+ startLoop();
1237
+ broadcastCamera();
1238
+ });
1239
+ map.on("error", (e) => console.warn("Map error:", e));
1240
+ const ro = new ResizeObserver(() => map?.resize());
1241
+ ro.observe(mapContainer.value);
1242
+ mapResizeObserver = ro;
1243
+ });
1244
+ watch(isDark, () => {
1245
+ if (map?.isStyleLoaded())
1246
+ map.setConfigProperty("basemap", "lightPreset", getLightPreset());
1247
+ });
1248
+ onUnmounted(() => {
1249
+ mapResizeObserver?.disconnect();
1250
+ mapResizeObserver = null;
1251
+ window.removeEventListener("keydown", onKeyDown);
1252
+ cancelAnimationFrame(rafId);
1253
+ setLocalState({ mapCamera: void 0, mapCursor: null, mapPing: null });
1254
+ for (const m of markerInstances.values()) m.remove();
1255
+ for (const m of linePointInstances.values()) m.remove();
1256
+ for (const p of presenceLerpMap.values()) p.marker.remove();
1257
+ for (const c of cursorLerpMap.values()) c.el.remove();
1258
+ for (const e of pingElements.values()) e.remove();
1259
+ for (const e of measureLabelEls.values()) e.remove();
1260
+ markerInstances.clear();
1261
+ linePointInstances.clear();
1262
+ presenceLerpMap.clear();
1263
+ cursorLerpMap.clear();
1264
+ pingElements.clear();
1265
+ measureLabelEls.clear();
1266
+ map?.remove();
1267
+ map = null;
1268
+ });
1269
+ function userInitials(name) {
1270
+ return name.split(" ").map((w) => w[0] ?? "").join("").substring(0, 2).toUpperCase();
1271
+ }
1272
+ defineExpose({ connectedUsers });
1273
+ </script>
1274
+
1275
+ <template>
1276
+ <div class="a-map-renderer-root">
1277
+ <!-- Mapbox not installed fallback -->
1278
+ <div
1279
+ v-if="mapboxError"
1280
+ class="flex flex-col items-center justify-center flex-1 gap-3"
1281
+ >
1282
+ <UIcon
1283
+ name="i-lucide-map"
1284
+ class="size-12 text-(--ui-text-dimmed)"
1285
+ />
1286
+ <p class="text-sm text-(--ui-text-muted) text-center max-w-xs">
1287
+ {{ mapboxError }}
1288
+ </p>
1289
+ </div>
1290
+
1291
+ <template v-else>
1292
+ <!-- Map canvas -->
1293
+ <div
1294
+ ref="mapContainer"
1295
+ class="absolute inset-0 transition-opacity duration-1000"
1296
+ :class="[mapFadedIn ? 'opacity-100' : 'opacity-0', mapCursorClass]"
1297
+ @pointermove="onMapPointerMove"
1298
+ @pointerleave="onMapPointerLeave"
1299
+ />
1300
+
1301
+ <!-- Toolbar (bottom-center) -->
1302
+ <div
1303
+ v-if="editable"
1304
+ class="a-map-toolbar-bar"
1305
+ >
1306
+ <div class="toolbar-group">
1307
+ <UButton
1308
+ icon="i-lucide-hand"
1309
+ v-bind="toolBtn('pan')"
1310
+ :title="locale.pan"
1311
+ @click="setMode('pan')"
1312
+ />
1313
+ <UButton
1314
+ icon="i-lucide-map-pin"
1315
+ v-bind="toolBtn('marker')"
1316
+ :title="locale.addMarker"
1317
+ @click="setMode('marker')"
1318
+ />
1319
+ <UButton
1320
+ icon="i-lucide-radio"
1321
+ v-bind="toolBtn('ping')"
1322
+ :title="locale.ping"
1323
+ @click="setMode('ping')"
1324
+ />
1325
+ <UButton
1326
+ icon="i-lucide-route"
1327
+ v-bind="toolBtn('line')"
1328
+ :title="locale.addLine"
1329
+ @click="setMode('line')"
1330
+ />
1331
+ <UButton
1332
+ icon="i-lucide-ruler"
1333
+ v-bind="toolBtn('measure')"
1334
+ :title="locale.addMeasure"
1335
+ @click="setMode('measure')"
1336
+ />
1337
+ </div>
1338
+ <div class="toolbar-divider" />
1339
+ <UButton
1340
+ icon="i-lucide-type"
1341
+ size="sm"
1342
+ :color="showLabels ? 'primary' : 'neutral'"
1343
+ :variant="showLabels ? 'soft' : 'ghost'"
1344
+ :title="showLabels ? locale.hideLabels : locale.showLabels"
1345
+ :class="{ 'opacity-40': !showLabels }"
1346
+ @click="showLabels = !showLabels"
1347
+ />
1348
+ </div>
1349
+
1350
+ <!-- Follow badge -->
1351
+ <Transition name="badge-fade">
1352
+ <div
1353
+ v-if="followingUser"
1354
+ class="follow-badge"
1355
+ >
1356
+ <UIcon
1357
+ name="i-lucide-eye"
1358
+ class="badge-icon"
1359
+ />
1360
+ <span>{{ locale.following }} {{ followingUser.name }}</span>
1361
+ <button
1362
+ class="badge-close"
1363
+ @click="followingClientId = null"
1364
+ >
1365
+ <UIcon name="i-lucide-x" />
1366
+ </button>
1367
+ </div>
1368
+ </Transition>
1369
+
1370
+ <!-- Mode hint bar -->
1371
+ <Transition name="badge-fade">
1372
+ <div
1373
+ v-if="modeHint"
1374
+ class="place-hint"
1375
+ >
1376
+ <UIcon :name="modeHintIcon" />
1377
+ <span>{{ modeHint }}</span>
1378
+ <template v-if="activeMode === 'line' && drawPoints.length >= 2">
1379
+ <span class="ml-1 opacity-50">&middot;</span>
1380
+ <button
1381
+ class="underline opacity-80 ml-1 pointer-events-auto"
1382
+ @click="finishLine"
1383
+ >
1384
+ {{ locale.finish }}
1385
+ </button>
1386
+ </template>
1387
+ <template v-if="activeMode === 'measure' && measurePoints.length >= 2">
1388
+ <span class="ml-1 opacity-50">&middot;</span>
1389
+ <button
1390
+ class="underline opacity-80 ml-1 pointer-events-auto"
1391
+ @click="finishMeasure"
1392
+ >
1393
+ {{ locale.save }}
1394
+ </button>
1395
+ </template>
1396
+ </div>
1397
+ </Transition>
1398
+
1399
+ <!-- In-progress measure distance badge -->
1400
+ <Transition name="badge-fade">
1401
+ <div
1402
+ v-if="activeMode === 'measure' && measurePoints.length >= 2"
1403
+ class="measure-preview-badge"
1404
+ :style="{
1405
+ left: measurePreviewScreenPos.x + 'px',
1406
+ top: measurePreviewScreenPos.y + 'px'
1407
+ }"
1408
+ >
1409
+ {{ formatDist(measurePreviewTotal) }}
1410
+ </div>
1411
+ </Transition>
1412
+
1413
+ <!-- Marker popup overlay -->
1414
+ <Transition name="popup-fade">
1415
+ <div
1416
+ v-if="selectedMarker"
1417
+ class="map-popup-overlay map-popup-editor"
1418
+ :style="{
1419
+ left: markerPopupScreenPos.x + 'px',
1420
+ top: markerPopupScreenPos.y + 'px'
1421
+ }"
1422
+ >
1423
+ <div class="popup-row popup-header">
1424
+ <UIcon
1425
+ :name="`i-lucide-${selectedMarker.meta?.icon ?? 'map-pin'}`"
1426
+ class="popup-icon"
1427
+ />
1428
+ <span class="popup-label">{{ selectedMarker.label }}</span>
1429
+ <UButton
1430
+ icon="i-lucide-external-link"
1431
+ size="xs"
1432
+ color="neutral"
1433
+ variant="ghost"
1434
+ :title="locale.openInPanel"
1435
+ @click="openNode(selectedMarkerKey, selectedMarker.label);
1436
+ closeMarkerPopup()"
1437
+ />
1438
+ <UButton
1439
+ v-if="editable"
1440
+ icon="i-lucide-trash-2"
1441
+ size="xs"
1442
+ color="error"
1443
+ variant="ghost"
1444
+ @click="deleteSelectedMarker"
1445
+ />
1446
+ <UButton
1447
+ icon="i-lucide-x"
1448
+ size="xs"
1449
+ color="neutral"
1450
+ variant="ghost"
1451
+ @click="closeMarkerPopup"
1452
+ />
1453
+ </div>
1454
+ <div class="popup-editor-body">
1455
+ <div
1456
+ v-if="!popupProvider"
1457
+ class="popup-editor-loading"
1458
+ >
1459
+ <UIcon
1460
+ name="i-lucide-loader-circle"
1461
+ class="size-4 animate-spin text-(--ui-text-dimmed)"
1462
+ />
1463
+ </div>
1464
+ <AEditor
1465
+ v-else
1466
+ :doc-id="selectedMarkerKey"
1467
+ :child-provider="popupProvider"
1468
+ :doc-label="selectedMarker.label"
1469
+ />
1470
+ </div>
1471
+ </div>
1472
+ </Transition>
1473
+
1474
+ <!-- Line selection popup -->
1475
+ <Transition name="popup-fade">
1476
+ <div
1477
+ v-if="selectedLine"
1478
+ class="map-popup-overlay map-popup-editor"
1479
+ :style="{
1480
+ left: linePopupScreenPos.x + 'px',
1481
+ top: linePopupScreenPos.y + 'px'
1482
+ }"
1483
+ >
1484
+ <div class="popup-row popup-header">
1485
+ <UIcon
1486
+ name="i-lucide-route"
1487
+ class="popup-icon"
1488
+ />
1489
+ <span class="popup-label">{{ selectedLine.label }}</span>
1490
+ <UButton
1491
+ icon="i-lucide-external-link"
1492
+ size="xs"
1493
+ color="neutral"
1494
+ variant="ghost"
1495
+ :title="locale.openInPanel"
1496
+ @click="openNode(selectedLineKey, selectedLine.label);
1497
+ selectedLineKey = null"
1498
+ />
1499
+ <UButton
1500
+ v-if="editable"
1501
+ icon="i-lucide-trash-2"
1502
+ size="xs"
1503
+ color="error"
1504
+ variant="ghost"
1505
+ @click="deleteSelectedLine"
1506
+ />
1507
+ <UButton
1508
+ icon="i-lucide-x"
1509
+ size="xs"
1510
+ color="neutral"
1511
+ variant="ghost"
1512
+ @click="selectedLineKey = null"
1513
+ />
1514
+ </div>
1515
+ <div class="popup-editor-body">
1516
+ <div
1517
+ v-if="!popupProvider"
1518
+ class="popup-editor-loading"
1519
+ >
1520
+ <UIcon
1521
+ name="i-lucide-loader-circle"
1522
+ class="size-4 animate-spin text-(--ui-text-dimmed)"
1523
+ />
1524
+ </div>
1525
+ <AEditor
1526
+ v-else
1527
+ :doc-id="selectedLineKey"
1528
+ :child-provider="popupProvider"
1529
+ :doc-label="selectedLine.label"
1530
+ />
1531
+ </div>
1532
+ </div>
1533
+ </Transition>
1534
+
1535
+ <!-- Measure selection popup -->
1536
+ <Transition name="popup-fade">
1537
+ <div
1538
+ v-if="selectedMeasureInfo"
1539
+ class="map-popup-overlay"
1540
+ :style="{
1541
+ left: measurePopupScreenPos.x + 'px',
1542
+ top: measurePopupScreenPos.y + 'px'
1543
+ }"
1544
+ >
1545
+ <div class="popup-row">
1546
+ <UIcon
1547
+ name="i-lucide-ruler"
1548
+ class="popup-icon"
1549
+ />
1550
+ <span class="popup-dist">{{
1551
+ formatDist(selectedMeasureInfo.dist)
1552
+ }}</span>
1553
+ <UButton
1554
+ icon="i-lucide-x"
1555
+ size="xs"
1556
+ color="neutral"
1557
+ variant="ghost"
1558
+ @click="selectedMeasureKey = null"
1559
+ />
1560
+ </div>
1561
+ <div class="popup-row popup-meta">
1562
+ <UButton
1563
+ v-if="editable"
1564
+ icon="i-lucide-trash-2"
1565
+ size="xs"
1566
+ color="error"
1567
+ variant="ghost"
1568
+ @click="deleteSelectedMeasure"
1569
+ />
1570
+ </div>
1571
+ </div>
1572
+ </Transition>
1573
+
1574
+ <!-- Presence HUD -->
1575
+ <div
1576
+ v-if="remoteUsers.length > 0"
1577
+ class="presence-hud"
1578
+ >
1579
+ <div
1580
+ v-for="user in remoteUsers"
1581
+ :key="user.clientId"
1582
+ class="presence-pill"
1583
+ :class="{ 'is-following': followingClientId === user.clientId }"
1584
+ :title="
1585
+ followingClientId === user.clientId ? `${locale.unfollow} ${user.name}` : `${locale.follow} ${user.name}`
1586
+ "
1587
+ @click="
1588
+ followingClientId === user.clientId ? followingClientId = null : followUser(user.clientId)
1589
+ "
1590
+ >
1591
+ <div
1592
+ class="hud-avatar"
1593
+ :style="{ background: user.colorHex }"
1594
+ >
1595
+ {{ userInitials(user.name) }}
1596
+ </div>
1597
+ <span class="hud-name">{{ user.name }}</span>
1598
+ <UIcon
1599
+ v-if="followingClientId === user.clientId"
1600
+ name="i-lucide-eye"
1601
+ class="hud-follow-icon"
1602
+ />
1603
+ </div>
1604
+ </div>
1605
+ </template>
1606
+
1607
+ <ANodePanel
1608
+ :node-id="openNodeId"
1609
+ :node-label="openNodeLabel"
1610
+ :child-provider="openNodeProvider"
1611
+ @close="closePanel"
1612
+ />
1613
+ </div>
1614
+ </template>
1615
+
1616
+ <style scoped>
1617
+ .a-map-renderer-root{display:flex;flex:1;flex-direction:column;height:100%;min-height:0;overflow:hidden;position:relative}.a-map-toolbar-bar{align-items:center;backdrop-filter:blur(8px);background:var(--ui-bg);border:1px solid var(--ui-border);border-radius:10px;bottom:2rem;display:flex;gap:3px;left:50%;padding:3px;position:absolute;transform:translateX(-50%);z-index:20}.toolbar-group{align-items:center;display:flex;gap:1px}.toolbar-divider{background:var(--ui-border);flex-shrink:0;height:20px;margin:0 2px;width:1px}.follow-badge{align-items:center;backdrop-filter:blur(8px);background:var(--ui-bg);border:1px solid var(--ui-border);border-radius:999px;color:var(--ui-text);display:flex;font-family:ui-monospace,SF Mono,monospace;font-size:.72rem;gap:.375rem;left:50%;padding:.3rem .5rem .3rem .75rem;position:absolute;top:1rem;transform:translateX(-50%);white-space:nowrap;z-index:20}.badge-icon{color:var(--ui-primary);font-size:.8rem}.badge-close{align-items:center;background:var(--ui-bg-elevated);border:none;border-radius:50%;color:var(--ui-text-dimmed);cursor:pointer;display:flex;font-size:.65rem;height:1.2rem;justify-content:center;margin-left:.2rem;transition:background .15s;width:1.2rem}.badge-close:hover{background:var(--ui-bg-accented);color:var(--ui-text)}.place-hint{align-items:center;backdrop-filter:blur(8px);background:var(--ui-bg);border:1px solid var(--ui-border);border-radius:999px;bottom:5.5rem;color:var(--ui-text);display:flex;font-family:ui-monospace,SF Mono,monospace;font-size:.7rem;gap:.375rem;left:50%;padding:.3rem .75rem;pointer-events:none;position:absolute;transform:translateX(-50%);white-space:nowrap;z-index:20}.place-hint button{pointer-events:auto}.measure-preview-badge{background:rgba(250,204,21,.9);border-radius:4px;color:#000;font-size:.65rem;font-weight:700;padding:2px 6px;pointer-events:none;position:absolute;white-space:nowrap;z-index:35}.map-popup-overlay{background:var(--ui-bg);border:1px solid var(--ui-border);border-radius:7px;box-shadow:0 4px 20px rgba(0,0,0,.45);display:flex;flex-direction:column;gap:4px;max-width:240px;min-width:180px;padding:5px;pointer-events:auto;position:absolute;z-index:40}.popup-row{align-items:center;display:flex;gap:4px}.popup-meta{justify-content:space-between;padding:0 2px}.popup-meta>span{color:var(--ui-text-muted);font-size:.65rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.popup-icon{color:var(--ui-text-muted);flex-shrink:0;font-size:.8rem}.popup-dist{flex:1;font-size:.8rem;font-weight:700}.popup-label{color:var(--ui-text);flex:1;font-size:.75rem;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.map-popup-editor{display:flex;flex-direction:column;max-height:400px;max-width:480px;min-width:400px}.popup-header{border-bottom:1px solid var(--ui-border);flex-shrink:0;padding-bottom:4px}.popup-editor-body{flex:1;max-height:340px;min-height:0;overflow-y:auto}.popup-editor-body :deep(.tiptap){font-size:.8rem;min-height:60px;padding:4px 8px}.popup-editor-body :deep(h1.document-header){font-size:.9rem;padding-top:4px}.popup-editor-body :deep([data-type=document-meta]){font-size:.75rem;padding:0 8px}.popup-editor-loading{align-items:center;display:flex;justify-content:center;padding:20px}.presence-hud{display:flex;flex-direction:column;gap:.375rem;position:absolute;right:.75rem;top:.75rem;z-index:20}.presence-pill{align-items:center;backdrop-filter:blur(8px);background:var(--ui-bg);border:1px solid var(--ui-border);border-radius:999px;cursor:pointer;display:flex;gap:.5rem;padding:.25rem .6rem .25rem .25rem;transition:background .15s,border-color .15s}.presence-pill:hover{background:var(--ui-bg-elevated)}.presence-pill.is-following{background:var(--ui-bg-accented);border-color:var(--ui-primary)}.hud-avatar{align-items:center;border:1.5px solid var(--ui-border);border-radius:50%;color:#fff;display:flex;flex-shrink:0;font-size:.6rem;font-weight:700;height:1.5rem;justify-content:center;width:1.5rem}.hud-name{color:var(--ui-text);font-family:ui-monospace,SF Mono,monospace;font-size:.72rem;white-space:nowrap}.hud-follow-icon{color:var(--ui-primary);flex-shrink:0;font-size:.7rem}.badge-fade-enter-active,.badge-fade-leave-active{transition:opacity .25s ease}.badge-fade-enter-from,.badge-fade-leave-to{opacity:0}.popup-fade-enter-active,.popup-fade-leave-active{transition:opacity .15s ease,transform .15s ease}.popup-fade-enter-from,.popup-fade-leave-to{opacity:0;transform:scale(.95) translateY(4px)}
1618
+ </style>
1619
+
1620
+ <style>
1621
+ .mapboxgl-map{height:100%!important;width:100%!important}.collab-marker-container{align-items:center;cursor:pointer;display:flex;flex-direction:column}.collab-marker-pin{align-items:center;border-radius:50%;display:flex;height:28px;justify-content:center;transition:transform .2s ease;width:28px;z-index:30}.collab-marker-pin:hover{transform:scale(1.15)}.collab-marker-label{background:rgba(0,0,0,.5);border-radius:3px;color:#fff;font-size:11px;font-weight:700;margin-top:3px;padding:1px 5px;pointer-events:none;text-shadow:0 1px 4px rgba(0,0,0,.7);white-space:nowrap}.collab-line-point{border:2px solid #fff;border-radius:50%;box-shadow:0 1px 4px rgba(0,0,0,.4);cursor:grab;height:10px;width:10px}.collab-line-point:hover{height:14px;width:14px}.presence-marker{align-items:center;display:flex;flex-direction:column;gap:2px;pointer-events:none}.presence-avatar{align-items:center;border:2px solid #fff;border-radius:50%;box-shadow:0 2px 8px rgba(0,0,0,.4);color:#fff;display:flex;font-size:.65rem;font-weight:700;height:1.75rem;justify-content:center;width:1.75rem}.presence-label{color:#fff;font-size:.6rem;font-weight:700;text-shadow:0 1px 4px rgba(0,0,0,.8);white-space:nowrap}.map-cursor-el{left:0;pointer-events:none;position:absolute;top:0;will-change:transform;z-index:50}.map-cursor-name{font-size:.6rem;font-weight:600;left:14px;top:8px}.map-cursor-name,.measure-label-el{border-radius:3px;color:#000;padding:1px 5px;position:absolute;white-space:nowrap}.measure-label-el{background:rgba(250,204,21,.92);box-shadow:0 1px 4px rgba(0,0,0,.3);font-size:.62rem;font-weight:700;left:0;pointer-events:none;top:0;transform:translate(-50%,calc(-100% - 5px));z-index:35}@keyframes ping-ring{0%{opacity:1;transform:scale(0)}to{opacity:0;transform:scale(3)}}.map-ping-root{height:12px;left:0;pointer-events:none;position:absolute;top:0;transform:translate(-50%,-50%);width:12px;z-index:45}.map-ping-ring{animation:ping-ring 1.5s ease-out forwards;border:3px solid var(--ping-color);border-radius:50%;inset:-16px;position:absolute}.map-ping-dot{background:var(--ping-color);border:2px solid #fff;border-radius:50%;height:12px;left:0;position:absolute;top:0;width:12px}
1622
+ </style>