@muhgholy/next-drive 0.1.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.
@@ -0,0 +1,2863 @@
1
+ // src/client/context.tsx
2
+ import React, { createContext, useContext, useState, useCallback, useMemo } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ var DriveContext = createContext(null);
5
+ var DriveProvider = (props) => {
6
+ const { children, apiEndpoint, initialActiveAccountId = null, initialSelectionMode = { type: "SINGLE" }, defaultSelectedFileIds = [] } = props;
7
+ const [accounts, setAccounts] = useState([]);
8
+ const [activeAccountId, setActiveAccountIdState] = useState(initialActiveAccountId);
9
+ const [currentFolderId, setCurrentFolderId] = useState(null);
10
+ const [path, setPath] = useState([{ id: null, name: "Home" }]);
11
+ const [allItems, setAllItems] = useState({});
12
+ const [searchResults, setSearchResults] = useState([]);
13
+ const [isLoading, setIsLoading] = useState(false);
14
+ const [error, setError] = useState(null);
15
+ const [quota, setQuota] = useState(null);
16
+ const activeCacheKey = activeAccountId || "LOCAL";
17
+ const setActiveAccountId = useCallback((id) => {
18
+ setActiveAccountIdState(id);
19
+ if (id) localStorage.setItem("drive_active_account", id);
20
+ else localStorage.removeItem("drive_active_account");
21
+ setCurrentFolderId(null);
22
+ setPath([{ id: null, name: "Home" }]);
23
+ setSearchResults([]);
24
+ }, []);
25
+ React.useEffect(() => {
26
+ const stored = localStorage.getItem("drive_active_account");
27
+ if (stored) setActiveAccountIdState(stored);
28
+ }, []);
29
+ const [viewMode, setViewMode] = useState("GRID");
30
+ const [currentView, setCurrentView] = useState("BROWSE");
31
+ const [searchQuery, setSearchQuery] = useState("");
32
+ const [searchScope, setSearchScope] = useState("ACTIVE");
33
+ const [groupBy, setGroupBy] = useState("NONE");
34
+ const [sortBy, setSortBy] = useState({ field: "createdAt", order: -1 });
35
+ const items = useMemo(() => {
36
+ const currentItems = allItems[activeCacheKey] || [];
37
+ if (currentView === "TRASH") return currentItems.filter((i) => i.trashedAt !== null);
38
+ if (currentView === "SEARCH") return searchResults;
39
+ return currentItems.filter((i) => i.parentId === currentFolderId && !i.trashedAt);
40
+ }, [allItems, activeCacheKey, currentFolderId, currentView, searchResults]);
41
+ const setItems = useCallback((updater) => {
42
+ setAllItems((prev) => {
43
+ const currentCache = prev[activeCacheKey] || [];
44
+ let newItems;
45
+ if (typeof updater === "function") {
46
+ const viewItems = currentCache.filter((i) => i.parentId === currentFolderId && !i.trashedAt);
47
+ const updatedViewItems = updater(viewItems);
48
+ newItems = [...currentCache.filter((i) => i.parentId !== currentFolderId || i.trashedAt), ...updatedViewItems];
49
+ } else {
50
+ newItems = [...currentCache.filter((i) => i.parentId !== currentFolderId || i.trashedAt), ...updater];
51
+ }
52
+ return { ...prev, [activeCacheKey]: newItems };
53
+ });
54
+ }, [activeCacheKey, currentFolderId]);
55
+ const [selectionMode] = useState(initialSelectionMode);
56
+ const [selectedFileIds, setSelectedFileIds] = useState(defaultSelectedFileIds);
57
+ const [hasMore, setHasMore] = useState(false);
58
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
59
+ const callAPI = useCallback(async (action, options) => {
60
+ const { query, ...fetchOptions } = options || {};
61
+ const params = new URLSearchParams({ action, ...query });
62
+ const url = `${apiEndpoint}?${params.toString()}`;
63
+ const headers = { "Content-Type": "application/json", ...fetchOptions?.headers || {} };
64
+ if (activeAccountId) headers["x-drive-account"] = activeAccountId;
65
+ try {
66
+ const response = await fetch(url, { ...fetchOptions, headers });
67
+ return await response.json();
68
+ } catch (err) {
69
+ return { status: 0, message: err instanceof Error ? err.message : "Network error" };
70
+ }
71
+ }, [apiEndpoint, activeAccountId]);
72
+ const refreshItems = useCallback(async () => {
73
+ setIsLoading(true);
74
+ setError(null);
75
+ setHasMore(false);
76
+ let response;
77
+ if (currentView === "TRASH") {
78
+ response = await callAPI("trash");
79
+ } else if (currentView === "SEARCH" && searchQuery) {
80
+ response = await callAPI("search", { query: { q: searchQuery, limit: "50", trashed: searchScope === "TRASH" ? "true" : "false" } });
81
+ } else {
82
+ response = await callAPI("list", { query: { folderId: currentFolderId || "root", limit: "50" } });
83
+ }
84
+ if (response.status === 200 && response.data) {
85
+ if (currentView === "SEARCH") {
86
+ setSearchResults(response.data.items);
87
+ } else {
88
+ setAllItems((prev) => {
89
+ const currentCache = prev[activeCacheKey] || [];
90
+ const existingIds = new Set(response.data.items.map((i) => i.id));
91
+ const filtered = currentCache.filter((i) => !existingIds.has(i.id));
92
+ return {
93
+ ...prev,
94
+ [activeCacheKey]: [...filtered, ...response.data.items]
95
+ };
96
+ });
97
+ }
98
+ setHasMore(!!response.data.hasMore);
99
+ } else {
100
+ setError(response.message || "Failed to load items");
101
+ }
102
+ setIsLoading(false);
103
+ }, [callAPI, currentFolderId, currentView, searchQuery, searchScope, activeCacheKey]);
104
+ const refreshAccounts = useCallback(async () => {
105
+ const response = await callAPI("listAccounts");
106
+ if (response.status === 200 && response.data) {
107
+ setAccounts(response.data.accounts);
108
+ }
109
+ }, [callAPI]);
110
+ const loadMore = useCallback(async () => {
111
+ if (!hasMore || isLoading || isLoadingMore || currentView === "TRASH") return;
112
+ setIsLoadingMore(true);
113
+ const currentItems = items;
114
+ const lastItem = currentItems[currentItems.length - 1];
115
+ const afterId = lastItem ? lastItem.id : void 0;
116
+ if (!afterId) {
117
+ setIsLoadingMore(false);
118
+ return;
119
+ }
120
+ let response;
121
+ if (currentView === "SEARCH" && searchQuery) {
122
+ setIsLoadingMore(false);
123
+ return;
124
+ } else {
125
+ response = await callAPI("list", {
126
+ query: { folderId: currentFolderId || "root", limit: "50", afterId }
127
+ });
128
+ }
129
+ if (response && response.status === 200 && response.data) {
130
+ setAllItems((prev) => {
131
+ const currentCache = prev[activeCacheKey] || [];
132
+ return {
133
+ ...prev,
134
+ [activeCacheKey]: [...currentCache, ...response.data.items]
135
+ };
136
+ });
137
+ setHasMore(!!response.data.hasMore);
138
+ }
139
+ setIsLoadingMore(false);
140
+ }, [callAPI, currentFolderId, hasMore, isLoading, isLoadingMore, items, currentView, searchQuery, activeCacheKey]);
141
+ const refreshQuota = useCallback(async () => {
142
+ const response = await callAPI("quota");
143
+ if (response.status === 200 && response.data) setQuota(response.data);
144
+ }, [callAPI]);
145
+ const navigateToFolder = useCallback((item) => {
146
+ setCurrentView("BROWSE");
147
+ setSearchQuery("");
148
+ if (item === null) {
149
+ setCurrentFolderId(null);
150
+ setPath([{ id: null, name: "Home" }]);
151
+ setSelectedFileIds([]);
152
+ return;
153
+ }
154
+ setCurrentFolderId(item.id);
155
+ setPath((prev) => {
156
+ const existingIndex = prev.findIndex((p) => p.id === item.id);
157
+ if (existingIndex !== -1) {
158
+ return prev.slice(0, existingIndex + 1);
159
+ }
160
+ return [...prev, { id: item.id, name: item.name }];
161
+ });
162
+ setSelectedFileIds([]);
163
+ }, []);
164
+ const navigateUp = useCallback(() => {
165
+ setCurrentView("BROWSE");
166
+ setSearchQuery("");
167
+ if (path.length <= 1) return;
168
+ const newPath = path.slice(0, -1);
169
+ setPath(newPath);
170
+ setCurrentFolderId(newPath[newPath.length - 1]?.id || null);
171
+ setSelectedFileIds([]);
172
+ }, [path]);
173
+ const moveItem = useCallback(async (itemId, targetFolderId) => {
174
+ setAllItems((prev) => {
175
+ const currentCache = prev[activeCacheKey] || [];
176
+ const newItems = currentCache.map(
177
+ (item) => item.id === itemId ? { ...item, parentId: targetFolderId === "root" ? null : targetFolderId } : item
178
+ );
179
+ return { ...prev, [activeCacheKey]: newItems };
180
+ });
181
+ await callAPI("move", {
182
+ method: "POST",
183
+ body: JSON.stringify({ ids: [itemId], targetFolderId })
184
+ });
185
+ }, [callAPI, activeCacheKey]);
186
+ const deleteItems = useCallback(async (ids) => {
187
+ setAllItems((prev) => {
188
+ const currentCache = prev[activeCacheKey] || [];
189
+ let newItems;
190
+ if (currentView === "TRASH") {
191
+ newItems = currentCache.filter((item) => !ids.includes(item.id));
192
+ } else {
193
+ newItems = currentCache.map(
194
+ (item) => ids.includes(item.id) ? { ...item, trashedAt: /* @__PURE__ */ new Date() } : item
195
+ );
196
+ }
197
+ return { ...prev, [activeCacheKey]: newItems };
198
+ });
199
+ setSelectedFileIds([]);
200
+ if (currentView === "TRASH") {
201
+ for (const id of ids) {
202
+ await callAPI("deletePermanent", { query: { id } });
203
+ }
204
+ } else {
205
+ for (const id of ids) {
206
+ await callAPI("delete", { query: { id } });
207
+ }
208
+ }
209
+ }, [callAPI, activeCacheKey, currentView]);
210
+ React.useEffect(() => {
211
+ refreshItems();
212
+ }, [currentFolderId, currentView, refreshItems, activeAccountId]);
213
+ React.useEffect(() => {
214
+ refreshQuota();
215
+ }, [refreshQuota, activeAccountId]);
216
+ React.useEffect(() => {
217
+ refreshAccounts();
218
+ }, [refreshAccounts]);
219
+ return /* @__PURE__ */ jsx(DriveContext.Provider, { value: {
220
+ apiEndpoint,
221
+ currentFolderId,
222
+ path,
223
+ items,
224
+ allItems,
225
+ setItems,
226
+ setAllItems,
227
+ isLoading,
228
+ error,
229
+ quota,
230
+ refreshQuota,
231
+ accounts,
232
+ activeAccountId,
233
+ setActiveAccountId,
234
+ refreshAccounts,
235
+ viewMode,
236
+ setViewMode,
237
+ groupBy,
238
+ setGroupBy,
239
+ sortBy,
240
+ setSortBy,
241
+ currentView,
242
+ setCurrentView,
243
+ searchQuery,
244
+ setSearchQuery,
245
+ searchScope,
246
+ setSearchScope,
247
+ selectionMode,
248
+ selectedFileIds,
249
+ setSelectedFileIds,
250
+ navigateToFolder,
251
+ navigateUp,
252
+ refreshItems,
253
+ callAPI,
254
+ moveItem,
255
+ deleteItems,
256
+ loadMore,
257
+ hasMore,
258
+ isLoadingMore
259
+ }, children });
260
+ };
261
+ var useDrive = () => {
262
+ const context = useContext(DriveContext);
263
+ if (!context) throw new Error("useDrive must be used within a DriveProvider");
264
+ return context;
265
+ };
266
+
267
+ // src/client/hooks/useUpload.ts
268
+ import { useState as useState2, useCallback as useCallback2, useRef, useEffect } from "react";
269
+ var MAX_CONCURRENT_UPLOADS = 2;
270
+ var getChunkSize = (fileSize) => {
271
+ if (fileSize < 50 * 1024 * 1024) return 2 * 1024 * 1024;
272
+ if (fileSize < 200 * 1024 * 1024) return 4 * 1024 * 1024;
273
+ if (fileSize < 1024 * 1024 * 1024) return 8 * 1024 * 1024;
274
+ return 16 * 1024 * 1024;
275
+ };
276
+ var useUpload = (apiEndpoint, activeAccountId, onUploadComplete) => {
277
+ const [uploads, setUploads] = useState2([]);
278
+ const abortControllers = useRef(/* @__PURE__ */ new Map());
279
+ const filesRef = useRef(/* @__PURE__ */ new Map());
280
+ const metaRef = useRef(/* @__PURE__ */ new Map());
281
+ const updateUpload = useCallback2((id, updates) => {
282
+ setUploads((prev) => prev.map((u) => u.id === id ? { ...u, ...updates } : u));
283
+ }, []);
284
+ const uploadChunk = async (formData) => {
285
+ try {
286
+ const headers = {};
287
+ if (activeAccountId) headers["x-drive-account"] = activeAccountId;
288
+ const response = await fetch(`${apiEndpoint}?action=upload`, { method: "POST", body: formData, headers });
289
+ if (!response.ok) {
290
+ const text = await response.text();
291
+ try {
292
+ const json = JSON.parse(text);
293
+ const msg = json.message || json.error?.message || `Status ${response.status}`;
294
+ const canRetry = [408, 429, 502, 503, 504].includes(response.status);
295
+ return [false, msg, canRetry];
296
+ } catch {
297
+ const isRetryable = response.status >= 500 || response.status === 429;
298
+ return [false, `Server error ${response.status}: ${text.slice(0, 100)}`, isRetryable];
299
+ }
300
+ }
301
+ const data = await response.json();
302
+ if (data.status !== 200) return [false, data.message || "Upload failed", false];
303
+ return [true, data.data, false];
304
+ } catch (error) {
305
+ return [false, error instanceof Error ? error.message : "Network error", true];
306
+ }
307
+ };
308
+ const processItem = async (item, file, folderId) => {
309
+ const controller = new AbortController();
310
+ abortControllers.current.set(item.id, controller);
311
+ updateUpload(item.id, { status: "uploading" });
312
+ const chunkSize = getChunkSize(file.size);
313
+ const totalChunks = Math.ceil(file.size / chunkSize);
314
+ let driveId = item.driveId;
315
+ try {
316
+ for (let i = item.currentChunk; i < totalChunks; i++) {
317
+ if (controller.signal.aborted) throw new Error("Cancelled");
318
+ updateUpload(item.id, { currentChunk: i });
319
+ const start = i * chunkSize;
320
+ const end = Math.min(start + chunkSize, file.size);
321
+ const chunk = file.slice(start, end);
322
+ const formData = new FormData();
323
+ formData.append("chunk", chunk);
324
+ formData.append("chunkIndex", String(i));
325
+ formData.append("totalChunks", String(totalChunks));
326
+ formData.append("fileName", file.name);
327
+ formData.append("fileSize", String(file.size));
328
+ formData.append("fileType", file.type);
329
+ formData.append("folderId", folderId || "root");
330
+ if (driveId) formData.append("driveId", driveId);
331
+ let attempts = 0;
332
+ let success = false;
333
+ while (!success && attempts < 3 && !controller.signal.aborted) {
334
+ const [ok, result, canRetry] = await uploadChunk(formData);
335
+ if (ok) {
336
+ success = true;
337
+ if (result.type === "UPLOAD_STARTED" && result.driveId) {
338
+ driveId = result.driveId;
339
+ updateUpload(item.id, { driveId });
340
+ } else if (result.type === "UPLOAD_COMPLETE") {
341
+ if (onUploadComplete) onUploadComplete(result.item);
342
+ }
343
+ } else {
344
+ if (!canRetry) throw new Error(result);
345
+ attempts++;
346
+ if (attempts === 3) throw new Error(result);
347
+ await new Promise((r) => setTimeout(r, 1e3 * attempts));
348
+ }
349
+ }
350
+ }
351
+ updateUpload(item.id, { status: "complete", currentChunk: totalChunks });
352
+ } catch (error) {
353
+ if (error.message === "Cancelled") {
354
+ updateUpload(item.id, { status: "cancelled" });
355
+ } else {
356
+ updateUpload(item.id, { status: "error", error: error.message });
357
+ }
358
+ } finally {
359
+ abortControllers.current.delete(item.id);
360
+ }
361
+ };
362
+ const uploadFiles = useCallback2(async (files, folderId) => {
363
+ const newUploads = [];
364
+ files.forEach((file) => {
365
+ const id = `upload_${Date.now()}_${Math.random().toString(36).slice(2)}`;
366
+ const chunkSize = getChunkSize(file.size);
367
+ filesRef.current.set(id, file);
368
+ metaRef.current.set(id, { folderId });
369
+ newUploads.push({
370
+ id,
371
+ name: file.name,
372
+ size: file.size,
373
+ status: "queued",
374
+ currentChunk: 0,
375
+ totalChunks: Math.ceil(file.size / chunkSize)
376
+ });
377
+ });
378
+ setUploads((prev) => [...prev, ...newUploads]);
379
+ }, []);
380
+ const cancelUpload = useCallback2(async (id) => {
381
+ const controller = abortControllers.current.get(id);
382
+ if (controller) {
383
+ controller.abort();
384
+ } else {
385
+ updateUpload(id, { status: "cancelled" });
386
+ }
387
+ const upload = uploads.find((u) => u.id === id);
388
+ if (upload?.driveId) {
389
+ fetch(`${apiEndpoint}?action=cancel&id=${upload.driveId}`, { method: "POST" }).catch(() => {
390
+ });
391
+ }
392
+ }, [apiEndpoint, updateUpload, uploads]);
393
+ const cancelAllUploads = useCallback2(async () => {
394
+ uploads.forEach((u) => {
395
+ if (["queued", "uploading", "pending"].includes(u.status)) {
396
+ cancelUpload(u.id);
397
+ }
398
+ });
399
+ }, [uploads, cancelUpload]);
400
+ useEffect(() => {
401
+ const activeCount = uploads.filter((u) => u.status === "uploading").length;
402
+ if (activeCount >= MAX_CONCURRENT_UPLOADS) return;
403
+ const queued = uploads.find((u) => u.status === "queued");
404
+ if (queued) {
405
+ const file = filesRef.current.get(queued.id);
406
+ const meta = metaRef.current.get(queued.id);
407
+ if (file) {
408
+ processItem(queued, file, meta?.folderId || null);
409
+ }
410
+ }
411
+ }, [uploads]);
412
+ return { uploads, uploadFiles, cancelUpload, cancelAllUploads };
413
+ };
414
+
415
+ // src/client/file-chooser.tsx
416
+ import { useState as useState7, useCallback as useCallback4, useMemo as useMemo3, useEffect as useEffect5 } from "react";
417
+
418
+ // src/client/utils.tsx
419
+ import { clsx } from "clsx";
420
+ import { twMerge } from "tailwind-merge";
421
+ import { File, Folder, Image, Video, Music, FileText, FileCode, FileArchive } from "lucide-react";
422
+ import { jsx as jsx2 } from "react/jsx-runtime";
423
+ function cn(...inputs) {
424
+ return twMerge(clsx(inputs));
425
+ }
426
+ var formatBytes = (bytes, decimals = 2) => {
427
+ if (bytes === 0) return "0 Bytes";
428
+ const k = 1024;
429
+ const dm = decimals < 0 ? 0 : decimals;
430
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
431
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
432
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
433
+ };
434
+ var getFileIcon = (mime, isFolder, className = "w-6 h-6") => {
435
+ if (isFolder) return /* @__PURE__ */ jsx2(Folder, { className: cn("text-blue-500 fill-blue-500/20", className) });
436
+ if (mime.startsWith("image/")) return /* @__PURE__ */ jsx2(Image, { className: cn("text-purple-500", className) });
437
+ if (mime.startsWith("video/")) return /* @__PURE__ */ jsx2(Video, { className: cn("text-red-500", className) });
438
+ if (mime.startsWith("audio/")) return /* @__PURE__ */ jsx2(Music, { className: cn("text-yellow-500", className) });
439
+ if (mime === "application/pdf") return /* @__PURE__ */ jsx2(FileText, { className: cn("text-orange-500", className) });
440
+ if (mime.includes("text") || mime.includes("document")) return /* @__PURE__ */ jsx2(FileText, { className: cn("text-slate-500", className) });
441
+ if (mime.includes("zip") || mime.includes("compressed")) return /* @__PURE__ */ jsx2(FileArchive, { className: cn("text-amber-500", className) });
442
+ if (mime.includes("javascript") || mime.includes("typescript") || mime.includes("json") || mime.includes("html") || mime.includes("css")) return /* @__PURE__ */ jsx2(FileCode, { className: cn("text-green-500", className) });
443
+ return /* @__PURE__ */ jsx2(File, { className: cn("text-gray-400", className) });
444
+ };
445
+ var matchesMimeFilter = (mime, isFolder, filter) => {
446
+ if (!filter) return true;
447
+ if (isFolder) return true;
448
+ if (filter === "*/*") return true;
449
+ const types = filter.split(",").map((t) => t.trim());
450
+ return types.some((type) => {
451
+ if (type === mime) return true;
452
+ if (type.endsWith("/*")) {
453
+ const prefix = type.slice(0, -2);
454
+ return mime.startsWith(`${prefix}/`);
455
+ }
456
+ return false;
457
+ });
458
+ };
459
+
460
+ // src/client/components/drive/explorer.tsx
461
+ import React7, { useMemo as useMemo2, useEffect as useEffect4, useRef as useRef3 } from "react";
462
+ import { Folder as Folder2, Loader2 as Loader22, Info, RotateCcw, ChevronRight as ChevronRight3 } from "lucide-react";
463
+ import { isToday, isYesterday, startOfWeek, subWeeks, isAfter } from "date-fns";
464
+
465
+ // src/client/components/ui/context-menu.tsx
466
+ import * as React2 from "react";
467
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
468
+ import { Check, ChevronRight, Circle } from "lucide-react";
469
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
470
+ var ContextMenu = ContextMenuPrimitive.Root;
471
+ var ContextMenuTrigger = ContextMenuPrimitive.Trigger;
472
+ var ContextMenuSubTrigger = React2.forwardRef(({ className, inset, children, ...props }, ref) => /* @__PURE__ */ jsxs(
473
+ ContextMenuPrimitive.SubTrigger,
474
+ {
475
+ ref,
476
+ className: cn(
477
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
478
+ inset && "pl-8",
479
+ className
480
+ ),
481
+ ...props,
482
+ children: [
483
+ children,
484
+ /* @__PURE__ */ jsx3(ChevronRight, { className: "ml-auto h-4 w-4" })
485
+ ]
486
+ }
487
+ ));
488
+ ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
489
+ var ContextMenuSubContent = React2.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx3(
490
+ ContextMenuPrimitive.SubContent,
491
+ {
492
+ ref,
493
+ className: cn(
494
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
495
+ className
496
+ ),
497
+ ...props
498
+ }
499
+ ));
500
+ ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
501
+ var ContextMenuContent = React2.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx3(ContextMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx3(
502
+ ContextMenuPrimitive.Content,
503
+ {
504
+ ref,
505
+ className: cn(
506
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
507
+ className
508
+ ),
509
+ ...props
510
+ }
511
+ ) }));
512
+ ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
513
+ var ContextMenuItem = React2.forwardRef(({ className, inset, ...props }, ref) => /* @__PURE__ */ jsx3(
514
+ ContextMenuPrimitive.Item,
515
+ {
516
+ ref,
517
+ className: cn(
518
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
519
+ inset && "pl-8",
520
+ className
521
+ ),
522
+ ...props
523
+ }
524
+ ));
525
+ ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
526
+ var ContextMenuCheckboxItem = React2.forwardRef(({ className, children, checked, ...props }, ref) => /* @__PURE__ */ jsxs(
527
+ ContextMenuPrimitive.CheckboxItem,
528
+ {
529
+ ref,
530
+ className: cn(
531
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
532
+ className
533
+ ),
534
+ checked,
535
+ ...props,
536
+ children: [
537
+ /* @__PURE__ */ jsx3("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ jsx3(ContextMenuPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx3(Check, { className: "h-4 w-4" }) }) }),
538
+ children
539
+ ]
540
+ }
541
+ ));
542
+ ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
543
+ var ContextMenuRadioItem = React2.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
544
+ ContextMenuPrimitive.RadioItem,
545
+ {
546
+ ref,
547
+ className: cn(
548
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
549
+ className
550
+ ),
551
+ ...props,
552
+ children: [
553
+ /* @__PURE__ */ jsx3("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ jsx3(ContextMenuPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx3(Circle, { className: "h-2 w-2 fill-current" }) }) }),
554
+ children
555
+ ]
556
+ }
557
+ ));
558
+ ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
559
+ var ContextMenuLabel = React2.forwardRef(({ className, inset, ...props }, ref) => /* @__PURE__ */ jsx3(
560
+ ContextMenuPrimitive.Label,
561
+ {
562
+ ref,
563
+ className: cn(
564
+ "px-2 py-1.5 text-sm font-semibold text-foreground",
565
+ inset && "pl-8",
566
+ className
567
+ ),
568
+ ...props
569
+ }
570
+ ));
571
+ ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
572
+ var ContextMenuSeparator = React2.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx3(
573
+ ContextMenuPrimitive.Separator,
574
+ {
575
+ ref,
576
+ className: cn("-mx-1 my-1 h-px bg-border", className),
577
+ ...props
578
+ }
579
+ ));
580
+ ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
581
+ var ContextMenuShortcut = ({
582
+ className,
583
+ ...props
584
+ }) => {
585
+ return /* @__PURE__ */ jsx3(
586
+ "span",
587
+ {
588
+ className: cn(
589
+ "ml-auto text-xs tracking-widest text-muted-foreground",
590
+ className
591
+ ),
592
+ ...props
593
+ }
594
+ );
595
+ };
596
+ ContextMenuShortcut.displayName = "ContextMenuShortcut";
597
+
598
+ // src/client/components/dialog.tsx
599
+ import { useEffect as useEffect2, useState as useState3 } from "react";
600
+ import { AlertCircle } from "lucide-react";
601
+
602
+ // src/client/components/ui/alert.tsx
603
+ import { cva } from "class-variance-authority";
604
+ import { jsx as jsx4 } from "react/jsx-runtime";
605
+ var alertVariants = cva(
606
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
607
+ {
608
+ variants: {
609
+ variant: {
610
+ default: "bg-card text-card-foreground",
611
+ destructive: "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90"
612
+ }
613
+ },
614
+ defaultVariants: {
615
+ variant: "default"
616
+ }
617
+ }
618
+ );
619
+ function Alert({
620
+ className,
621
+ variant,
622
+ ...props
623
+ }) {
624
+ return /* @__PURE__ */ jsx4(
625
+ "div",
626
+ {
627
+ "data-slot": "alert",
628
+ role: "alert",
629
+ className: cn(alertVariants({ variant }), className),
630
+ ...props
631
+ }
632
+ );
633
+ }
634
+ function AlertTitle({ className, ...props }) {
635
+ return /* @__PURE__ */ jsx4(
636
+ "div",
637
+ {
638
+ "data-slot": "alert-title",
639
+ className: cn(
640
+ "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
641
+ className
642
+ ),
643
+ ...props
644
+ }
645
+ );
646
+ }
647
+ function AlertDescription({
648
+ className,
649
+ ...props
650
+ }) {
651
+ return /* @__PURE__ */ jsx4(
652
+ "div",
653
+ {
654
+ "data-slot": "alert-description",
655
+ className: cn(
656
+ "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
657
+ className
658
+ ),
659
+ ...props
660
+ }
661
+ );
662
+ }
663
+
664
+ // src/client/components/ui/button.tsx
665
+ import { Slot } from "@radix-ui/react-slot";
666
+ import { cva as cva2 } from "class-variance-authority";
667
+ import { jsx as jsx5 } from "react/jsx-runtime";
668
+ var buttonVariants = cva2(
669
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
670
+ {
671
+ variants: {
672
+ variant: {
673
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
674
+ destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
675
+ outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
676
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
677
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
678
+ link: "text-primary underline-offset-4 hover:underline"
679
+ },
680
+ size: {
681
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
682
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
683
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
684
+ icon: "size-9",
685
+ "icon-sm": "size-8",
686
+ "icon-lg": "size-10"
687
+ }
688
+ },
689
+ defaultVariants: {
690
+ variant: "default",
691
+ size: "default"
692
+ }
693
+ }
694
+ );
695
+ function Button({
696
+ className,
697
+ variant,
698
+ size,
699
+ asChild = false,
700
+ ...props
701
+ }) {
702
+ const Comp = asChild ? Slot : "button";
703
+ return /* @__PURE__ */ jsx5(
704
+ Comp,
705
+ {
706
+ "data-slot": "button",
707
+ className: cn(buttonVariants({ variant, size, className })),
708
+ ...props
709
+ }
710
+ );
711
+ }
712
+
713
+ // src/client/components/ui/dialog.tsx
714
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
715
+ import { X } from "lucide-react";
716
+ import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
717
+ function Dialog({
718
+ ...props
719
+ }) {
720
+ return /* @__PURE__ */ jsx6(DialogPrimitive.Root, { "data-slot": "dialog", ...props });
721
+ }
722
+ function DialogPortal({
723
+ ...props
724
+ }) {
725
+ return /* @__PURE__ */ jsx6(DialogPrimitive.Portal, { "data-slot": "dialog-portal", ...props });
726
+ }
727
+ function DialogOverlay({
728
+ className,
729
+ ...props
730
+ }) {
731
+ return /* @__PURE__ */ jsx6(
732
+ DialogPrimitive.Overlay,
733
+ {
734
+ "data-slot": "dialog-overlay",
735
+ className: cn(
736
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
737
+ className
738
+ ),
739
+ ...props
740
+ }
741
+ );
742
+ }
743
+ function DialogContent({
744
+ className,
745
+ children,
746
+ showCloseButton = true,
747
+ ...props
748
+ }) {
749
+ return /* @__PURE__ */ jsxs2(DialogPortal, { "data-slot": "dialog-portal", children: [
750
+ /* @__PURE__ */ jsx6(DialogOverlay, {}),
751
+ /* @__PURE__ */ jsxs2(
752
+ DialogPrimitive.Content,
753
+ {
754
+ "data-slot": "dialog-content",
755
+ className: cn(
756
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
757
+ className
758
+ ),
759
+ ...props,
760
+ children: [
761
+ children,
762
+ showCloseButton && /* @__PURE__ */ jsxs2(
763
+ DialogPrimitive.Close,
764
+ {
765
+ "data-slot": "dialog-close",
766
+ className: "ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
767
+ children: [
768
+ /* @__PURE__ */ jsx6(X, { className: "size-4" }),
769
+ /* @__PURE__ */ jsx6("span", { className: "sr-only", children: "Close" })
770
+ ]
771
+ }
772
+ )
773
+ ]
774
+ }
775
+ )
776
+ ] });
777
+ }
778
+ function DialogHeader({ className, ...props }) {
779
+ return /* @__PURE__ */ jsx6(
780
+ "div",
781
+ {
782
+ "data-slot": "dialog-header",
783
+ className: cn("flex flex-col gap-2 text-center sm:text-left", className),
784
+ ...props
785
+ }
786
+ );
787
+ }
788
+ function DialogFooter({ className, ...props }) {
789
+ return /* @__PURE__ */ jsx6(
790
+ "div",
791
+ {
792
+ "data-slot": "dialog-footer",
793
+ className: cn(
794
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
795
+ className
796
+ ),
797
+ ...props
798
+ }
799
+ );
800
+ }
801
+ function DialogTitle({
802
+ className,
803
+ ...props
804
+ }) {
805
+ return /* @__PURE__ */ jsx6(
806
+ DialogPrimitive.Title,
807
+ {
808
+ "data-slot": "dialog-title",
809
+ className: cn("text-lg leading-none font-semibold", className),
810
+ ...props
811
+ }
812
+ );
813
+ }
814
+ function DialogDescription({
815
+ className,
816
+ ...props
817
+ }) {
818
+ return /* @__PURE__ */ jsx6(
819
+ DialogPrimitive.Description,
820
+ {
821
+ "data-slot": "dialog-description",
822
+ className: cn("text-muted-foreground text-sm", className),
823
+ ...props
824
+ }
825
+ );
826
+ }
827
+
828
+ // src/client/components/ui/input.tsx
829
+ import { jsx as jsx7 } from "react/jsx-runtime";
830
+ function Input({ className, type, ...props }) {
831
+ return /* @__PURE__ */ jsx7(
832
+ "input",
833
+ {
834
+ type,
835
+ "data-slot": "input",
836
+ className: cn(
837
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
838
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
839
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
840
+ className
841
+ ),
842
+ ...props
843
+ }
844
+ );
845
+ }
846
+
847
+ // src/client/components/ui/label.tsx
848
+ import * as LabelPrimitive from "@radix-ui/react-label";
849
+ import { jsx as jsx8 } from "react/jsx-runtime";
850
+ function Label2({
851
+ className,
852
+ ...props
853
+ }) {
854
+ return /* @__PURE__ */ jsx8(
855
+ LabelPrimitive.Root,
856
+ {
857
+ "data-slot": "label",
858
+ className: cn(
859
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
860
+ className
861
+ ),
862
+ ...props
863
+ }
864
+ );
865
+ }
866
+
867
+ // src/client/components/dialog.tsx
868
+ import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
869
+ var DialogConfirmation = (props) => {
870
+ const { title, description, onConfirm, disableEscapeKeyDown, inputs, onClose, open } = props;
871
+ const [error, setError] = useState3(null);
872
+ const [isLoading, setIsLoading] = useState3(false);
873
+ const [inputsValue, setInputsValue] = useState3(
874
+ () => inputs?.reduce((acc, input) => ({
875
+ ...acc,
876
+ [input.id]: input.default ?? ""
877
+ }), {}) ?? {}
878
+ );
879
+ const handleConfirm = async () => {
880
+ setIsLoading(true);
881
+ setError(null);
882
+ try {
883
+ const response = await (inputs ? onConfirm(inputsValue) : onConfirm());
884
+ if (response[0]) {
885
+ onClose();
886
+ } else {
887
+ setError(response[1]);
888
+ }
889
+ } catch (err) {
890
+ setError(String(err));
891
+ } finally {
892
+ setIsLoading(false);
893
+ }
894
+ };
895
+ const handleClose = () => {
896
+ if (isLoading) return;
897
+ onClose();
898
+ };
899
+ const handleOpenChange = (isOpen) => {
900
+ if (!isOpen) {
901
+ handleClose();
902
+ }
903
+ };
904
+ const handleKeyPress = (event) => {
905
+ if (event.key === "Enter" && isFormValid()) {
906
+ event.preventDefault();
907
+ handleConfirm();
908
+ }
909
+ };
910
+ const isFormValid = () => {
911
+ if (!inputs) return true;
912
+ return inputs.every(
913
+ (input) => !input.required || inputsValue[input.id] && inputsValue[input.id].trim() !== ""
914
+ );
915
+ };
916
+ useEffect2(() => {
917
+ setInputsValue(
918
+ inputs?.reduce((acc, input) => ({
919
+ ...acc,
920
+ [input.id]: input.default ?? ""
921
+ }), {}) ?? {}
922
+ );
923
+ setError(null);
924
+ }, [inputs]);
925
+ return /* @__PURE__ */ jsx9(Dialog, { open, onOpenChange: handleOpenChange, children: /* @__PURE__ */ jsxs3(
926
+ DialogContent,
927
+ {
928
+ onInteractOutside: (e) => {
929
+ if (disableEscapeKeyDown) {
930
+ e.preventDefault();
931
+ }
932
+ },
933
+ onEscapeKeyDown: (e) => {
934
+ if (disableEscapeKeyDown) {
935
+ e.preventDefault();
936
+ }
937
+ },
938
+ children: [
939
+ /* @__PURE__ */ jsxs3(DialogHeader, { children: [
940
+ /* @__PURE__ */ jsx9(DialogTitle, { children: title }),
941
+ /* @__PURE__ */ jsx9(DialogDescription, { children: description })
942
+ ] }),
943
+ error && /* @__PURE__ */ jsxs3(Alert, { variant: "destructive", children: [
944
+ /* @__PURE__ */ jsx9(AlertCircle, { className: "h-4 w-4" }),
945
+ /* @__PURE__ */ jsx9(AlertTitle, { children: "Error" }),
946
+ /* @__PURE__ */ jsx9(AlertDescription, { children: error })
947
+ ] }),
948
+ inputs && inputs.length > 0 && /* @__PURE__ */ jsx9("div", { className: "grid gap-4 py-4", children: inputs.map((input, index) => /* @__PURE__ */ jsxs3("div", { className: "grid gap-2", children: [
949
+ /* @__PURE__ */ jsx9(Label2, { htmlFor: input.id, children: input.name }),
950
+ /* @__PURE__ */ jsx9(
951
+ Input,
952
+ {
953
+ id: input.id,
954
+ required: input.required,
955
+ value: inputsValue[input.id] || "",
956
+ onChange: (e) => setInputsValue({ ...inputsValue, [input.id]: e.target.value }),
957
+ onKeyDown: index === inputs.length - 1 ? handleKeyPress : void 0
958
+ }
959
+ )
960
+ ] }, input.id)) }),
961
+ /* @__PURE__ */ jsxs3(DialogFooter, { children: [
962
+ /* @__PURE__ */ jsx9(Button, { variant: "outline", onClick: handleClose, disabled: isLoading, children: "Cancel" }),
963
+ /* @__PURE__ */ jsx9(
964
+ Button,
965
+ {
966
+ onClick: handleConfirm,
967
+ disabled: isLoading || !isFormValid(),
968
+ children: "Confirm"
969
+ }
970
+ )
971
+ ] })
972
+ ]
973
+ }
974
+ ) });
975
+ };
976
+
977
+ // src/client/components/drive/explorer.tsx
978
+ import { Pencil, Trash2 as Trash22, FolderPlus } from "lucide-react";
979
+
980
+ // src/client/components/ui/sheet.tsx
981
+ import * as React3 from "react";
982
+ import * as SheetPrimitive from "@radix-ui/react-dialog";
983
+ import { cva as cva3 } from "class-variance-authority";
984
+ import { X as X2 } from "lucide-react";
985
+ import { jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
986
+ var Sheet = SheetPrimitive.Root;
987
+ var SheetTrigger = SheetPrimitive.Trigger;
988
+ var SheetPortal = SheetPrimitive.Portal;
989
+ var SheetOverlay = React3.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx10(
990
+ SheetPrimitive.Overlay,
991
+ {
992
+ className: cn(
993
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
994
+ className
995
+ ),
996
+ ...props,
997
+ ref
998
+ }
999
+ ));
1000
+ SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
1001
+ var sheetVariants = cva3(
1002
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
1003
+ {
1004
+ variants: {
1005
+ side: {
1006
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
1007
+ bottom: "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
1008
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
1009
+ right: "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm"
1010
+ }
1011
+ },
1012
+ defaultVariants: {
1013
+ side: "right"
1014
+ }
1015
+ }
1016
+ );
1017
+ var SheetContent = React3.forwardRef(({ side = "right", className, children, ...props }, ref) => /* @__PURE__ */ jsxs4(SheetPortal, { children: [
1018
+ /* @__PURE__ */ jsx10(SheetOverlay, {}),
1019
+ /* @__PURE__ */ jsxs4(
1020
+ SheetPrimitive.Content,
1021
+ {
1022
+ ref,
1023
+ className: cn(sheetVariants({ side }), className),
1024
+ ...props,
1025
+ children: [
1026
+ children,
1027
+ /* @__PURE__ */ jsxs4(SheetPrimitive.Close, { className: "absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary", children: [
1028
+ /* @__PURE__ */ jsx10(X2, { className: "h-4 w-4" }),
1029
+ /* @__PURE__ */ jsx10("span", { className: "sr-only", children: "Close" })
1030
+ ] })
1031
+ ]
1032
+ }
1033
+ )
1034
+ ] }));
1035
+ SheetContent.displayName = SheetPrimitive.Content.displayName;
1036
+ var SheetHeader = ({
1037
+ className,
1038
+ ...props
1039
+ }) => /* @__PURE__ */ jsx10(
1040
+ "div",
1041
+ {
1042
+ className: cn(
1043
+ "flex flex-col space-y-2 text-center sm:text-left",
1044
+ className
1045
+ ),
1046
+ ...props
1047
+ }
1048
+ );
1049
+ SheetHeader.displayName = "SheetHeader";
1050
+ var SheetFooter = ({
1051
+ className,
1052
+ ...props
1053
+ }) => /* @__PURE__ */ jsx10(
1054
+ "div",
1055
+ {
1056
+ className: cn(
1057
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
1058
+ className
1059
+ ),
1060
+ ...props
1061
+ }
1062
+ );
1063
+ SheetFooter.displayName = "SheetFooter";
1064
+ var SheetTitle = React3.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx10(
1065
+ SheetPrimitive.Title,
1066
+ {
1067
+ ref,
1068
+ className: cn("text-lg font-semibold text-foreground", className),
1069
+ ...props
1070
+ }
1071
+ ));
1072
+ SheetTitle.displayName = SheetPrimitive.Title.displayName;
1073
+ var SheetDescription = React3.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx10(
1074
+ SheetPrimitive.Description,
1075
+ {
1076
+ ref,
1077
+ className: cn("text-sm text-muted-foreground", className),
1078
+ ...props
1079
+ }
1080
+ ));
1081
+ SheetDescription.displayName = SheetPrimitive.Description.displayName;
1082
+
1083
+ // src/client/components/drive/file/details.tsx
1084
+ import { Copy, FileText as FileText2, Calendar, HardDrive, Hash, Film, Image as ImageIcon } from "lucide-react";
1085
+
1086
+ // src/client/components/ui/separator.tsx
1087
+ import * as SeparatorPrimitive from "@radix-ui/react-separator";
1088
+ import { jsx as jsx11 } from "react/jsx-runtime";
1089
+ function Separator2({
1090
+ className,
1091
+ orientation = "horizontal",
1092
+ decorative = true,
1093
+ ...props
1094
+ }) {
1095
+ return /* @__PURE__ */ jsx11(
1096
+ SeparatorPrimitive.Root,
1097
+ {
1098
+ "data-slot": "separator",
1099
+ decorative,
1100
+ orientation,
1101
+ className: cn(
1102
+ "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
1103
+ className
1104
+ ),
1105
+ ...props
1106
+ }
1107
+ );
1108
+ }
1109
+
1110
+ // src/client/components/drive/file/details.tsx
1111
+ import { Fragment, jsx as jsx12, jsxs as jsxs5 } from "react/jsx-runtime";
1112
+ var DriveFileDetails = (props) => {
1113
+ const { item, isOpen, onClose, apiEndpoint } = props;
1114
+ if (!item) return null;
1115
+ const isFile = item.information.type === "FILE";
1116
+ const isFolder = item.information.type === "FOLDER";
1117
+ const tokenParam = item.token ? `&token=${item.token}` : "";
1118
+ const thumbnailUrl = `${apiEndpoint}?action=thumbnail&id=${item.id}&size=medium${tokenParam}`;
1119
+ const fileUrl = `${apiEndpoint}?action=serve&id=${item.id}${tokenParam}`;
1120
+ const DetailItem = ({ icon: Icon, label, value }) => /* @__PURE__ */ jsxs5("div", { className: "flex items-start gap-3 py-3", children: [
1121
+ /* @__PURE__ */ jsx12(Icon, { className: "size-4 text-muted-foreground mt-0.5" }),
1122
+ /* @__PURE__ */ jsxs5("div", { className: "flex-1 space-y-0.5", children: [
1123
+ /* @__PURE__ */ jsx12("p", { className: "text-xs font-medium text-muted-foreground", children: label }),
1124
+ /* @__PURE__ */ jsx12("div", { className: "text-sm text-foreground break-all", children: value })
1125
+ ] })
1126
+ ] });
1127
+ return /* @__PURE__ */ jsx12(Sheet, { open: isOpen, onOpenChange: onClose, children: /* @__PURE__ */ jsxs5(SheetContent, { className: "w-[90%] sm:max-w-md overflow-y-auto", children: [
1128
+ /* @__PURE__ */ jsxs5(SheetHeader, { className: "pb-4", children: [
1129
+ /* @__PURE__ */ jsx12(SheetTitle, { children: "Details" }),
1130
+ /* @__PURE__ */ jsx12(SheetDescription, { children: "View information about this item." })
1131
+ ] }),
1132
+ /* @__PURE__ */ jsxs5("div", { className: "space-y-6", children: [
1133
+ /* @__PURE__ */ jsxs5("div", { className: "flex flex-col items-center p-6 bg-muted/20 rounded-lg border", children: [
1134
+ item.information.type === "FILE" && (item.information.mime.startsWith("image/") || item.information.mime.startsWith("video/")) ? /* @__PURE__ */ jsx12("div", { className: "aspect-video w-full rounded-md overflow-hidden bg-background border shadow-sm flex items-center justify-center", children: /* @__PURE__ */ jsx12("img", { src: thumbnailUrl, alt: item.name, className: "object-contain size-full" }) }) : /* @__PURE__ */ jsx12("div", { className: "size-20 bg-muted rounded-full flex items-center justify-center", children: /* @__PURE__ */ jsx12(FileText2, { className: "size-10 text-muted-foreground" }) }),
1135
+ /* @__PURE__ */ jsx12("h3", { className: "mt-4 font-medium text-center break-all", children: item.name }),
1136
+ /* @__PURE__ */ jsx12("p", { className: "text-xs text-muted-foreground mt-1", children: item.information.type === "FOLDER" ? "Folder" : item.information.mime })
1137
+ ] }),
1138
+ /* @__PURE__ */ jsx12(Separator2, {}),
1139
+ /* @__PURE__ */ jsxs5("div", { className: "space-y-1", children: [
1140
+ /* @__PURE__ */ jsx12(DetailItem, { icon: HardDrive, label: "Size", value: item.information.type === "FOLDER" ? "-" : formatBytes(item.information.sizeInBytes) }),
1141
+ /* @__PURE__ */ jsx12(DetailItem, { icon: Calendar, label: "Created", value: new Date(item.createdAt).toLocaleString() }),
1142
+ item.information.type === "FILE" && /* @__PURE__ */ jsxs5(Fragment, { children: [
1143
+ /* @__PURE__ */ jsx12(DetailItem, { icon: FileText2, label: "Type", value: item.information.mime }),
1144
+ item.information.width && /* @__PURE__ */ jsx12(DetailItem, { icon: ImageIcon, label: "Dimensions", value: `${item.information.width} x ${item.information.height}` }),
1145
+ item.information.duration && /* @__PURE__ */ jsx12(DetailItem, { icon: Film, label: "Duration", value: `${Math.round(item.information.duration)}s` }),
1146
+ item.information.hash && /* @__PURE__ */ jsx12(DetailItem, { icon: Hash, label: "Hash (SHA-256)", value: /* @__PURE__ */ jsxs5("span", { className: "font-mono text-xs", children: [
1147
+ item.information.hash.substring(0, 16),
1148
+ "..."
1149
+ ] }) })
1150
+ ] })
1151
+ ] }),
1152
+ /* @__PURE__ */ jsx12(Separator2, {}),
1153
+ /* @__PURE__ */ jsxs5("div", { className: "flex gap-2", children: [
1154
+ item.information.type === "FILE" && /* @__PURE__ */ jsx12(Button, { className: "w-full", variant: "outline", onClick: () => window.open(fileUrl, "_blank"), children: "Download / View" }),
1155
+ /* @__PURE__ */ jsxs5(Button, { className: "w-full", variant: "secondary", onClick: () => {
1156
+ navigator.clipboard.writeText(item.id);
1157
+ }, children: [
1158
+ /* @__PURE__ */ jsx12(Copy, { className: "mr-2 size-3.5" }),
1159
+ " Copy ID"
1160
+ ] })
1161
+ ] })
1162
+ ] })
1163
+ ] }) });
1164
+ };
1165
+
1166
+ // src/client/components/drive/path-bar.tsx
1167
+ import { useDroppable } from "@dnd-kit/core";
1168
+ import { jsx as jsx13, jsxs as jsxs6 } from "react/jsx-runtime";
1169
+ var DroppablePathItem = (props) => {
1170
+ const { id, name, isLast, onClick } = props;
1171
+ const { currentFolderId } = useDrive();
1172
+ const droppableId = `path-${id ?? "root"}`;
1173
+ const { isOver, setNodeRef } = useDroppable({
1174
+ id: droppableId,
1175
+ data: { type: "pathItem", folderId: id }
1176
+ });
1177
+ const isCurrentFolder = id === currentFolderId;
1178
+ if (isLast) {
1179
+ return /* @__PURE__ */ jsx13(
1180
+ "span",
1181
+ {
1182
+ ref: setNodeRef,
1183
+ className: cn(
1184
+ "font-medium text-foreground px-1 text-xs sm:text-sm truncate max-w-30 sm:max-w-none",
1185
+ isOver && !isCurrentFolder && "bg-primary/20 rounded"
1186
+ ),
1187
+ "aria-current": "page",
1188
+ title: name,
1189
+ children: name
1190
+ }
1191
+ );
1192
+ }
1193
+ return /* @__PURE__ */ jsx13(
1194
+ Button,
1195
+ {
1196
+ ref: setNodeRef,
1197
+ variant: "ghost",
1198
+ size: "sm",
1199
+ className: cn(
1200
+ "h-auto font-normal text-xs sm:text-sm px-1.5 sm:px-2 py-0.5 sm:py-1 truncate max-w-25 sm:max-w-37.5",
1201
+ isOver && !isCurrentFolder && "ring-2 ring-primary bg-primary/10 scale-105"
1202
+ ),
1203
+ onClick,
1204
+ type: "button",
1205
+ title: name,
1206
+ children: name
1207
+ }
1208
+ );
1209
+ };
1210
+ var DrivePathBar = () => {
1211
+ const { path, navigateToFolder } = useDrive();
1212
+ return /* @__PURE__ */ jsx13("ol", { className: "flex items-center gap-1 sm:gap-1.5 text-sm text-muted-foreground bg-muted/30 px-2 sm:px-3 py-1.5 sm:py-2 rounded-md border w-full overflow-x-auto flex-nowrap min-w-0", "aria-label": "Breadcrumb", role: "navigation", children: path.map((item, index) => {
1213
+ const isLast = index === path.length - 1;
1214
+ return /* @__PURE__ */ jsxs6("li", { className: "flex items-center gap-1 sm:gap-1.5 shrink-0", children: [
1215
+ index > 0 && /* @__PURE__ */ jsx13("span", { className: "text-muted-foreground/50 text-xs", "aria-hidden": "true", children: "/" }),
1216
+ /* @__PURE__ */ jsx13(
1217
+ DroppablePathItem,
1218
+ {
1219
+ id: item.id,
1220
+ name: item.name,
1221
+ isLast,
1222
+ onClick: () => navigateToFolder(item)
1223
+ }
1224
+ )
1225
+ ] }, item.id ?? "root");
1226
+ }) });
1227
+ };
1228
+
1229
+ // src/client/components/drive/upload.tsx
1230
+ import React4, { useState as useState4, useRef as useRef2, useCallback as useCallback3 } from "react";
1231
+ import { Upload as UploadIcon, X as X3, Loader2, CheckCircle2, AlertCircle as AlertCircle2, Clock } from "lucide-react";
1232
+
1233
+ // src/client/components/ui/progress.tsx
1234
+ import * as ProgressPrimitive from "@radix-ui/react-progress";
1235
+ import { jsx as jsx14 } from "react/jsx-runtime";
1236
+ function Progress({
1237
+ className,
1238
+ value,
1239
+ indicatorClassName,
1240
+ ...props
1241
+ }) {
1242
+ return /* @__PURE__ */ jsx14(
1243
+ ProgressPrimitive.Root,
1244
+ {
1245
+ "data-slot": "progress",
1246
+ className: cn(
1247
+ "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
1248
+ className
1249
+ ),
1250
+ ...props,
1251
+ children: /* @__PURE__ */ jsx14(
1252
+ ProgressPrimitive.Indicator,
1253
+ {
1254
+ "data-slot": "progress-indicator",
1255
+ className: cn("bg-primary h-full w-full flex-1 transition-all", indicatorClassName),
1256
+ style: { transform: `translateX(-${100 - (value || 0)}%)` }
1257
+ }
1258
+ )
1259
+ }
1260
+ );
1261
+ }
1262
+
1263
+ // src/client/components/drive/upload.tsx
1264
+ import { Fragment as Fragment2, jsx as jsx15, jsxs as jsxs7 } from "react/jsx-runtime";
1265
+ var UploadStatusIcon = ({ status }) => {
1266
+ switch (status) {
1267
+ case "complete":
1268
+ return /* @__PURE__ */ jsx15(CheckCircle2, { className: "size-4 text-emerald-500" });
1269
+ case "error":
1270
+ return /* @__PURE__ */ jsx15(AlertCircle2, { className: "size-4 text-destructive" });
1271
+ case "cancelled":
1272
+ return /* @__PURE__ */ jsx15(X3, { className: "size-4 text-muted-foreground" });
1273
+ case "uploading":
1274
+ return /* @__PURE__ */ jsx15(Loader2, { className: "size-4 text-primary animate-spin" });
1275
+ default:
1276
+ return /* @__PURE__ */ jsx15(Clock, { className: "size-4 text-muted-foreground" });
1277
+ }
1278
+ };
1279
+ var DriveUpload = (props) => {
1280
+ const { compact = false, onComplete } = props;
1281
+ const [isDragging, setIsDragging] = useState4(false);
1282
+ const [showUploadsDialog, setShowUploadsDialog] = useState4(false);
1283
+ const inputRef = useRef2(null);
1284
+ const { currentFolderId, refreshItems, apiEndpoint, activeAccountId } = useDrive();
1285
+ const { uploads, uploadFiles, cancelUpload, cancelAllUploads } = useUpload(apiEndpoint, activeAccountId, () => {
1286
+ refreshItems();
1287
+ onComplete?.(null);
1288
+ });
1289
+ React4.useEffect(() => {
1290
+ if (!showUploadsDialog || uploads.length === 0) return;
1291
+ const allFinished = uploads.every(
1292
+ (u) => ["complete", "error", "cancelled"].includes(u.status)
1293
+ );
1294
+ if (allFinished) {
1295
+ const timer = setTimeout(() => {
1296
+ setShowUploadsDialog(false);
1297
+ }, 2e3);
1298
+ return () => clearTimeout(timer);
1299
+ }
1300
+ }, [uploads, showUploadsDialog]);
1301
+ const handleFiles = useCallback3((files) => {
1302
+ if (!files || files.length === 0) return;
1303
+ uploadFiles(Array.from(files), currentFolderId);
1304
+ setShowUploadsDialog(true);
1305
+ }, [uploadFiles, currentFolderId, refreshItems, onComplete]);
1306
+ const handleDrag = useCallback3((e, dragging) => {
1307
+ e.preventDefault();
1308
+ e.stopPropagation();
1309
+ setIsDragging(dragging);
1310
+ }, []);
1311
+ const handleDrop = useCallback3((e) => {
1312
+ e.preventDefault();
1313
+ e.stopPropagation();
1314
+ setIsDragging(false);
1315
+ handleFiles(e.dataTransfer.files);
1316
+ }, [handleFiles]);
1317
+ const hasUploadsInProgress = uploads.some((u) => ["uploading", "queued", "pending"].includes(u.status));
1318
+ const activeUploads = uploads.filter((u) => ["uploading", "queued", "pending"].includes(u.status));
1319
+ const renderDialog = () => /* @__PURE__ */ jsx15(Dialog, { open: showUploadsDialog, onOpenChange: setShowUploadsDialog, children: /* @__PURE__ */ jsxs7(DialogContent, { className: "sm:max-w-md p-0 gap-0", children: [
1320
+ /* @__PURE__ */ jsxs7(DialogHeader, { className: "px-4 py-3 border-b flex-row items-center justify-between space-y-0", children: [
1321
+ /* @__PURE__ */ jsx15(DialogTitle, { className: "text-base", children: "Upload Status" }),
1322
+ hasUploadsInProgress && /* @__PURE__ */ jsx15(
1323
+ Button,
1324
+ {
1325
+ type: "button",
1326
+ size: "sm",
1327
+ variant: "ghost",
1328
+ className: "text-destructive hover:text-destructive",
1329
+ onClick: cancelAllUploads,
1330
+ children: "Cancel All"
1331
+ }
1332
+ )
1333
+ ] }),
1334
+ /* @__PURE__ */ jsxs7("div", { className: "divide-y max-h-80 overflow-y-auto", children: [
1335
+ uploads.length === 0 && /* @__PURE__ */ jsx15("div", { className: "p-4 text-center text-sm text-muted-foreground", children: "No uploads" }),
1336
+ uploads.map((upload) => {
1337
+ const percent = upload.status === "complete" ? 100 : upload.status === "error" || !upload.totalChunks ? 0 : Math.round(upload.currentChunk / upload.totalChunks * 100);
1338
+ return /* @__PURE__ */ jsxs7("div", { className: "px-4 py-2.5", children: [
1339
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-start gap-2 mb-1.5", children: [
1340
+ /* @__PURE__ */ jsx15(UploadStatusIcon, { status: upload.status }),
1341
+ /* @__PURE__ */ jsxs7("div", { className: "flex-1 min-w-0", children: [
1342
+ /* @__PURE__ */ jsx15("p", { className: "text-sm font-medium truncate", children: upload.name }),
1343
+ /* @__PURE__ */ jsxs7("p", { className: cn(
1344
+ "text-xs",
1345
+ upload.status === "error" ? "text-destructive" : "text-muted-foreground"
1346
+ ), children: [
1347
+ upload.status === "uploading" && "Uploading...",
1348
+ upload.status === "queued" && "Waiting in queue",
1349
+ upload.status === "pending" && "Preparing...",
1350
+ upload.status === "complete" && "Upload complete",
1351
+ upload.status === "error" && (upload.error || "Upload failed"),
1352
+ upload.status === "cancelled" && "Upload cancelled"
1353
+ ] })
1354
+ ] }),
1355
+ ["uploading", "queued", "pending"].includes(upload.status) && /* @__PURE__ */ jsx15(
1356
+ Button,
1357
+ {
1358
+ type: "button",
1359
+ size: "icon",
1360
+ variant: "ghost",
1361
+ className: "shrink-0 text-muted-foreground hover:text-destructive",
1362
+ onClick: () => cancelUpload(upload.id),
1363
+ children: /* @__PURE__ */ jsx15(X3, { className: "size-3.5" })
1364
+ }
1365
+ )
1366
+ ] }),
1367
+ upload.status === "uploading" && /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2 pl-6", children: [
1368
+ /* @__PURE__ */ jsx15(Progress, { value: percent, className: "flex-1" }),
1369
+ /* @__PURE__ */ jsxs7("span", { className: "text-xs tabular-nums text-muted-foreground w-8", children: [
1370
+ percent,
1371
+ "%"
1372
+ ] })
1373
+ ] })
1374
+ ] }, upload.id);
1375
+ })
1376
+ ] })
1377
+ ] }) });
1378
+ if (compact) {
1379
+ return /* @__PURE__ */ jsxs7(Fragment2, { children: [
1380
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
1381
+ /* @__PURE__ */ jsx15("input", { ref: inputRef, type: "file", multiple: true, onChange: (e) => {
1382
+ handleFiles(e.target.files);
1383
+ e.target.value = "";
1384
+ }, className: "hidden", "aria-hidden": "true" }),
1385
+ /* @__PURE__ */ jsxs7(
1386
+ Button,
1387
+ {
1388
+ onClick: () => inputRef.current?.click(),
1389
+ type: "button",
1390
+ size: "sm",
1391
+ disabled: hasUploadsInProgress,
1392
+ children: [
1393
+ /* @__PURE__ */ jsx15(UploadIcon, { className: "size-4 mr-1.5" }),
1394
+ " Upload Files"
1395
+ ]
1396
+ }
1397
+ ),
1398
+ uploads.length > 0 && /* @__PURE__ */ jsxs7(
1399
+ Button,
1400
+ {
1401
+ type: "button",
1402
+ variant: "outline",
1403
+ size: "sm",
1404
+ onClick: () => setShowUploadsDialog(true),
1405
+ children: [
1406
+ activeUploads.length > 0 && /* @__PURE__ */ jsx15(Loader2, { className: "size-3.5 mr-1.5 animate-spin" }),
1407
+ activeUploads.length > 0 ? `Uploading (${activeUploads.length})` : "Upload Status"
1408
+ ]
1409
+ }
1410
+ )
1411
+ ] }),
1412
+ renderDialog()
1413
+ ] });
1414
+ }
1415
+ return /* @__PURE__ */ jsxs7("div", { className: "w-full", children: [
1416
+ /* @__PURE__ */ jsxs7(
1417
+ "div",
1418
+ {
1419
+ className: cn(
1420
+ "flex flex-col items-center justify-center p-8 border-2 border-dashed rounded-lg cursor-pointer transition-colors",
1421
+ isDragging ? "border-primary bg-primary/5" : "border-muted-foreground/25 hover:border-primary/50 hover:bg-muted/50"
1422
+ ),
1423
+ onDragEnter: (e) => handleDrag(e, true),
1424
+ onDragLeave: (e) => handleDrag(e, false),
1425
+ onDragOver: (e) => handleDrag(e, true),
1426
+ onDrop: handleDrop,
1427
+ onClick: () => inputRef.current?.click(),
1428
+ role: "button",
1429
+ tabIndex: 0,
1430
+ onKeyDown: (e) => e.key === "Enter" && inputRef.current?.click(),
1431
+ children: [
1432
+ /* @__PURE__ */ jsx15("input", { ref: inputRef, type: "file", multiple: true, onChange: (e) => {
1433
+ handleFiles(e.target.files);
1434
+ e.target.value = "";
1435
+ }, className: "hidden", "aria-hidden": "true" }),
1436
+ /* @__PURE__ */ jsxs7("div", { className: "flex flex-col items-center gap-2 text-center", children: [
1437
+ /* @__PURE__ */ jsx15("div", { className: "p-3 rounded-full bg-background border shadow-sm", children: /* @__PURE__ */ jsx15(UploadIcon, { className: "size-6 text-muted-foreground" }) }),
1438
+ /* @__PURE__ */ jsx15("div", { className: "text-sm font-medium text-foreground", children: isDragging ? "Drop files here" : "Click or drag files to upload" })
1439
+ ] })
1440
+ ]
1441
+ }
1442
+ ),
1443
+ hasUploadsInProgress && /* @__PURE__ */ jsx15("div", { className: "mt-4 text-center", children: /* @__PURE__ */ jsx15(Button, { variant: "link", onClick: () => setShowUploadsDialog(true), children: "View Upload Progress" }) }),
1444
+ renderDialog()
1445
+ ] });
1446
+ };
1447
+
1448
+ // src/client/components/drive/sidebar.tsx
1449
+ import {
1450
+ Database,
1451
+ HardDrive as HardDrive2,
1452
+ Plus,
1453
+ LogOut,
1454
+ Check as Check3,
1455
+ ChevronsUpDown,
1456
+ FolderOpen,
1457
+ Trash2,
1458
+ Menu
1459
+ } from "lucide-react";
1460
+
1461
+ // src/client/components/ui/dropdown-menu.tsx
1462
+ import * as React5 from "react";
1463
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
1464
+ import { Check as Check2, ChevronRight as ChevronRight2, Circle as Circle2 } from "lucide-react";
1465
+ import { jsx as jsx16, jsxs as jsxs8 } from "react/jsx-runtime";
1466
+ var DropdownMenu = DropdownMenuPrimitive.Root;
1467
+ var DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
1468
+ var DropdownMenuSub = DropdownMenuPrimitive.Sub;
1469
+ var DropdownMenuSubTrigger = React5.forwardRef(({ className, inset, children, ...props }, ref) => /* @__PURE__ */ jsxs8(
1470
+ DropdownMenuPrimitive.SubTrigger,
1471
+ {
1472
+ ref,
1473
+ className: cn(
1474
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
1475
+ inset && "pl-8",
1476
+ className
1477
+ ),
1478
+ ...props,
1479
+ children: [
1480
+ children,
1481
+ /* @__PURE__ */ jsx16(ChevronRight2, { className: "ml-auto h-4 w-4" })
1482
+ ]
1483
+ }
1484
+ ));
1485
+ DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
1486
+ var DropdownMenuSubContent = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx16(
1487
+ DropdownMenuPrimitive.SubContent,
1488
+ {
1489
+ ref,
1490
+ className: cn(
1491
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
1492
+ className
1493
+ ),
1494
+ ...props
1495
+ }
1496
+ ));
1497
+ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
1498
+ var DropdownMenuContent = React5.forwardRef(({ className, sideOffset = 4, ...props }, ref) => /* @__PURE__ */ jsx16(DropdownMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx16(
1499
+ DropdownMenuPrimitive.Content,
1500
+ {
1501
+ ref,
1502
+ sideOffset,
1503
+ className: cn(
1504
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
1505
+ className
1506
+ ),
1507
+ ...props
1508
+ }
1509
+ ) }));
1510
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
1511
+ var DropdownMenuItem = React5.forwardRef(({ className, inset, ...props }, ref) => /* @__PURE__ */ jsx16(
1512
+ DropdownMenuPrimitive.Item,
1513
+ {
1514
+ ref,
1515
+ className: cn(
1516
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
1517
+ inset && "pl-8",
1518
+ className
1519
+ ),
1520
+ ...props
1521
+ }
1522
+ ));
1523
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
1524
+ var DropdownMenuCheckboxItem = React5.forwardRef(({ className, children, checked, ...props }, ref) => /* @__PURE__ */ jsxs8(
1525
+ DropdownMenuPrimitive.CheckboxItem,
1526
+ {
1527
+ ref,
1528
+ className: cn(
1529
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
1530
+ className
1531
+ ),
1532
+ checked,
1533
+ ...props,
1534
+ children: [
1535
+ /* @__PURE__ */ jsx16("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ jsx16(DropdownMenuPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx16(Check2, { className: "h-4 w-4" }) }) }),
1536
+ children
1537
+ ]
1538
+ }
1539
+ ));
1540
+ DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
1541
+ var DropdownMenuRadioItem = React5.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs8(
1542
+ DropdownMenuPrimitive.RadioItem,
1543
+ {
1544
+ ref,
1545
+ className: cn(
1546
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
1547
+ className
1548
+ ),
1549
+ ...props,
1550
+ children: [
1551
+ /* @__PURE__ */ jsx16("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ jsx16(DropdownMenuPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx16(Circle2, { className: "h-2 w-2 fill-current" }) }) }),
1552
+ children
1553
+ ]
1554
+ }
1555
+ ));
1556
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
1557
+ var DropdownMenuLabel = React5.forwardRef(({ className, inset, ...props }, ref) => /* @__PURE__ */ jsx16(
1558
+ DropdownMenuPrimitive.Label,
1559
+ {
1560
+ ref,
1561
+ className: cn(
1562
+ "px-2 py-1.5 text-sm font-semibold",
1563
+ inset && "pl-8",
1564
+ className
1565
+ ),
1566
+ ...props
1567
+ }
1568
+ ));
1569
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
1570
+ var DropdownMenuSeparator = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx16(
1571
+ DropdownMenuPrimitive.Separator,
1572
+ {
1573
+ ref,
1574
+ className: cn("-mx-1 my-1 h-px bg-muted", className),
1575
+ ...props
1576
+ }
1577
+ ));
1578
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
1579
+ var DropdownMenuShortcut = ({
1580
+ className,
1581
+ ...props
1582
+ }) => {
1583
+ return /* @__PURE__ */ jsx16(
1584
+ "span",
1585
+ {
1586
+ className: cn("ml-auto text-xs tracking-widest opacity-60", className),
1587
+ ...props
1588
+ }
1589
+ );
1590
+ };
1591
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
1592
+
1593
+ // src/client/components/drive/storage/indicator.tsx
1594
+ import { useEffect as useEffect3 } from "react";
1595
+ import { Cloud, AlertCircle as AlertCircle3 } from "lucide-react";
1596
+ import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs9 } from "react/jsx-runtime";
1597
+ var DriveStorageIndicator = (props) => {
1598
+ const { compact = false, className } = props;
1599
+ const { quota, refreshQuota } = useDrive();
1600
+ useEffect3(() => {
1601
+ refreshQuota();
1602
+ }, []);
1603
+ if (!quota) return null;
1604
+ const { usedInBytes, totalInBytes, percentage } = quota;
1605
+ const isNearFull = percentage >= 90;
1606
+ const isFull = percentage >= 100;
1607
+ const stateColor = isFull ? "text-destructive" : isNearFull ? "text-yellow-600 dark:text-yellow-500" : "text-primary";
1608
+ const solidColor = isFull ? "bg-destructive" : isNearFull ? "bg-yellow-500" : "bg-primary";
1609
+ if (compact) {
1610
+ return /* @__PURE__ */ jsxs9("div", { className: cn("flex items-center gap-3 text-xs font-medium text-muted-foreground", className), children: [
1611
+ /* @__PURE__ */ jsxs9("span", { className: "shrink-0 flex items-center gap-1.5", children: [
1612
+ /* @__PURE__ */ jsx17(Cloud, { className: "size-3.5" }),
1613
+ "Storage"
1614
+ ] }),
1615
+ /* @__PURE__ */ jsx17(Progress, { value: percentage, indicatorClassName: cn("bg-gradient-to-r from-blue-500 to-cyan-500", isNearFull && "from-yellow-500 to-orange-500", isFull && "bg-destructive"), className: "w-24 sm:w-32" }),
1616
+ /* @__PURE__ */ jsxs9("span", { className: "shrink-0 whitespace-nowrap", children: [
1617
+ formatBytes(usedInBytes),
1618
+ " / ",
1619
+ formatBytes(totalInBytes)
1620
+ ] })
1621
+ ] });
1622
+ }
1623
+ return /* @__PURE__ */ jsxs9(Fragment3, { children: [
1624
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between gap-2 mb-2", children: [
1625
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2", children: [
1626
+ /* @__PURE__ */ jsx17(Cloud, { className: cn("size-4", stateColor) }),
1627
+ /* @__PURE__ */ jsx17("span", { className: "text-xs font-medium text-muted-foreground", children: "Storage" })
1628
+ ] }),
1629
+ /* @__PURE__ */ jsxs9("span", { className: "text-xs font-semibold tabular-nums", children: [
1630
+ percentage,
1631
+ "%"
1632
+ ] })
1633
+ ] }),
1634
+ /* @__PURE__ */ jsx17(
1635
+ Progress,
1636
+ {
1637
+ value: percentage,
1638
+ className: "h-1.5",
1639
+ indicatorClassName: cn(
1640
+ "transition-all duration-500",
1641
+ isFull ? "bg-destructive" : isNearFull ? "bg-yellow-500" : "bg-gradient-to-r from-blue-500 to-purple-500"
1642
+ )
1643
+ }
1644
+ ),
1645
+ /* @__PURE__ */ jsxs9("div", { className: "flex justify-between items-center mt-1.5", children: [
1646
+ /* @__PURE__ */ jsx17("span", { className: "text-[10px] text-muted-foreground tabular-nums", children: formatBytes(usedInBytes) }),
1647
+ /* @__PURE__ */ jsx17("span", { className: "text-[10px] text-muted-foreground tabular-nums", children: formatBytes(totalInBytes) })
1648
+ ] }),
1649
+ isNearFull && /* @__PURE__ */ jsxs9("div", { className: cn(
1650
+ "flex items-center gap-1.5 text-[10px] font-medium px-2 py-1 rounded-md mt-2",
1651
+ isFull ? "bg-destructive/10 text-destructive" : "bg-yellow-500/10 text-yellow-600 dark:text-yellow-500"
1652
+ ), children: [
1653
+ /* @__PURE__ */ jsx17(AlertCircle3, { className: "size-3 shrink-0" }),
1654
+ /* @__PURE__ */ jsx17("span", { children: isFull ? "Storage full" : "Almost full" })
1655
+ ] })
1656
+ ] });
1657
+ };
1658
+
1659
+ // src/client/components/drive/sidebar.tsx
1660
+ import { Fragment as Fragment4, jsx as jsx18, jsxs as jsxs10 } from "react/jsx-runtime";
1661
+ var SidebarContent = () => {
1662
+ const {
1663
+ accounts,
1664
+ activeAccountId,
1665
+ setActiveAccountId,
1666
+ callAPI,
1667
+ refreshAccounts,
1668
+ currentView,
1669
+ setCurrentView
1670
+ } = useDrive();
1671
+ const currentAccountName = activeAccountId ? accounts.find((a) => a.id === activeAccountId)?.name || "Unknown Account" : "Local Storage";
1672
+ const currentAccountEmail = activeAccountId ? accounts.find((a) => a.id === activeAccountId)?.email : "On this device";
1673
+ return /* @__PURE__ */ jsxs10("div", { className: "w-full h-full flex flex-col bg-muted/10 border-r", children: [
1674
+ /* @__PURE__ */ jsx18("div", { className: "p-3 border-b", children: /* @__PURE__ */ jsxs10(DropdownMenu, { children: [
1675
+ /* @__PURE__ */ jsx18(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs10(Button, { variant: "ghost", className: "w-full justify-between px-2 h-auto min-h-12 py-2 hover:bg-muted/50", children: [
1676
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2 sm:gap-3 text-left min-w-0 flex-1", children: [
1677
+ /* @__PURE__ */ jsx18("div", { className: "size-8 sm:size-9 rounded-md bg-primary/10 flex items-center justify-center shrink-0", children: activeAccountId ? /* @__PURE__ */ jsx18(Database, { className: "size-4" }) : /* @__PURE__ */ jsx18(HardDrive2, { className: "size-4" }) }),
1678
+ /* @__PURE__ */ jsxs10("div", { className: "flex flex-col truncate min-w-0", children: [
1679
+ /* @__PURE__ */ jsx18("span", { className: "text-sm font-semibold truncate", children: currentAccountName }),
1680
+ /* @__PURE__ */ jsx18("span", { className: "text-xs text-muted-foreground truncate font-normal", children: currentAccountEmail })
1681
+ ] })
1682
+ ] }),
1683
+ /* @__PURE__ */ jsx18(ChevronsUpDown, { className: "size-4 text-muted-foreground shrink-0 opacity-50 ml-1" })
1684
+ ] }) }),
1685
+ /* @__PURE__ */ jsxs10(DropdownMenuContent, { className: "w-60", align: "start", children: [
1686
+ /* @__PURE__ */ jsx18(DropdownMenuLabel, { className: "text-xs font-normal text-muted-foreground", children: "Switch Account" }),
1687
+ /* @__PURE__ */ jsxs10(DropdownMenuItem, { onClick: () => setActiveAccountId(null), className: "gap-2", children: [
1688
+ /* @__PURE__ */ jsx18("div", { className: "flex items-center justify-center size-6 rounded bg-muted", children: /* @__PURE__ */ jsx18(HardDrive2, { className: "size-3.5" }) }),
1689
+ /* @__PURE__ */ jsx18("div", { className: "flex flex-col flex-1", children: /* @__PURE__ */ jsx18("span", { className: "text-sm font-medium", children: "Local Storage" }) }),
1690
+ activeAccountId === null && /* @__PURE__ */ jsx18(Check3, { className: "size-3.5 text-primary" })
1691
+ ] }),
1692
+ accounts.length > 0 && /* @__PURE__ */ jsx18(DropdownMenuSeparator, {}),
1693
+ accounts.map((account) => /* @__PURE__ */ jsxs10(DropdownMenuItem, { onClick: () => setActiveAccountId(account.id), className: "gap-2 group", children: [
1694
+ /* @__PURE__ */ jsx18("div", { className: "flex items-center justify-center size-6 rounded bg-muted", children: /* @__PURE__ */ jsx18(Database, { className: "size-3.5" }) }),
1695
+ /* @__PURE__ */ jsxs10("div", { className: "flex flex-col flex-1 overflow-hidden", children: [
1696
+ /* @__PURE__ */ jsx18("span", { className: "text-sm font-medium truncate", children: account.name }),
1697
+ /* @__PURE__ */ jsx18("span", { className: "text-xs text-muted-foreground truncate", children: account.email })
1698
+ ] }),
1699
+ activeAccountId === account.id ? /* @__PURE__ */ jsx18(Check3, { className: "size-3.5 text-primary" }) : /* @__PURE__ */ jsx18(
1700
+ LogOut,
1701
+ {
1702
+ className: "size-3.5 text-destructive transition-opacity hover:bg-destructive/10 rounded-sm",
1703
+ onClick: async (e) => {
1704
+ e.stopPropagation();
1705
+ if (confirm("Are you sure you want to disconnect this account? Synced files will be deleted from local cache.")) {
1706
+ await callAPI("removeAccount", { query: { id: account.id } });
1707
+ await refreshAccounts();
1708
+ if (activeAccountId === account.id) setActiveAccountId(null);
1709
+ }
1710
+ }
1711
+ }
1712
+ )
1713
+ ] }, account.id)),
1714
+ /* @__PURE__ */ jsx18(DropdownMenuSeparator, {}),
1715
+ /* @__PURE__ */ jsxs10(DropdownMenuSub, { children: [
1716
+ /* @__PURE__ */ jsxs10(DropdownMenuSubTrigger, { className: "gap-2 text-primary focus:text-primary", children: [
1717
+ /* @__PURE__ */ jsx18(Plus, { className: "size-4" }),
1718
+ /* @__PURE__ */ jsx18("span", { className: "font-medium", children: "Add Storage Account" })
1719
+ ] }),
1720
+ /* @__PURE__ */ jsx18(DropdownMenuSubContent, { children: /* @__PURE__ */ jsx18(DropdownMenuItem, { onClick: async () => {
1721
+ const res = await callAPI("getAuthUrl", { query: { provider: "GOOGLE" } });
1722
+ if (res.status !== 200 || !res.data?.url) {
1723
+ alert(res.message || "Failed to initialize account connection");
1724
+ return;
1725
+ }
1726
+ if (res.status === 200 && res.data?.url) {
1727
+ const width = 600;
1728
+ const height = 600;
1729
+ const left = window.screen.width / 2 - width / 2;
1730
+ const top = window.screen.height / 2 - height / 2;
1731
+ window.open(
1732
+ res.data.url,
1733
+ "Connect to Google Drive",
1734
+ `width=${width},height=${height},top=${top},left=${left}`
1735
+ );
1736
+ const listener = (event) => {
1737
+ if (event.data === "oauth-success") {
1738
+ refreshAccounts();
1739
+ window.removeEventListener("message", listener);
1740
+ }
1741
+ };
1742
+ window.addEventListener("message", listener);
1743
+ }
1744
+ }, children: "Google Drive" }) })
1745
+ ] })
1746
+ ] })
1747
+ ] }) }),
1748
+ /* @__PURE__ */ jsxs10("div", { className: "flex-1 px-3 py-2 space-y-1", children: [
1749
+ /* @__PURE__ */ jsxs10(
1750
+ Button,
1751
+ {
1752
+ variant: currentView !== "TRASH" ? "secondary" : "ghost",
1753
+ className: cn("w-full justify-start gap-3", currentView !== "TRASH" && "bg-primary/10 text-primary hover:bg-primary/15"),
1754
+ onClick: () => setCurrentView("BROWSE"),
1755
+ children: [
1756
+ /* @__PURE__ */ jsx18(FolderOpen, { className: "size-4" }),
1757
+ "My Files"
1758
+ ]
1759
+ }
1760
+ ),
1761
+ /* @__PURE__ */ jsxs10(
1762
+ Button,
1763
+ {
1764
+ variant: currentView === "TRASH" ? "secondary" : "ghost",
1765
+ className: cn("w-full justify-start gap-3", currentView === "TRASH" && "bg-destructive/10 text-destructive hover:bg-destructive/15"),
1766
+ onClick: () => setCurrentView("TRASH"),
1767
+ children: [
1768
+ /* @__PURE__ */ jsx18(Trash2, { className: "size-4" }),
1769
+ "Trash"
1770
+ ]
1771
+ }
1772
+ )
1773
+ ] }),
1774
+ /* @__PURE__ */ jsx18("div", { className: "px-3 py-2.5 mt-auto border-t bg-background/50", children: /* @__PURE__ */ jsx18(DriveStorageIndicator, {}) })
1775
+ ] });
1776
+ };
1777
+ var DriveSidebar = () => {
1778
+ return /* @__PURE__ */ jsxs10(Fragment4, { children: [
1779
+ /* @__PURE__ */ jsx18("div", { className: "lg:hidden", children: /* @__PURE__ */ jsxs10(Sheet, { children: [
1780
+ /* @__PURE__ */ jsx18(SheetTrigger, { asChild: true, children: /* @__PURE__ */ jsx18(
1781
+ Button,
1782
+ {
1783
+ variant: "ghost",
1784
+ size: "icon",
1785
+ className: "h-9 w-9",
1786
+ "aria-label": "Open menu",
1787
+ children: /* @__PURE__ */ jsx18(Menu, { className: "h-5 w-5" })
1788
+ }
1789
+ ) }),
1790
+ /* @__PURE__ */ jsx18(SheetContent, { side: "left", className: "w-70 sm:w-80 p-0", children: /* @__PURE__ */ jsx18(SidebarContent, {}) })
1791
+ ] }) }),
1792
+ /* @__PURE__ */ jsx18("div", { className: "hidden lg:flex w-full h-full", children: /* @__PURE__ */ jsx18(SidebarContent, {}) })
1793
+ ] });
1794
+ };
1795
+
1796
+ // src/client/components/drive/explorer.tsx
1797
+ import {
1798
+ DndContext,
1799
+ closestCenter,
1800
+ KeyboardSensor,
1801
+ PointerSensor,
1802
+ useSensor,
1803
+ useSensors
1804
+ } from "@dnd-kit/core";
1805
+ import {
1806
+ arrayMove,
1807
+ SortableContext,
1808
+ sortableKeyboardCoordinates,
1809
+ rectSortingStrategy,
1810
+ useSortable
1811
+ } from "@dnd-kit/sortable";
1812
+ import { CSS } from "@dnd-kit/utilities";
1813
+ import { Fragment as Fragment5, jsx as jsx19, jsxs as jsxs11 } from "react/jsx-runtime";
1814
+ var SortableItem = ({ id, children, disabled, isDragOverTarget }) => {
1815
+ const {
1816
+ attributes,
1817
+ listeners,
1818
+ setNodeRef,
1819
+ transform,
1820
+ transition,
1821
+ isDragging
1822
+ } = useSortable({ id, disabled });
1823
+ const style = {
1824
+ transform: isDragOverTarget ? void 0 : CSS.Transform.toString(transform),
1825
+ transition: isDragOverTarget ? "transform 0.15s ease" : transition,
1826
+ opacity: isDragging ? 0.5 : 1,
1827
+ zIndex: isDragging ? 50 : "auto",
1828
+ position: "relative"
1829
+ };
1830
+ return /* @__PURE__ */ jsx19("div", { ref: setNodeRef, style, ...attributes, ...listeners, children });
1831
+ };
1832
+ var DriveExplorer = (props) => {
1833
+ const { onItemClick, onItemDoubleClick, mimeFilter, className, selectableFolders = false } = props;
1834
+ const {
1835
+ items,
1836
+ isLoading,
1837
+ error,
1838
+ apiEndpoint,
1839
+ currentFolderId,
1840
+ setItems,
1841
+ viewMode,
1842
+ groupBy,
1843
+ sortBy,
1844
+ setSortBy,
1845
+ selectedFileIds,
1846
+ setSelectedFileIds,
1847
+ selectionMode,
1848
+ navigateToFolder,
1849
+ hasMore,
1850
+ loadMore,
1851
+ isLoadingMore,
1852
+ refreshItems,
1853
+ callAPI,
1854
+ currentView,
1855
+ moveItem,
1856
+ deleteItems
1857
+ } = useDrive();
1858
+ const [dialogs, setDialogs] = React7.useState({
1859
+ newFolder: false,
1860
+ rename: false,
1861
+ delete: false
1862
+ });
1863
+ const [itemToDelete, setItemToDelete] = React7.useState(null);
1864
+ const [renameItem, setRenameItem] = React7.useState(null);
1865
+ const [detailsItem, setDetailsItem] = React7.useState(null);
1866
+ const [dragOverFolderId, setDragOverFolderId] = React7.useState(null);
1867
+ const [draggingItemId, setDraggingItemId] = React7.useState(null);
1868
+ const sensors = useSensors(
1869
+ useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
1870
+ // Prevent drag on simple click
1871
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
1872
+ );
1873
+ const handleDragStart = (event) => {
1874
+ setDraggingItemId(event.active.id);
1875
+ };
1876
+ const handleDragOver = (event) => {
1877
+ const { over } = event;
1878
+ if (!over) {
1879
+ setDragOverFolderId(null);
1880
+ return;
1881
+ }
1882
+ const overItem = items.find((i) => i.id === over.id);
1883
+ if (overItem?.information.type === "FOLDER" && over.id !== draggingItemId) {
1884
+ setDragOverFolderId(over.id);
1885
+ } else {
1886
+ setDragOverFolderId(null);
1887
+ }
1888
+ };
1889
+ const handleDragEnd = async (event) => {
1890
+ const { active, over } = event;
1891
+ setDragOverFolderId(null);
1892
+ setDraggingItemId(null);
1893
+ if (!over || active.id === over.id) {
1894
+ return;
1895
+ }
1896
+ const overId = over.id;
1897
+ if (overId.startsWith("path-")) {
1898
+ const targetFolderId = overId.replace("path-", "");
1899
+ await moveItem(active.id, targetFolderId === "root" ? "root" : targetFolderId);
1900
+ return;
1901
+ }
1902
+ const overItem = items.find((i) => i.id === over.id);
1903
+ const activeItem = items.find((i) => i.id === active.id);
1904
+ if (overItem?.information.type === "FOLDER" && activeItem) {
1905
+ await moveItem(active.id, over.id);
1906
+ return;
1907
+ }
1908
+ setItems((prevItems) => {
1909
+ const oldIndex2 = prevItems.findIndex((i) => i.id === active.id);
1910
+ const newIndex2 = prevItems.findIndex((i) => i.id === over.id);
1911
+ return arrayMove(prevItems, oldIndex2, newIndex2);
1912
+ });
1913
+ if (sortBy.field !== "order") {
1914
+ setSortBy({ field: "order", order: 1 });
1915
+ }
1916
+ const oldIndex = items.findIndex((i) => i.id === active.id);
1917
+ const newIndex = items.findIndex((i) => i.id === over.id);
1918
+ const newOrderIds = arrayMove(items, oldIndex, newIndex).map((i) => i.id);
1919
+ await callAPI("reorder", {
1920
+ method: "POST",
1921
+ body: JSON.stringify({ ids: newOrderIds })
1922
+ });
1923
+ };
1924
+ const processedItems = useMemo2(() => {
1925
+ let filtered = items;
1926
+ if (mimeFilter) {
1927
+ filtered = filtered.filter((item) => matchesMimeFilter(item.information.type === "FILE" ? item.information.mime : "", item.information.type === "FOLDER", mimeFilter));
1928
+ }
1929
+ return [...filtered].sort((a, b) => {
1930
+ if (a.information.type === "FOLDER" && b.information.type !== "FOLDER") return -1;
1931
+ if (a.information.type !== "FOLDER" && b.information.type === "FOLDER") return 1;
1932
+ const field = sortBy.field;
1933
+ const order = sortBy.order;
1934
+ if (field === "order") {
1935
+ return 0;
1936
+ }
1937
+ let valA, valB;
1938
+ if (field === "name") {
1939
+ valA = a.name.toLowerCase();
1940
+ valB = b.name.toLowerCase();
1941
+ } else if (field === "size") {
1942
+ valA = a.information.type === "FILE" ? a.information.sizeInBytes : 0;
1943
+ valB = b.information.type === "FILE" ? b.information.sizeInBytes : 0;
1944
+ } else {
1945
+ valA = new Date(a.createdAt).getTime();
1946
+ valB = new Date(b.createdAt).getTime();
1947
+ }
1948
+ if (valA < valB) return -1 * order;
1949
+ if (valA > valB) return 1 * order;
1950
+ return 0;
1951
+ });
1952
+ }, [items, mimeFilter, sortBy]);
1953
+ const groupedItems = useMemo2(() => {
1954
+ if (groupBy === "NONE") return { "All": processedItems };
1955
+ const groups = {
1956
+ "Today": [],
1957
+ "Yesterday": [],
1958
+ "Earlier this Week": [],
1959
+ "Last Week": [],
1960
+ "Older": []
1961
+ };
1962
+ const now = /* @__PURE__ */ new Date();
1963
+ const startOfCurrentWeek = startOfWeek(now);
1964
+ const startOfLastWeek = startOfWeek(subWeeks(now, 1));
1965
+ processedItems.forEach((item) => {
1966
+ const date = new Date(item.createdAt);
1967
+ if (isToday(date)) groups["Today"].push(item);
1968
+ else if (isYesterday(date)) groups["Yesterday"].push(item);
1969
+ else if (isAfter(date, startOfCurrentWeek)) groups["Earlier this Week"].push(item);
1970
+ else if (isAfter(date, startOfLastWeek)) groups["Last Week"].push(item);
1971
+ else groups["Older"].push(item);
1972
+ });
1973
+ return Object.fromEntries(Object.entries(groups).filter(([_, items2]) => items2.length > 0));
1974
+ }, [processedItems, groupBy]);
1975
+ const observerTarget = useRef3(null);
1976
+ useEffect4(() => {
1977
+ const observer = new IntersectionObserver((entries) => {
1978
+ if (entries[0].isIntersecting && hasMore && !isLoadingMore) loadMore();
1979
+ }, { threshold: 0.1 });
1980
+ if (observerTarget.current) observer.observe(observerTarget.current);
1981
+ return () => {
1982
+ if (observerTarget.current) observer.unobserve(observerTarget.current);
1983
+ };
1984
+ }, [hasMore, isLoadingMore, loadMore]);
1985
+ const lastTapTime = useRef3(0);
1986
+ const lastTapItemId = useRef3(null);
1987
+ const tapTimeout = useRef3(null);
1988
+ const handleItemClick = (e, item) => {
1989
+ const isTouchEvent = e.type === "touchend";
1990
+ if (isTouchEvent) {
1991
+ const now = Date.now();
1992
+ const timeSinceLastTap = now - lastTapTime.current;
1993
+ const isSameItem = lastTapItemId.current === item.id;
1994
+ if (timeSinceLastTap < 300 && isSameItem) {
1995
+ if (tapTimeout.current) {
1996
+ clearTimeout(tapTimeout.current);
1997
+ tapTimeout.current = null;
1998
+ }
1999
+ if (item.information.type === "FOLDER" && currentView === "BROWSE") {
2000
+ navigateToFolder(item);
2001
+ } else {
2002
+ onItemDoubleClick?.(item);
2003
+ }
2004
+ lastTapTime.current = 0;
2005
+ lastTapItemId.current = null;
2006
+ return;
2007
+ }
2008
+ lastTapTime.current = now;
2009
+ lastTapItemId.current = item.id;
2010
+ if (tapTimeout.current) clearTimeout(tapTimeout.current);
2011
+ if (item.information.type === "FOLDER" && currentView === "BROWSE" && !selectableFolders) {
2012
+ tapTimeout.current = setTimeout(() => {
2013
+ navigateToFolder(item);
2014
+ if (onItemClick) onItemClick(item);
2015
+ }, 250);
2016
+ } else {
2017
+ if (onItemClick) onItemClick(item);
2018
+ if (selectionMode.type === "MULTIPLE") {
2019
+ if (selectedFileIds.includes(item.id)) {
2020
+ setSelectedFileIds((prev) => prev.filter((id) => id !== item.id));
2021
+ } else {
2022
+ if (selectionMode.maxFile && selectedFileIds.length >= selectionMode.maxFile) return;
2023
+ setSelectedFileIds((prev) => [...prev, item.id]);
2024
+ }
2025
+ } else {
2026
+ setSelectedFileIds([item.id]);
2027
+ }
2028
+ }
2029
+ } else {
2030
+ if (item.information.type === "FOLDER" && !selectableFolders) {
2031
+ if (onItemClick) onItemClick(item);
2032
+ return;
2033
+ }
2034
+ if (onItemClick) onItemClick(item);
2035
+ if (selectionMode.type === "MULTIPLE") {
2036
+ if (selectedFileIds.includes(item.id)) {
2037
+ setSelectedFileIds((prev) => prev.filter((id) => id !== item.id));
2038
+ } else {
2039
+ if (selectionMode.maxFile && selectedFileIds.length >= selectionMode.maxFile) return;
2040
+ setSelectedFileIds((prev) => [...prev, item.id]);
2041
+ }
2042
+ } else {
2043
+ setSelectedFileIds([item.id]);
2044
+ }
2045
+ }
2046
+ };
2047
+ const handleItemDoubleClick = (e, item) => {
2048
+ if (item.information.type === "FOLDER") {
2049
+ if (currentView === "BROWSE") navigateToFolder(item);
2050
+ } else onItemDoubleClick?.(item);
2051
+ };
2052
+ useEffect4(() => {
2053
+ return () => {
2054
+ if (tapTimeout.current) clearTimeout(tapTimeout.current);
2055
+ };
2056
+ }, []);
2057
+ const enableDrag = currentView === "BROWSE";
2058
+ const stateContent = (() => {
2059
+ if (isLoading && items.length === 0) {
2060
+ return /* @__PURE__ */ jsx19("div", { className: "flex items-center justify-center py-12 flex-1", children: /* @__PURE__ */ jsx19(Loader22, { className: "size-6 animate-spin text-muted-foreground" }) });
2061
+ }
2062
+ if (error) {
2063
+ return /* @__PURE__ */ jsx19("div", { className: "flex items-center justify-center p-12 text-destructive bg-destructive/10 rounded-lg flex-1", children: error });
2064
+ }
2065
+ if (processedItems.length === 0) {
2066
+ return /* @__PURE__ */ jsxs11(ContextMenu, { children: [
2067
+ /* @__PURE__ */ jsx19(ContextMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs11("div", { className: cn("flex flex-col items-center justify-center py-12 text-center flex-1", className), children: [
2068
+ /* @__PURE__ */ jsx19("div", { className: "size-12 rounded-full bg-muted flex items-center justify-center mb-3", children: /* @__PURE__ */ jsx19(Folder2, { className: "size-6 text-muted-foreground" }) }),
2069
+ /* @__PURE__ */ jsx19("p", { className: "text-sm text-muted-foreground", children: currentView === "SEARCH" ? "No files match your search" : currentView === "TRASH" ? "Trash is empty" : "No files found" }),
2070
+ currentView === "BROWSE" && /* @__PURE__ */ jsx19("p", { className: "text-xs text-muted-foreground/60 mt-1", children: "Right-click to create a folder" })
2071
+ ] }) }),
2072
+ /* @__PURE__ */ jsx19(ContextMenuContent, { children: currentView === "BROWSE" ? /* @__PURE__ */ jsxs11(ContextMenuItem, { onClick: () => setDialogs((prev) => ({ ...prev, newFolder: true })), children: [
2073
+ /* @__PURE__ */ jsx19(FolderPlus, { className: "mr-2 size-4" }),
2074
+ " New Folder"
2075
+ ] }) : /* @__PURE__ */ jsx19("div", { className: "px-2 py-6 text-center", children: /* @__PURE__ */ jsx19("p", { className: "text-xs text-muted-foreground", children: "No actions available" }) }) })
2076
+ ] });
2077
+ }
2078
+ return null;
2079
+ })();
2080
+ return /* @__PURE__ */ jsx19(DndContext, { sensors, collisionDetection: closestCenter, onDragStart: handleDragStart, onDragOver: handleDragOver, onDragEnd: handleDragEnd, children: /* @__PURE__ */ jsxs11("div", { className: "flex flex-col h-full w-full overflow-hidden bg-background/50", children: [
2081
+ /* @__PURE__ */ jsxs11("div", { className: "h-14 px-3 sm:px-4 border-b bg-background/95 backdrop-blur-sm shrink-0 flex items-center gap-3", children: [
2082
+ /* @__PURE__ */ jsx19("div", { className: "lg:hidden", children: /* @__PURE__ */ jsx19(DriveSidebar, {}) }),
2083
+ /* @__PURE__ */ jsx19("div", { className: "hidden lg:flex flex-1 min-w-0", children: /* @__PURE__ */ jsx19(DrivePathBar, {}) }),
2084
+ /* @__PURE__ */ jsx19(DriveUpload, { compact: true, onComplete: () => refreshItems() })
2085
+ ] }),
2086
+ /* @__PURE__ */ jsx19("div", { className: "lg:hidden px-3 py-2 border-b bg-background/95 backdrop-blur-sm shrink-0", children: /* @__PURE__ */ jsx19(DrivePathBar, {}) }),
2087
+ stateContent || /* @__PURE__ */ jsxs11(ContextMenu, { children: [
2088
+ /* @__PURE__ */ jsx19(ContextMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs11("div", { className: cn("flex-1 overflow-y-auto min-h-0 container mx-auto p-2 sm:p-3 md:p-4", className), children: [
2089
+ /* @__PURE__ */ jsx19("div", { className: "space-y-4 sm:space-y-6 pb-8 sm:pb-12", children: Object.entries(groupedItems).map(([groupName, groupItems]) => /* @__PURE__ */ jsxs11("div", { className: "space-y-3", children: [
2090
+ groupBy !== "NONE" && /* @__PURE__ */ jsxs11("h3", { className: "text-sm font-medium text-muted-foreground flex items-center gap-2", children: [
2091
+ groupName,
2092
+ " ",
2093
+ /* @__PURE__ */ jsxs11("span", { className: "text-xs opacity-50", children: [
2094
+ "(",
2095
+ groupItems.length,
2096
+ ")"
2097
+ ] })
2098
+ ] }),
2099
+ /* @__PURE__ */ jsx19(SortableContext, { items: groupItems.map((i) => i.id), strategy: rectSortingStrategy, disabled: !enableDrag, children: /* @__PURE__ */ jsx19("div", { className: cn(
2100
+ viewMode === "GRID" ? "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-2 sm:gap-3 md:gap-4" : "flex flex-col gap-1"
2101
+ ), children: groupItems.map((item) => {
2102
+ const isSelected = selectedFileIds.includes(item.id);
2103
+ const isFolder = item.information.type === "FOLDER";
2104
+ const isDragOver = isFolder && dragOverFolderId === item.id;
2105
+ const tokenParam = item.token ? `&token=${item.token}` : "";
2106
+ const fileUrl = `${apiEndpoint}?action=serve&id=${item.id}${tokenParam}`;
2107
+ const thumbnailUrl = `${apiEndpoint}?action=thumbnail&id=${item.id}&size=${viewMode === "GRID" ? "medium" : "small"}${tokenParam}`;
2108
+ const isThumbnailable = !isFolder && item.information.type === "FILE" && (item.information.mime.startsWith("image/") || item.information.mime.startsWith("video/"));
2109
+ return /* @__PURE__ */ jsx19(SortableItem, { id: item.id, disabled: !enableDrag, isDragOverTarget: isDragOver, children: /* @__PURE__ */ jsxs11(ContextMenu, { children: [
2110
+ /* @__PURE__ */ jsx19(ContextMenuTrigger, { children: /* @__PURE__ */ jsx19(
2111
+ "div",
2112
+ {
2113
+ className: cn(
2114
+ "group relative cursor-pointer transition-all focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2",
2115
+ viewMode === "GRID" ? "flex flex-col rounded-xl border bg-card hover:bg-accent/50 hover:shadow-sm overflow-hidden" : "flex items-center p-2 rounded-lg hover:bg-accent/50 gap-3 border border-transparent hover:border-border",
2116
+ isSelected && "ring-2 ring-primary border-primary/50 bg-accent/30",
2117
+ isDragOver && "ring-2 ring-primary border-primary scale-[1.02] bg-primary/10 shadow-lg transition-transform"
2118
+ ),
2119
+ onClick: (e) => handleItemClick(e, item),
2120
+ onTouchEnd: (e) => {
2121
+ e.preventDefault();
2122
+ handleItemClick(e, item);
2123
+ },
2124
+ onDoubleClick: (e) => handleItemDoubleClick(e, item),
2125
+ role: "button",
2126
+ tabIndex: 0,
2127
+ children: viewMode === "GRID" ? /* @__PURE__ */ jsxs11(Fragment5, { children: [
2128
+ /* @__PURE__ */ jsxs11("div", { className: "aspect-square w-full bg-muted/20 flex items-center justify-center overflow-hidden relative", children: [
2129
+ isThumbnailable ? /* @__PURE__ */ jsx19("img", { src: thumbnailUrl, alt: item.name, className: "size-full object-contain transition-transform group-hover:scale-105 duration-300", loading: "lazy" }) : /* @__PURE__ */ jsx19("div", { className: "transition-transform group-hover:scale-110 duration-200", children: getFileIcon(item.information.type === "FILE" ? item.information.mime : "", isFolder, "size-10 text-muted-foreground/70") }),
2130
+ isSelected && /* @__PURE__ */ jsx19("div", { className: "absolute top-2 right-2 size-5 bg-primary rounded-full flex items-center justify-center shadow-sm animate-in zoom-in-50", children: /* @__PURE__ */ jsx19("div", { className: "size-2 bg-primary-foreground rounded-full" }) }),
2131
+ isFolder && currentView === "BROWSE" && /* @__PURE__ */ jsx19("div", { className: "absolute bottom-2 right-2 lg:hidden size-6 bg-primary/90 rounded-full flex items-center justify-center shadow-md", children: /* @__PURE__ */ jsx19(ChevronRight3, { className: "size-3.5 text-primary-foreground" }) })
2132
+ ] }),
2133
+ /* @__PURE__ */ jsxs11("div", { className: "p-3", children: [
2134
+ /* @__PURE__ */ jsx19("div", { className: "flex items-start justify-between gap-2", children: /* @__PURE__ */ jsx19("span", { className: "text-sm font-medium truncate w-full text-card-foreground", title: item.name, children: item.name }) }),
2135
+ /* @__PURE__ */ jsx19("div", { className: "flex items-center justify-between mt-1 text-xs text-muted-foreground", children: /* @__PURE__ */ jsx19("span", { children: isFolder ? "Folder" : formatBytes(item.information.type === "FILE" ? item.information.sizeInBytes : 0) }) })
2136
+ ] })
2137
+ ] }) : /* @__PURE__ */ jsxs11(Fragment5, { children: [
2138
+ /* @__PURE__ */ jsx19("div", { className: "size-9 shrink-0 rounded-md overflow-hidden bg-muted/40 flex items-center justify-center border", children: isThumbnailable ? /* @__PURE__ */ jsx19("img", { src: thumbnailUrl, alt: item.name, className: "size-full object-contain", loading: "lazy" }) : getFileIcon(item.information.type === "FILE" ? item.information.mime : "", isFolder, "size-4 text-muted-foreground") }),
2139
+ /* @__PURE__ */ jsx19("span", { className: "text-sm font-medium truncate flex-1 text-card-foreground", title: item.name, children: item.name }),
2140
+ /* @__PURE__ */ jsx19("span", { className: "text-xs text-muted-foreground w-20 text-right", children: isFolder ? "-" : formatBytes(item.information.type === "FILE" ? item.information.sizeInBytes : 0) }),
2141
+ isFolder && currentView === "BROWSE" && /* @__PURE__ */ jsx19(ChevronRight3, { className: "size-4 text-muted-foreground lg:hidden shrink-0" })
2142
+ ] })
2143
+ }
2144
+ ) }),
2145
+ /* @__PURE__ */ jsx19(ContextMenuContent, { children: currentView === "TRASH" ? /* @__PURE__ */ jsxs11(Fragment5, { children: [
2146
+ /* @__PURE__ */ jsxs11(ContextMenuItem, { onClick: async () => {
2147
+ await callAPI("restore", { method: "POST", query: { id: item.id } });
2148
+ await refreshItems();
2149
+ }, children: [
2150
+ /* @__PURE__ */ jsx19(RotateCcw, { className: "mr-2 size-4" }),
2151
+ " Restore"
2152
+ ] }),
2153
+ /* @__PURE__ */ jsx19(ContextMenuSeparator, {}),
2154
+ /* @__PURE__ */ jsxs11(ContextMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => {
2155
+ setItemToDelete(item);
2156
+ setDialogs((prev) => ({ ...prev, delete: true }));
2157
+ }, children: [
2158
+ /* @__PURE__ */ jsx19(Trash22, { className: "mr-2 size-4" }),
2159
+ " Delete Forever"
2160
+ ] })
2161
+ ] }) : /* @__PURE__ */ jsxs11(Fragment5, { children: [
2162
+ /* @__PURE__ */ jsxs11(ContextMenuItem, { onClick: () => setDetailsItem(item), children: [
2163
+ /* @__PURE__ */ jsx19(Info, { className: "mr-2 size-4" }),
2164
+ " Details"
2165
+ ] }),
2166
+ /* @__PURE__ */ jsxs11(ContextMenuItem, { onClick: () => {
2167
+ setRenameItem(item);
2168
+ setDialogs((prev) => ({ ...prev, rename: true }));
2169
+ }, children: [
2170
+ /* @__PURE__ */ jsx19(Pencil, { className: "mr-2 size-4" }),
2171
+ " Rename"
2172
+ ] }),
2173
+ /* @__PURE__ */ jsx19(ContextMenuSeparator, {}),
2174
+ /* @__PURE__ */ jsxs11(ContextMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => {
2175
+ setItemToDelete(item);
2176
+ setDialogs((prev) => ({ ...prev, delete: true }));
2177
+ }, children: [
2178
+ /* @__PURE__ */ jsx19(Trash22, { className: "mr-2 size-4" }),
2179
+ " Delete"
2180
+ ] })
2181
+ ] }) })
2182
+ ] }) }, item.id);
2183
+ }) }) })
2184
+ ] }, groupName)) }),
2185
+ hasMore && /* @__PURE__ */ jsxs11("div", { ref: observerTarget, className: "flex justify-center py-4", children: [
2186
+ isLoadingMore && /* @__PURE__ */ jsx19(Loader22, { className: "size-6 animate-spin text-muted-foreground" }),
2187
+ !isLoadingMore && /* @__PURE__ */ jsx19("div", { className: "h-4 w-full" })
2188
+ ] }),
2189
+ /* @__PURE__ */ jsx19(
2190
+ DriveFileDetails,
2191
+ {
2192
+ item: detailsItem,
2193
+ isOpen: !!detailsItem,
2194
+ onClose: () => setDetailsItem(null),
2195
+ apiEndpoint
2196
+ }
2197
+ ),
2198
+ /* @__PURE__ */ jsx19(
2199
+ DialogConfirmation,
2200
+ {
2201
+ open: dialogs.newFolder,
2202
+ onClose: () => setDialogs((prev) => ({ ...prev, newFolder: false })),
2203
+ title: "Create New Folder",
2204
+ description: "Enter a name for the new folder",
2205
+ inputs: [{ type: "INPUT", id: "name", name: "Folder name", required: true }],
2206
+ onConfirm: async (inputs) => {
2207
+ try {
2208
+ await callAPI("createFolder", {
2209
+ method: "POST",
2210
+ body: JSON.stringify({ name: inputs.name, parentId: currentFolderId || "root" })
2211
+ });
2212
+ await refreshItems();
2213
+ return [true];
2214
+ } catch (error2) {
2215
+ return [false, error2 instanceof Error ? error2.message : "Failed to create folder"];
2216
+ }
2217
+ }
2218
+ }
2219
+ ),
2220
+ /* @__PURE__ */ jsx19(
2221
+ DialogConfirmation,
2222
+ {
2223
+ open: dialogs.rename,
2224
+ onClose: () => {
2225
+ setDialogs((prev) => ({ ...prev, rename: false }));
2226
+ setRenameItem(null);
2227
+ },
2228
+ title: `Rename ${renameItem?.information.type === "FOLDER" ? "Folder" : "File"}`,
2229
+ description: "Enter a new name",
2230
+ inputs: [{ type: "INPUT", id: "name", name: "Name", default: renameItem?.name, required: true }],
2231
+ onConfirm: async (inputs) => {
2232
+ if (!renameItem) return [false, "No item selected"];
2233
+ try {
2234
+ await callAPI("rename", {
2235
+ method: "PATCH",
2236
+ query: { id: renameItem.id },
2237
+ body: JSON.stringify({ newName: inputs.name })
2238
+ });
2239
+ await refreshItems();
2240
+ setRenameItem(null);
2241
+ return [true];
2242
+ } catch (error2) {
2243
+ return [false, error2 instanceof Error ? error2.message : "Failed to rename"];
2244
+ }
2245
+ }
2246
+ }
2247
+ ),
2248
+ /* @__PURE__ */ jsx19(
2249
+ DialogConfirmation,
2250
+ {
2251
+ open: dialogs.delete,
2252
+ onClose: () => {
2253
+ setDialogs((prev) => ({ ...prev, delete: false }));
2254
+ setItemToDelete(null);
2255
+ },
2256
+ title: currentView === "TRASH" ? "Delete Permanently?" : "Move to Trash?",
2257
+ description: currentView === "TRASH" ? `This will permanently delete "${itemToDelete?.name}". You cannot undo this action.` : `Are you sure you want to move "${itemToDelete?.name}" to trash?`,
2258
+ onConfirm: async () => {
2259
+ if (!itemToDelete) return [false, "No item selected"];
2260
+ try {
2261
+ await deleteItems([itemToDelete.id]);
2262
+ await refreshItems();
2263
+ setItemToDelete(null);
2264
+ return [true];
2265
+ } catch (error2) {
2266
+ return [false, error2 instanceof Error ? error2.message : "Failed to delete"];
2267
+ }
2268
+ }
2269
+ }
2270
+ )
2271
+ ] }) }),
2272
+ /* @__PURE__ */ jsx19(ContextMenuContent, { children: currentView === "BROWSE" ? /* @__PURE__ */ jsxs11(ContextMenuItem, { onClick: () => setDialogs((prev) => ({ ...prev, newFolder: true })), children: [
2273
+ /* @__PURE__ */ jsx19(FolderPlus, { className: "mr-2 size-4" }),
2274
+ " New Folder"
2275
+ ] }) : /* @__PURE__ */ jsx19("div", { className: "px-2 py-6 text-center", children: /* @__PURE__ */ jsx19("p", { className: "text-xs text-muted-foreground", children: "No actions available" }) }) })
2276
+ ] })
2277
+ ] }) });
2278
+ };
2279
+
2280
+ // src/client/components/drive/header.tsx
2281
+ import { useState as useState5 } from "react";
2282
+ import {
2283
+ Trash2 as Trash23,
2284
+ ArrowUpDown,
2285
+ LayoutGrid,
2286
+ List,
2287
+ Group as Group3,
2288
+ Calendar as Calendar2,
2289
+ ArrowDownAZ,
2290
+ ArrowUpAZ,
2291
+ ArrowDown01,
2292
+ ArrowUp01,
2293
+ Check as Check4,
2294
+ RotateCcw as RotateCcw2,
2295
+ Menu as Menu2
2296
+ } from "lucide-react";
2297
+ import { jsx as jsx20, jsxs as jsxs12 } from "react/jsx-runtime";
2298
+ var DriveHeader = () => {
2299
+ const {
2300
+ viewMode,
2301
+ setViewMode,
2302
+ groupBy,
2303
+ setGroupBy,
2304
+ sortBy,
2305
+ setSortBy,
2306
+ selectedFileIds,
2307
+ setItems,
2308
+ apiEndpoint,
2309
+ setSelectedFileIds,
2310
+ currentView,
2311
+ setCurrentView,
2312
+ searchQuery,
2313
+ setSearchQuery,
2314
+ callAPI,
2315
+ refreshItems,
2316
+ searchScope,
2317
+ setSearchScope
2318
+ } = useDrive();
2319
+ const [dialogs, setDialogs] = useState5({ delete: false, emptyTrash: false });
2320
+ return /* @__PURE__ */ jsxs12("div", { className: "flex flex-wrap items-center gap-2 border-b bg-muted/30 p-2", children: [
2321
+ (selectedFileIds.length > 0 || currentView === "TRASH") && /* @__PURE__ */ jsxs12(DropdownMenu, { children: [
2322
+ /* @__PURE__ */ jsx20(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx20(
2323
+ Button,
2324
+ {
2325
+ type: "button",
2326
+ variant: "outline",
2327
+ size: "sm",
2328
+ className: "gap-1.5",
2329
+ children: /* @__PURE__ */ jsx20(Menu2, { className: "size-4" })
2330
+ }
2331
+ ) }),
2332
+ /* @__PURE__ */ jsxs12(DropdownMenuContent, { align: "start", children: [
2333
+ selectedFileIds.length > 0 && currentView === "TRASH" && /* @__PURE__ */ jsxs12(
2334
+ DropdownMenuItem,
2335
+ {
2336
+ onClick: async () => {
2337
+ try {
2338
+ await Promise.all(selectedFileIds.map(
2339
+ (id) => fetch(`${apiEndpoint}?action=restore&id=${id}`, { method: "POST" })
2340
+ ));
2341
+ await refreshItems();
2342
+ setSelectedFileIds([]);
2343
+ } catch (error) {
2344
+ console.error("Error restoring files:", error);
2345
+ }
2346
+ },
2347
+ children: [
2348
+ /* @__PURE__ */ jsx20(RotateCcw2, { className: "size-3.5 mr-2" }),
2349
+ "Restore (",
2350
+ selectedFileIds.length,
2351
+ ")"
2352
+ ]
2353
+ }
2354
+ ),
2355
+ selectedFileIds.length > 0 && /* @__PURE__ */ jsxs12(
2356
+ DropdownMenuItem,
2357
+ {
2358
+ onClick: () => setDialogs((prev) => ({ ...prev, delete: true })),
2359
+ className: "text-destructive focus:text-destructive",
2360
+ children: [
2361
+ /* @__PURE__ */ jsx20(Trash23, { className: "size-3.5 mr-2" }),
2362
+ currentView === "TRASH" ? "Delete Forever" : "Delete",
2363
+ " (",
2364
+ selectedFileIds.length,
2365
+ ")"
2366
+ ]
2367
+ }
2368
+ ),
2369
+ currentView === "TRASH" && selectedFileIds.length === 0 && /* @__PURE__ */ jsxs12(
2370
+ DropdownMenuItem,
2371
+ {
2372
+ onClick: () => setDialogs((prev) => ({ ...prev, emptyTrash: true })),
2373
+ className: "text-destructive focus:text-destructive",
2374
+ children: [
2375
+ /* @__PURE__ */ jsx20(Trash23, { className: "size-3.5 mr-2" }),
2376
+ "Empty Trash"
2377
+ ]
2378
+ }
2379
+ )
2380
+ ] })
2381
+ ] }),
2382
+ /* @__PURE__ */ jsxs12(DropdownMenu, { children: [
2383
+ /* @__PURE__ */ jsx20(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs12(
2384
+ Button,
2385
+ {
2386
+ type: "button",
2387
+ variant: groupBy !== "NONE" ? "secondary" : "ghost",
2388
+ size: "sm",
2389
+ className: "gap-1.5",
2390
+ children: [
2391
+ /* @__PURE__ */ jsx20(Group3, { className: "size-3.5" }),
2392
+ /* @__PURE__ */ jsx20("span", { className: "hidden sm:inline", children: groupBy === "NONE" ? "Group" : "Grouped" })
2393
+ ]
2394
+ }
2395
+ ) }),
2396
+ /* @__PURE__ */ jsxs12(DropdownMenuContent, { align: "start", children: [
2397
+ /* @__PURE__ */ jsxs12(DropdownMenuItem, { onClick: () => setGroupBy("NONE"), children: [
2398
+ groupBy === "NONE" && /* @__PURE__ */ jsx20(Check4, { className: "size-3.5 mr-2" }),
2399
+ /* @__PURE__ */ jsx20("span", { className: cn(groupBy !== "NONE" && "pl-5.5"), children: "No Grouping" })
2400
+ ] }),
2401
+ /* @__PURE__ */ jsxs12(DropdownMenuItem, { onClick: () => setGroupBy("CREATED_AT"), children: [
2402
+ groupBy === "CREATED_AT" && /* @__PURE__ */ jsx20(Check4, { className: "size-3.5 mr-2" }),
2403
+ /* @__PURE__ */ jsx20("span", { className: cn(groupBy !== "CREATED_AT" && "pl-5.5"), children: "Created Date" })
2404
+ ] })
2405
+ ] })
2406
+ ] }),
2407
+ /* @__PURE__ */ jsxs12("div", { className: "flex-1 min-w-0 sm:min-w-50 relative", children: [
2408
+ /* @__PURE__ */ jsx20(
2409
+ Input,
2410
+ {
2411
+ type: "text",
2412
+ placeholder: "Search files...",
2413
+ className: "w-full pl-8 pr-7 h-9",
2414
+ value: searchQuery,
2415
+ onChange: (e) => {
2416
+ const val = e.target.value;
2417
+ setSearchQuery(val);
2418
+ if (val.trim().length > 0) {
2419
+ if (currentView !== "SEARCH") {
2420
+ setSearchScope(currentView === "TRASH" ? "TRASH" : "ACTIVE");
2421
+ }
2422
+ setCurrentView("SEARCH");
2423
+ } else if (currentView === "SEARCH") {
2424
+ setCurrentView(searchScope === "TRASH" ? "TRASH" : "BROWSE");
2425
+ }
2426
+ }
2427
+ }
2428
+ ),
2429
+ /* @__PURE__ */ jsx20("div", { className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none", children: /* @__PURE__ */ jsxs12("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "lucide lucide-search", children: [
2430
+ /* @__PURE__ */ jsx20("circle", { cx: "11", cy: "11", r: "8" }),
2431
+ /* @__PURE__ */ jsx20("path", { d: "m21 21-4.3-4.3" })
2432
+ ] }) }),
2433
+ searchQuery && /* @__PURE__ */ jsx20(
2434
+ Button,
2435
+ {
2436
+ type: "button",
2437
+ variant: "ghost",
2438
+ size: "icon",
2439
+ className: "absolute right-0.5 top-1/2 -translate-y-1/2 h-7 w-7 text-muted-foreground",
2440
+ onClick: () => {
2441
+ setSearchQuery("");
2442
+ setCurrentView(searchScope === "TRASH" ? "TRASH" : "BROWSE");
2443
+ },
2444
+ children: /* @__PURE__ */ jsxs12("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "lucide lucide-x", children: [
2445
+ /* @__PURE__ */ jsx20("path", { d: "M18 6 6 18" }),
2446
+ /* @__PURE__ */ jsx20("path", { d: "m6 6 12 12" })
2447
+ ] })
2448
+ }
2449
+ )
2450
+ ] }),
2451
+ /* @__PURE__ */ jsxs12(DropdownMenu, { children: [
2452
+ /* @__PURE__ */ jsx20(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs12(Button, { type: "button", variant: "ghost", size: "sm", className: "gap-1.5", children: [
2453
+ /* @__PURE__ */ jsx20(ArrowUpDown, { className: "size-3.5" }),
2454
+ /* @__PURE__ */ jsx20("span", { className: "hidden sm:inline max-w-24 truncate", children: sortBy.field === "id" ? "Default" : sortBy.field === "order" ? "Custom" : sortBy.field === "createdAt" ? sortBy.order === -1 ? "Date: Newest" : "Date: Oldest" : sortBy.field === "name" ? sortBy.order === 1 ? "Name: A to Z" : "Name: Z to A" : sortBy.field === "size" ? sortBy.order === -1 ? "Size: Large" : "Size: Small" : "Sort" })
2455
+ ] }) }),
2456
+ /* @__PURE__ */ jsxs12(DropdownMenuContent, { align: "end", children: [
2457
+ /* @__PURE__ */ jsxs12(DropdownMenuItem, { onClick: () => setSortBy({ field: "order", order: 1 }), children: [
2458
+ sortBy.field === "order" && /* @__PURE__ */ jsx20(Check4, { className: "size-3.5 mr-2" }),
2459
+ /* @__PURE__ */ jsx20("span", { className: cn(sortBy.field !== "order" && "pl-5.5"), children: "Custom Order" })
2460
+ ] }),
2461
+ /* @__PURE__ */ jsxs12(DropdownMenuItem, { onClick: () => setSortBy({ field: "id", order: 1 }), children: [
2462
+ sortBy.field === "id" && /* @__PURE__ */ jsx20(Check4, { className: "size-3.5 mr-2" }),
2463
+ /* @__PURE__ */ jsx20("span", { className: cn(sortBy.field !== "id" && "pl-5.5"), children: "Default" })
2464
+ ] }),
2465
+ /* @__PURE__ */ jsx20(DropdownMenuSeparator, {}),
2466
+ /* @__PURE__ */ jsxs12(DropdownMenuItem, { onClick: () => setSortBy({ field: "createdAt", order: -1 }), children: [
2467
+ sortBy.field === "createdAt" && sortBy.order === -1 && /* @__PURE__ */ jsx20(Check4, { className: "size-3.5 mr-2" }),
2468
+ /* @__PURE__ */ jsx20(Calendar2, { className: cn("size-3.5 mr-2", sortBy.field === "createdAt" && sortBy.order === -1 ? "" : "ml-5.5") }),
2469
+ "Date: Newest"
2470
+ ] }),
2471
+ /* @__PURE__ */ jsxs12(DropdownMenuItem, { onClick: () => setSortBy({ field: "createdAt", order: 1 }), children: [
2472
+ sortBy.field === "createdAt" && sortBy.order === 1 && /* @__PURE__ */ jsx20(Check4, { className: "size-3.5 mr-2" }),
2473
+ /* @__PURE__ */ jsx20(Calendar2, { className: cn("size-3.5 mr-2", sortBy.field === "createdAt" && sortBy.order === 1 ? "" : "ml-5.5") }),
2474
+ "Date: Oldest"
2475
+ ] }),
2476
+ /* @__PURE__ */ jsx20(DropdownMenuSeparator, {}),
2477
+ /* @__PURE__ */ jsxs12(DropdownMenuItem, { onClick: () => setSortBy({ field: "name", order: 1 }), children: [
2478
+ sortBy.field === "name" && sortBy.order === 1 && /* @__PURE__ */ jsx20(Check4, { className: "size-3.5 mr-2" }),
2479
+ /* @__PURE__ */ jsx20(ArrowDownAZ, { className: cn("size-3.5 mr-2", sortBy.field === "name" && sortBy.order === 1 ? "" : "ml-5.5") }),
2480
+ "Name: A to Z"
2481
+ ] }),
2482
+ /* @__PURE__ */ jsxs12(DropdownMenuItem, { onClick: () => setSortBy({ field: "name", order: -1 }), children: [
2483
+ sortBy.field === "name" && sortBy.order === -1 && /* @__PURE__ */ jsx20(Check4, { className: "size-3.5 mr-2" }),
2484
+ /* @__PURE__ */ jsx20(ArrowUpAZ, { className: cn("size-3.5 mr-2", sortBy.field === "name" && sortBy.order === -1 ? "" : "ml-5.5") }),
2485
+ "Name: Z to A"
2486
+ ] }),
2487
+ /* @__PURE__ */ jsx20(DropdownMenuSeparator, {}),
2488
+ /* @__PURE__ */ jsxs12(DropdownMenuItem, { onClick: () => setSortBy({ field: "size", order: -1 }), children: [
2489
+ sortBy.field === "size" && sortBy.order === -1 && /* @__PURE__ */ jsx20(Check4, { className: "size-3.5 mr-2" }),
2490
+ /* @__PURE__ */ jsx20(ArrowDown01, { className: cn("size-3.5 mr-2", sortBy.field === "size" && sortBy.order === -1 ? "" : "ml-5.5") }),
2491
+ "Size: Large"
2492
+ ] }),
2493
+ /* @__PURE__ */ jsxs12(DropdownMenuItem, { onClick: () => setSortBy({ field: "size", order: 1 }), children: [
2494
+ sortBy.field === "size" && sortBy.order === 1 && /* @__PURE__ */ jsx20(Check4, { className: "size-3.5 mr-2" }),
2495
+ /* @__PURE__ */ jsx20(ArrowUp01, { className: cn("size-3.5 mr-2", sortBy.field === "size" && sortBy.order === 1 ? "" : "ml-5.5") }),
2496
+ "Size: Small"
2497
+ ] })
2498
+ ] })
2499
+ ] }),
2500
+ /* @__PURE__ */ jsx20("div", { className: "w-px h-5 bg-border" }),
2501
+ /* @__PURE__ */ jsxs12("div", { className: "flex bg-muted rounded-md p-0.5 gap-0.5", children: [
2502
+ /* @__PURE__ */ jsx20(
2503
+ Button,
2504
+ {
2505
+ type: "button",
2506
+ variant: "ghost",
2507
+ size: "icon",
2508
+ className: cn("h-8 w-8", viewMode === "GRID" && "bg-white shadow-sm hover:bg-white"),
2509
+ onClick: () => setViewMode("GRID"),
2510
+ "aria-label": "Grid View",
2511
+ "aria-pressed": viewMode === "GRID",
2512
+ children: /* @__PURE__ */ jsx20(LayoutGrid, { className: "size-4" })
2513
+ }
2514
+ ),
2515
+ /* @__PURE__ */ jsx20(
2516
+ Button,
2517
+ {
2518
+ type: "button",
2519
+ variant: "ghost",
2520
+ size: "icon",
2521
+ className: cn("h-8 w-8", viewMode === "LIST" && "bg-white shadow-sm hover:bg-white"),
2522
+ onClick: () => setViewMode("LIST"),
2523
+ "aria-label": "List View",
2524
+ "aria-pressed": viewMode === "LIST",
2525
+ children: /* @__PURE__ */ jsx20(List, { className: "size-4" })
2526
+ }
2527
+ )
2528
+ ] }),
2529
+ /* @__PURE__ */ jsx20(
2530
+ DialogConfirmation,
2531
+ {
2532
+ open: dialogs.delete,
2533
+ onClose: () => setDialogs((prev) => ({ ...prev, delete: false })),
2534
+ title: currentView === "TRASH" ? "Delete Permanently?" : "Move to Trash?",
2535
+ description: currentView === "TRASH" ? `This will permanently delete ${selectedFileIds.length} item(s). You cannot undo this action.` : `Are you sure you want to move ${selectedFileIds.length} item(s) to trash?`,
2536
+ onConfirm: async () => {
2537
+ if (selectedFileIds.length === 0) return [false, "No files selected"];
2538
+ try {
2539
+ if (currentView === "TRASH") {
2540
+ await Promise.all(selectedFileIds.map(
2541
+ (id) => fetch(`${apiEndpoint}?action=deletePermanent&id=${id}`, { method: "DELETE" })
2542
+ ));
2543
+ await refreshItems();
2544
+ setSelectedFileIds([]);
2545
+ } else {
2546
+ const response = await fetch(`${apiEndpoint}?action=deleteMany`, {
2547
+ method: "DELETE",
2548
+ headers: { "Content-Type": "application/json" },
2549
+ body: JSON.stringify({ ids: selectedFileIds })
2550
+ });
2551
+ const result = await response.json();
2552
+ if (result.status === 200) {
2553
+ setItems((prevItems) => prevItems.filter((item) => !selectedFileIds.includes(item.id)));
2554
+ setSelectedFileIds([]);
2555
+ } else {
2556
+ return [false, result.message || "Failed to delete files"];
2557
+ }
2558
+ }
2559
+ return [true];
2560
+ } catch (error) {
2561
+ return [false, error instanceof Error ? error.message : "Error deleting files"];
2562
+ }
2563
+ }
2564
+ }
2565
+ ),
2566
+ /* @__PURE__ */ jsx20(
2567
+ DialogConfirmation,
2568
+ {
2569
+ open: dialogs.emptyTrash,
2570
+ onClose: () => setDialogs((prev) => ({ ...prev, emptyTrash: false })),
2571
+ title: "Empty Trash?",
2572
+ description: "All items in the trash will be permanently deleted. This action cannot be undone.",
2573
+ onConfirm: async () => {
2574
+ try {
2575
+ await callAPI("emptyTrash", { method: "POST" });
2576
+ await refreshItems();
2577
+ return [true];
2578
+ } catch (e) {
2579
+ return [false, e instanceof Error ? e.message : "Failed to empty trash"];
2580
+ }
2581
+ }
2582
+ }
2583
+ )
2584
+ ] });
2585
+ };
2586
+
2587
+ // src/client/components/drive/file/preview.tsx
2588
+ import { useState as useState6 } from "react";
2589
+ import { Document, Page, pdfjs } from "react-pdf";
2590
+ import { jsx as jsx21, jsxs as jsxs13 } from "react/jsx-runtime";
2591
+ if (typeof window !== "undefined") {
2592
+ pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
2593
+ }
2594
+ var DriveFilePreview = (props) => {
2595
+ const { item, onClose } = props;
2596
+ const { apiEndpoint } = useDrive();
2597
+ const [pdfNumPages, setPdfNumPages] = useState6(null);
2598
+ if (item.information.type === "FOLDER") return null;
2599
+ const mime = item.information.mime;
2600
+ const tokenParam = item.token ? `&token=${item.token}` : "";
2601
+ const fileUrl = `${apiEndpoint}?action=serve&id=${item.id}${tokenParam}`;
2602
+ const renderContent = () => {
2603
+ if (mime.startsWith("image/")) return /* @__PURE__ */ jsx21("img", { src: fileUrl, alt: item.name, className: "max-w-full max-h-[70vh] rounded-md object-contain shadow-sm" });
2604
+ if (mime.startsWith("video/")) return /* @__PURE__ */ jsx21("video", { src: fileUrl, controls: true, autoPlay: true, className: "max-w-full max-h-[70vh] rounded-md shadow-sm bg-black", children: "Your browser does not support video playback." });
2605
+ if (mime.startsWith("audio/")) return /* @__PURE__ */ jsxs13("div", { className: "flex flex-col items-center gap-6 py-8 px-4 w-full", children: [
2606
+ /* @__PURE__ */ jsx21("div", { className: "w-24 h-24 rounded-full bg-primary/10 flex items-center justify-center", children: /* @__PURE__ */ jsx21("span", { className: "text-4xl", children: "\u{1F3B5}" }) }),
2607
+ /* @__PURE__ */ jsxs13("div", { className: "text-center space-y-1", children: [
2608
+ /* @__PURE__ */ jsx21("h4", { className: "font-medium text-lg", children: item.name }),
2609
+ /* @__PURE__ */ jsx21("p", { className: "text-sm text-muted-foreground", children: mime })
2610
+ ] }),
2611
+ /* @__PURE__ */ jsx21("audio", { src: fileUrl, controls: true, autoPlay: true, className: "w-full max-w-md mt-2", children: "Your browser does not support audio playback." })
2612
+ ] });
2613
+ if (mime === "application/pdf") {
2614
+ return /* @__PURE__ */ jsxs13("div", { className: "max-h-[70vh] overflow-y-auto rounded-md border bg-white dark:bg-zinc-900 mx-auto w-full", children: [
2615
+ /* @__PURE__ */ jsx21(Document, { file: fileUrl, onLoadSuccess: ({ numPages }) => setPdfNumPages(numPages), className: "flex flex-col items-center gap-4 p-4", children: pdfNumPages && Array.from(new Array(Math.min(pdfNumPages, 10)), (_, index) => /* @__PURE__ */ jsx21(
2616
+ Page,
2617
+ {
2618
+ pageNumber: index + 1,
2619
+ renderTextLayer: false,
2620
+ renderAnnotationLayer: false,
2621
+ className: "shadow-md",
2622
+ width: 600
2623
+ },
2624
+ `page_${index + 1}`
2625
+ )) }),
2626
+ pdfNumPages && pdfNumPages > 10 && /* @__PURE__ */ jsxs13("div", { className: "p-4 text-center text-sm text-muted-foreground border-t bg-muted/20", children: [
2627
+ "Showing 10 of ",
2628
+ pdfNumPages,
2629
+ " pages (Preview limited)"
2630
+ ] })
2631
+ ] });
2632
+ }
2633
+ return /* @__PURE__ */ jsxs13("div", { className: "flex flex-col items-center justify-center gap-6 py-12", children: [
2634
+ /* @__PURE__ */ jsx21("div", { className: "p-4 rounded-full bg-muted", children: /* @__PURE__ */ jsx21("span", { className: "text-4xl opacity-50", children: "\u{1F4C4}" }) }),
2635
+ /* @__PURE__ */ jsxs13("div", { className: "text-center space-y-2", children: [
2636
+ /* @__PURE__ */ jsx21("h4", { className: "font-medium", children: "Preview not available" }),
2637
+ /* @__PURE__ */ jsx21("p", { className: "text-sm text-muted-foreground max-w-xs mx-auto", children: "This file type usually cannot be previewed in the browser." })
2638
+ ] }),
2639
+ /* @__PURE__ */ jsx21(Button, { asChild: true, children: /* @__PURE__ */ jsx21("a", { href: fileUrl, download: item.name, children: "Download File" }) })
2640
+ ] });
2641
+ };
2642
+ return /* @__PURE__ */ jsx21(Dialog, { open: true, onOpenChange: (open) => !open && onClose(), children: /* @__PURE__ */ jsxs13(DialogContent, { className: "sm:max-w-5xl max-h-[90vh] flex flex-col gap-0 p-0", children: [
2643
+ /* @__PURE__ */ jsx21(DialogHeader, { className: "px-4 py-3 border-b shrink-0 flex flex-row items-center justify-between", children: /* @__PURE__ */ jsx21(DialogTitle, { className: "truncate pr-8", children: item.name }) }),
2644
+ /* @__PURE__ */ jsx21("div", { className: "p-6 overflow-auto flex items-center justify-center bg-muted/5 min-h-75 flex-1", children: renderContent() })
2645
+ ] }) });
2646
+ };
2647
+
2648
+ // src/client/file-chooser.tsx
2649
+ import { Upload as UploadIcon2, X as X4 } from "lucide-react";
2650
+ import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
2651
+ var DriveFileChooser = (props) => {
2652
+ const {
2653
+ value,
2654
+ onChange,
2655
+ multiple,
2656
+ accept,
2657
+ placeholder,
2658
+ className,
2659
+ disabled,
2660
+ error,
2661
+ helperText
2662
+ } = {
2663
+ multiple: false,
2664
+ placeholder: "Choose files...",
2665
+ className: "",
2666
+ disabled: false,
2667
+ ...props
2668
+ };
2669
+ const { items, selectedFileIds, setSelectedFileIds } = useDrive();
2670
+ const [isOpen, setIsOpen] = useState7(false);
2671
+ const [previewFile, setPreviewFile] = useState7(null);
2672
+ useEffect5(() => {
2673
+ if (isOpen) {
2674
+ if (!value) setSelectedFileIds([]);
2675
+ else if (Array.isArray(value)) setSelectedFileIds(value.map((f) => f.id));
2676
+ else setSelectedFileIds([value.id]);
2677
+ }
2678
+ }, [isOpen, value, setSelectedFileIds]);
2679
+ const handleConfirm = useCallback4(() => {
2680
+ const selectedItems = items.filter((item) => selectedFileIds.includes(item.id));
2681
+ const files = selectedItems.map((item) => ({
2682
+ id: item.id,
2683
+ file: { name: item.name, mime: item.information.type === "FILE" ? item.information.mime : "", size: item.information.type === "FILE" ? item.information.sizeInBytes : 0 }
2684
+ }));
2685
+ onChange(multiple ? files : files[0] || null);
2686
+ setIsOpen(false);
2687
+ }, [items, selectedFileIds, multiple, onChange]);
2688
+ const handleRemove = (idToRemove) => {
2689
+ if (multiple && Array.isArray(value)) {
2690
+ onChange(value.filter((f) => f.id !== idToRemove));
2691
+ } else {
2692
+ onChange(null);
2693
+ }
2694
+ };
2695
+ const hasSelection = value && (Array.isArray(value) ? value.length > 0 : true);
2696
+ const displayFiles = useMemo3(() => {
2697
+ if (!value) return [];
2698
+ return Array.isArray(value) ? value : [value];
2699
+ }, [value]);
2700
+ const isSingle = !multiple;
2701
+ return /* @__PURE__ */ jsxs14("div", { className: cn("space-y-1.5", className), children: [
2702
+ !hasSelection && /* @__PURE__ */ jsxs14(
2703
+ "button",
2704
+ {
2705
+ type: "button",
2706
+ onClick: () => !disabled && setIsOpen(true),
2707
+ disabled,
2708
+ className: cn(
2709
+ "w-full flex items-center gap-3 px-3 py-2.5 rounded-md border border-dashed text-left transition-colors",
2710
+ "hover:bg-accent hover:border-accent-foreground/20",
2711
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
2712
+ error && "border-destructive bg-destructive/5 hover:bg-destructive/10",
2713
+ disabled && "opacity-50 cursor-not-allowed hover:bg-transparent"
2714
+ ),
2715
+ children: [
2716
+ /* @__PURE__ */ jsx22("div", { className: cn(
2717
+ "flex items-center justify-center size-9 rounded-md bg-muted",
2718
+ error && "bg-destructive/10"
2719
+ ), children: /* @__PURE__ */ jsx22(UploadIcon2, { className: cn("size-4", error ? "text-destructive" : "text-muted-foreground") }) }),
2720
+ /* @__PURE__ */ jsxs14("div", { className: "flex-1 min-w-0", children: [
2721
+ /* @__PURE__ */ jsx22("p", { className: cn("text-sm font-medium", error && "text-destructive"), children: isSingle ? "Select a file" : "Select files" }),
2722
+ /* @__PURE__ */ jsx22("p", { className: "text-xs text-muted-foreground", children: placeholder })
2723
+ ] })
2724
+ ]
2725
+ }
2726
+ ),
2727
+ hasSelection && isSingle && displayFiles[0] && /* @__PURE__ */ jsxs14("div", { className: cn(
2728
+ "flex items-center gap-2.5 px-2.5 py-2 rounded-md border bg-muted/30",
2729
+ error && "border-destructive",
2730
+ disabled && "opacity-50"
2731
+ ), children: [
2732
+ /* @__PURE__ */ jsx22("div", { className: "size-10 shrink-0 rounded overflow-hidden bg-muted flex items-center justify-center", children: getFileIcon(displayFiles[0].file.mime, false, "size-5 text-muted-foreground") }),
2733
+ /* @__PURE__ */ jsxs14("div", { className: "flex-1 min-w-0", children: [
2734
+ /* @__PURE__ */ jsx22("p", { className: "text-sm font-medium truncate", children: displayFiles[0].file.name }),
2735
+ /* @__PURE__ */ jsx22("p", { className: "text-xs text-muted-foreground truncate", children: displayFiles[0].file.mime })
2736
+ ] }),
2737
+ /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-1 shrink-0", children: [
2738
+ /* @__PURE__ */ jsx22(
2739
+ Button,
2740
+ {
2741
+ type: "button",
2742
+ variant: "ghost",
2743
+ size: "sm",
2744
+ onClick: () => setIsOpen(true),
2745
+ disabled,
2746
+ children: "Change"
2747
+ }
2748
+ ),
2749
+ !disabled && /* @__PURE__ */ jsx22(
2750
+ Button,
2751
+ {
2752
+ type: "button",
2753
+ variant: "ghost",
2754
+ size: "icon",
2755
+ onClick: () => handleRemove(displayFiles[0].id),
2756
+ children: /* @__PURE__ */ jsx22(X4, { className: "size-3.5" })
2757
+ }
2758
+ )
2759
+ ] })
2760
+ ] }),
2761
+ hasSelection && !isSingle && /* @__PURE__ */ jsxs14("div", { className: cn(
2762
+ "rounded-md border",
2763
+ error && "border-destructive",
2764
+ disabled && "opacity-50"
2765
+ ), children: [
2766
+ /* @__PURE__ */ jsxs14("div", { className: "flex items-center justify-between px-2.5 py-1.5 border-b bg-muted/30", children: [
2767
+ /* @__PURE__ */ jsxs14("span", { className: "text-xs text-muted-foreground", children: [
2768
+ displayFiles.length,
2769
+ " files selected"
2770
+ ] }),
2771
+ /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-1", children: [
2772
+ /* @__PURE__ */ jsx22(
2773
+ Button,
2774
+ {
2775
+ type: "button",
2776
+ variant: "ghost",
2777
+ size: "sm",
2778
+ onClick: () => setIsOpen(true),
2779
+ disabled,
2780
+ children: "Add more"
2781
+ }
2782
+ ),
2783
+ !disabled && /* @__PURE__ */ jsx22(
2784
+ Button,
2785
+ {
2786
+ type: "button",
2787
+ variant: "ghost",
2788
+ size: "sm",
2789
+ onClick: () => onChange([]),
2790
+ children: "Clear all"
2791
+ }
2792
+ )
2793
+ ] })
2794
+ ] }),
2795
+ /* @__PURE__ */ jsx22("div", { className: "divide-y max-h-40 overflow-y-auto", children: displayFiles.map((file) => /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2 px-2.5 py-1.5 hover:bg-muted/30", children: [
2796
+ /* @__PURE__ */ jsx22("div", { className: "size-7 shrink-0 rounded overflow-hidden bg-muted flex items-center justify-center", children: getFileIcon(file.file.mime, false, "size-3.5 text-muted-foreground") }),
2797
+ /* @__PURE__ */ jsx22("span", { className: "flex-1 text-sm truncate", children: file.file.name }),
2798
+ !disabled && /* @__PURE__ */ jsx22(
2799
+ Button,
2800
+ {
2801
+ type: "button",
2802
+ variant: "ghost",
2803
+ size: "icon",
2804
+ onClick: () => handleRemove(file.id),
2805
+ children: /* @__PURE__ */ jsx22(X4, { className: "size-3" })
2806
+ }
2807
+ )
2808
+ ] }, file.id)) })
2809
+ ] }),
2810
+ error && helperText && /* @__PURE__ */ jsx22("p", { className: "text-xs text-destructive", children: helperText }),
2811
+ /* @__PURE__ */ jsx22(Dialog, { open: isOpen, onOpenChange: setIsOpen, children: /* @__PURE__ */ jsxs14(DialogContent, { className: "sm:max-w-5xl max-h-[85vh] w-[95vw] flex flex-col gap-0 p-0", children: [
2812
+ /* @__PURE__ */ jsx22(DialogHeader, { className: "px-0 py-0 border-b shrink-0 flex flex-col gap-0", children: /* @__PURE__ */ jsx22("div", { className: "flex items-center justify-between px-3 sm:px-4 py-3 border-b", children: /* @__PURE__ */ jsx22(DialogTitle, { className: "text-sm sm:text-base", children: "File Explorer" }) }) }),
2813
+ /* @__PURE__ */ jsxs14("div", { className: "flex-1 overflow-hidden flex min-h-0 bg-muted/5", children: [
2814
+ /* @__PURE__ */ jsx22("div", { className: "hidden md:flex w-48 lg:w-52 border-r bg-background/50 flex-col", children: /* @__PURE__ */ jsx22(DriveSidebar, {}) }),
2815
+ /* @__PURE__ */ jsxs14("div", { className: "flex-1 flex flex-col min-w-0", children: [
2816
+ /* @__PURE__ */ jsx22(DriveHeader, {}),
2817
+ /* @__PURE__ */ jsx22(
2818
+ DriveExplorer,
2819
+ {
2820
+ mimeFilter: accept,
2821
+ onItemDoubleClick: (item) => {
2822
+ if (item.information.type === "FILE") {
2823
+ setPreviewFile(item);
2824
+ }
2825
+ }
2826
+ }
2827
+ )
2828
+ ] })
2829
+ ] }),
2830
+ /* @__PURE__ */ jsxs14(DialogFooter, { className: "px-3 sm:px-4 py-3 border-t bg-background shrink-0 gap-2 flex-row justify-end", children: [
2831
+ /* @__PURE__ */ jsx22(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setIsOpen(false), children: "Cancel" }),
2832
+ /* @__PURE__ */ jsxs14(
2833
+ Button,
2834
+ {
2835
+ type: "button",
2836
+ size: "sm",
2837
+ onClick: handleConfirm,
2838
+ disabled: selectedFileIds.length === 0,
2839
+ children: [
2840
+ "Select ",
2841
+ selectedFileIds.length > 0 ? `(${selectedFileIds.length})` : ""
2842
+ ]
2843
+ }
2844
+ )
2845
+ ] })
2846
+ ] }) }),
2847
+ previewFile && /* @__PURE__ */ jsx22(DriveFilePreview, { item: previewFile, onClose: () => setPreviewFile(null) })
2848
+ ] });
2849
+ };
2850
+ export {
2851
+ DriveExplorer,
2852
+ DriveFileChooser,
2853
+ DriveFilePreview,
2854
+ DriveHeader,
2855
+ DrivePathBar,
2856
+ DriveProvider,
2857
+ DriveSidebar,
2858
+ DriveStorageIndicator,
2859
+ DriveUpload,
2860
+ useDrive,
2861
+ useUpload
2862
+ };
2863
+ //# sourceMappingURL=index.js.map