@muhgholy/next-drive 1.0.2 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,6 +23,7 @@ npm install @muhgholy/next-drive
23
23
  - Next.js >= 14
24
24
  - React >= 18
25
25
  - Mongoose >= 7
26
+ - Tailwind CSS >= 3
26
27
 
27
28
  **System Requirements:**
28
29
 
@@ -31,6 +32,29 @@ npm install @muhgholy/next-drive
31
32
  - Ubuntu: `sudo apt install ffmpeg`
32
33
  - Windows: Download from official site and add to PATH.
33
34
 
35
+ ### Tailwind CSS Configuration
36
+
37
+ Since this package uses Tailwind CSS for styling, you **must** configure Tailwind to scan the package's files:
38
+
39
+ ```js
40
+ // tailwind.config.js
41
+ export default {
42
+ content: [
43
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
44
+ "./pages/**/*.{js,ts,jsx,tsx,mdx}",
45
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
46
+ // Add the next-drive package
47
+ "./node_modules/@muhgholy/next-drive/dist/**/*.{js,mjs}",
48
+ ],
49
+ theme: {
50
+ extend: {},
51
+ },
52
+ plugins: [],
53
+ };
54
+ ```
55
+
56
+ > **Note**: The CSS is automatically injected when you import from `@muhgholy/next-drive/client` - no need to manually import stylesheets.
57
+
34
58
  ## Quick Start
35
59
 
36
60
  ### 1. Configure Server
@@ -74,7 +98,10 @@ export const drive = driveConfiguration({
74
98
 
75
99
  Set up the API route handler that `next-drive` will use to communicate with the client.
76
100
 
77
- **Important:** The API route must be in the `pages` folder (Pages Router) for Next.js to handle the request properly.
101
+ **Important:**
102
+
103
+ - The API route must be in the `pages` folder (Pages Router)
104
+ - **You MUST disable Next.js body parser** for uploads to work properly
78
105
 
79
106
  ```typescript
80
107
  // pages/api/drive.ts
@@ -83,8 +110,35 @@ import { driveAPIHandler } from "@muhgholy/next-drive/server";
83
110
  import type { NextApiRequest, NextApiResponse } from "next";
84
111
 
85
112
  export default async function handler(req: NextApiRequest, res: NextApiResponse) {
113
+ // Manually parse JSON body for non-upload requests
114
+ if (!req.body) req.body = {};
115
+
116
+ if (req.headers["content-type"]?.includes("application/json")) {
117
+ try {
118
+ const buffer = await new Promise<Buffer>((resolve, reject) => {
119
+ const chunks: Buffer[] = [];
120
+ req.on("data", (chunk) => chunks.push(chunk));
121
+ req.on("end", () => resolve(Buffer.concat(chunks)));
122
+ req.on("error", reject);
123
+ });
124
+
125
+ if (buffer.length > 0) {
126
+ req.body = JSON.parse(buffer.toString());
127
+ }
128
+ } catch (e) {
129
+ console.error("Failed to parse JSON body", e);
130
+ }
131
+ }
132
+
86
133
  return driveAPIHandler(req, res);
87
134
  }
135
+
136
+ // ⚠️ CRITICAL: Disable body parser for file uploads
137
+ export const config = {
138
+ api: {
139
+ bodyParser: false,
140
+ },
141
+ };
88
142
  ```
89
143
 
90
144
  ### 3. Add Provider
@@ -147,6 +201,49 @@ type MyFormData = z.infer<typeof myFormSchema>;
147
201
 
148
202
  ## Key Capabilities
149
203
 
204
+ ### Client-Side File URLs
205
+
206
+ **Generate File URL:**
207
+
208
+ ```typescript
209
+ import { useDrive } from "@muhgholy/next-drive/client";
210
+ import type { TDriveFile } from "@muhgholy/next-drive/client";
211
+
212
+ function MyComponent() {
213
+ const { createUrl } = useDrive();
214
+
215
+ // Basic URL generation
216
+ const url = createUrl(driveFile);
217
+ // Returns: /api/drive?action=serve&id={fileId}
218
+
219
+ // With image quality and format
220
+ const url = createUrl(driveFile, {
221
+ quality: "medium",
222
+ format: "webp",
223
+ });
224
+ // Returns: /api/drive?action=serve&id={fileId}&q=medium&format=webp
225
+
226
+ // Use in Next.js Image component
227
+ return <Image src={createUrl(driveFile)} alt={driveFile.file.name} />;
228
+ }
229
+ ```
230
+
231
+ **Responsive Image SrcSet:**
232
+
233
+ ```typescript
234
+ import { useDrive } from "@muhgholy/next-drive/client";
235
+
236
+ function ResponsiveImage({ driveFile }: { driveFile: TDriveFile }) {
237
+ const { createUrl, createSrcSet } = useDrive();
238
+
239
+ // Generate responsive srcSet for optimal image loading
240
+ const { srcSet, sizes } = createSrcSet(driveFile, "webp");
241
+
242
+ // Use in img tag
243
+ return <img src={createUrl(driveFile, { quality: "medium" })} srcSet={srcSet} sizes={sizes} alt={driveFile.file.name} />;
244
+ }
245
+ ```
246
+
150
247
  ### Server-Side File Access
151
248
 
152
249
  **Get File URL:**
@@ -1,48 +1,53 @@
1
1
  /* src/client/styles.css */
2
- :root {
3
- --background: 0 0% 100%;
4
- --foreground: 222.2 84% 4.9%;
5
- --card: 0 0% 100%;
6
- --card-foreground: 222.2 84% 4.9%;
7
- --popover: 0 0% 100%;
8
- --popover-foreground: 222.2 84% 4.9%;
9
- --primary: 222.2 47.4% 11.2%;
10
- --primary-foreground: 210 40% 98%;
11
- --secondary: 210 40% 96.1%;
12
- --secondary-foreground: 222.2 47.4% 11.2%;
13
- --muted: 210 40% 96.1%;
14
- --muted-foreground: 215.4 16.3% 46.9%;
15
- --accent: 210 40% 96.1%;
16
- --accent-foreground: 222.2 47.4% 11.2%;
17
- --destructive: 0 84.2% 60.2%;
18
- --destructive-foreground: 210 40% 98%;
19
- --border: 214.3 31.8% 91.4%;
20
- --input: 214.3 31.8% 91.4%;
21
- --ring: 222.2 84% 4.9%;
22
- --radius: 0.5rem;
23
- }
24
- .dark {
25
- --background: 222.2 84% 4.9%;
26
- --foreground: 210 40% 98%;
27
- --card: 222.2 84% 4.9%;
28
- --card-foreground: 210 40% 98%;
29
- --popover: 222.2 84% 4.9%;
30
- --popover-foreground: 210 40% 98%;
31
- --primary: 210 40% 98%;
32
- --primary-foreground: 222.2 47.4% 11.2%;
33
- --secondary: 217.2 32.6% 17.5%;
34
- --secondary-foreground: 210 40% 98%;
35
- --muted: 217.2 32.6% 17.5%;
36
- --muted-foreground: 215 20.2% 65.1%;
37
- --accent: 217.2 32.6% 17.5%;
38
- --accent-foreground: 210 40% 98%;
39
- --destructive: 0 62.8% 30.6%;
40
- --destructive-foreground: 210 40% 98%;
41
- --border: 217.2 32.6% 17.5%;
42
- --input: 217.2 32.6% 17.5%;
43
- --ring: 212.7 26.8% 83.9%;
44
- }
45
- * {
46
- border-color: hsl(var(--border));
2
+ @tailwind base;
3
+ @tailwind components;
4
+ @tailwind utilities;
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 222.2 84% 4.9%;
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 222.2 84% 4.9%;
11
+ --popover: 0 0% 100%;
12
+ --popover-foreground: 222.2 84% 4.9%;
13
+ --primary: 222.2 47.4% 11.2%;
14
+ --primary-foreground: 210 40% 98%;
15
+ --secondary: 210 40% 96.1%;
16
+ --secondary-foreground: 222.2 47.4% 11.2%;
17
+ --muted: 210 40% 96.1%;
18
+ --muted-foreground: 215.4 16.3% 46.9%;
19
+ --accent: 210 40% 96.1%;
20
+ --accent-foreground: 222.2 47.4% 11.2%;
21
+ --destructive: 0 84.2% 60.2%;
22
+ --destructive-foreground: 210 40% 98%;
23
+ --border: 214.3 31.8% 91.4%;
24
+ --input: 214.3 31.8% 91.4%;
25
+ --ring: 222.2 84% 4.9%;
26
+ --radius: 0.5rem;
27
+ }
28
+ .dark {
29
+ --background: 222.2 84% 4.9%;
30
+ --foreground: 210 40% 98%;
31
+ --card: 222.2 84% 4.9%;
32
+ --card-foreground: 210 40% 98%;
33
+ --popover: 222.2 84% 4.9%;
34
+ --popover-foreground: 210 40% 98%;
35
+ --primary: 210 40% 98%;
36
+ --primary-foreground: 222.2 47.4% 11.2%;
37
+ --secondary: 217.2 32.6% 17.5%;
38
+ --secondary-foreground: 210 40% 98%;
39
+ --muted: 217.2 32.6% 17.5%;
40
+ --muted-foreground: 215 20.2% 65.1%;
41
+ --accent: 217.2 32.6% 17.5%;
42
+ --accent-foreground: 210 40% 98%;
43
+ --destructive: 0 62.8% 30.6%;
44
+ --destructive-foreground: 210 40% 98%;
45
+ --border: 217.2 32.6% 17.5%;
46
+ --input: 217.2 32.6% 17.5%;
47
+ --ring: 212.7 26.8% 83.9%;
48
+ }
49
+ * {
50
+ border-color: hsl(var(--border));
51
+ }
47
52
  }
48
53
  /*# sourceMappingURL=index.css.map */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/styles.css"],"sourcesContent":[":root {\n --background: 0 0% 100%;\n --foreground: 222.2 84% 4.9%;\n\n --card: 0 0% 100%;\n --card-foreground: 222.2 84% 4.9%;\n\n --popover: 0 0% 100%;\n --popover-foreground: 222.2 84% 4.9%;\n\n --primary: 222.2 47.4% 11.2%;\n --primary-foreground: 210 40% 98%;\n\n --secondary: 210 40% 96.1%;\n --secondary-foreground: 222.2 47.4% 11.2%;\n\n --muted: 210 40% 96.1%;\n --muted-foreground: 215.4 16.3% 46.9%;\n\n --accent: 210 40% 96.1%;\n --accent-foreground: 222.2 47.4% 11.2%;\n\n --destructive: 0 84.2% 60.2%;\n --destructive-foreground: 210 40% 98%;\n\n --border: 214.3 31.8% 91.4%;\n --input: 214.3 31.8% 91.4%;\n --ring: 222.2 84% 4.9%;\n\n --radius: 0.5rem;\n}\n\n.dark {\n --background: 222.2 84% 4.9%;\n --foreground: 210 40% 98%;\n\n --card: 222.2 84% 4.9%;\n --card-foreground: 210 40% 98%;\n\n --popover: 222.2 84% 4.9%;\n --popover-foreground: 210 40% 98%;\n\n --primary: 210 40% 98%;\n --primary-foreground: 222.2 47.4% 11.2%;\n\n --secondary: 217.2 32.6% 17.5%;\n --secondary-foreground: 210 40% 98%;\n\n --muted: 217.2 32.6% 17.5%;\n --muted-foreground: 215 20.2% 65.1%;\n\n --accent: 217.2 32.6% 17.5%;\n --accent-foreground: 210 40% 98%;\n\n --destructive: 0 62.8% 30.6%;\n --destructive-foreground: 210 40% 98%;\n\n --border: 217.2 32.6% 17.5%;\n --input: 217.2 32.6% 17.5%;\n --ring: 212.7 26.8% 83.9%;\n}\n\n* {\n border-color: hsl(var(--border));\n}"],"mappings":";AAAA;AACI,gBAAc,EAAE,GAAG;AACnB,gBAAc,MAAM,IAAI;AAExB,UAAQ,EAAE,GAAG;AACb,qBAAmB,MAAM,IAAI;AAE7B,aAAW,EAAE,GAAG;AAChB,wBAAsB,MAAM,IAAI;AAEhC,aAAW,MAAM,MAAM;AACvB,wBAAsB,IAAI,IAAI;AAE9B,eAAa,IAAI,IAAI;AACrB,0BAAwB,MAAM,MAAM;AAEpC,WAAS,IAAI,IAAI;AACjB,sBAAoB,MAAM,MAAM;AAEhC,YAAU,IAAI,IAAI;AAClB,uBAAqB,MAAM,MAAM;AAEjC,iBAAe,EAAE,MAAM;AACvB,4BAA0B,IAAI,IAAI;AAElC,YAAU,MAAM,MAAM;AACtB,WAAS,MAAM,MAAM;AACrB,UAAQ,MAAM,IAAI;AAElB,YAAU;AACd;AAEA,CAAC;AACG,gBAAc,MAAM,IAAI;AACxB,gBAAc,IAAI,IAAI;AAEtB,UAAQ,MAAM,IAAI;AAClB,qBAAmB,IAAI,IAAI;AAE3B,aAAW,MAAM,IAAI;AACrB,wBAAsB,IAAI,IAAI;AAE9B,aAAW,IAAI,IAAI;AACnB,wBAAsB,MAAM,MAAM;AAElC,eAAa,MAAM,MAAM;AACzB,0BAAwB,IAAI,IAAI;AAEhC,WAAS,MAAM,MAAM;AACrB,sBAAoB,IAAI,MAAM;AAE9B,YAAU,MAAM,MAAM;AACtB,uBAAqB,IAAI,IAAI;AAE7B,iBAAe,EAAE,MAAM;AACvB,4BAA0B,IAAI,IAAI;AAElC,YAAU,MAAM,MAAM;AACtB,WAAS,MAAM,MAAM;AACrB,UAAQ,MAAM,MAAM;AACxB;AAEA;AACI,gBAAc,IAAI,IAAI;AAC1B;","names":[]}
1
+ {"version":3,"sources":["../../src/client/styles.css"],"sourcesContent":["@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n :root {\n --background: 0 0% 100%;\n --foreground: 222.2 84% 4.9%;\n\n --card: 0 0% 100%;\n --card-foreground: 222.2 84% 4.9%;\n\n --popover: 0 0% 100%;\n --popover-foreground: 222.2 84% 4.9%;\n\n --primary: 222.2 47.4% 11.2%;\n --primary-foreground: 210 40% 98%;\n\n --secondary: 210 40% 96.1%;\n --secondary-foreground: 222.2 47.4% 11.2%;\n\n --muted: 210 40% 96.1%;\n --muted-foreground: 215.4 16.3% 46.9%;\n\n --accent: 210 40% 96.1%;\n --accent-foreground: 222.2 47.4% 11.2%;\n\n --destructive: 0 84.2% 60.2%;\n --destructive-foreground: 210 40% 98%;\n\n --border: 214.3 31.8% 91.4%;\n --input: 214.3 31.8% 91.4%;\n --ring: 222.2 84% 4.9%;\n\n --radius: 0.5rem;\n }\n\n .dark {\n --background: 222.2 84% 4.9%;\n --foreground: 210 40% 98%;\n\n --card: 222.2 84% 4.9%;\n --card-foreground: 210 40% 98%;\n\n --popover: 222.2 84% 4.9%;\n --popover-foreground: 210 40% 98%;\n\n --primary: 210 40% 98%;\n --primary-foreground: 222.2 47.4% 11.2%;\n\n --secondary: 217.2 32.6% 17.5%;\n --secondary-foreground: 210 40% 98%;\n\n --muted: 217.2 32.6% 17.5%;\n --muted-foreground: 215 20.2% 65.1%;\n\n --accent: 217.2 32.6% 17.5%;\n --accent-foreground: 210 40% 98%;\n\n --destructive: 0 62.8% 30.6%;\n --destructive-foreground: 210 40% 98%;\n\n --border: 217.2 32.6% 17.5%;\n --input: 217.2 32.6% 17.5%;\n --ring: 212.7 26.8% 83.9%;\n }\n\n * {\n border-color: hsl(var(--border));\n }\n}"],"mappings":";AAAA,UAAU;AACV,UAAU;AACV,UAAU;AAEV;AACI;AACI,kBAAc,EAAE,GAAG;AACnB,kBAAc,MAAM,IAAI;AAExB,YAAQ,EAAE,GAAG;AACb,uBAAmB,MAAM,IAAI;AAE7B,eAAW,EAAE,GAAG;AAChB,0BAAsB,MAAM,IAAI;AAEhC,eAAW,MAAM,MAAM;AACvB,0BAAsB,IAAI,IAAI;AAE9B,iBAAa,IAAI,IAAI;AACrB,4BAAwB,MAAM,MAAM;AAEpC,aAAS,IAAI,IAAI;AACjB,wBAAoB,MAAM,MAAM;AAEhC,cAAU,IAAI,IAAI;AAClB,yBAAqB,MAAM,MAAM;AAEjC,mBAAe,EAAE,MAAM;AACvB,8BAA0B,IAAI,IAAI;AAElC,cAAU,MAAM,MAAM;AACtB,aAAS,MAAM,MAAM;AACrB,YAAQ,MAAM,IAAI;AAElB,cAAU;AACd;AAEA,GAAC;AACG,kBAAc,MAAM,IAAI;AACxB,kBAAc,IAAI,IAAI;AAEtB,YAAQ,MAAM,IAAI;AAClB,uBAAmB,IAAI,IAAI;AAE3B,eAAW,MAAM,IAAI;AACrB,0BAAsB,IAAI,IAAI;AAE9B,eAAW,IAAI,IAAI;AACnB,0BAAsB,MAAM,MAAM;AAElC,iBAAa,MAAM,MAAM;AACzB,4BAAwB,IAAI,IAAI;AAEhC,aAAS,MAAM,MAAM;AACrB,wBAAoB,IAAI,MAAM;AAE9B,cAAU,MAAM,MAAM;AACtB,yBAAqB,IAAI,IAAI;AAE7B,mBAAe,EAAE,MAAM;AACvB,8BAA0B,IAAI,IAAI;AAElC,cAAU,MAAM,MAAM;AACtB,aAAS,MAAM,MAAM;AACrB,YAAQ,MAAM,MAAM;AACxB;AAEA;AACI,kBAAc,IAAI,IAAI;AAC1B;AACJ;","names":[]}
@@ -1,6 +1,6 @@
1
1
  import React, { ReactNode } from 'react';
2
- import { l as TDrivePathItem, d as TDatabaseDrive, m as TDriveQuota, i as TDriveAPIResponse, n as TDriveUploadState, e as TDriveFile } from '../index-CIbbTroj.js';
3
- export { o as driveFileSchemaZod } from '../index-CIbbTroj.js';
2
+ import { n as TDrivePathItem, d as TDatabaseDrive, o as TDriveQuota, e as TDriveFile, f as TImageQuality, g as TImageFormat, k as TDriveAPIResponse, p as TDriveUploadState } from '../index-C4pw1JjP.js';
3
+ export { q as driveFileSchemaZod } from '../index-C4pw1JjP.js';
4
4
  import 'zod';
5
5
 
6
6
  type TDriveContext = {
@@ -47,6 +47,14 @@ type TDriveContext = {
47
47
  };
48
48
  selectedFileIds: string[];
49
49
  setSelectedFileIds: React.Dispatch<React.SetStateAction<string[]>>;
50
+ createUrl: (driveFile: TDriveFile, options?: {
51
+ quality?: TImageQuality;
52
+ format?: TImageFormat;
53
+ }) => string;
54
+ createSrcSet: (driveFile: TDriveFile, format?: TImageFormat) => {
55
+ srcSet: string;
56
+ sizes: string;
57
+ };
50
58
  navigateToFolder: (item: {
51
59
  id: string | null;
52
60
  name: string;
@@ -122,4 +130,4 @@ declare const DriveHeader: () => React.JSX.Element;
122
130
 
123
131
  declare const DriveSidebar: () => React.JSX.Element;
124
132
 
125
- export { DriveExplorer, DriveFileChooser, DriveHeader, DrivePathBar, DriveProvider, DriveSidebar, DriveStorageIndicator, DriveUpload, type TDriveContext, TDriveFile, useDrive, useUpload };
133
+ export { DriveExplorer, DriveFileChooser, DriveHeader, DrivePathBar, DriveProvider, DriveSidebar, DriveStorageIndicator, DriveUpload, type TDriveContext, TDriveFile, TImageFormat, TImageQuality, useDrive, useUpload };
@@ -1,6 +1,75 @@
1
1
  // src/client/context.tsx
2
2
  import React, { createContext, useContext, useState, useCallback, useMemo } from "react";
3
+
4
+ // src/client/utils.tsx
5
+ import { clsx } from "clsx";
6
+ import { twMerge } from "tailwind-merge";
7
+ import { File, Folder, Image, Video, Music, FileText, FileCode, FileArchive } from "lucide-react";
3
8
  import { jsx } from "react/jsx-runtime";
9
+ function cn(...inputs) {
10
+ return twMerge(clsx(inputs));
11
+ }
12
+ var formatBytes = (bytes, decimals = 2) => {
13
+ if (bytes === 0) return "0 Bytes";
14
+ const k = 1024;
15
+ const dm = decimals < 0 ? 0 : decimals;
16
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
17
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
18
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
19
+ };
20
+ var getFileIcon = (mime, isFolder, className = "w-6 h-6") => {
21
+ if (isFolder) return /* @__PURE__ */ jsx(Folder, { className: cn("text-blue-500 fill-blue-500/20", className) });
22
+ if (mime.startsWith("image/")) return /* @__PURE__ */ jsx(Image, { className: cn("text-purple-500", className) });
23
+ if (mime.startsWith("video/")) return /* @__PURE__ */ jsx(Video, { className: cn("text-red-500", className) });
24
+ if (mime.startsWith("audio/")) return /* @__PURE__ */ jsx(Music, { className: cn("text-yellow-500", className) });
25
+ if (mime === "application/pdf") return /* @__PURE__ */ jsx(FileText, { className: cn("text-orange-500", className) });
26
+ if (mime.includes("text") || mime.includes("document")) return /* @__PURE__ */ jsx(FileText, { className: cn("text-slate-500", className) });
27
+ if (mime.includes("zip") || mime.includes("compressed")) return /* @__PURE__ */ jsx(FileArchive, { className: cn("text-amber-500", className) });
28
+ if (mime.includes("javascript") || mime.includes("typescript") || mime.includes("json") || mime.includes("html") || mime.includes("css")) return /* @__PURE__ */ jsx(FileCode, { className: cn("text-green-500", className) });
29
+ return /* @__PURE__ */ jsx(File, { className: cn("text-gray-400", className) });
30
+ };
31
+ var matchesMimeFilter = (mime, isFolder, filter) => {
32
+ if (!filter) return true;
33
+ if (isFolder) return true;
34
+ if (filter === "*/*") return true;
35
+ const types = filter.split(",").map((t) => t.trim());
36
+ return types.some((type) => {
37
+ if (type === mime) return true;
38
+ if (type.endsWith("/*")) {
39
+ const prefix = type.slice(0, -2);
40
+ return mime.startsWith(`${prefix}/`);
41
+ }
42
+ return false;
43
+ });
44
+ };
45
+ var driveCreateUrl = (driveFile, apiEndpoint, options) => {
46
+ const params = new URLSearchParams({
47
+ action: "serve",
48
+ id: driveFile.id
49
+ });
50
+ if (options?.quality) params.set("q", options.quality);
51
+ if (options?.format) params.set("format", options.format);
52
+ return `${apiEndpoint}?${params.toString()}`;
53
+ };
54
+ var driveCreateSrcSet = (driveFile, apiEndpoint, format = "webp") => {
55
+ const qualities = ["ultralow", "low", "medium", "high"];
56
+ const qualityWidthMap = {
57
+ ultralow: 200,
58
+ low: 400,
59
+ medium: 800,
60
+ high: 1200,
61
+ normal: 1600
62
+ };
63
+ const srcSet = qualities.map((quality) => {
64
+ const url = driveCreateUrl(driveFile, apiEndpoint, { quality, format });
65
+ return `${url} ${qualityWidthMap[quality]}w`;
66
+ }).join(", ");
67
+ const sizes = "(max-width: 320px) 200px, (max-width: 480px) 400px, (max-width: 768px) 800px, 1200px";
68
+ return { srcSet, sizes };
69
+ };
70
+
71
+ // src/client/context.tsx
72
+ import { jsx as jsx2 } from "react/jsx-runtime";
4
73
  var DriveContext = createContext(null);
5
74
  var DriveProvider = (props) => {
6
75
  const { children, apiEndpoint, initialActiveAccountId = null, initialSelectionMode = { type: "SINGLE" }, defaultSelectedFileIds = [] } = props;
@@ -216,7 +285,13 @@ var DriveProvider = (props) => {
216
285
  React.useEffect(() => {
217
286
  refreshAccounts();
218
287
  }, [refreshAccounts]);
219
- return /* @__PURE__ */ jsx(DriveContext.Provider, { value: {
288
+ const createUrl = useCallback((driveFile, options) => {
289
+ return driveCreateUrl(driveFile, apiEndpoint, options);
290
+ }, [apiEndpoint]);
291
+ const createSrcSet = useCallback((driveFile, format) => {
292
+ return driveCreateSrcSet(driveFile, apiEndpoint, format);
293
+ }, [apiEndpoint]);
294
+ return /* @__PURE__ */ jsx2(DriveContext.Provider, { value: {
220
295
  apiEndpoint,
221
296
  currentFolderId,
222
297
  path,
@@ -247,6 +322,8 @@ var DriveProvider = (props) => {
247
322
  selectionMode,
248
323
  selectedFileIds,
249
324
  setSelectedFileIds,
325
+ createUrl,
326
+ createSrcSet,
250
327
  navigateToFolder,
251
328
  navigateUp,
252
329
  refreshItems,
@@ -415,48 +492,6 @@ var useUpload = (apiEndpoint, activeAccountId, onUploadComplete) => {
415
492
  // src/client/file-chooser.tsx
416
493
  import { useState as useState6, useCallback as useCallback4, useMemo as useMemo3, useEffect as useEffect5 } from "react";
417
494
 
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
495
  // src/client/components/drive/explorer.tsx
461
496
  import React7, { useMemo as useMemo2, useEffect as useEffect4, useRef as useRef3 } from "react";
462
497
  import { Folder as Folder2, Loader2 as Loader22, RotateCcw, ChevronRight as ChevronRight3 } from "lucide-react";