@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.
- package/dist/module.d.mts +46 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +95 -2
- package/dist/runtime/assets/editor.css +1 -0
- package/dist/runtime/components/ACommandPalette.vue +4 -1
- package/dist/runtime/components/ADocRenderer.d.vue.ts +29 -0
- package/dist/runtime/components/ADocRenderer.vue +99 -0
- package/dist/runtime/components/ADocRenderer.vue.d.ts +29 -0
- package/dist/runtime/components/ADocTypeSelect.vue +4 -1
- package/dist/runtime/components/ADocumentTree.vue +78 -19
- package/dist/runtime/components/AEditor.d.vue.ts +9 -4
- package/dist/runtime/components/AEditor.vue +102 -7
- package/dist/runtime/components/AEditor.vue.d.ts +9 -4
- package/dist/runtime/components/AFloatingWindow.vue +1 -1
- package/dist/runtime/components/AIconPicker.vue +8 -2
- package/dist/runtime/components/ANodePanel.vue +100 -61
- package/dist/runtime/components/ANotifications.vue +35 -8
- package/dist/runtime/components/APermissionGuard.vue +3 -1
- package/dist/runtime/components/APresence.vue +14 -3
- package/dist/runtime/components/AProvider.vue +7 -1
- package/dist/runtime/components/AVoiceBar.vue +57 -15
- package/dist/runtime/components/AVoiceTile.vue +4 -1
- package/dist/runtime/components/AWindowLayer.vue +1 -1
- package/dist/runtime/components/aware/AArea.vue +1 -1
- package/dist/runtime/components/aware/AAvatar.vue +85 -16
- package/dist/runtime/components/aware/AButton.vue +5 -1
- package/dist/runtime/components/aware/ACursorLabel.vue +5 -1
- package/dist/runtime/components/aware/ADocBadge.vue +4 -1
- package/dist/runtime/components/aware/AFacepile.vue +13 -3
- package/dist/runtime/components/aware/AInput.vue +5 -1
- package/dist/runtime/components/aware/ATextarea.vue +5 -1
- package/dist/runtime/components/aware/AUserList.vue +8 -2
- package/dist/runtime/components/renderers/ACalendarRenderer.d.vue.ts +12 -1
- package/dist/runtime/components/renderers/ACalendarRenderer.vue +388 -114
- package/dist/runtime/components/renderers/ACalendarRenderer.vue.d.ts +12 -1
- package/dist/runtime/components/renderers/ACallRenderer.d.vue.ts +13 -0
- package/dist/runtime/components/renderers/ACallRenderer.vue +169 -0
- package/dist/runtime/components/renderers/ACallRenderer.vue.d.ts +13 -0
- package/dist/runtime/components/renderers/AChecklistRenderer.d.vue.ts +19 -0
- package/dist/runtime/components/renderers/AChecklistRenderer.vue +581 -0
- package/dist/runtime/components/renderers/AChecklistRenderer.vue.d.ts +19 -0
- package/dist/runtime/components/renderers/ADashboardRenderer.d.vue.ts +19 -0
- package/dist/runtime/components/renderers/ADashboardRenderer.vue +1372 -0
- package/dist/runtime/components/renderers/ADashboardRenderer.vue.d.ts +19 -0
- package/dist/runtime/components/renderers/AGalleryCoverImage.d.vue.ts +8 -0
- package/dist/runtime/components/renderers/AGalleryCoverImage.vue +60 -0
- package/dist/runtime/components/renderers/AGalleryCoverImage.vue.d.ts +8 -0
- package/dist/runtime/components/renderers/AGalleryRenderer.d.vue.ts +12 -1
- package/dist/runtime/components/renderers/AGalleryRenderer.vue +221 -55
- package/dist/runtime/components/renderers/AGalleryRenderer.vue.d.ts +12 -1
- package/dist/runtime/components/renderers/AGraphRenderer.d.vue.ts +19 -0
- package/dist/runtime/components/renderers/AGraphRenderer.vue +1027 -0
- package/dist/runtime/components/renderers/AGraphRenderer.vue.d.ts +19 -0
- package/dist/runtime/components/renderers/AKanbanRenderer.d.vue.ts +13 -1
- package/dist/runtime/components/renderers/AKanbanRenderer.vue +474 -140
- package/dist/runtime/components/renderers/AKanbanRenderer.vue.d.ts +13 -1
- package/dist/runtime/components/renderers/AMapRenderer.d.vue.ts +19 -0
- package/dist/runtime/components/renderers/AMapRenderer.vue +1622 -0
- package/dist/runtime/components/renderers/AMapRenderer.vue.d.ts +19 -0
- package/dist/runtime/components/renderers/AOutlineRenderer.d.vue.ts +12 -1
- package/dist/runtime/components/renderers/AOutlineRenderer.vue +294 -134
- package/dist/runtime/components/renderers/AOutlineRenderer.vue.d.ts +12 -1
- package/dist/runtime/components/renderers/ATableRenderer.d.vue.ts +12 -1
- package/dist/runtime/components/renderers/ATableRenderer.vue +437 -145
- package/dist/runtime/components/renderers/ATableRenderer.vue.d.ts +12 -1
- package/dist/runtime/components/renderers/ATimelineRenderer.d.vue.ts +19 -0
- package/dist/runtime/components/renderers/ATimelineRenderer.vue +446 -0
- package/dist/runtime/components/renderers/ATimelineRenderer.vue.d.ts +19 -0
- package/dist/runtime/composables/useAwareness.js +5 -0
- package/dist/runtime/composables/useBroadcastSync.d.ts +18 -0
- package/dist/runtime/composables/useBroadcastSync.js +26 -0
- package/dist/runtime/composables/useChat.js +4 -2
- package/dist/runtime/composables/useChatUsers.js +2 -1
- package/dist/runtime/composables/useCommandPalette.js +62 -3
- package/dist/runtime/composables/useConnectionStatus.js +7 -0
- package/dist/runtime/composables/useDevicePairing.d.ts +58 -0
- package/dist/runtime/composables/useDevicePairing.js +108 -0
- package/dist/runtime/composables/useDocExport.d.ts +5 -0
- package/dist/runtime/composables/useDocExport.js +2 -2
- package/dist/runtime/composables/useDocImport.js +4 -3
- package/dist/runtime/composables/useDocSeo.d.ts +20 -0
- package/dist/runtime/composables/useDocSeo.js +44 -0
- package/dist/runtime/composables/useDocSlugs.d.ts +7 -0
- package/dist/runtime/composables/useDocSlugs.js +20 -0
- package/dist/runtime/composables/useDocTree.d.ts +34 -0
- package/dist/runtime/composables/useDocTree.js +35 -0
- package/dist/runtime/composables/useEditorDragHandle.js +2 -1
- package/dist/runtime/composables/useEditorMentions.js +4 -2
- package/dist/runtime/composables/useEditorSuggestions.d.ts +1 -0
- package/dist/runtime/composables/useEditorSuggestions.js +9 -2
- package/dist/runtime/composables/useEditorToolbar.js +2 -1
- package/dist/runtime/composables/useFileIndex.js +2 -1
- package/dist/runtime/composables/useFileTransfer.d.ts +112 -0
- package/dist/runtime/composables/useFileTransfer.js +171 -0
- package/dist/runtime/composables/useFollowUser.js +2 -1
- package/dist/runtime/composables/useInvites.d.ts +56 -0
- package/dist/runtime/composables/useInvites.js +77 -0
- package/dist/runtime/composables/useNodePanel.d.ts +14 -0
- package/dist/runtime/composables/useNodePanel.js +52 -0
- package/dist/runtime/composables/useNotifications.js +4 -2
- package/dist/runtime/composables/usePasskeyAccounts.js +4 -2
- package/dist/runtime/composables/useSearchIndex.d.ts +1 -0
- package/dist/runtime/composables/useSearchIndex.js +13 -5
- package/dist/runtime/composables/useServerInfo.d.ts +31 -0
- package/dist/runtime/composables/useServerInfo.js +80 -0
- package/dist/runtime/composables/useSlugRoute.d.ts +6 -0
- package/dist/runtime/composables/useSlugRoute.js +19 -0
- package/dist/runtime/composables/useSpaces.d.ts +37 -0
- package/dist/runtime/composables/useSpaces.js +83 -0
- package/dist/runtime/composables/useTouchDrag.d.ts +34 -0
- package/dist/runtime/composables/useTouchDrag.js +191 -0
- package/dist/runtime/composables/useTrash.d.ts +1 -1
- package/dist/runtime/composables/useTrash.js +6 -3
- package/dist/runtime/composables/useWebRTC.d.ts +50 -0
- package/dist/runtime/composables/useWebRTC.js +177 -0
- package/dist/runtime/extensions/meta-field.d.ts +4 -1
- package/dist/runtime/extensions/steps.js +1 -1
- package/dist/runtime/extensions/views/AccordionItemView.vue +13 -3
- package/dist/runtime/extensions/views/AccordionView.vue +4 -1
- package/dist/runtime/extensions/views/BadgeView.vue +11 -2
- package/dist/runtime/extensions/views/CalloutView.vue +4 -1
- package/dist/runtime/extensions/views/CardGroupView.vue +4 -1
- package/dist/runtime/extensions/views/CardView.vue +17 -3
- package/dist/runtime/extensions/views/CodeGroupView.vue +4 -1
- package/dist/runtime/extensions/views/CollapsibleView.vue +8 -2
- package/dist/runtime/extensions/views/FileNodeView.vue +32 -8
- package/dist/runtime/extensions/views/KbdView.vue +8 -2
- package/dist/runtime/extensions/views/MetaFieldView.vue +208 -46
- package/dist/runtime/extensions/views/ProseIconView.vue +8 -2
- package/dist/runtime/extensions/views/TabsView.vue +17 -4
- package/dist/runtime/locale.d.ts +71 -0
- package/dist/runtime/locale.js +71 -0
- package/dist/runtime/plugin-abracadabra.client.js +29 -3
- package/dist/runtime/plugin-abracadabra.server.js +2 -0
- package/dist/runtime/server/api/_abracadabra/render/[docId].get.d.ts +1 -1
- package/dist/runtime/server/api/_abracadabra/render/[docId].get.js +29 -4
- package/dist/runtime/server/api/_abracadabra/resolve/[...slug].get.d.ts +2 -0
- package/dist/runtime/server/api/_abracadabra/resolve/[...slug].get.js +43 -0
- package/dist/runtime/server/api/_abracadabra/slugs.get.d.ts +2 -0
- package/dist/runtime/server/api/_abracadabra/slugs.get.js +7 -0
- package/dist/runtime/server/plugins/abracadabra-service.js +10 -5
- package/dist/runtime/server/runners/doc-tree-cache.js +4 -0
- package/dist/runtime/server/utils/slugMap.d.ts +32 -0
- package/dist/runtime/server/utils/slugMap.js +58 -0
- package/dist/runtime/types.d.ts +1 -0
- package/dist/runtime/utils/docTypes.d.ts +29 -1
- package/dist/runtime/utils/docTypes.js +129 -1
- package/dist/runtime/utils/markdownToYjs.js +2 -2
- package/dist/runtime/utils/sdkRef.d.ts +2 -0
- package/dist/runtime/utils/sdkRef.js +7 -0
- package/dist/runtime/utils/slugify.d.ts +40 -0
- package/dist/runtime/utils/slugify.js +36 -0
- package/dist/types.d.mts +6 -0
- 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>
|