@abraca/nuxt 0.1.1 → 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 (154) 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/AFloatingWindow.vue +1 -1
  15. package/dist/runtime/components/AIconPicker.vue +8 -2
  16. package/dist/runtime/components/ANodePanel.vue +100 -61
  17. package/dist/runtime/components/ANotifications.vue +35 -8
  18. package/dist/runtime/components/APermissionGuard.vue +3 -1
  19. package/dist/runtime/components/APresence.vue +14 -3
  20. package/dist/runtime/components/AProvider.vue +7 -1
  21. package/dist/runtime/components/AVoiceBar.vue +57 -15
  22. package/dist/runtime/components/AVoiceTile.vue +4 -1
  23. package/dist/runtime/components/AWindowLayer.vue +1 -1
  24. package/dist/runtime/components/aware/AArea.vue +1 -1
  25. package/dist/runtime/components/aware/AAvatar.vue +85 -16
  26. package/dist/runtime/components/aware/AButton.vue +5 -1
  27. package/dist/runtime/components/aware/ACursorLabel.vue +5 -1
  28. package/dist/runtime/components/aware/ADocBadge.vue +4 -1
  29. package/dist/runtime/components/aware/AFacepile.vue +13 -3
  30. package/dist/runtime/components/aware/AInput.vue +5 -1
  31. package/dist/runtime/components/aware/ATextarea.vue +5 -1
  32. package/dist/runtime/components/aware/AUserList.vue +8 -2
  33. package/dist/runtime/components/renderers/ACalendarRenderer.d.vue.ts +12 -1
  34. package/dist/runtime/components/renderers/ACalendarRenderer.vue +388 -114
  35. package/dist/runtime/components/renderers/ACalendarRenderer.vue.d.ts +12 -1
  36. package/dist/runtime/components/renderers/ACallRenderer.d.vue.ts +13 -0
  37. package/dist/runtime/components/renderers/ACallRenderer.vue +169 -0
  38. package/dist/runtime/components/renderers/ACallRenderer.vue.d.ts +13 -0
  39. package/dist/runtime/components/renderers/AChecklistRenderer.d.vue.ts +19 -0
  40. package/dist/runtime/components/renderers/AChecklistRenderer.vue +581 -0
  41. package/dist/runtime/components/renderers/AChecklistRenderer.vue.d.ts +19 -0
  42. package/dist/runtime/components/renderers/ADashboardRenderer.d.vue.ts +19 -0
  43. package/dist/runtime/components/renderers/ADashboardRenderer.vue +1372 -0
  44. package/dist/runtime/components/renderers/ADashboardRenderer.vue.d.ts +19 -0
  45. package/dist/runtime/components/renderers/AGalleryCoverImage.d.vue.ts +8 -0
  46. package/dist/runtime/components/renderers/AGalleryCoverImage.vue +60 -0
  47. package/dist/runtime/components/renderers/AGalleryCoverImage.vue.d.ts +8 -0
  48. package/dist/runtime/components/renderers/AGalleryRenderer.d.vue.ts +12 -1
  49. package/dist/runtime/components/renderers/AGalleryRenderer.vue +221 -55
  50. package/dist/runtime/components/renderers/AGalleryRenderer.vue.d.ts +12 -1
  51. package/dist/runtime/components/renderers/AGraphRenderer.d.vue.ts +19 -0
  52. package/dist/runtime/components/renderers/AGraphRenderer.vue +1027 -0
  53. package/dist/runtime/components/renderers/AGraphRenderer.vue.d.ts +19 -0
  54. package/dist/runtime/components/renderers/AKanbanRenderer.d.vue.ts +13 -1
  55. package/dist/runtime/components/renderers/AKanbanRenderer.vue +474 -140
  56. package/dist/runtime/components/renderers/AKanbanRenderer.vue.d.ts +13 -1
  57. package/dist/runtime/components/renderers/AMapRenderer.d.vue.ts +19 -0
  58. package/dist/runtime/components/renderers/AMapRenderer.vue +1622 -0
  59. package/dist/runtime/components/renderers/AMapRenderer.vue.d.ts +19 -0
  60. package/dist/runtime/components/renderers/AOutlineRenderer.d.vue.ts +12 -1
  61. package/dist/runtime/components/renderers/AOutlineRenderer.vue +294 -134
  62. package/dist/runtime/components/renderers/AOutlineRenderer.vue.d.ts +12 -1
  63. package/dist/runtime/components/renderers/ATableRenderer.d.vue.ts +12 -1
  64. package/dist/runtime/components/renderers/ATableRenderer.vue +437 -145
  65. package/dist/runtime/components/renderers/ATableRenderer.vue.d.ts +12 -1
  66. package/dist/runtime/components/renderers/ATimelineRenderer.d.vue.ts +19 -0
  67. package/dist/runtime/components/renderers/ATimelineRenderer.vue +446 -0
  68. package/dist/runtime/components/renderers/ATimelineRenderer.vue.d.ts +19 -0
  69. package/dist/runtime/composables/useAwareness.js +5 -0
  70. package/dist/runtime/composables/useBroadcastSync.d.ts +18 -0
  71. package/dist/runtime/composables/useBroadcastSync.js +26 -0
  72. package/dist/runtime/composables/useChat.js +4 -2
  73. package/dist/runtime/composables/useChatUsers.js +2 -1
  74. package/dist/runtime/composables/useCommandPalette.js +62 -3
  75. package/dist/runtime/composables/useConnectionStatus.js +7 -0
  76. package/dist/runtime/composables/useDevicePairing.d.ts +58 -0
  77. package/dist/runtime/composables/useDevicePairing.js +108 -0
  78. package/dist/runtime/composables/useDocExport.d.ts +5 -0
  79. package/dist/runtime/composables/useDocExport.js +2 -2
  80. package/dist/runtime/composables/useDocImport.js +4 -3
  81. package/dist/runtime/composables/useDocSeo.d.ts +20 -0
  82. package/dist/runtime/composables/useDocSeo.js +44 -0
  83. package/dist/runtime/composables/useDocSlugs.d.ts +7 -0
  84. package/dist/runtime/composables/useDocSlugs.js +20 -0
  85. package/dist/runtime/composables/useDocTree.d.ts +34 -0
  86. package/dist/runtime/composables/useDocTree.js +35 -0
  87. package/dist/runtime/composables/useEditorDragHandle.js +2 -1
  88. package/dist/runtime/composables/useEditorMentions.js +4 -2
  89. package/dist/runtime/composables/useEditorSuggestions.d.ts +1 -0
  90. package/dist/runtime/composables/useEditorSuggestions.js +9 -2
  91. package/dist/runtime/composables/useEditorToolbar.js +2 -1
  92. package/dist/runtime/composables/useFileIndex.js +2 -1
  93. package/dist/runtime/composables/useFileTransfer.d.ts +112 -0
  94. package/dist/runtime/composables/useFileTransfer.js +171 -0
  95. package/dist/runtime/composables/useFollowUser.js +2 -1
  96. package/dist/runtime/composables/useInvites.d.ts +56 -0
  97. package/dist/runtime/composables/useInvites.js +77 -0
  98. package/dist/runtime/composables/useNodePanel.d.ts +14 -0
  99. package/dist/runtime/composables/useNodePanel.js +52 -0
  100. package/dist/runtime/composables/useNotifications.js +4 -2
  101. package/dist/runtime/composables/usePasskeyAccounts.js +4 -2
  102. package/dist/runtime/composables/useSearchIndex.d.ts +1 -0
  103. package/dist/runtime/composables/useSearchIndex.js +13 -5
  104. package/dist/runtime/composables/useServerInfo.d.ts +31 -0
  105. package/dist/runtime/composables/useServerInfo.js +80 -0
  106. package/dist/runtime/composables/useSlugRoute.d.ts +6 -0
  107. package/dist/runtime/composables/useSlugRoute.js +19 -0
  108. package/dist/runtime/composables/useSpaces.d.ts +37 -0
  109. package/dist/runtime/composables/useSpaces.js +83 -0
  110. package/dist/runtime/composables/useTouchDrag.d.ts +34 -0
  111. package/dist/runtime/composables/useTouchDrag.js +191 -0
  112. package/dist/runtime/composables/useTrash.d.ts +1 -1
  113. package/dist/runtime/composables/useTrash.js +6 -3
  114. package/dist/runtime/composables/useWebRTC.d.ts +50 -0
  115. package/dist/runtime/composables/useWebRTC.js +177 -0
  116. package/dist/runtime/extensions/meta-field.d.ts +4 -1
  117. package/dist/runtime/extensions/steps.js +1 -1
  118. package/dist/runtime/extensions/views/AccordionItemView.vue +13 -3
  119. package/dist/runtime/extensions/views/AccordionView.vue +4 -1
  120. package/dist/runtime/extensions/views/BadgeView.vue +11 -2
  121. package/dist/runtime/extensions/views/CalloutView.vue +4 -1
  122. package/dist/runtime/extensions/views/CardGroupView.vue +4 -1
  123. package/dist/runtime/extensions/views/CardView.vue +17 -3
  124. package/dist/runtime/extensions/views/CodeGroupView.vue +4 -1
  125. package/dist/runtime/extensions/views/CollapsibleView.vue +8 -2
  126. package/dist/runtime/extensions/views/FileNodeView.vue +32 -8
  127. package/dist/runtime/extensions/views/KbdView.vue +8 -2
  128. package/dist/runtime/extensions/views/MetaFieldView.vue +208 -46
  129. package/dist/runtime/extensions/views/ProseIconView.vue +8 -2
  130. package/dist/runtime/extensions/views/TabsView.vue +17 -4
  131. package/dist/runtime/locale.d.ts +71 -0
  132. package/dist/runtime/locale.js +71 -0
  133. package/dist/runtime/plugin-abracadabra.client.js +29 -3
  134. package/dist/runtime/plugin-abracadabra.server.js +2 -0
  135. package/dist/runtime/server/api/_abracadabra/render/[docId].get.d.ts +1 -1
  136. package/dist/runtime/server/api/_abracadabra/render/[docId].get.js +29 -4
  137. package/dist/runtime/server/api/_abracadabra/resolve/[...slug].get.d.ts +2 -0
  138. package/dist/runtime/server/api/_abracadabra/resolve/[...slug].get.js +43 -0
  139. package/dist/runtime/server/api/_abracadabra/slugs.get.d.ts +2 -0
  140. package/dist/runtime/server/api/_abracadabra/slugs.get.js +7 -0
  141. package/dist/runtime/server/plugins/abracadabra-service.js +10 -5
  142. package/dist/runtime/server/runners/doc-tree-cache.js +4 -0
  143. package/dist/runtime/server/utils/slugMap.d.ts +32 -0
  144. package/dist/runtime/server/utils/slugMap.js +58 -0
  145. package/dist/runtime/types.d.ts +1 -0
  146. package/dist/runtime/utils/docTypes.d.ts +29 -1
  147. package/dist/runtime/utils/docTypes.js +129 -1
  148. package/dist/runtime/utils/markdownToYjs.js +2 -2
  149. package/dist/runtime/utils/sdkRef.d.ts +2 -0
  150. package/dist/runtime/utils/sdkRef.js +7 -0
  151. package/dist/runtime/utils/slugify.d.ts +40 -0
  152. package/dist/runtime/utils/slugify.js +36 -0
  153. package/dist/types.d.mts +6 -0
  154. package/package.json +32 -19
@@ -0,0 +1,1372 @@
1
+ <script setup>
2
+ import { ref, reactive, computed, watch, nextTick, onMounted, onBeforeUnmount, markRaw } from "vue";
3
+ import { useRendererBase } from "../../composables/useRendererBase";
4
+ import { useNodePanel } from "../../composables/useNodePanel";
5
+ import { resolveDocType } from "../../utils/docTypes";
6
+ import { DEFAULT_LOCALE } from "../../locale";
7
+ const props = defineProps({
8
+ docId: { type: String, required: true },
9
+ childProvider: { type: null, required: true },
10
+ docLabel: { type: String, required: true },
11
+ pageTypes: { type: Array, required: false },
12
+ labels: { type: Object, required: false },
13
+ editable: { type: Boolean, required: false, default: true }
14
+ });
15
+ const config = useRuntimeConfig();
16
+ const locale = computed(() => ({
17
+ ...DEFAULT_LOCALE.renderers.dashboard,
18
+ ...config.public?.abracadabra?.locale?.renderers?.dashboard ?? {},
19
+ ...props.labels ?? {}
20
+ }));
21
+ const { tree, childDoc, childProviderRef, cursors, states, setLocalState, connectedUsers } = useRendererBase(props);
22
+ const {
23
+ openNodeId,
24
+ openNodeLabel,
25
+ openNodeProvider,
26
+ openNode,
27
+ closePanel
28
+ } = useNodePanel(childProviderRef);
29
+ const GRID_SIZE = 80;
30
+ const ICON_MARGIN = 16;
31
+ const WIDGET_SM = { w: 240, h: 180 };
32
+ const WIDGET_LG = { w: 400, h: 320 };
33
+ const MIN_WIDGET_W = 160;
34
+ const MIN_WIDGET_H = 120;
35
+ const resizeDirs = ["n", "s", "e", "w", "ne", "nw", "se", "sw"];
36
+ const selectedIds = ref(/* @__PURE__ */ new Set());
37
+ const gridRef = ref(null);
38
+ const scrollContainerRef = ref(null);
39
+ const temporaryPositions = reactive({});
40
+ const temporarySizes = reactive({});
41
+ const children = computed(() => tree.childrenOf(null));
42
+ const CANVAS_PADDING = 200;
43
+ const canvasSize = computed(() => {
44
+ let maxX = 0;
45
+ let maxY = 0;
46
+ children.value.forEach((item) => {
47
+ const pos = getIconPosition(item.id);
48
+ const size = getItemSize(item.id);
49
+ maxX = Math.max(maxX, pos.x + size.w);
50
+ maxY = Math.max(maxY, pos.y + size.h);
51
+ });
52
+ const el = scrollContainerRef.value;
53
+ const minW = el?.clientWidth ?? window.innerWidth;
54
+ const minH = el?.clientHeight ?? window.innerHeight;
55
+ return {
56
+ w: Math.max(minW, maxX + CANVAS_PADDING),
57
+ h: Math.max(minH, maxY + CANVAS_PADDING)
58
+ };
59
+ });
60
+ const zCounter = ref(1);
61
+ watch(children, (items) => {
62
+ const maxZ = items.reduce((max, item) => Math.max(max, item.meta?.deskZ ?? 0), 0);
63
+ if (maxZ >= zCounter.value) zCounter.value = maxZ + 1;
64
+ }, { immediate: true });
65
+ function nextZIndex() {
66
+ return zCounter.value++;
67
+ }
68
+ const contextMenuPosition = ref(null);
69
+ const contextMenuOpen = ref(false);
70
+ const rightClickedItem = ref(null);
71
+ const isSelecting = ref(false);
72
+ const selectionRect = ref(null);
73
+ const pointerDragActive = ref(false);
74
+ const pointerDragAnchorId = ref(null);
75
+ const pointerDragStart = ref(null);
76
+ const pointerDragOrigPositions = ref({});
77
+ const touchStartTime = ref(0);
78
+ const touchStartPos = ref(null);
79
+ const isTouchDragging = ref(false);
80
+ const showLongPressIndicator = ref(false);
81
+ const longPressPosition = ref(null);
82
+ const DOUBLE_TAP_DELAY = 300;
83
+ const lastTapTime = ref(0);
84
+ const lastTapItemId = ref(null);
85
+ const widgetProviders = reactive({});
86
+ const widgetLoadingIds = ref(/* @__PURE__ */ new Set());
87
+ const widgetErrors = reactive({});
88
+ async function loadWidgetProvider(childId) {
89
+ if (widgetProviders[childId] || widgetLoadingIds.value.has(childId)) return;
90
+ widgetLoadingIds.value.add(childId);
91
+ delete widgetErrors[childId];
92
+ try {
93
+ const prov = await childProviderRef.value.loadChild(childId);
94
+ if (!prov.isSynced) {
95
+ await new Promise((resolve) => {
96
+ const done = () => {
97
+ prov.off("synced", done);
98
+ resolve();
99
+ };
100
+ prov.on("synced", done);
101
+ setTimeout(resolve, 6e3);
102
+ });
103
+ }
104
+ await nextTick();
105
+ widgetProviders[childId] = markRaw(prov);
106
+ } catch (e) {
107
+ console.error("Failed to load widget provider:", e);
108
+ widgetErrors[childId] = e instanceof Error ? e.message : "Failed to load";
109
+ } finally {
110
+ widgetLoadingIds.value.delete(childId);
111
+ }
112
+ }
113
+ onErrorCaptured((err) => {
114
+ if (err?.message?.includes("Unexpected case") || err?.message?.includes("findRootTypeKey")) {
115
+ console.warn("Widget Yjs sync error (non-fatal):", err);
116
+ return false;
117
+ }
118
+ });
119
+ watch(
120
+ children,
121
+ (items) => {
122
+ items.forEach((item) => {
123
+ const mode = item.meta?.deskMode;
124
+ if ((mode === "widget-sm" || mode === "widget-lg") && !widgetProviders[item.id]) {
125
+ loadWidgetProvider(item.id);
126
+ }
127
+ });
128
+ },
129
+ { immediate: true }
130
+ );
131
+ let activeResizeId = null;
132
+ let resizeDir = "";
133
+ let resizeStartX = 0;
134
+ let resizeStartY = 0;
135
+ let resizeStartW = 0;
136
+ let resizeStartH = 0;
137
+ let resizeStartPosX = 0;
138
+ let resizeStartPosY = 0;
139
+ const isRenameModalOpen = ref(false);
140
+ const renameNewName = ref("");
141
+ const itemToRename = ref(null);
142
+ const isConfirmModalOpen = ref(false);
143
+ const confirmModalTitle = ref("");
144
+ const confirmModalMessage = ref("");
145
+ const confirmAction = ref(() => {
146
+ });
147
+ const widgetScrollEls = /* @__PURE__ */ new Map();
148
+ const widgetBodyCleanup = /* @__PURE__ */ new Map();
149
+ const suppressScrollEcho = /* @__PURE__ */ new Set();
150
+ const scrollTrailingTimers = /* @__PURE__ */ new Map();
151
+ function registerWidgetBody(el, id) {
152
+ widgetBodyCleanup.get(id)?.();
153
+ widgetBodyCleanup.delete(id);
154
+ widgetScrollEls.delete(id);
155
+ if (el instanceof HTMLElement) {
156
+ const handler = (event) => {
157
+ const scrollEl = event.target;
158
+ widgetScrollEls.set(id, scrollEl);
159
+ if (suppressScrollEcho.has(id)) return;
160
+ const existing = scrollTrailingTimers.get(id);
161
+ if (existing) clearTimeout(existing);
162
+ scrollTrailingTimers.set(id, setTimeout(() => {
163
+ scrollTrailingTimers.delete(id);
164
+ tree.updateMeta(id, { deskScrollY: Math.round(scrollEl.scrollTop) });
165
+ }, 80));
166
+ };
167
+ el.addEventListener("scroll", handler, true);
168
+ widgetBodyCleanup.set(id, () => {
169
+ el.removeEventListener("scroll", handler, true);
170
+ const t = scrollTrailingTimers.get(id);
171
+ if (t) clearTimeout(t);
172
+ scrollTrailingTimers.delete(id);
173
+ });
174
+ const entry = children.value.find((c) => c.id === id);
175
+ const scrollY2 = entry?.meta?.deskScrollY;
176
+ if (scrollY2 != null && scrollY2 > 0) {
177
+ nextTick(() => {
178
+ suppressScrollEcho.add(id);
179
+ el.scrollTop = scrollY2;
180
+ const inner = el.querySelector('[class*="overflow"]');
181
+ if (inner) {
182
+ inner.scrollTop = scrollY2;
183
+ widgetScrollEls.set(id, inner);
184
+ }
185
+ requestAnimationFrame(() => suppressScrollEcho.delete(id));
186
+ });
187
+ }
188
+ }
189
+ }
190
+ watch(children, (items) => {
191
+ for (const item of items) {
192
+ const scrollY2 = item.meta?.deskScrollY;
193
+ if (scrollY2 == null) continue;
194
+ if (scrollTrailingTimers.has(item.id)) continue;
195
+ const el = widgetScrollEls.get(item.id);
196
+ if (el && Math.abs(el.scrollTop - scrollY2) > 2) {
197
+ suppressScrollEcho.add(item.id);
198
+ el.scrollTop = scrollY2;
199
+ requestAnimationFrame(() => suppressScrollEcho.delete(item.id));
200
+ }
201
+ }
202
+ }, { deep: true });
203
+ function getWidgetMode(id) {
204
+ const entry = children.value.find((c) => c.id === id);
205
+ return entry?.meta?.deskMode || "icon";
206
+ }
207
+ function getItemSize(id) {
208
+ if (temporarySizes[id]) return temporarySizes[id];
209
+ const entry = children.value.find((c) => c.id === id);
210
+ const mode = getWidgetMode(id);
211
+ if (mode === "widget-sm" || mode === "widget-lg") {
212
+ if (entry?.meta?.deskW && entry?.meta?.deskH) {
213
+ return { w: entry.meta.deskW, h: entry.meta.deskH };
214
+ }
215
+ return mode === "widget-sm" ? { ...WIDGET_SM } : { ...WIDGET_LG };
216
+ }
217
+ return { w: GRID_SIZE, h: GRID_SIZE + 24 };
218
+ }
219
+ function getIconPosition(id) {
220
+ if (temporaryPositions[id]) return temporaryPositions[id];
221
+ const entry = children.value.find((c) => c.id === id);
222
+ if (entry?.meta?.deskX !== void 0 && entry?.meta?.deskY !== void 0) {
223
+ return { x: entry.meta.deskX, y: entry.meta.deskY };
224
+ }
225
+ const pos = getDefaultPosition(id);
226
+ tree.updateMeta(id, { deskX: pos.x, deskY: pos.y });
227
+ return pos;
228
+ }
229
+ function getIconStyle(id) {
230
+ const pos = getIconPosition(id);
231
+ const entry = children.value.find((c) => c.id === id);
232
+ const z = entry?.meta?.deskZ ?? 0;
233
+ const dragging = pointerDragActive.value && (selectedIds.value.has(id) || pointerDragAnchorId.value === id);
234
+ return {
235
+ transform: `translate(${pos.x}px, ${pos.y}px)`,
236
+ zIndex: String(dragging ? 999999 : z),
237
+ willChange: dragging ? "transform" : "auto",
238
+ transition: dragging ? "none" : "transform 0.12s cubic-bezier(0.4, 0, 0.2, 1)"
239
+ };
240
+ }
241
+ const selectionRectStyle = computed(() => {
242
+ if (!selectionRect.value || !gridRef.value) return {};
243
+ const { startX, startY, endX, endY } = selectionRect.value;
244
+ const rect = gridRef.value.getBoundingClientRect();
245
+ return {
246
+ left: `${Math.min(startX, endX) - rect.left}px`,
247
+ top: `${Math.min(startY, endY) - rect.top}px`,
248
+ width: `${Math.abs(endX - startX)}px`,
249
+ height: `${Math.abs(endY - startY)}px`
250
+ };
251
+ });
252
+ function getDefaultPosition(excludeId) {
253
+ const columns = 10;
254
+ for (let row = 0; row < 100; row++) {
255
+ for (let col = 0; col < columns; col++) {
256
+ const pos = {
257
+ x: col * (GRID_SIZE + ICON_MARGIN) + ICON_MARGIN,
258
+ y: row * (GRID_SIZE + ICON_MARGIN) + ICON_MARGIN
259
+ };
260
+ if (!isPositionOccupied(pos, excludeId)) return pos;
261
+ }
262
+ }
263
+ return { x: ICON_MARGIN, y: ICON_MARGIN };
264
+ }
265
+ function isPositionOccupied(position, excludeId) {
266
+ const threshold = 40;
267
+ const checkTemp = Object.entries(temporaryPositions).some(([id, pos]) => {
268
+ if (id === excludeId) return false;
269
+ return Math.abs(pos.x - position.x) < threshold && Math.abs(pos.y - position.y) < threshold;
270
+ });
271
+ if (checkTemp) return true;
272
+ return children.value.some((item) => {
273
+ if (item.id === excludeId) return false;
274
+ const x = item.meta?.deskX;
275
+ const y = item.meta?.deskY;
276
+ if (x === void 0 || y === void 0) return false;
277
+ return Math.abs(x - position.x) < threshold && Math.abs(y - position.y) < threshold;
278
+ });
279
+ }
280
+ watch(
281
+ children,
282
+ (items, oldItems) => {
283
+ const oldIds = new Set(oldItems?.map((i) => i.id) ?? []);
284
+ items.forEach((item) => {
285
+ if (item.meta?.deskX === void 0) {
286
+ const pos = !oldIds.has(item.id) && contextMenuPosition.value ? { ...contextMenuPosition.value } : getDefaultPosition(item.id);
287
+ contextMenuPosition.value = null;
288
+ const z = nextZIndex();
289
+ tree.updateMeta(item.id, { deskX: pos.x, deskY: pos.y, deskZ: z });
290
+ }
291
+ });
292
+ },
293
+ { immediate: true }
294
+ );
295
+ function cleanUp() {
296
+ if (!props.editable) return;
297
+ const el = scrollContainerRef.value;
298
+ if (!el) return;
299
+ const containerW = el.clientWidth - ICON_MARGIN * 2;
300
+ const gap = ICON_MARGIN;
301
+ const sorted = [...children.value].sort((a, b) => {
302
+ const sa = getItemSize(a.id);
303
+ const sb = getItemSize(b.id);
304
+ const areaA = sa.w * sa.h;
305
+ const areaB = sb.w * sb.h;
306
+ if (areaA !== areaB) return areaB - areaA;
307
+ const pa = { x: a.meta?.deskX ?? 0, y: a.meta?.deskY ?? 0 };
308
+ const pb = { x: b.meta?.deskX ?? 0, y: b.meta?.deskY ?? 0 };
309
+ if (Math.abs(pa.y - pb.y) < GRID_SIZE / 2) return pa.x - pb.x;
310
+ return pa.y - pb.y;
311
+ });
312
+ const skyline = [{ x: 0, y: 0, w: containerW }];
313
+ function findPlacement(itemW, itemH) {
314
+ let bestY = Infinity;
315
+ let bestX = 0;
316
+ for (let i = 0; i < skyline.length; i++) {
317
+ let maxY = 0;
318
+ let spanW = 0;
319
+ let fits = true;
320
+ for (let j = i; j < skyline.length && spanW < itemW; j++) {
321
+ maxY = Math.max(maxY, skyline[j].y);
322
+ spanW += skyline[j].w;
323
+ if (skyline[j].x + skyline[j].w > containerW) {
324
+ fits = false;
325
+ break;
326
+ }
327
+ }
328
+ if (!fits || spanW < itemW) continue;
329
+ if (maxY < bestY) {
330
+ bestY = maxY;
331
+ bestX = skyline[i].x;
332
+ }
333
+ }
334
+ if (bestY === Infinity) {
335
+ bestY = Math.max(...skyline.map((s) => s.y));
336
+ bestX = 0;
337
+ }
338
+ const newBottom = bestY + itemH + gap;
339
+ const leftEdge = bestX;
340
+ const rightEdge = bestX + itemW;
341
+ const updated = [];
342
+ for (const seg of skyline) {
343
+ const segRight = seg.x + seg.w;
344
+ if (segRight <= leftEdge || seg.x >= rightEdge) {
345
+ updated.push(seg);
346
+ } else {
347
+ if (seg.x < leftEdge) updated.push({ x: seg.x, y: seg.y, w: leftEdge - seg.x });
348
+ if (segRight > rightEdge) updated.push({ x: rightEdge, y: seg.y, w: segRight - rightEdge });
349
+ }
350
+ }
351
+ updated.push({ x: leftEdge, y: newBottom, w: itemW });
352
+ updated.sort((a, b) => a.x - b.x);
353
+ skyline.length = 0;
354
+ skyline.push(...updated);
355
+ return { x: leftEdge, y: bestY };
356
+ }
357
+ Object.keys(temporaryPositions).forEach((id) => delete temporaryPositions[id]);
358
+ for (const item of sorted) {
359
+ const size = getItemSize(item.id);
360
+ const w = Math.min(size.w, containerW);
361
+ const pos = findPlacement(w, size.h);
362
+ tree.updateMeta(item.id, {
363
+ deskX: pos.x + ICON_MARGIN,
364
+ deskY: pos.y + ICON_MARGIN
365
+ });
366
+ }
367
+ }
368
+ function handleDesktopMouseDown(event) {
369
+ if (!props.editable) return;
370
+ if (event.button !== 0) return;
371
+ const target = event.target;
372
+ if (target !== gridRef.value) return;
373
+ isSelecting.value = true;
374
+ selectionRect.value = {
375
+ startX: event.clientX,
376
+ startY: event.clientY,
377
+ endX: event.clientX,
378
+ endY: event.clientY
379
+ };
380
+ if (!event.ctrlKey && !event.metaKey) selectedIds.value.clear();
381
+ document.body.classList.add("nointeract");
382
+ document.addEventListener("mousemove", handleSelectionMouseMove);
383
+ document.addEventListener("mouseup", handleSelectionMouseUp);
384
+ }
385
+ function handleSelectionMouseMove(event) {
386
+ if (!isSelecting.value || !selectionRect.value) return;
387
+ selectionRect.value.endX = event.clientX;
388
+ selectionRect.value.endY = event.clientY;
389
+ children.value.forEach((item) => {
390
+ if (isItemInSelection(item.id)) selectedIds.value.add(item.id);
391
+ else if (!event.ctrlKey && !event.metaKey) selectedIds.value.delete(item.id);
392
+ });
393
+ }
394
+ function handleSelectionMouseUp() {
395
+ isSelecting.value = false;
396
+ selectionRect.value = null;
397
+ document.body.classList.remove("nointeract");
398
+ document.removeEventListener("mousemove", handleSelectionMouseMove);
399
+ document.removeEventListener("mouseup", handleSelectionMouseUp);
400
+ }
401
+ function handleDesktopTouchStart(event) {
402
+ if (!props.editable) return;
403
+ if (event.touches.length !== 1 || event.target !== gridRef.value) return;
404
+ const touch = event.touches[0];
405
+ touchStartPos.value = { x: touch.clientX, y: touch.clientY };
406
+ touchStartTime.value = Date.now();
407
+ isTouchDragging.value = false;
408
+ isSelecting.value = true;
409
+ selectionRect.value = {
410
+ startX: touch.clientX,
411
+ startY: touch.clientY,
412
+ endX: touch.clientX,
413
+ endY: touch.clientY
414
+ };
415
+ selectedIds.value.clear();
416
+ setTimeout(() => {
417
+ if (touchStartPos.value && !isTouchDragging.value && Date.now() - touchStartTime.value >= 200) {
418
+ showLongPressIndicator.value = true;
419
+ longPressPosition.value = { ...touchStartPos.value };
420
+ }
421
+ }, 200);
422
+ }
423
+ function handleTouchMove(event) {
424
+ if (event.touches.length !== 1 || !isSelecting.value || !selectionRect.value || !touchStartPos.value) return;
425
+ const touch = event.touches[0];
426
+ if (Math.abs(touch.clientX - touchStartPos.value.x) > 10 || Math.abs(touch.clientY - touchStartPos.value.y) > 10) {
427
+ isTouchDragging.value = true;
428
+ showLongPressIndicator.value = false;
429
+ selectionRect.value.endX = touch.clientX;
430
+ selectionRect.value.endY = touch.clientY;
431
+ children.value.forEach((item) => {
432
+ if (isItemInSelection(item.id)) selectedIds.value.add(item.id);
433
+ else selectedIds.value.delete(item.id);
434
+ });
435
+ }
436
+ }
437
+ function handleTouchEnd() {
438
+ isSelecting.value = false;
439
+ selectionRect.value = null;
440
+ showLongPressIndicator.value = false;
441
+ longPressPosition.value = null;
442
+ touchStartPos.value = null;
443
+ touchStartTime.value = 0;
444
+ isTouchDragging.value = false;
445
+ }
446
+ function handleIconTouchStart(event, item) {
447
+ if (!props.editable) return;
448
+ if (event.touches.length !== 1) return;
449
+ const touch = event.touches[0];
450
+ touchStartPos.value = { x: touch.clientX, y: touch.clientY };
451
+ touchStartTime.value = Date.now();
452
+ isTouchDragging.value = false;
453
+ if (!selectedIds.value.has(item.id)) {
454
+ selectedIds.value.clear();
455
+ selectedIds.value.add(item.id);
456
+ }
457
+ setTimeout(() => {
458
+ if (touchStartPos.value && !isTouchDragging.value && Date.now() - touchStartTime.value >= 500) {
459
+ showLongPressIndicator.value = false;
460
+ rightClickedItem.value = item;
461
+ }
462
+ }, 500);
463
+ }
464
+ function handleIconTouchEnd(event, item) {
465
+ if (!isTouchDragging.value && touchStartTime.value) {
466
+ const duration = Date.now() - touchStartTime.value;
467
+ if (duration < 500 && touchStartPos.value) {
468
+ const touch = event.changedTouches[0];
469
+ if (Math.abs(touch.clientX - touchStartPos.value.x) < 10 && Math.abs(touch.clientY - touchStartPos.value.y) < 10) {
470
+ const now = Date.now();
471
+ if (now - lastTapTime.value < DOUBLE_TAP_DELAY && lastTapItemId.value === item.id) {
472
+ openNode(item.id, item.label);
473
+ lastTapTime.value = 0;
474
+ lastTapItemId.value = null;
475
+ } else {
476
+ lastTapTime.value = now;
477
+ lastTapItemId.value = item.id;
478
+ }
479
+ }
480
+ }
481
+ }
482
+ touchStartPos.value = null;
483
+ touchStartTime.value = 0;
484
+ isTouchDragging.value = false;
485
+ showLongPressIndicator.value = false;
486
+ }
487
+ function isItemInSelection(id) {
488
+ if (!selectionRect.value || !gridRef.value) return false;
489
+ const { startX, startY, endX, endY } = selectionRect.value;
490
+ const gridRect = gridRef.value.getBoundingClientRect();
491
+ const selLeft = Math.min(startX, endX);
492
+ const selRight = Math.max(startX, endX);
493
+ const selTop = Math.min(startY, endY);
494
+ const selBottom = Math.max(startY, endY);
495
+ const pos = getIconPosition(id);
496
+ const size = getItemSize(id);
497
+ const itemLeft = pos.x + gridRect.left;
498
+ const itemTop = pos.y + gridRect.top;
499
+ const itemRight = itemLeft + size.w;
500
+ const itemBottom = itemTop + size.h;
501
+ return itemRight >= selLeft && itemLeft <= selRight && itemBottom >= selTop && itemTop <= selBottom;
502
+ }
503
+ function handleWidgetHeaderPointerDown(event, id) {
504
+ if (event.target.closest('button, a, input, select, [role="button"]')) return;
505
+ handleIconPointerDown(event, id);
506
+ }
507
+ function handleIconPointerDown(event, id) {
508
+ if (!props.editable) return;
509
+ if (event.button !== 0) return;
510
+ tree.updateMeta(id, { deskZ: nextZIndex() });
511
+ if (!event.ctrlKey && !event.metaKey && !selectedIds.value.has(id)) selectedIds.value.clear();
512
+ selectedIds.value.add(id);
513
+ const el = event.currentTarget;
514
+ el.setPointerCapture(event.pointerId);
515
+ pointerDragAnchorId.value = id;
516
+ pointerDragStart.value = { ex: event.clientX, ey: event.clientY };
517
+ const origPositions = {};
518
+ selectedIds.value.forEach((sid) => {
519
+ origPositions[sid] = { ...getIconPosition(sid) };
520
+ });
521
+ pointerDragOrigPositions.value = origPositions;
522
+ }
523
+ function handleIconPointerMove(event, id) {
524
+ if (pointerDragAnchorId.value !== id || !pointerDragStart.value) return;
525
+ const dx = event.clientX - pointerDragStart.value.ex;
526
+ const dy = event.clientY - pointerDragStart.value.ey;
527
+ if (!pointerDragActive.value && (Math.abs(dx) > 3 || Math.abs(dy) > 3)) {
528
+ pointerDragActive.value = true;
529
+ document.body.classList.add("nointeract");
530
+ }
531
+ if (!pointerDragActive.value) return;
532
+ Object.entries(pointerDragOrigPositions.value).forEach(([sid, orig]) => {
533
+ const newX = Math.max(0, orig.x + dx);
534
+ const newY = Math.max(0, orig.y + dy);
535
+ temporaryPositions[sid] = { x: newX, y: newY };
536
+ });
537
+ if (scrollContainerRef.value) {
538
+ const sr = scrollContainerRef.value.getBoundingClientRect();
539
+ const edge = 50;
540
+ const speed = 12;
541
+ if (event.clientX > sr.right - edge) scrollContainerRef.value.scrollBy(speed, 0);
542
+ else if (event.clientX < sr.left + edge) scrollContainerRef.value.scrollBy(-speed, 0);
543
+ if (event.clientY > sr.bottom - edge) scrollContainerRef.value.scrollBy(0, speed);
544
+ else if (event.clientY < sr.top + edge) scrollContainerRef.value.scrollBy(0, -speed);
545
+ }
546
+ }
547
+ function handleIconPointerUp(event, id) {
548
+ if (pointerDragAnchorId.value !== id) return;
549
+ event.currentTarget.releasePointerCapture(event.pointerId);
550
+ if (pointerDragActive.value) {
551
+ Object.keys(pointerDragOrigPositions.value).forEach((sid) => {
552
+ const pos = temporaryPositions[sid];
553
+ if (pos) {
554
+ tree.updateMeta(sid, { deskX: pos.x, deskY: pos.y });
555
+ delete temporaryPositions[sid];
556
+ }
557
+ });
558
+ document.body.classList.remove("nointeract");
559
+ }
560
+ pointerDragActive.value = false;
561
+ pointerDragAnchorId.value = null;
562
+ pointerDragStart.value = null;
563
+ pointerDragOrigPositions.value = {};
564
+ }
565
+ function handleIconDblClick(item) {
566
+ openNode(item.id, item.label);
567
+ }
568
+ function onWidgetResizeStart(event, id, dir) {
569
+ if (!props.editable) return;
570
+ event.preventDefault();
571
+ event.stopPropagation();
572
+ event.currentTarget.setPointerCapture(event.pointerId);
573
+ const size = getItemSize(id);
574
+ const pos = getIconPosition(id);
575
+ activeResizeId = id;
576
+ resizeDir = dir;
577
+ resizeStartX = event.clientX;
578
+ resizeStartY = event.clientY;
579
+ resizeStartW = size.w;
580
+ resizeStartH = size.h;
581
+ resizeStartPosX = pos.x;
582
+ resizeStartPosY = pos.y;
583
+ temporarySizes[id] = { ...size };
584
+ temporaryPositions[id] = { ...pos };
585
+ document.body.classList.add("nointeract");
586
+ }
587
+ function onWidgetResizeMove(event, id) {
588
+ if (activeResizeId !== id) return;
589
+ const dx = event.clientX - resizeStartX;
590
+ const dy = event.clientY - resizeStartY;
591
+ let newW = resizeStartW;
592
+ let newH = resizeStartH;
593
+ let newX = resizeStartPosX;
594
+ let newY = resizeStartPosY;
595
+ if (resizeDir.includes("e")) newW = Math.max(MIN_WIDGET_W, resizeStartW + dx);
596
+ if (resizeDir.includes("w")) {
597
+ newW = Math.max(MIN_WIDGET_W, resizeStartW - dx);
598
+ newX = resizeStartPosX + (resizeStartW - newW);
599
+ }
600
+ if (resizeDir.includes("s")) newH = Math.max(MIN_WIDGET_H, resizeStartH + dy);
601
+ if (resizeDir.includes("n")) {
602
+ newH = Math.max(MIN_WIDGET_H, resizeStartH - dy);
603
+ newY = resizeStartPosY + (resizeStartH - newH);
604
+ }
605
+ temporarySizes[id] = { w: newW, h: newH };
606
+ temporaryPositions[id] = { x: newX, y: newY };
607
+ }
608
+ function onWidgetResizeEnd(event, id) {
609
+ if (activeResizeId !== id) return;
610
+ event.currentTarget.releasePointerCapture(event.pointerId);
611
+ const size = temporarySizes[id];
612
+ const pos = temporaryPositions[id];
613
+ if (size) {
614
+ tree.updateMeta(id, { deskW: size.w, deskH: size.h });
615
+ delete temporarySizes[id];
616
+ }
617
+ if (pos) {
618
+ tree.updateMeta(id, { deskX: pos.x, deskY: pos.y });
619
+ delete temporaryPositions[id];
620
+ }
621
+ activeResizeId = null;
622
+ document.body.classList.remove("nointeract");
623
+ }
624
+ function onIconResizeStart(event, id, dir) {
625
+ if (!props.editable) return;
626
+ event.preventDefault();
627
+ event.stopPropagation();
628
+ const currentSize = { w: GRID_SIZE, h: GRID_SIZE + 24 };
629
+ const pos = getIconPosition(id);
630
+ activeResizeId = id;
631
+ resizeDir = dir;
632
+ resizeStartX = event.clientX;
633
+ resizeStartY = event.clientY;
634
+ resizeStartW = currentSize.w;
635
+ resizeStartH = currentSize.h;
636
+ resizeStartPosX = pos.x;
637
+ resizeStartPosY = pos.y;
638
+ temporarySizes[id] = { ...currentSize };
639
+ temporaryPositions[id] = { ...pos };
640
+ document.body.classList.add("nointeract");
641
+ tree.updateMeta(id, { deskMode: "widget-sm", deskW: currentSize.w, deskH: currentSize.h });
642
+ loadWidgetProvider(id);
643
+ document.addEventListener("pointermove", onIconResizeDocMove);
644
+ document.addEventListener("pointerup", onIconResizeDocUp);
645
+ }
646
+ function onIconResizeDocMove(event) {
647
+ if (!activeResizeId) return;
648
+ const id = activeResizeId;
649
+ const dx = event.clientX - resizeStartX;
650
+ const dy = event.clientY - resizeStartY;
651
+ let newW = resizeStartW;
652
+ let newH = resizeStartH;
653
+ let newX = resizeStartPosX;
654
+ let newY = resizeStartPosY;
655
+ if (resizeDir.includes("e")) newW = Math.max(MIN_WIDGET_W, resizeStartW + dx);
656
+ if (resizeDir.includes("w")) {
657
+ newW = Math.max(MIN_WIDGET_W, resizeStartW - dx);
658
+ newX = resizeStartPosX + (resizeStartW - newW);
659
+ }
660
+ if (resizeDir.includes("s")) newH = Math.max(MIN_WIDGET_H, resizeStartH + dy);
661
+ if (resizeDir.includes("n")) {
662
+ newH = Math.max(MIN_WIDGET_H, resizeStartH - dy);
663
+ newY = resizeStartPosY + (resizeStartH - newH);
664
+ }
665
+ temporarySizes[id] = { w: newW, h: newH };
666
+ temporaryPositions[id] = { x: newX, y: newY };
667
+ }
668
+ function onIconResizeDocUp() {
669
+ document.removeEventListener("pointermove", onIconResizeDocMove);
670
+ document.removeEventListener("pointerup", onIconResizeDocUp);
671
+ if (!activeResizeId) return;
672
+ const id = activeResizeId;
673
+ const size = temporarySizes[id];
674
+ const pos = temporaryPositions[id];
675
+ if (size) {
676
+ if (size.w <= GRID_SIZE + 20 && size.h <= GRID_SIZE + 30) {
677
+ tree.updateMeta(id, { deskMode: "icon" });
678
+ } else {
679
+ tree.updateMeta(id, { deskW: size.w, deskH: size.h });
680
+ }
681
+ delete temporarySizes[id];
682
+ }
683
+ if (pos) {
684
+ tree.updateMeta(id, { deskX: pos.x, deskY: pos.y });
685
+ delete temporaryPositions[id];
686
+ }
687
+ activeResizeId = null;
688
+ document.body.classList.remove("nointeract");
689
+ }
690
+ function handleContextMenuCapture(event) {
691
+ if (!props.editable) return;
692
+ if (gridRef.value) {
693
+ const rect = gridRef.value.getBoundingClientRect();
694
+ contextMenuPosition.value = {
695
+ x: event.clientX - rect.left,
696
+ y: event.clientY - rect.top
697
+ };
698
+ }
699
+ const target = event.target;
700
+ const iconEl = target.closest("[data-icon-id]");
701
+ if (iconEl) {
702
+ const id = iconEl.getAttribute("data-icon-id");
703
+ const item = children.value.find((c) => c.id === id);
704
+ if (item) {
705
+ if (!selectedIds.value.has(item.id)) {
706
+ selectedIds.value.clear();
707
+ selectedIds.value.add(item.id);
708
+ }
709
+ rightClickedItem.value = item;
710
+ }
711
+ } else {
712
+ rightClickedItem.value = null;
713
+ }
714
+ }
715
+ function createNewItem() {
716
+ if (!props.editable) return;
717
+ tree.createChild(null, locale.value.untitled);
718
+ }
719
+ function startRename(item) {
720
+ if (!props.editable) return;
721
+ itemToRename.value = item;
722
+ renameNewName.value = item.label;
723
+ isRenameModalOpen.value = true;
724
+ }
725
+ function confirmRename() {
726
+ if (!itemToRename.value || !renameNewName.value.trim() || renameNewName.value === itemToRename.value.label) {
727
+ isRenameModalOpen.value = false;
728
+ return;
729
+ }
730
+ tree.renameEntry(itemToRename.value.id, renameNewName.value);
731
+ isRenameModalOpen.value = false;
732
+ itemToRename.value = null;
733
+ }
734
+ function deleteItem(item) {
735
+ confirmModalTitle.value = "Confirm Delete";
736
+ confirmModalMessage.value = `Are you sure you want to delete "${item.label}"?`;
737
+ confirmAction.value = () => {
738
+ tree.deleteEntry(item.id);
739
+ selectedIds.value.delete(item.id);
740
+ delete temporaryPositions[item.id];
741
+ delete widgetProviders[item.id];
742
+ };
743
+ isConfirmModalOpen.value = true;
744
+ }
745
+ function deleteSelected() {
746
+ const count = selectedIds.value.size;
747
+ confirmModalTitle.value = "Confirm Delete";
748
+ confirmModalMessage.value = `Are you sure you want to delete ${count} item${count > 1 ? "s" : ""}?`;
749
+ confirmAction.value = () => {
750
+ selectedIds.value.forEach((id) => {
751
+ tree.deleteEntry(id);
752
+ delete temporaryPositions[id];
753
+ delete widgetProviders[id];
754
+ });
755
+ selectedIds.value.clear();
756
+ };
757
+ isConfirmModalOpen.value = true;
758
+ }
759
+ function handleConfirmAction() {
760
+ confirmAction.value();
761
+ isConfirmModalOpen.value = false;
762
+ }
763
+ function setWidgetMode(id, mode) {
764
+ if (!props.editable) return;
765
+ tree.updateMeta(id, { deskMode: mode });
766
+ if (mode !== "icon" && !widgetProviders[id]) {
767
+ loadWidgetProvider(id);
768
+ }
769
+ }
770
+ const contextMenuItems = computed(() => {
771
+ if (rightClickedItem.value) {
772
+ const item = rightClickedItem.value;
773
+ const currentMode = getWidgetMode(item.id);
774
+ return [
775
+ [
776
+ { label: "Open", icon: "i-lucide-external-link", onSelect: () => openNode(item.id, item.label) }
777
+ ],
778
+ [
779
+ { label: "Rename", icon: "i-lucide-pencil", onSelect: () => startRename(item) },
780
+ {
781
+ label: "Duplicate",
782
+ icon: "i-lucide-copy",
783
+ onSelect: () => {
784
+ const newId = tree.duplicateEntry(item.id);
785
+ const pos = getIconPosition(item.id);
786
+ tree.updateMeta(newId, { deskX: pos.x + 20, deskY: pos.y + 20 });
787
+ }
788
+ }
789
+ ],
790
+ [
791
+ { label: locale.value.modeIcon, icon: "i-lucide-square", disabled: currentMode === "icon", onSelect: () => setWidgetMode(item.id, "icon") },
792
+ { label: locale.value.modeWidgetSm, icon: "i-lucide-rectangle-horizontal", disabled: currentMode === "widget-sm", onSelect: () => setWidgetMode(item.id, "widget-sm") },
793
+ { label: locale.value.modeWidgetLg, icon: "i-lucide-maximize-2", disabled: currentMode === "widget-lg", onSelect: () => setWidgetMode(item.id, "widget-lg") }
794
+ ],
795
+ [
796
+ ...selectedIds.value.size > 1 ? [{ label: `Delete Selected (${selectedIds.value.size})`, icon: "i-lucide-trash-2", color: "error", onSelect: deleteSelected }] : [{ label: "Delete", icon: "i-lucide-trash-2", color: "error", onSelect: () => deleteItem(item) }]
797
+ ]
798
+ ];
799
+ }
800
+ const groups = [
801
+ [{ label: locale.value.addItem, icon: "i-lucide-file-plus", onSelect: createNewItem }],
802
+ [{ label: locale.value.cleanUp, icon: "i-lucide-layout-grid", onSelect: cleanUp }]
803
+ ];
804
+ if (selectedIds.value.size > 0) {
805
+ groups.push([{
806
+ label: `Delete Selected (${selectedIds.value.size})`,
807
+ icon: "i-lucide-trash-2",
808
+ color: "error",
809
+ onSelect: deleteSelected
810
+ }]);
811
+ }
812
+ return groups;
813
+ });
814
+ watch(childDoc, (doc) => {
815
+ if (!doc) return;
816
+ const legacyPos = doc.getMap("desktop-icon-positions");
817
+ legacyPos.forEach((val, id) => {
818
+ const entry = tree.treeMap.get(id);
819
+ if (entry && entry.meta?.deskX === void 0) {
820
+ tree.updateMeta(id, { deskX: val.x ?? 0, deskY: val.y ?? 0 });
821
+ }
822
+ });
823
+ const legacyModes = doc.getMap("dashboard-widget-modes");
824
+ legacyModes.forEach((val, id) => {
825
+ const entry = tree.treeMap.get(id);
826
+ if (entry && entry.meta?.deskMode === void 0) {
827
+ tree.updateMeta(id, { deskMode: val });
828
+ }
829
+ });
830
+ }, { immediate: true });
831
+ let cursorBroadcastFrame = 0;
832
+ function onGridPointerMove(e) {
833
+ if (!gridRef.value) return;
834
+ cursorBroadcastFrame++;
835
+ if (cursorBroadcastFrame % 2 !== 0) return;
836
+ const rect = gridRef.value.getBoundingClientRect();
837
+ const x = e.clientX - rect.left;
838
+ const y = e.clientY - rect.top;
839
+ setLocalState({ pos: { x, y } });
840
+ }
841
+ function onGridPointerLeave() {
842
+ setLocalState({ pos: null });
843
+ }
844
+ const localHoveredItemId = ref(null);
845
+ function broadcastItemState() {
846
+ const ids = [...selectedIds.value];
847
+ setLocalState({
848
+ itemSelection: ids.length ? ids : null,
849
+ itemDragging: pointerDragActive.value,
850
+ itemHover: localHoveredItemId.value
851
+ });
852
+ }
853
+ function onItemPointerEnter(id) {
854
+ localHoveredItemId.value = id;
855
+ broadcastItemState();
856
+ }
857
+ function onItemPointerLeave() {
858
+ localHoveredItemId.value = null;
859
+ broadcastItemState();
860
+ }
861
+ watch(selectedIds, broadcastItemState, { deep: true });
862
+ watch(pointerDragActive, broadcastItemState);
863
+ const remoteItemPresence = computed(() => {
864
+ const map = /* @__PURE__ */ new Map();
865
+ const localId = childProviderRef.value?.awareness?.clientID ?? 0;
866
+ for (const s of states.value) {
867
+ if (s.clientId === localId) continue;
868
+ const selection = s.itemSelection;
869
+ const dragging = !!s.itemDragging;
870
+ const hoverId = s.itemHover;
871
+ const name = s.user?.name ?? "";
872
+ const color = s.user?.color || "#6b7280";
873
+ const itemIds = /* @__PURE__ */ new Set();
874
+ if (selection) selection.forEach((id) => itemIds.add(id));
875
+ if (hoverId) itemIds.add(hoverId);
876
+ for (const id of itemIds) {
877
+ if (!map.has(id)) map.set(id, []);
878
+ const isSelected = selection?.includes(id) ?? false;
879
+ map.get(id).push({ clientId: s.clientId, name, color, dragging: dragging && isSelected });
880
+ }
881
+ }
882
+ return map;
883
+ });
884
+ function getItemPresenceStyle(id) {
885
+ const users = remoteItemPresence.value.get(id);
886
+ if (!users?.length) return null;
887
+ const primary = users[0];
888
+ const isSelected = states.value.some((s) => {
889
+ if (s.clientId === (childProviderRef.value?.awareness?.clientID ?? 0)) return false;
890
+ return s.itemSelection?.includes(id);
891
+ });
892
+ const style = isSelected || primary.dragging ? "solid" : "dashed";
893
+ return {
894
+ outline: `2px ${style} ${primary.color}`,
895
+ outlineOffset: "2px",
896
+ borderRadius: "10px"
897
+ };
898
+ }
899
+ const scrollX = ref(0);
900
+ const scrollY = ref(0);
901
+ const viewportW = ref(0);
902
+ const viewportH = ref(0);
903
+ function onScrollContainerScroll() {
904
+ const el = scrollContainerRef.value;
905
+ if (!el) return;
906
+ scrollX.value = el.scrollLeft;
907
+ scrollY.value = el.scrollTop;
908
+ viewportW.value = el.clientWidth;
909
+ viewportH.value = el.clientHeight;
910
+ }
911
+ onMounted(() => {
912
+ const el = scrollContainerRef.value;
913
+ if (el) {
914
+ el.addEventListener("scroll", onScrollContainerScroll, { passive: true });
915
+ onScrollContainerScroll();
916
+ const ro = new ResizeObserver(onScrollContainerScroll);
917
+ ro.observe(el);
918
+ onBeforeUnmount(() => {
919
+ el.removeEventListener("scroll", onScrollContainerScroll);
920
+ ro.disconnect();
921
+ });
922
+ }
923
+ });
924
+ const EDGE_INSET = 12;
925
+ const edgeCursors = computed(() => {
926
+ const vx = scrollX.value;
927
+ const vy = scrollY.value;
928
+ const vw = viewportW.value;
929
+ const vh = viewportH.value;
930
+ if (!vw || !vh) return [];
931
+ const result = [];
932
+ for (const c of cursors.value) {
933
+ const cx = c.x;
934
+ const cy = c.y;
935
+ if (cx >= vx && cx <= vx + vw && cy >= vy && cy <= vy + vh) continue;
936
+ const localX = cx - vx;
937
+ const localY = cy - vy;
938
+ const clampedX = Math.max(EDGE_INSET, Math.min(vw - EDGE_INSET, localX));
939
+ const clampedY = Math.max(EDGE_INSET, Math.min(vh - EDGE_INSET, localY));
940
+ const angle = Math.atan2(localY - vh / 2, localX - vw / 2) * (180 / Math.PI);
941
+ result.push({
942
+ clientId: c.clientId,
943
+ x: clampedX,
944
+ y: clampedY,
945
+ angle,
946
+ color: c.state?.user?.color || "#6b7280",
947
+ name: c.state?.user?.name || ""
948
+ });
949
+ }
950
+ return result;
951
+ });
952
+ onBeforeUnmount(() => {
953
+ setLocalState({ pos: null, itemSelection: null, itemDragging: false, itemHover: null });
954
+ for (const cleanup of widgetBodyCleanup.values()) cleanup();
955
+ widgetBodyCleanup.clear();
956
+ });
957
+ defineExpose({ connectedUsers });
958
+ </script>
959
+
960
+ <template>
961
+ <div
962
+ class="dashboard-renderer relative w-full flex-1 min-h-0 select-none"
963
+ @contextmenu.capture="handleContextMenuCapture"
964
+ >
965
+ <!-- Empty state -->
966
+ <div
967
+ v-if="children.length === 0"
968
+ class="flex flex-col items-center justify-center h-full gap-3 text-center"
969
+ >
970
+ <UIcon
971
+ name="i-lucide-layout-dashboard"
972
+ class="size-10 text-(--ui-text-dimmed)"
973
+ />
974
+ <p class="text-sm text-(--ui-text-muted)">
975
+ {{ locale.empty }}
976
+ </p>
977
+ <UButton
978
+ v-if="editable"
979
+ icon="i-lucide-plus"
980
+ :label="locale.addItem"
981
+ size="sm"
982
+ @click="createNewItem"
983
+ />
984
+ </div>
985
+
986
+ <UContextMenu
987
+ v-else
988
+ v-model:open="contextMenuOpen"
989
+ :items="editable ? contextMenuItems : []"
990
+ class="absolute inset-0"
991
+ >
992
+ <div
993
+ ref="scrollContainerRef"
994
+ class="absolute inset-0 overflow-auto"
995
+ >
996
+ <div
997
+ ref="gridRef"
998
+ class="relative"
999
+ :style="{ width: canvasSize.w + 'px', height: canvasSize.h + 'px', minWidth: '100%', minHeight: '100%' }"
1000
+ @mousedown="handleDesktopMouseDown"
1001
+ @touchstart="handleDesktopTouchStart"
1002
+ @touchmove="handleTouchMove"
1003
+ @touchend="handleTouchEnd"
1004
+ @pointermove="onGridPointerMove"
1005
+ @pointerleave="onGridPointerLeave"
1006
+ >
1007
+ <!-- Collaborative Cursors -->
1008
+ <TransitionGroup name="cursor">
1009
+ <div
1010
+ v-for="c in cursors"
1011
+ :key="c.clientId"
1012
+ class="dashboard-cursor"
1013
+ :style="{ transform: `translate(${c.x}px, ${c.y}px)` }"
1014
+ >
1015
+ <svg
1016
+ width="16"
1017
+ height="16"
1018
+ viewBox="0 0 16 16"
1019
+ fill="none"
1020
+ >
1021
+ <path
1022
+ d="M2 2L7 14L9 9L14 7L2 2Z"
1023
+ :fill="c.state?.user?.color || '#6b7280'"
1024
+ stroke="rgba(0,0,0,.25)"
1025
+ stroke-width="1"
1026
+ stroke-linejoin="round"
1027
+ />
1028
+ </svg>
1029
+ <span
1030
+ class="dashboard-cursor-name"
1031
+ :style="{ background: c.state?.user?.color || '#6b7280' }"
1032
+ >{{ c.state?.user?.name || "" }}</span>
1033
+ </div>
1034
+ </TransitionGroup>
1035
+
1036
+ <!-- Selection Rectangle -->
1037
+ <div
1038
+ v-if="selectionRect"
1039
+ class="dashboard-selection-rect absolute pointer-events-none"
1040
+ :style="selectionRectStyle"
1041
+ />
1042
+
1043
+ <!-- Long-press indicator -->
1044
+ <div
1045
+ v-if="showLongPressIndicator && longPressPosition && gridRef"
1046
+ class="absolute pointer-events-none"
1047
+ :style="{ left: longPressPosition.x - 20 - gridRef.getBoundingClientRect().left + 'px', top: longPressPosition.y - 20 - gridRef.getBoundingClientRect().top + 'px' }"
1048
+ >
1049
+ <div class="w-10 h-10 rounded-full bg-white/20 backdrop-blur-sm border border-white/30 flex items-center justify-center">
1050
+ <div class="w-6 h-6 rounded-full border-2 border-white/60 border-t-white animate-spin" />
1051
+ </div>
1052
+ </div>
1053
+
1054
+ <!-- Icons & Widgets -->
1055
+ <div
1056
+ v-for="item in children"
1057
+ :key="item.id"
1058
+ :data-icon-id="item.id"
1059
+ class="absolute select-none"
1060
+ :style="getIconStyle(item.id)"
1061
+ >
1062
+ <!-- Icon Mode -->
1063
+ <template v-if="getWidgetMode(item.id) === 'icon'">
1064
+ <div
1065
+ class="group relative flex flex-col items-center cursor-pointer touch-none"
1066
+ @pointerdown.stop="handleIconPointerDown($event, item.id)"
1067
+ @pointermove="handleIconPointerMove($event, item.id)"
1068
+ @pointerup="handleIconPointerUp($event, item.id)"
1069
+ @pointerenter="onItemPointerEnter(item.id)"
1070
+ @pointerleave="onItemPointerLeave()"
1071
+ @dblclick="handleIconDblClick(item)"
1072
+ @touchstart.stop="handleIconTouchStart($event, item)"
1073
+ @touchend="handleIconTouchEnd($event, item)"
1074
+ >
1075
+ <div
1076
+ :class="[
1077
+ 'w-16 h-16 flex items-center justify-center rounded-lg transition-colors duration-150',
1078
+ selectedIds.has(item.id) ? 'dashboard-icon-selected' : 'hover:bg-(--ui-bg-elevated)/50'
1079
+ ]"
1080
+ :style="getItemPresenceStyle(item.id) || {}"
1081
+ >
1082
+ <UIcon
1083
+ :name="resolveDocType(item.type).icon"
1084
+ class="w-12 h-12 text-(--ui-text)"
1085
+ />
1086
+ </div>
1087
+ <div
1088
+ :class="[
1089
+ 'mt-1 px-2 py-0.5 rounded text-xs text-center max-w-24 line-clamp-2 break-words',
1090
+ selectedIds.has(item.id) ? 'dashboard-label-selected text-white' : 'text-(--ui-text) drop-shadow-[0_1px_1px_rgba(0,0,0,0.3)]'
1091
+ ]"
1092
+ >
1093
+ {{ item.label }}
1094
+ </div>
1095
+ <!-- Remote user presence badges -->
1096
+ <div
1097
+ v-if="remoteItemPresence.has(item.id)"
1098
+ class="flex gap-0.5 mt-0.5"
1099
+ >
1100
+ <span
1101
+ v-for="u in remoteItemPresence.get(item.id)"
1102
+ :key="u.clientId"
1103
+ class="dashboard-presence-badge"
1104
+ :style="{ background: u.color }"
1105
+ >{{ u.name }}</span>
1106
+ </div>
1107
+ <!-- Resize handles (corners, visible on hover) -->
1108
+ <div
1109
+ v-for="dir in ['se', 'sw', 'ne', 'nw']"
1110
+ v-if="editable"
1111
+ :key="dir"
1112
+ class="dw-resize dw-icon-resize opacity-0 group-hover:opacity-100 transition-opacity"
1113
+ :class="'dw-' + dir"
1114
+ @pointerdown.stop="onIconResizeStart($event, item.id, dir)"
1115
+ @pointermove="onWidgetResizeMove($event, item.id)"
1116
+ @pointerup="onWidgetResizeEnd($event, item.id)"
1117
+ />
1118
+ </div>
1119
+ </template>
1120
+
1121
+ <!-- Widget Mode -->
1122
+ <template v-else>
1123
+ <div
1124
+ class="dashboard-widget"
1125
+ :class="{ 'dashboard-widget-selected': selectedIds.has(item.id) }"
1126
+ :style="{ width: getItemSize(item.id).w + 'px', height: getItemSize(item.id).h + 'px', ...getItemPresenceStyle(item.id) || {} }"
1127
+ @pointerenter="onItemPointerEnter(item.id)"
1128
+ @pointerleave="onItemPointerLeave()"
1129
+ >
1130
+ <!-- Header -->
1131
+ <div
1132
+ class="dashboard-widget-header touch-none"
1133
+ @pointerdown.stop="handleWidgetHeaderPointerDown($event, item.id)"
1134
+ @pointermove="handleIconPointerMove($event, item.id)"
1135
+ @pointerup="handleIconPointerUp($event, item.id)"
1136
+ @touchstart.stop="handleIconTouchStart($event, item)"
1137
+ @touchend="handleIconTouchEnd($event, item)"
1138
+ >
1139
+ <UIcon
1140
+ :name="resolveDocType(item.type).icon"
1141
+ class="size-3.5 text-(--ui-text-muted) shrink-0"
1142
+ />
1143
+ <span
1144
+ class="text-xs font-medium text-(--ui-text) truncate"
1145
+ style="max-width: 80px"
1146
+ >{{ item.label }}</span>
1147
+ <div class="flex-1" />
1148
+ <div class="flex items-center gap-0.5 shrink-0">
1149
+ <UTooltip
1150
+ text="Open"
1151
+ :content="{ side: 'bottom' }"
1152
+ >
1153
+ <button
1154
+ class="dashboard-widget-btn"
1155
+ @click.stop="openNode(item.id, item.label)"
1156
+ >
1157
+ <UIcon
1158
+ name="i-lucide-expand"
1159
+ class="size-3"
1160
+ />
1161
+ </button>
1162
+ </UTooltip>
1163
+ </div>
1164
+ </div>
1165
+
1166
+ <!-- Body -->
1167
+ <div
1168
+ :ref="(el) => registerWidgetBody(el, item.id)"
1169
+ class="dashboard-widget-body"
1170
+ >
1171
+ <template v-if="widgetProviders[item.id]">
1172
+ <component
1173
+ :is="resolveDocType(item.type).component"
1174
+ :doc-id="item.id"
1175
+ :child-provider="widgetProviders[item.id]"
1176
+ :doc-label="item.label"
1177
+ class="w-full h-full"
1178
+ />
1179
+ </template>
1180
+ <div
1181
+ v-else-if="widgetLoadingIds.has(item.id)"
1182
+ class="flex items-center justify-center h-full"
1183
+ >
1184
+ <UIcon
1185
+ name="i-lucide-loader-circle"
1186
+ class="size-5 animate-spin text-(--ui-text-muted)"
1187
+ />
1188
+ </div>
1189
+ <div
1190
+ v-else-if="widgetErrors[item.id]"
1191
+ class="flex flex-col items-center justify-center gap-2 h-full text-xs text-(--ui-text-dimmed)"
1192
+ >
1193
+ <UIcon
1194
+ name="i-lucide-alert-triangle"
1195
+ class="size-5 text-(--ui-text-muted)"
1196
+ />
1197
+ <span>{{ widgetErrors[item.id] }}</span>
1198
+ <UButton
1199
+ size="2xs"
1200
+ variant="soft"
1201
+ @click.stop="() => {
1202
+ delete widgetProviders[item.id];
1203
+ loadWidgetProvider(item.id);
1204
+ }"
1205
+ >
1206
+ Retry
1207
+ </UButton>
1208
+ </div>
1209
+ <div
1210
+ v-else
1211
+ class="flex items-center justify-center h-full text-xs text-(--ui-text-dimmed)"
1212
+ >
1213
+ Unable to load
1214
+ </div>
1215
+ </div>
1216
+
1217
+ <!-- Resize handles -->
1218
+ <template v-if="editable">
1219
+ <div
1220
+ v-for="dir in resizeDirs"
1221
+ :key="dir"
1222
+ class="dw-resize"
1223
+ :class="'dw-' + dir"
1224
+ @pointerdown.stop="onWidgetResizeStart($event, item.id, dir)"
1225
+ @pointermove="onWidgetResizeMove($event, item.id)"
1226
+ @pointerup="onWidgetResizeEnd($event, item.id)"
1227
+ />
1228
+ </template>
1229
+ </div>
1230
+ </template>
1231
+ </div>
1232
+ </div>
1233
+ </div>
1234
+ </UContextMenu>
1235
+
1236
+ <!-- Off-screen cursor edge indicators -->
1237
+ <TransitionGroup
1238
+ name="cursor"
1239
+ tag="div"
1240
+ class="absolute inset-0 pointer-events-none"
1241
+ style="z-index: 2147483647; overflow: hidden"
1242
+ >
1243
+ <div
1244
+ v-for="ec in edgeCursors"
1245
+ :key="'edge-' + ec.clientId"
1246
+ class="dashboard-edge-cursor"
1247
+ :style="{ transform: `translate(${ec.x}px, ${ec.y}px)` }"
1248
+ >
1249
+ <svg
1250
+ width="14"
1251
+ height="14"
1252
+ viewBox="0 0 16 16"
1253
+ fill="none"
1254
+ :style="{ transform: `rotate(${ec.angle + 90}deg)` }"
1255
+ >
1256
+ <path
1257
+ d="M8 2L14 14H2L8 2Z"
1258
+ :fill="ec.color"
1259
+ stroke="rgba(0,0,0,.25)"
1260
+ stroke-width="1"
1261
+ stroke-linejoin="round"
1262
+ />
1263
+ </svg>
1264
+ <span
1265
+ class="dashboard-edge-name"
1266
+ :style="{ background: ec.color }"
1267
+ >{{ ec.name }}</span>
1268
+ </div>
1269
+ </TransitionGroup>
1270
+
1271
+ <!-- Rename Modal -->
1272
+ <UModal v-model:open="isRenameModalOpen">
1273
+ <template #content>
1274
+ <UCard>
1275
+ <template #header>
1276
+ <div class="flex items-center justify-between">
1277
+ <h3 class="text-lg font-semibold">
1278
+ Rename
1279
+ </h3>
1280
+ <UButton
1281
+ color="neutral"
1282
+ variant="ghost"
1283
+ icon="i-lucide-x"
1284
+ @click="isRenameModalOpen = false"
1285
+ />
1286
+ </div>
1287
+ </template>
1288
+ <div class="space-y-4">
1289
+ <UInput
1290
+ v-model="renameNewName"
1291
+ placeholder="Enter new name"
1292
+ autofocus
1293
+ @keyup.enter="confirmRename"
1294
+ />
1295
+ </div>
1296
+ <template #footer>
1297
+ <div class="flex justify-end gap-2">
1298
+ <UButton
1299
+ color="neutral"
1300
+ variant="outline"
1301
+ @click="isRenameModalOpen = false"
1302
+ >
1303
+ Cancel
1304
+ </UButton>
1305
+ <UButton
1306
+ :disabled="!renameNewName.trim()"
1307
+ @click="confirmRename"
1308
+ >
1309
+ Rename
1310
+ </UButton>
1311
+ </div>
1312
+ </template>
1313
+ </UCard>
1314
+ </template>
1315
+ </UModal>
1316
+
1317
+ <!-- Confirm Delete Modal -->
1318
+ <UModal v-model:open="isConfirmModalOpen">
1319
+ <template #content>
1320
+ <UCard>
1321
+ <template #header>
1322
+ <div class="flex items-center justify-between">
1323
+ <h3 class="text-lg font-semibold">
1324
+ {{ confirmModalTitle }}
1325
+ </h3>
1326
+ <UButton
1327
+ color="neutral"
1328
+ variant="ghost"
1329
+ icon="i-lucide-x"
1330
+ @click="isConfirmModalOpen = false"
1331
+ />
1332
+ </div>
1333
+ </template>
1334
+ <p>{{ confirmModalMessage }}</p>
1335
+ <template #footer>
1336
+ <div class="flex justify-end gap-2">
1337
+ <UButton
1338
+ color="neutral"
1339
+ variant="outline"
1340
+ @click="isConfirmModalOpen = false"
1341
+ >
1342
+ Cancel
1343
+ </UButton>
1344
+ <UButton
1345
+ color="error"
1346
+ @click="handleConfirmAction"
1347
+ >
1348
+ Delete
1349
+ </UButton>
1350
+ </div>
1351
+ </template>
1352
+ </UCard>
1353
+ </template>
1354
+ </UModal>
1355
+
1356
+ <!-- Node panel -->
1357
+ <ANodePanel
1358
+ :node-id="openNodeId"
1359
+ :node-label="openNodeLabel"
1360
+ :child-provider="openNodeProvider"
1361
+ @close="closePanel"
1362
+ />
1363
+ </div>
1364
+ </template>
1365
+
1366
+ <style scoped>
1367
+ .dashboard-selection-rect{background:color-mix(in srgb,var(--ui-primary) 10%,transparent);border:1px solid color-mix(in srgb,var(--ui-primary) 50%,transparent)}.dashboard-icon-selected{background:color-mix(in srgb,var(--ui-primary) 20%,transparent)}.dashboard-label-selected{background:var(--ui-primary)}.line-clamp-2{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.dashboard-widget{backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px);background:var(--ui-bg);border:1px solid var(--ui-border);border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,.1),0 2px 8px rgba(0,0,0,.06);display:flex;flex-direction:column;opacity:.88;overflow:hidden;position:relative}.dashboard-widget-selected{border-color:var(--ui-primary);box-shadow:0 12px 48px rgba(0,0,0,.18),0 4px 12px rgba(0,0,0,.12);opacity:1;outline:2px solid var(--ui-primary);outline-offset:-1px}.dashboard-widget-header{align-items:center;background:var(--ui-bg-elevated);border-bottom:1px solid var(--ui-border);cursor:grab;display:flex;flex-shrink:0;gap:6px;height:34px;padding:0 4px 0 10px;-webkit-user-select:none;-moz-user-select:none;user-select:none}.dashboard-widget-header:active{cursor:grabbing}.dashboard-widget-btn{align-items:center;border-radius:var(--ui-radius);color:var(--ui-text-dimmed);cursor:pointer;display:flex;flex-shrink:0;height:22px;justify-content:center;transition:background .1s,color .1s;width:22px}.dashboard-widget-btn:hover{background:var(--ui-bg-accented);color:var(--ui-text)}.dashboard-widget-body{display:flex;flex:1;flex-direction:column;min-height:0;overflow:auto;-webkit-user-select:text;-moz-user-select:text;user-select:text}.dw-resize{position:absolute;z-index:10}.dw-n{cursor:n-resize;top:-3px}.dw-n,.dw-s{height:6px;left:10px;right:10px}.dw-s{bottom:-3px;cursor:s-resize}.dw-e{cursor:e-resize;right:-3px}.dw-e,.dw-w{bottom:10px;top:10px;width:6px}.dw-w{cursor:w-resize;left:-3px}.dw-ne{cursor:ne-resize;right:-3px}.dw-ne,.dw-nw{height:14px;top:-3px;width:14px}.dw-nw{cursor:nw-resize;left:-3px}.dw-se{cursor:se-resize;right:-3px}.dw-se,.dw-sw{bottom:-3px;height:14px;width:14px}.dw-sw{cursor:sw-resize;left:-3px}.dw-icon-resize{z-index:20}.dashboard-presence-badge{border-radius:3px;color:#000;font-size:.55rem;font-weight:600;line-height:1.4;padding:0 4px;white-space:nowrap}.dashboard-cursor{left:0;pointer-events:none;position:absolute;top:0;will-change:transform;z-index:2147483647}.dashboard-cursor-name{border-radius:3px;color:#000;font-size:.6rem;font-weight:600;left:14px;padding:1px 5px;position:absolute;top:8px;white-space:nowrap}.dashboard-edge-cursor{align-items:center;display:flex;gap:4px;left:0;pointer-events:none;position:absolute;top:0;will-change:transform;z-index:2147483647}.dashboard-edge-name{border-radius:3px;color:#000;font-size:.6rem;font-weight:600;padding:1px 5px;white-space:nowrap}.cursor-enter-active{transition:opacity .2s ease}.cursor-leave-active{transition:opacity .3s ease}.cursor-enter-from,.cursor-leave-to{opacity:0}
1368
+ </style>
1369
+
1370
+ <style>
1371
+ body.nointeract .dashboard-widget-body *,body.nointeract .wb-body *,body.nointeract embed,body.nointeract iframe,body.nointeract object{pointer-events:none!important}
1372
+ </style>