@thetechfossil/upfiles 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,36 +1,89 @@
1
+ import * as React4 from 'react';
2
+ import React4__default, { useRef, useMemo, useState, useEffect, useCallback } from 'react';
3
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
+ import * as Dialog2 from '@radix-ui/react-dialog';
5
+
1
6
  // src/client.ts
7
+ var StorageQuotaError = class extends Error {
8
+ constructor(message, details) {
9
+ super(message);
10
+ this.name = "StorageQuotaError";
11
+ this.details = details;
12
+ }
13
+ };
2
14
  var UpfilesClient = class {
3
15
  constructor(opts) {
4
16
  this.baseUrl = opts.baseUrl?.replace(/\/$/, "");
5
- this.presignPath = opts.presignPath ?? "/api/plugin/upload/presigned-url";
17
+ this.presignPath = opts.presignPath ?? "/api/plugin/upload";
6
18
  this.presignUrl = opts.presignUrl;
7
19
  this.headers = opts.headers;
8
20
  this.withCredentials = opts.withCredentials;
9
21
  this.apiKey = opts.apiKey;
10
22
  this.apiKeyHeader = opts.apiKeyHeader ?? "authorization";
11
23
  this.thumbnailsPath = opts.thumbnailsPath ?? "/api/plugin/thumbnails";
12
- this.completionPath = opts.completionPath ?? "/api/plugin/upload/complete";
24
+ this.completionPath = opts.completionPath ?? "/api/plugin/files";
13
25
  this.foldersPath = opts.foldersPath ?? "/api/plugin/folders";
14
26
  this.callCompletionEndpoint = opts.callCompletionEndpoint ?? false;
27
+ this.getToken = opts.getToken;
28
+ this.projectId = opts.projectId;
29
+ this.projectName = opts.projectName;
15
30
  }
16
- async getPresignedUrl(params) {
17
- if (!params.fileName) throw new Error("fileName is required");
18
- if (!params.fileType) throw new Error("fileType is required");
19
- if (!params.fileSize || params.fileSize <= 0) throw new Error("fileSize must be > 0");
20
- const target = this.presignUrl ? this.presignUrl : this.baseUrl ? `${this.baseUrl}${this.presignPath}` : this.presignPath;
21
- const headers = {
22
- "content-type": "application/json",
23
- ...this.headers || {}
24
- };
31
+ /** Build auth headers for API requests. Priority: getToken (JWT) > apiKey > static headers */
32
+ async buildAuthHeaders() {
33
+ const headers = { ...this.headers || {} };
34
+ if (this.getToken) {
35
+ const token = await this.getToken();
36
+ if (token) {
37
+ headers["authorization"] = `Bearer ${token}`;
38
+ }
39
+ if (this.projectName) {
40
+ headers["x-project-name"] = this.projectName;
41
+ } else if (this.projectId) {
42
+ headers["x-project-id"] = this.projectId;
43
+ }
44
+ return headers;
45
+ }
25
46
  if (this.apiKey) {
26
47
  if (this.apiKeyHeader === "authorization") {
27
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
48
+ headers["authorization"] = this.apiKey.startsWith("upf_") ? `Bearer ${this.apiKey}` : this.apiKey;
28
49
  } else if (this.apiKeyHeader === "x-api-key") {
29
50
  headers["x-api-key"] = this.apiKey;
30
51
  } else {
31
52
  headers["x-up-api-key"] = this.apiKey;
32
53
  }
33
54
  }
55
+ return headers;
56
+ }
57
+ /** Get storage quota info for the project */
58
+ async getQuota() {
59
+ const basePath = this.presignPath.replace(/\/upload$/, "");
60
+ const target = this.baseUrl ? `${this.baseUrl}${basePath}/quota` : `${basePath}/quota`;
61
+ const headers = await this.buildAuthHeaders();
62
+ const res = await fetch(target, {
63
+ method: "GET",
64
+ headers,
65
+ credentials: this.withCredentials ? "include" : "same-origin"
66
+ });
67
+ if (!res.ok) {
68
+ let msg = "Failed to get storage quota";
69
+ try {
70
+ const data = await res.json();
71
+ msg = data.message || data.error || msg;
72
+ } catch {
73
+ }
74
+ throw new Error(msg);
75
+ }
76
+ return res.json();
77
+ }
78
+ async getPresignedUrl(params) {
79
+ if (!params.fileName) throw new Error("fileName is required");
80
+ if (!params.fileType) throw new Error("fileType is required");
81
+ if (!params.fileSize || params.fileSize <= 0) throw new Error("fileSize must be > 0");
82
+ const target = this.presignUrl ? this.presignUrl : this.baseUrl ? `${this.baseUrl}${this.presignPath}` : this.presignPath;
83
+ const headers = {
84
+ "content-type": "application/json",
85
+ ...await this.buildAuthHeaders()
86
+ };
34
87
  const res = await fetch(target, {
35
88
  method: "POST",
36
89
  headers,
@@ -41,8 +94,12 @@ var UpfilesClient = class {
41
94
  let msg = "Failed to get upload URL";
42
95
  try {
43
96
  const data = await res.json();
44
- msg = data.error || msg;
45
- } catch {
97
+ if (res.status === 413 && data.error === "STORAGE_QUOTA_EXCEEDED") {
98
+ throw new StorageQuotaError(data.message || "Storage quota exceeded", data.details);
99
+ }
100
+ msg = data.message || data.error || msg;
101
+ } catch (e) {
102
+ if (e instanceof StorageQuotaError) throw e;
46
103
  }
47
104
  throw new Error(msg);
48
105
  }
@@ -78,7 +135,11 @@ var UpfilesClient = class {
78
135
  originalName: file.name,
79
136
  size: file.size,
80
137
  contentType: file.type || "application/octet-stream",
81
- projectId: extras?.projectId
138
+ projectId: extras?.projectId,
139
+ url: presign.publicUrl,
140
+ userId: extras?.userId
141
+ // userId might be passed in extras but not typed yet in this method signature, easy fix is to cast or update signature. Better to update signature but for now access via cast to keep change minimal if extras type isn't fully updated here locally. Wait, the plan said "Update upload method to pass...". I should probably check if I can update extras type locally or just cast. The `upload` method signature has `extras`. I should probably update `extras` type in the signature if I can.
142
+ // Actually, looking at the code, `extras` is defined inline: `extras?: { projectId?: string; folderPath?: string; fetchThumbnails?: boolean; callCompletionEndpoint?: boolean }`. I should update this type definition too.
82
143
  });
83
144
  } catch (error) {
84
145
  throw new Error(`Upload verification failed: ${error.message}`);
@@ -107,22 +168,16 @@ var UpfilesClient = class {
107
168
  const target = this.baseUrl ? `${this.baseUrl}${this.completionPath}` : this.completionPath;
108
169
  const headers = {
109
170
  "content-type": "application/json",
110
- ...this.headers || {}
171
+ ...await this.buildAuthHeaders()
111
172
  };
112
- if (this.apiKey) {
113
- if (this.apiKeyHeader === "authorization") {
114
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
115
- } else if (this.apiKeyHeader === "x-api-key") {
116
- headers["x-api-key"] = this.apiKey;
117
- } else {
118
- headers["x-up-api-key"] = this.apiKey;
119
- }
120
- }
121
173
  const res = await fetch(target, {
122
174
  method: "POST",
123
175
  headers,
124
176
  credentials: this.withCredentials ? "include" : "same-origin",
125
- body: JSON.stringify(params)
177
+ body: JSON.stringify({
178
+ ...params,
179
+ key: params.fileKey
180
+ })
126
181
  });
127
182
  if (!res.ok) {
128
183
  let msg = "Failed to complete upload";
@@ -138,16 +193,7 @@ var UpfilesClient = class {
138
193
  async getThumbnails(fileKey) {
139
194
  const target = this.baseUrl ? `${this.baseUrl}${this.thumbnailsPath}` : this.thumbnailsPath;
140
195
  const url = `${target}?fileKey=${encodeURIComponent(fileKey)}`;
141
- const headers = { ...this.headers || {} };
142
- if (this.apiKey) {
143
- if (this.apiKeyHeader === "authorization") {
144
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
145
- } else if (this.apiKeyHeader === "x-api-key") {
146
- headers["x-api-key"] = this.apiKey;
147
- } else {
148
- headers["x-up-api-key"] = this.apiKey;
149
- }
150
- }
196
+ const headers = await this.buildAuthHeaders();
151
197
  const res = await fetch(url, { method: "GET", headers, credentials: this.withCredentials ? "include" : "same-origin" });
152
198
  if (!res.ok) {
153
199
  let msg = "Failed to fetch thumbnails";
@@ -161,26 +207,19 @@ var UpfilesClient = class {
161
207
  const data = await res.json();
162
208
  return data?.thumbnails ?? [];
163
209
  }
164
- async generateThumbnails(fileKey) {
210
+ async generateThumbnails(fileKey, sizes) {
165
211
  const target = this.baseUrl ? `${this.baseUrl}${this.thumbnailsPath}` : this.thumbnailsPath;
166
212
  const headers = {
167
213
  "content-type": "application/json",
168
- ...this.headers || {}
214
+ ...await this.buildAuthHeaders()
169
215
  };
170
- if (this.apiKey) {
171
- if (this.apiKeyHeader === "authorization") {
172
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
173
- } else if (this.apiKeyHeader === "x-api-key") {
174
- headers["x-api-key"] = this.apiKey;
175
- } else {
176
- headers["x-up-api-key"] = this.apiKey;
177
- }
178
- }
216
+ const body = { fileKey };
217
+ if (sizes && sizes.length > 0) body.sizes = sizes;
179
218
  const res = await fetch(target, {
180
219
  method: "POST",
181
220
  headers,
182
221
  credentials: this.withCredentials ? "include" : "same-origin",
183
- body: JSON.stringify({ fileKey })
222
+ body: JSON.stringify(body)
184
223
  });
185
224
  if (!res.ok) {
186
225
  let msg = "Failed to generate thumbnails";
@@ -194,19 +233,53 @@ var UpfilesClient = class {
194
233
  const data = await res.json();
195
234
  return { masterWebp: data.masterWebp, thumbnails: data.thumbnails ?? [] };
196
235
  }
236
+ /** Get thumbnail settings for the project (requires API key) */
237
+ async getThumbnailSettings() {
238
+ const basePath = this.presignPath.replace(/\/upload$/, "");
239
+ const target = this.baseUrl ? `${this.baseUrl}${basePath}/settings/thumbnails` : `${basePath}/settings/thumbnails`;
240
+ const headers = await this.buildAuthHeaders();
241
+ const res = await fetch(target, {
242
+ method: "GET",
243
+ headers,
244
+ credentials: this.withCredentials ? "include" : "same-origin"
245
+ });
246
+ if (!res.ok) {
247
+ let msg = "Failed to get thumbnail settings";
248
+ try {
249
+ const d = await res.json();
250
+ msg = d.message || msg;
251
+ } catch {
252
+ }
253
+ throw new Error(msg);
254
+ }
255
+ return res.json();
256
+ }
257
+ /** Update thumbnail settings for the project (requires API key with write scope) */
258
+ async updateThumbnailSettings(settings) {
259
+ const basePath = this.presignPath.replace(/\/upload$/, "");
260
+ const target = this.baseUrl ? `${this.baseUrl}${basePath}/settings/thumbnails` : `${basePath}/settings/thumbnails`;
261
+ const headers = { "content-type": "application/json", ...await this.buildAuthHeaders() };
262
+ const res = await fetch(target, {
263
+ method: "PATCH",
264
+ headers,
265
+ credentials: this.withCredentials ? "include" : "same-origin",
266
+ body: JSON.stringify(settings)
267
+ });
268
+ if (!res.ok) {
269
+ let msg = "Failed to update thumbnail settings";
270
+ try {
271
+ const d = await res.json();
272
+ msg = d.message || msg;
273
+ } catch {
274
+ }
275
+ throw new Error(msg);
276
+ }
277
+ return res.json();
278
+ }
197
279
  async getFolders(parentId) {
198
280
  const target = this.baseUrl ? `${this.baseUrl}${this.foldersPath}` : this.foldersPath;
199
281
  const url = parentId ? `${target}?parentId=${encodeURIComponent(parentId)}` : target;
200
- const headers = { ...this.headers || {} };
201
- if (this.apiKey) {
202
- if (this.apiKeyHeader === "authorization") {
203
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
204
- } else if (this.apiKeyHeader === "x-api-key") {
205
- headers["x-api-key"] = this.apiKey;
206
- } else {
207
- headers["x-up-api-key"] = this.apiKey;
208
- }
209
- }
282
+ const headers = await this.buildAuthHeaders();
210
283
  const res = await fetch(url, { method: "GET", headers, credentials: this.withCredentials ? "include" : "same-origin" });
211
284
  if (!res.ok) {
212
285
  let msg = "Failed to fetch folders";
@@ -224,17 +297,8 @@ var UpfilesClient = class {
224
297
  const target = this.baseUrl ? `${this.baseUrl}${this.foldersPath}` : this.foldersPath;
225
298
  const headers = {
226
299
  "content-type": "application/json",
227
- ...this.headers || {}
300
+ ...await this.buildAuthHeaders()
228
301
  };
229
- if (this.apiKey) {
230
- if (this.apiKeyHeader === "authorization") {
231
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
232
- } else if (this.apiKeyHeader === "x-api-key") {
233
- headers["x-api-key"] = this.apiKey;
234
- } else {
235
- headers["x-up-api-key"] = this.apiKey;
236
- }
237
- }
238
302
  const res = await fetch(target, {
239
303
  method: "POST",
240
304
  headers,
@@ -258,16 +322,7 @@ var UpfilesClient = class {
258
322
  const filesPath = `${basePath}/files`;
259
323
  const target = this.baseUrl ? `${this.baseUrl}${filesPath}` : filesPath;
260
324
  const url = params?.folderPath ? `${target}?folderPath=${encodeURIComponent(params.folderPath)}` : target;
261
- const headers = { ...this.headers || {} };
262
- if (this.apiKey) {
263
- if (this.apiKeyHeader === "authorization") {
264
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
265
- } else if (this.apiKeyHeader === "x-api-key") {
266
- headers["x-api-key"] = this.apiKey;
267
- } else {
268
- headers["x-up-api-key"] = this.apiKey;
269
- }
270
- }
325
+ const headers = await this.buildAuthHeaders();
271
326
  const res = await fetch(url, { method: "GET", headers, credentials: this.withCredentials ? "include" : "same-origin" });
272
327
  if (!res.ok) {
273
328
  let msg = "Failed to fetch files";
@@ -285,29 +340,21 @@ var UpfilesClient = class {
285
340
  };
286
341
  }
287
342
  getEventsUrl(projectId) {
288
- const basePath = this.thumbnailsPath.replace(/\/thumbnails$/, "");
289
- const eventsPath = `${basePath}/events`;
290
343
  const target = this.baseUrl ? `${this.baseUrl}/api/events` : "/api/events";
291
344
  const url = new URL(target, window.location.href);
292
- if (this.apiKey) {
293
- url.searchParams.set("apiKey", this.apiKey);
294
- }
295
- if (projectId) {
296
- url.searchParams.set("projectId", projectId);
345
+ if (!this.getToken && !this.apiKey && (projectId || this.projectId)) {
346
+ url.searchParams.set("projectId", projectId || this.projectId);
297
347
  }
298
348
  return url.toString();
299
349
  }
300
350
  };
301
-
302
- // src/Uploader.tsx
303
- import React, { useCallback, useMemo, useRef, useState } from "react";
304
- import { jsx, jsxs } from "react/jsx-runtime";
351
+ var THUMBNAIL_SIZE_OPTIONS = [64, 128, 256, 512];
305
352
  var Uploader = ({
306
353
  clientOptions,
307
354
  multiple = true,
308
355
  accept = ["image/*", "video/*", "audio/*", "application/pdf", "text/plain", "application/json"],
309
- maxFileSize = 100 * 1024 * 1024,
310
- maxFiles = 10,
356
+ maxFileSize = 0,
357
+ maxFiles = 0,
311
358
  onComplete,
312
359
  onError,
313
360
  onUploadBegin,
@@ -318,36 +365,124 @@ var Uploader = ({
318
365
  dropzoneClassName,
319
366
  children,
320
367
  projectId,
368
+ userId,
321
369
  folderPath,
322
370
  fetchThumbnails,
323
371
  autoRecordToDb = true,
324
372
  recordUrl = "/api/files",
325
- autoUpload = false
373
+ autoUpload = false,
374
+ thumbnailEnabled,
375
+ thumbnailSizes
326
376
  }) => {
327
377
  const inputRef = useRef(null);
328
378
  const client = useMemo(() => new UpfilesClient(clientOptions ?? {}), [clientOptions]);
329
379
  const [files, setFiles] = useState([]);
330
380
  const [isUploading, setIsUploading] = useState(false);
331
381
  const [isDragOver, setIsDragOver] = useState(false);
332
- const addFiles = useCallback((incoming) => {
333
- const filtered = incoming.filter((f2) => {
334
- if (f2.size > maxFileSize) {
335
- return false;
336
- }
337
- return true;
382
+ const [quotaError, setQuotaError] = useState(null);
383
+ const showThumbnailUI = thumbnailEnabled !== void 0;
384
+ const [thumbEnabled, setThumbEnabled] = useState(thumbnailEnabled ?? true);
385
+ const [thumbSizes, setThumbSizes] = useState(thumbnailSizes ?? [256, 512]);
386
+ const [thumbFileIds, setThumbFileIds] = useState(/* @__PURE__ */ new Set());
387
+ const thumbEnabledRef = useRef(thumbnailEnabled ?? true);
388
+ const thumbSizesRef = useRef(thumbnailSizes ?? [256, 512]);
389
+ const thumbFileIdsRef = useRef(/* @__PURE__ */ new Set());
390
+ useEffect(() => {
391
+ thumbEnabledRef.current = thumbEnabled;
392
+ }, [thumbEnabled]);
393
+ useEffect(() => {
394
+ thumbSizesRef.current = thumbSizes;
395
+ }, [thumbSizes]);
396
+ useEffect(() => {
397
+ thumbFileIdsRef.current = thumbFileIds;
398
+ }, [thumbFileIds]);
399
+ const pendingImages = useMemo(
400
+ () => files.filter((f2) => f2.type.startsWith("image/") && f2.status === "pending"),
401
+ [files]
402
+ );
403
+ const thumbSelectedCount = useMemo(
404
+ () => pendingImages.filter((f2) => thumbFileIds.has(f2.id)).length,
405
+ [pendingImages, thumbFileIds]
406
+ );
407
+ const handleThumbEnabledToggle = useCallback(() => {
408
+ const newVal = !thumbEnabledRef.current;
409
+ thumbEnabledRef.current = newVal;
410
+ setThumbEnabled(newVal);
411
+ if (newVal) {
412
+ setFiles((prev) => {
413
+ const ids = prev.filter((f2) => f2.type.startsWith("image/") && f2.status === "pending").map((f2) => f2.id);
414
+ if (ids.length > 0) {
415
+ const next = new Set(thumbFileIdsRef.current);
416
+ ids.forEach((id) => next.add(id));
417
+ thumbFileIdsRef.current = next;
418
+ setThumbFileIds(next);
419
+ }
420
+ return prev;
421
+ });
422
+ }
423
+ }, []);
424
+ const toggleThumbSize = useCallback((size) => {
425
+ setThumbSizes((prev) => {
426
+ const next = prev.includes(size) ? prev.filter((s) => s !== size) : [...prev, size].sort((a, b) => a - b);
427
+ thumbSizesRef.current = next;
428
+ return next;
429
+ });
430
+ }, []);
431
+ const toggleFileThumb = useCallback((fileId) => {
432
+ setThumbFileIds((prev) => {
433
+ const next = new Set(prev);
434
+ if (next.has(fileId)) next.delete(fileId);
435
+ else next.add(fileId);
436
+ thumbFileIdsRef.current = next;
437
+ return next;
338
438
  });
339
- const limited = filtered.slice(0, Math.max(0, maxFiles - files.length));
439
+ }, []);
440
+ const selectAllThumbs = useCallback(() => {
441
+ const ids = pendingImages.map((f2) => f2.id);
442
+ const next = new Set(thumbFileIdsRef.current);
443
+ ids.forEach((id) => next.add(id));
444
+ thumbFileIdsRef.current = next;
445
+ setThumbFileIds(next);
446
+ }, [pendingImages]);
447
+ const deselectAllThumbs = useCallback(() => {
448
+ const pendingSet = new Set(pendingImages.map((f2) => f2.id));
449
+ const next = new Set([...thumbFileIdsRef.current].filter((id) => !pendingSet.has(id)));
450
+ thumbFileIdsRef.current = next;
451
+ setThumbFileIds(next);
452
+ }, [pendingImages]);
453
+ const addFiles = useCallback((incoming) => {
454
+ setQuotaError(null);
455
+ const filtered = maxFileSize > 0 ? incoming.filter((f2) => f2.size <= maxFileSize) : incoming;
456
+ const limited = maxFiles > 0 ? filtered.slice(0, Math.max(0, maxFiles - files.length)) : filtered;
340
457
  const mapped = limited.map((f2) => ({
341
458
  id: Math.random().toString(36).slice(2),
342
459
  file: f2,
343
460
  name: f2.name,
344
461
  size: f2.size,
345
- type: f2.type,
462
+ type: f2.type || "application/octet-stream",
346
463
  progress: 0,
347
464
  status: "pending"
348
465
  }));
349
466
  setFiles((prev) => [...prev, ...mapped]);
350
- }, [files.length, maxFileSize, maxFiles]);
467
+ if (showThumbnailUI && thumbEnabledRef.current) {
468
+ const imageIds = mapped.filter((f2) => f2.type.startsWith("image/")).map((f2) => f2.id);
469
+ if (imageIds.length > 0) {
470
+ const next = new Set(thumbFileIdsRef.current);
471
+ imageIds.forEach((id) => next.add(id));
472
+ thumbFileIdsRef.current = next;
473
+ setThumbFileIds(next);
474
+ }
475
+ }
476
+ }, [files.length, maxFileSize, maxFiles, showThumbnailUI]);
477
+ const removeFile = useCallback((fileId) => {
478
+ setFiles((prev) => prev.filter((f2) => f2.id !== fileId));
479
+ if (showThumbnailUI) {
480
+ const next = new Set(thumbFileIdsRef.current);
481
+ next.delete(fileId);
482
+ thumbFileIdsRef.current = next;
483
+ setThumbFileIds(next);
484
+ }
485
+ }, [showThumbnailUI]);
351
486
  const onInputChange = useCallback((e) => {
352
487
  const fs = Array.from(e.target.files || []);
353
488
  addFiles(fs);
@@ -368,7 +503,7 @@ var Uploader = ({
368
503
  update({ status: "uploading", progress: 1 });
369
504
  const presign = await client.getPresignedUrl({
370
505
  fileName: item.name,
371
- fileType: item.type,
506
+ fileType: item.type || "application/octet-stream",
372
507
  fileSize: item.size,
373
508
  projectId,
374
509
  folderPath
@@ -395,14 +530,17 @@ var Uploader = ({
395
530
  xhr.send(item.file);
396
531
  });
397
532
  let completionResult = null;
398
- if (autoRecordToDb && projectId) {
533
+ const canRecordToDb = autoRecordToDb && (projectId || clientOptions?.projectName || clientOptions?.projectId || clientOptions?.apiKey);
534
+ if (canRecordToDb) {
399
535
  try {
400
536
  completionResult = await client.completeUpload({
401
537
  fileKey: presign.fileKey,
402
538
  originalName: item.name,
403
539
  size: item.size,
404
- contentType: item.type,
405
- projectId
540
+ contentType: item.type || "application/octet-stream",
541
+ projectId,
542
+ url: presign.publicUrl,
543
+ userId
406
544
  });
407
545
  } catch (e) {
408
546
  console.error("Failed to complete upload:", e);
@@ -426,39 +564,86 @@ var Uploader = ({
426
564
  }
427
565
  const finalFileKey = completionResult?.file?.key || presign.fileKey;
428
566
  const finalPublicUrl = completionResult?.file?.url || presign.publicUrl;
567
+ const fileThumbEnabled = showThumbnailUI && thumbEnabledRef.current && thumbFileIdsRef.current.has(item.id);
429
568
  const result = {
430
569
  ...item,
431
570
  status: "success",
432
571
  progress: 100,
433
- fileKey: presign.fileKey,
434
- publicUrl: presign.publicUrl,
572
+ fileKey: finalFileKey,
573
+ publicUrl: finalPublicUrl,
435
574
  projectId: presign.projectId,
436
575
  apiKeyId: presign.apiKeyId,
576
+ thumbnailEnabled: showThumbnailUI ? fileThumbEnabled : void 0,
577
+ thumbnailSizesSelected: showThumbnailUI ? fileThumbEnabled ? [...thumbSizesRef.current] : [] : void 0,
437
578
  // @ts-ignore - allow downstream to access thumbs if present
438
579
  thumbnails: thumbs,
439
580
  // @ts-ignore - allow downstream to access masterWebp if present
440
581
  masterWebp
441
582
  };
442
- update({ status: "success", progress: 100, fileKey: presign.fileKey, publicUrl: presign.publicUrl });
583
+ update({ status: "success", progress: 100, fileKey: finalFileKey, publicUrl: finalPublicUrl });
443
584
  onClientUploadComplete?.(result);
444
585
  return result;
445
586
  } catch (err) {
446
- const message = err instanceof Error ? err.message : "Upload failed";
587
+ const message = err instanceof StorageQuotaError ? err.message : err instanceof Error ? err.message : "Upload failed";
447
588
  update({ status: "error", error: message });
448
589
  throw err;
449
590
  }
450
- }, [client, projectId, folderPath, fetchThumbnails, autoRecordToDb, recordUrl, clientOptions, onUploadBegin, onUploadProgress, onClientUploadComplete]);
591
+ }, [client, projectId, userId, folderPath, fetchThumbnails, autoRecordToDb, recordUrl, clientOptions, onUploadBegin, onUploadProgress, onClientUploadComplete, showThumbnailUI]);
451
592
  const uploadAll = useCallback(async () => {
452
593
  const targets = files.filter((f2) => f2.status === "pending");
453
594
  if (!targets.length) return;
595
+ setQuotaError(null);
454
596
  setIsUploading(true);
455
597
  try {
598
+ try {
599
+ const quota = await client.getQuota();
600
+ if (quota.limitBytes > 0) {
601
+ const totalPendingSize = targets.reduce((sum, f2) => sum + f2.size, 0);
602
+ const remaining = quota.limitBytes - quota.usedBytes;
603
+ if (totalPendingSize > remaining) {
604
+ const fmt = (n) => {
605
+ if (n >= 1024 * 1024 * 1024) return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
606
+ if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(2)} MB`;
607
+ if (n >= 1024) return `${(n / 1024).toFixed(2)} KB`;
608
+ return `${n} B`;
609
+ };
610
+ const errMsg = `Storage quota exceeded. Remaining: ${fmt(remaining)}, Upload size: ${fmt(totalPendingSize)}`;
611
+ setQuotaError(errMsg);
612
+ targets.forEach((t) => {
613
+ setFiles((prev) => prev.map((f2) => f2.id === t.id ? { ...f2, status: "error", error: errMsg } : f2));
614
+ });
615
+ onError?.(new StorageQuotaError(errMsg, {
616
+ limit: quota.limitBytes,
617
+ used: quota.usedBytes,
618
+ remaining,
619
+ fileSize: totalPendingSize
620
+ }));
621
+ return;
622
+ }
623
+ }
624
+ } catch (e) {
625
+ if (e instanceof StorageQuotaError) {
626
+ setQuotaError(e.message);
627
+ targets.forEach((t) => {
628
+ setFiles((prev) => prev.map((f2) => f2.id === t.id ? { ...f2, status: "error", error: e.message } : f2));
629
+ });
630
+ onError?.(e);
631
+ return;
632
+ }
633
+ }
456
634
  const results = [];
457
635
  for (const f2 of targets) {
458
636
  try {
459
637
  const done = await uploadOne(f2);
460
638
  results.push(done);
461
639
  } catch (e) {
640
+ if (e instanceof StorageQuotaError) {
641
+ setQuotaError(e.message);
642
+ setFiles((prev) => prev.map(
643
+ (file) => file.status === "pending" ? { ...file, status: "error", error: e.message } : file
644
+ ));
645
+ break;
646
+ }
462
647
  }
463
648
  }
464
649
  onComplete?.(results.filter((f2) => f2.status === "success"));
@@ -467,8 +652,8 @@ var Uploader = ({
467
652
  } finally {
468
653
  setIsUploading(false);
469
654
  }
470
- }, [files, onComplete, onError, uploadOne]);
471
- React.useEffect(() => {
655
+ }, [files, client, onComplete, onError, uploadOne]);
656
+ React4__default.useEffect(() => {
472
657
  if (autoUpload && files.some((f2) => f2.status === "pending") && !isUploading) {
473
658
  const timer = setTimeout(() => {
474
659
  uploadAll();
@@ -515,15 +700,32 @@ var Uploader = ({
515
700
  children ?? /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
516
701
  /* @__PURE__ */ jsx("div", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100", children: "Drop files here or click to browse" }),
517
702
  /* @__PURE__ */ jsxs("div", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: [
518
- multiple ? `Up to ${maxFiles} files` : "Single file",
519
- " \u2022 Max ",
520
- (maxFileSize / (1024 * 1024)).toFixed(0),
521
- "MB"
703
+ !multiple ? "Single file" : maxFiles > 0 ? `Up to ${maxFiles} files` : "Multiple files",
704
+ maxFileSize > 0 ? ` \u2022 Max ${(maxFileSize / (1024 * 1024)).toFixed(0)}MB per file` : ""
522
705
  ] })
523
706
  ] })
524
707
  ]
525
708
  }
526
709
  ),
710
+ quotaError && /* @__PURE__ */ jsx("div", { className: "mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
711
+ /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 w-8 h-8 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center", children: /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "text-red-600 dark:text-red-400", children: [
712
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
713
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
714
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
715
+ ] }) }),
716
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-red-800 dark:text-red-300", children: quotaError }) }),
717
+ /* @__PURE__ */ jsx(
718
+ "button",
719
+ {
720
+ onClick: () => setQuotaError(null),
721
+ className: "text-red-400 hover:text-red-600 dark:hover:text-red-300 transition-colors",
722
+ children: /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
723
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
724
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
725
+ ] })
726
+ }
727
+ )
728
+ ] }) }),
527
729
  files.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-8 space-y-4", children: [
528
730
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 dark:border-gray-800 pb-4", children: [
529
731
  /* @__PURE__ */ jsx("h4", { className: "font-semibold text-gray-900 dark:text-gray-100", children: "Selected Files" }),
@@ -548,12 +750,76 @@ var Uploader = ({
548
750
  {
549
751
  className: "px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-sm font-semibold rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50 transition-colors",
550
752
  disabled: isUploading,
551
- onClick: () => setFiles([]),
753
+ onClick: () => {
754
+ setFiles([]);
755
+ if (showThumbnailUI) {
756
+ const empty = /* @__PURE__ */ new Set();
757
+ thumbFileIdsRef.current = empty;
758
+ setThumbFileIds(empty);
759
+ }
760
+ },
552
761
  children: "Clear"
553
762
  }
554
763
  )
555
764
  ] })
556
765
  ] }),
766
+ showThumbnailUI && pendingImages.length > 0 && /* @__PURE__ */ jsxs("div", { className: "bg-purple-50 dark:bg-purple-900/10 border border-purple-200 dark:border-purple-800/50 rounded-xl p-4", children: [
767
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-3", children: [
768
+ /* @__PURE__ */ jsxs("div", { children: [
769
+ /* @__PURE__ */ jsxs("p", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2", children: [
770
+ /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "text-purple-500", children: [
771
+ /* @__PURE__ */ jsx("polygon", { points: "12 2 2 7 12 12 22 7 12 2" }),
772
+ /* @__PURE__ */ jsx("polyline", { points: "2 17 12 22 22 17" }),
773
+ /* @__PURE__ */ jsx("polyline", { points: "2 12 12 17 22 12" })
774
+ ] }),
775
+ "Generate Thumbnails"
776
+ ] }),
777
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: thumbEnabled ? /* @__PURE__ */ jsxs(Fragment, { children: [
778
+ thumbSelectedCount,
779
+ " of ",
780
+ pendingImages.length,
781
+ " image",
782
+ pendingImages.length !== 1 ? "s" : "",
783
+ " selected",
784
+ " \xB7 ",
785
+ /* @__PURE__ */ jsx("button", { onClick: selectAllThumbs, className: "text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300", children: "Select all" }),
786
+ " \xB7 ",
787
+ /* @__PURE__ */ jsx("button", { onClick: deselectAllThumbs, className: "text-gray-500 hover:text-gray-600 dark:text-gray-400 dark:hover:text-gray-300", children: "Deselect all" })
788
+ ] }) : "Disabled for all images" })
789
+ ] }),
790
+ /* @__PURE__ */ jsx(
791
+ "button",
792
+ {
793
+ type: "button",
794
+ onClick: handleThumbEnabledToggle,
795
+ className: `relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none ${thumbEnabled ? "bg-blue-600" : "bg-gray-200 dark:bg-gray-700"}`,
796
+ role: "switch",
797
+ "aria-checked": thumbEnabled,
798
+ children: /* @__PURE__ */ jsx("span", { className: `pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition ${thumbEnabled ? "translate-x-5" : "translate-x-0"}` })
799
+ }
800
+ )
801
+ ] }),
802
+ thumbEnabled && /* @__PURE__ */ jsxs("div", { children: [
803
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-gray-600 dark:text-gray-400 mb-2", children: "Sizes:" }),
804
+ /* @__PURE__ */ jsx("div", { className: "flex gap-2 flex-wrap", children: THUMBNAIL_SIZE_OPTIONS.map((size) => {
805
+ const active = thumbSizes.includes(size);
806
+ return /* @__PURE__ */ jsxs(
807
+ "button",
808
+ {
809
+ type: "button",
810
+ onClick: () => toggleThumbSize(size),
811
+ className: `px-3 py-1 rounded-full text-xs font-medium border transition-colors ${active ? "bg-purple-600 border-purple-600 text-white" : "bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:border-purple-400 hover:text-purple-600"}`,
812
+ children: [
813
+ size,
814
+ "px"
815
+ ]
816
+ },
817
+ size
818
+ );
819
+ }) }),
820
+ thumbSizes.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-xs text-amber-600 dark:text-amber-400 mt-2", children: "Select at least one size." })
821
+ ] })
822
+ ] }),
557
823
  /* @__PURE__ */ jsx("div", { className: "grid gap-3", children: files.map((f2) => /* @__PURE__ */ jsx("div", { className: "group relative bg-white dark:bg-gray-900 border border-gray-100 dark:border-gray-800 rounded-xl p-4 transition-all hover:shadow-md", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
558
824
  /* @__PURE__ */ jsx("div", { className: "w-10 h-10 rounded-lg bg-gray-50 dark:bg-gray-800 flex items-center justify-center flex-shrink-0", children: f2.type.startsWith("image/") ? /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "text-indigo-500", children: [
559
825
  /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
@@ -577,7 +843,19 @@ var Uploader = ({
577
843
  className: `h-full transition-all duration-300 ${f2.status === "success" ? "bg-green-500" : "bg-blue-500"}`,
578
844
  style: { width: `${f2.progress}%` }
579
845
  }
580
- ) })
846
+ ) }),
847
+ showThumbnailUI && thumbEnabled && f2.type.startsWith("image/") && f2.status === "pending" && /* @__PURE__ */ jsxs("label", { className: "mt-1.5 inline-flex items-center gap-1.5 cursor-pointer select-none", children: [
848
+ /* @__PURE__ */ jsx(
849
+ "input",
850
+ {
851
+ type: "checkbox",
852
+ checked: thumbFileIds.has(f2.id),
853
+ onChange: () => toggleFileThumb(f2.id),
854
+ className: "w-3.5 h-3.5 rounded accent-purple-600 cursor-pointer"
855
+ }
856
+ ),
857
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: "Generate thumbnail" })
858
+ ] })
581
859
  ] }),
582
860
  /* @__PURE__ */ jsxs("div", { className: "flex-shrink-0 ml-4", children: [
583
861
  f2.status === "success" && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
@@ -611,7 +889,7 @@ var Uploader = ({
611
889
  {
612
890
  onClick: (e) => {
613
891
  e.stopPropagation();
614
- setFiles((prev) => prev.filter((file) => file.id !== f2.id));
892
+ removeFile(f2.id);
615
893
  },
616
894
  className: "p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-md transition-colors",
617
895
  title: "Remove",
@@ -626,10 +904,6 @@ var Uploader = ({
626
904
  ] })
627
905
  ] });
628
906
  };
629
-
630
- // src/ProjectFilesWidget.tsx
631
- import React2, { useEffect, useMemo as useMemo2, useState as useState2 } from "react";
632
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
633
907
  var ProjectFilesWidget = ({
634
908
  clientOptions,
635
909
  folderPath,
@@ -645,16 +919,16 @@ var ProjectFilesWidget = ({
645
919
  showDelete = false,
646
920
  refreshInterval = 5e3
647
921
  }) => {
648
- const client = useMemo2(() => new UpfilesClient(clientOptions ?? {}), [clientOptions]);
649
- const [files, setFiles] = useState2([]);
650
- const [loading, setLoading] = useState2(false);
651
- const [error, setError] = useState2(null);
652
- const [query, setQuery] = useState2("");
653
- const [savingKey, setSavingKey] = useState2(null);
654
- const [deletingKey, setDeletingKey] = useState2(null);
655
- const lastUpdatedRef = React2.useRef(null);
656
- const isFetchingRef = React2.useRef(false);
657
- const fetchFiles = React2.useCallback(async (isPolling = false) => {
922
+ const client = useMemo(() => new UpfilesClient(clientOptions ?? {}), [clientOptions]);
923
+ const [files, setFiles] = useState([]);
924
+ const [loading, setLoading] = useState(false);
925
+ const [error, setError] = useState(null);
926
+ const [query, setQuery] = useState("");
927
+ const [savingKey, setSavingKey] = useState(null);
928
+ const [deletingKey, setDeletingKey] = useState(null);
929
+ const lastUpdatedRef = React4__default.useRef(null);
930
+ const isFetchingRef = React4__default.useRef(false);
931
+ const fetchFiles = React4__default.useCallback(async (isPolling = false) => {
658
932
  if (isFetchingRef.current) return;
659
933
  isFetchingRef.current = true;
660
934
  if (!isPolling) setLoading(true);
@@ -699,10 +973,18 @@ var ProjectFilesWidget = ({
699
973
  "content-type": "application/json",
700
974
  ...clientOptions?.headers || {}
701
975
  };
702
- if (clientOptions?.apiKey) {
976
+ const token = clientOptions?.getToken?.();
977
+ if (token) {
978
+ headers["authorization"] = `Bearer ${token}`;
979
+ if (clientOptions.projectName) {
980
+ headers["x-project-name"] = clientOptions.projectName;
981
+ } else if (clientOptions.projectId) {
982
+ headers["x-project-id"] = clientOptions.projectId;
983
+ }
984
+ } else if (clientOptions?.apiKey) {
703
985
  const keyHeader = clientOptions.apiKeyHeader || "authorization";
704
986
  if (keyHeader === "authorization") {
705
- headers["authorization"] = clientOptions.apiKey.startsWith("upk_") ? `Bearer ${clientOptions.apiKey}` : clientOptions.apiKey;
987
+ headers["authorization"] = clientOptions.apiKey.startsWith("upf_") ? `Bearer ${clientOptions.apiKey}` : clientOptions.apiKey;
706
988
  } else {
707
989
  headers[keyHeader] = clientOptions.apiKey;
708
990
  }
@@ -722,9 +1004,9 @@ var ProjectFilesWidget = ({
722
1004
  setDeletingKey(null);
723
1005
  }
724
1006
  };
725
- return /* @__PURE__ */ jsxs2("div", { className, children: [
726
- /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, alignItems: "center", marginBottom: 8 }, children: [
727
- /* @__PURE__ */ jsx2(
1007
+ return /* @__PURE__ */ jsxs("div", { className, children: [
1008
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center", marginBottom: 8 }, children: [
1009
+ /* @__PURE__ */ jsx(
728
1010
  "input",
729
1011
  {
730
1012
  type: "text",
@@ -734,7 +1016,7 @@ var ProjectFilesWidget = ({
734
1016
  style: { flex: 1, padding: 8, border: "1px solid #e5e7eb", borderRadius: 6 }
735
1017
  }
736
1018
  ),
737
- /* @__PURE__ */ jsx2(
1019
+ /* @__PURE__ */ jsx(
738
1020
  "button",
739
1021
  {
740
1022
  onClick: () => {
@@ -746,26 +1028,26 @@ var ProjectFilesWidget = ({
746
1028
  }
747
1029
  )
748
1030
  ] }),
749
- loading && /* @__PURE__ */ jsx2("div", { style: { fontSize: 12, color: "#666" }, children: "Loading files\u2026" }),
750
- error && /* @__PURE__ */ jsx2("div", { style: { fontSize: 12, color: "#b91c1c" }, children: error }),
751
- !loading && !error && /* @__PURE__ */ jsxs2("ul", { className: listClassName, style: { listStyle: "none", padding: 0, margin: 0, display: "grid", gap: 8 }, children: [
752
- visible.map((f2) => /* @__PURE__ */ jsxs2(
1031
+ loading && /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#666" }, children: "Loading files\u2026" }),
1032
+ error && /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#b91c1c" }, children: error }),
1033
+ !loading && !error && /* @__PURE__ */ jsxs("ul", { className: listClassName, style: { listStyle: "none", padding: 0, margin: 0, display: "grid", gap: 8 }, children: [
1034
+ visible.map((f2) => /* @__PURE__ */ jsxs(
753
1035
  "li",
754
1036
  {
755
1037
  className: itemClassName,
756
1038
  style: { border: "1px solid #e5e7eb", borderRadius: 8, padding: 10, display: "flex", justifyContent: "space-between", alignItems: "center" },
757
1039
  children: [
758
- /* @__PURE__ */ jsxs2("div", { style: { minWidth: 0 }, children: [
759
- /* @__PURE__ */ jsx2("div", { style: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }, children: f2.originalName }),
760
- /* @__PURE__ */ jsxs2("div", { style: { fontSize: 12, color: "#666" }, children: [
1040
+ /* @__PURE__ */ jsxs("div", { style: { minWidth: 0 }, children: [
1041
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }, children: f2.originalName }),
1042
+ /* @__PURE__ */ jsxs("div", { style: { fontSize: 12, color: "#666" }, children: [
761
1043
  (f2.size / (1024 * 1024)).toFixed(2),
762
1044
  " MB \u2022 ",
763
1045
  f2.contentType
764
1046
  ] })
765
1047
  ] }),
766
- /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
767
- /* @__PURE__ */ jsx2("a", { href: f2.url, target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "#2563eb" }, children: "View" }),
768
- /* @__PURE__ */ jsx2(
1048
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
1049
+ /* @__PURE__ */ jsx("a", { href: f2.url, target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "#2563eb" }, children: "View" }),
1050
+ /* @__PURE__ */ jsx(
769
1051
  "button",
770
1052
  {
771
1053
  onClick: async () => {
@@ -802,7 +1084,7 @@ var ProjectFilesWidget = ({
802
1084
  children: savingKey === f2.key ? "Saving\u2026" : "Use"
803
1085
  }
804
1086
  ),
805
- showDelete && /* @__PURE__ */ jsx2(
1087
+ showDelete && /* @__PURE__ */ jsx(
806
1088
  "button",
807
1089
  {
808
1090
  onClick: () => handleDelete(f2.key),
@@ -816,17 +1098,26 @@ var ProjectFilesWidget = ({
816
1098
  },
817
1099
  f2.key
818
1100
  )),
819
- visible.length === 0 && /* @__PURE__ */ jsx2("li", { style: { fontSize: 12, color: "#666" }, children: "No files found" })
1101
+ visible.length === 0 && /* @__PURE__ */ jsx("li", { style: { fontSize: 12, color: "#666" }, children: "No files found" })
820
1102
  ] })
821
1103
  ] });
822
1104
  };
823
1105
 
824
- // src/ConnectProjectDialog.tsx
825
- import * as React3 from "react";
826
- import * as Dialog from "@radix-ui/react-dialog";
827
-
828
1106
  // src/projectConnect.ts
829
1107
  var f = (globalThis.fetch || fetch).bind(globalThis);
1108
+ function buildInit(opts, extra) {
1109
+ const headers = {
1110
+ ...extra?.headers || {}
1111
+ };
1112
+ if (opts.token) {
1113
+ headers["authorization"] = `Bearer ${opts.token}`;
1114
+ }
1115
+ return {
1116
+ ...extra,
1117
+ headers,
1118
+ credentials: opts.token ? "same-origin" : "include"
1119
+ };
1120
+ }
830
1121
  async function httpJson(input, init, useFetch) {
831
1122
  const doFetch = useFetch ?? f;
832
1123
  const res = await doFetch(input, init);
@@ -834,7 +1125,7 @@ async function httpJson(input, init, useFetch) {
834
1125
  let msg = `Request failed (${res.status})`;
835
1126
  try {
836
1127
  const j = await res.json();
837
- msg = j?.error || msg;
1128
+ msg = j?.error || j?.message || msg;
838
1129
  } catch {
839
1130
  }
840
1131
  throw new Error(msg);
@@ -845,7 +1136,7 @@ async function listProjects(opts) {
845
1136
  const url = `${opts.baseUrl.replace(/\/$/, "")}/api/projects`;
846
1137
  const data = await httpJson(
847
1138
  url,
848
- { method: "GET", credentials: "include" },
1139
+ buildInit(opts, { method: "GET" }),
849
1140
  opts.fetch
850
1141
  );
851
1142
  return data?.projects ?? [];
@@ -854,53 +1145,74 @@ async function createProject(opts) {
854
1145
  const base = opts.baseUrl.replace(/\/$/, "");
855
1146
  const created = await httpJson(
856
1147
  `${base}/api/projects`,
857
- {
1148
+ buildInit(opts, {
858
1149
  method: "POST",
859
- credentials: "include",
860
1150
  headers: { "content-type": "application/json" },
861
- body: JSON.stringify({ name: opts.name })
862
- },
1151
+ body: JSON.stringify({
1152
+ name: opts.name,
1153
+ isInHouse: opts.isInHouse,
1154
+ freeStorageLimitBytes: opts.freeStorageLimitBytes
1155
+ })
1156
+ }),
863
1157
  opts.fetch
864
1158
  );
865
1159
  const projectId = created?.project?.id;
866
1160
  if (!projectId) throw new Error("Failed to create project");
1161
+ return { projectId, project: created?.project };
1162
+ }
1163
+ async function createProjectWithApiKey(opts) {
1164
+ const base = opts.baseUrl.replace(/\/$/, "");
1165
+ const { projectId } = await createProject(opts);
867
1166
  const keyLabel = opts.keyName || `Plugin key ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
868
1167
  const key = await httpJson(
869
1168
  `${base}/api/projects/${projectId}/keys`,
870
- {
1169
+ buildInit(opts, {
871
1170
  method: "POST",
872
- credentials: "include",
873
1171
  headers: { "content-type": "application/json" },
874
1172
  body: JSON.stringify({ name: keyLabel })
875
- },
1173
+ }),
876
1174
  opts.fetch
877
1175
  );
878
- const plaintext = key?.key?.plaintext;
1176
+ const plaintext = key?.key?.secret ?? key?.key?.plaintext;
879
1177
  if (!plaintext) throw new Error("Failed to create API key");
880
1178
  return { apiKey: plaintext, projectId };
881
1179
  }
1180
+ async function updateProject(opts) {
1181
+ const base = opts.baseUrl.replace(/\/$/, "");
1182
+ const body = {};
1183
+ if (opts.isInHouse !== void 0) body.isInHouse = opts.isInHouse;
1184
+ if (opts.freeStorageLimitBytes !== void 0) body.freeStorageLimitBytes = opts.freeStorageLimitBytes;
1185
+ await httpJson(
1186
+ `${base}/api/projects/${opts.projectId}`,
1187
+ buildInit(opts, {
1188
+ method: "PUT",
1189
+ headers: { "content-type": "application/json" },
1190
+ body: JSON.stringify(body)
1191
+ }),
1192
+ opts.fetch
1193
+ );
1194
+ }
882
1195
  async function connectProject(opts) {
883
1196
  const base = opts.baseUrl.replace(/\/$/, "");
884
1197
  const keyLabel = opts.keyName || `Plugin key ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
885
1198
  const key = await httpJson(
886
1199
  `${base}/api/projects/${opts.projectId}/keys`,
887
- {
1200
+ buildInit(opts, {
888
1201
  method: "POST",
889
- credentials: "include",
890
1202
  headers: { "content-type": "application/json" },
891
1203
  body: JSON.stringify({ name: keyLabel })
892
- },
1204
+ }),
893
1205
  opts.fetch
894
1206
  );
895
- const plaintext = key?.key?.plaintext;
1207
+ const plaintext = key?.key?.secret ?? key?.key?.plaintext;
896
1208
  if (!plaintext) throw new Error("Failed to create API key");
897
1209
  return { apiKey: plaintext };
898
1210
  }
899
1211
  function addApiKeyManually(apiKey) {
900
1212
  const k = (apiKey || "").trim();
901
1213
  if (!k) throw new Error("API key is required");
902
- if (!/^upk_[A-Za-z0-9]+/.test(k)) {
903
- console.warn("Provided API key does not match expected format upk_*");
1214
+ if (!/^upf_[A-Za-z0-9_-]+/.test(k)) {
1215
+ console.warn("Provided API key does not match expected format upf_*");
904
1216
  }
905
1217
  return { apiKey: k };
906
1218
  }
@@ -914,19 +1226,16 @@ function connectUsingApiKey(opts) {
914
1226
  const client = new UpfilesClient({ baseUrl, apiKey, apiKeyHeader: opts.apiKeyHeader ?? "authorization" });
915
1227
  return { apiKey, client };
916
1228
  }
917
-
918
- // src/ConnectProjectDialog.tsx
919
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
920
1229
  function ConnectProjectDialog(props) {
921
1230
  const { baseUrl, open, onOpenChange, onConnected, fetchImpl, title, description, className } = props;
922
- const [mode, setMode] = React3.useState("existing");
923
- const [loading, setLoading] = React3.useState(false);
924
- const [error, setError] = React3.useState(null);
925
- const [projects, setProjects] = React3.useState([]);
926
- const [selectedProjectId, setSelectedProjectId] = React3.useState("");
927
- const [manualKey, setManualKey] = React3.useState("");
928
- const [newProjectName, setNewProjectName] = React3.useState("");
929
- const resetState = React3.useCallback(() => {
1231
+ const [mode, setMode] = React4.useState("existing");
1232
+ const [loading, setLoading] = React4.useState(false);
1233
+ const [error, setError] = React4.useState(null);
1234
+ const [projects, setProjects] = React4.useState([]);
1235
+ const [selectedProjectId, setSelectedProjectId] = React4.useState("");
1236
+ const [manualKey, setManualKey] = React4.useState("");
1237
+ const [newProjectName, setNewProjectName] = React4.useState("");
1238
+ const resetState = React4.useCallback(() => {
930
1239
  setMode("existing");
931
1240
  setLoading(false);
932
1241
  setError(null);
@@ -935,7 +1244,7 @@ function ConnectProjectDialog(props) {
935
1244
  setManualKey("");
936
1245
  setNewProjectName("");
937
1246
  }, []);
938
- React3.useEffect(() => {
1247
+ React4.useEffect(() => {
939
1248
  const run = async () => {
940
1249
  if (!open || mode !== "existing") return;
941
1250
  try {
@@ -987,7 +1296,7 @@ function ConnectProjectDialog(props) {
987
1296
  setLoading(true);
988
1297
  setError(null);
989
1298
  if (!newProjectName.trim()) throw new Error("Project name is required");
990
- const { apiKey, projectId } = await createProject({ baseUrl, name: newProjectName.trim(), fetch: fetchImpl });
1299
+ const { apiKey, projectId } = await createProjectWithApiKey({ baseUrl, name: newProjectName.trim(), fetch: fetchImpl });
991
1300
  onConnected(apiKey, { projectId, source: "new" });
992
1301
  onOpenChange(false);
993
1302
  resetState();
@@ -997,8 +1306,8 @@ function ConnectProjectDialog(props) {
997
1306
  setLoading(false);
998
1307
  }
999
1308
  };
1000
- const ModeSwitcher = /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-3 gap-2", children: [
1001
- /* @__PURE__ */ jsx3(
1309
+ const ModeSwitcher = /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 gap-2", children: [
1310
+ /* @__PURE__ */ jsx(
1002
1311
  "button",
1003
1312
  {
1004
1313
  type: "button",
@@ -1007,7 +1316,7 @@ function ConnectProjectDialog(props) {
1007
1316
  children: "Connect existing"
1008
1317
  }
1009
1318
  ),
1010
- /* @__PURE__ */ jsx3(
1319
+ /* @__PURE__ */ jsx(
1011
1320
  "button",
1012
1321
  {
1013
1322
  type: "button",
@@ -1016,7 +1325,7 @@ function ConnectProjectDialog(props) {
1016
1325
  children: "Add API key"
1017
1326
  }
1018
1327
  ),
1019
- /* @__PURE__ */ jsx3(
1328
+ /* @__PURE__ */ jsx(
1020
1329
  "button",
1021
1330
  {
1022
1331
  type: "button",
@@ -1026,19 +1335,19 @@ function ConnectProjectDialog(props) {
1026
1335
  }
1027
1336
  )
1028
1337
  ] });
1029
- return /* @__PURE__ */ jsx3(Dialog.Root, { open, onOpenChange: (o) => {
1338
+ return /* @__PURE__ */ jsx(Dialog2.Root, { open, onOpenChange: (o) => {
1030
1339
  if (!o) resetState();
1031
1340
  onOpenChange(o);
1032
- }, children: /* @__PURE__ */ jsxs3(Dialog.Portal, { children: [
1033
- /* @__PURE__ */ jsx3(Dialog.Overlay, { className: "fixed inset-0 bg-black/40" }),
1034
- /* @__PURE__ */ jsx3(Dialog.Content, { className: `fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[95vw] max-w-md rounded-lg border bg-white p-4 shadow-lg ${className ?? ""}`, children: /* @__PURE__ */ jsxs3("div", { className: "space-y-3", children: [
1035
- /* @__PURE__ */ jsx3(Dialog.Title, { className: "text-lg font-semibold", children: title ?? "Connect to Upfiles" }),
1036
- /* @__PURE__ */ jsx3(Dialog.Description, { className: "text-sm text-muted-foreground", children: description ?? "Choose how to connect your project to retrieve an API key." }),
1341
+ }, children: /* @__PURE__ */ jsxs(Dialog2.Portal, { children: [
1342
+ /* @__PURE__ */ jsx(Dialog2.Overlay, { className: "fixed inset-0 bg-black/40" }),
1343
+ /* @__PURE__ */ jsx(Dialog2.Content, { className: `fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[95vw] max-w-md rounded-lg border bg-white p-4 shadow-lg ${className ?? ""}`, children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1344
+ /* @__PURE__ */ jsx(Dialog2.Title, { className: "text-lg font-semibold", children: title ?? "Connect to Upfiles" }),
1345
+ /* @__PURE__ */ jsx(Dialog2.Description, { className: "text-sm text-muted-foreground", children: description ?? "Choose how to connect your project to retrieve an API key." }),
1037
1346
  ModeSwitcher,
1038
- error && /* @__PURE__ */ jsx3("div", { className: "rounded border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700", children: error }),
1039
- mode === "existing" && /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
1040
- /* @__PURE__ */ jsx3("label", { className: "text-sm font-medium", children: "Project" }),
1041
- /* @__PURE__ */ jsxs3(
1347
+ error && /* @__PURE__ */ jsx("div", { className: "rounded border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700", children: error }),
1348
+ mode === "existing" && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1349
+ /* @__PURE__ */ jsx("label", { className: "text-sm font-medium", children: "Project" }),
1350
+ /* @__PURE__ */ jsxs(
1042
1351
  "select",
1043
1352
  {
1044
1353
  className: "w-full rounded border px-3 py-2 text-sm",
@@ -1046,14 +1355,14 @@ function ConnectProjectDialog(props) {
1046
1355
  onChange: (e) => setSelectedProjectId(e.target.value),
1047
1356
  disabled: loading,
1048
1357
  children: [
1049
- projects.length === 0 && /* @__PURE__ */ jsx3("option", { value: "", children: loading ? "Loading..." : "No projects found" }),
1050
- projects.map((p) => /* @__PURE__ */ jsx3("option", { value: p.id, children: p.name }, p.id))
1358
+ projects.length === 0 && /* @__PURE__ */ jsx("option", { value: "", children: loading ? "Loading..." : "No projects found" }),
1359
+ projects.map((p) => /* @__PURE__ */ jsx("option", { value: p.id, children: p.name }, p.id))
1051
1360
  ]
1052
1361
  }
1053
1362
  ),
1054
- /* @__PURE__ */ jsxs3("div", { className: "flex justify-end gap-2 pt-2", children: [
1055
- /* @__PURE__ */ jsx3(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx3("button", { type: "button", className: "px-3 py-2 text-sm rounded border", children: "Cancel" }) }),
1056
- /* @__PURE__ */ jsx3(
1363
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 pt-2", children: [
1364
+ /* @__PURE__ */ jsx(Dialog2.Close, { asChild: true, children: /* @__PURE__ */ jsx("button", { type: "button", className: "px-3 py-2 text-sm rounded border", children: "Cancel" }) }),
1365
+ /* @__PURE__ */ jsx(
1057
1366
  "button",
1058
1367
  {
1059
1368
  type: "button",
@@ -1065,9 +1374,9 @@ function ConnectProjectDialog(props) {
1065
1374
  )
1066
1375
  ] })
1067
1376
  ] }),
1068
- mode === "manual" && /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
1069
- /* @__PURE__ */ jsx3("label", { className: "text-sm font-medium", children: "API key" }),
1070
- /* @__PURE__ */ jsx3(
1377
+ mode === "manual" && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1378
+ /* @__PURE__ */ jsx("label", { className: "text-sm font-medium", children: "API key" }),
1379
+ /* @__PURE__ */ jsx(
1071
1380
  "input",
1072
1381
  {
1073
1382
  className: "w-full rounded border px-3 py-2 text-sm",
@@ -1077,9 +1386,9 @@ function ConnectProjectDialog(props) {
1077
1386
  disabled: loading
1078
1387
  }
1079
1388
  ),
1080
- /* @__PURE__ */ jsxs3("div", { className: "flex justify-end gap-2 pt-2", children: [
1081
- /* @__PURE__ */ jsx3(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx3("button", { type: "button", className: "px-3 py-2 text-sm rounded border", children: "Cancel" }) }),
1082
- /* @__PURE__ */ jsx3(
1389
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 pt-2", children: [
1390
+ /* @__PURE__ */ jsx(Dialog2.Close, { asChild: true, children: /* @__PURE__ */ jsx("button", { type: "button", className: "px-3 py-2 text-sm rounded border", children: "Cancel" }) }),
1391
+ /* @__PURE__ */ jsx(
1083
1392
  "button",
1084
1393
  {
1085
1394
  type: "button",
@@ -1091,9 +1400,9 @@ function ConnectProjectDialog(props) {
1091
1400
  )
1092
1401
  ] })
1093
1402
  ] }),
1094
- mode === "new" && /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
1095
- /* @__PURE__ */ jsx3("label", { className: "text-sm font-medium", children: "Project name" }),
1096
- /* @__PURE__ */ jsx3(
1403
+ mode === "new" && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1404
+ /* @__PURE__ */ jsx("label", { className: "text-sm font-medium", children: "Project name" }),
1405
+ /* @__PURE__ */ jsx(
1097
1406
  "input",
1098
1407
  {
1099
1408
  className: "w-full rounded border px-3 py-2 text-sm",
@@ -1103,10 +1412,10 @@ function ConnectProjectDialog(props) {
1103
1412
  disabled: loading
1104
1413
  }
1105
1414
  ),
1106
- /* @__PURE__ */ jsx3("p", { className: "text-xs text-muted-foreground", children: "A project will be created in your Upfiles account and an API key generated." }),
1107
- /* @__PURE__ */ jsxs3("div", { className: "flex justify-end gap-2 pt-2", children: [
1108
- /* @__PURE__ */ jsx3(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx3("button", { type: "button", className: "px-3 py-2 text-sm rounded border", children: "Cancel" }) }),
1109
- /* @__PURE__ */ jsx3(
1415
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "A project will be created in your Upfiles account and an API key generated." }),
1416
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 pt-2", children: [
1417
+ /* @__PURE__ */ jsx(Dialog2.Close, { asChild: true, children: /* @__PURE__ */ jsx("button", { type: "button", className: "px-3 py-2 text-sm rounded border", children: "Cancel" }) }),
1418
+ /* @__PURE__ */ jsx(
1110
1419
  "button",
1111
1420
  {
1112
1421
  type: "button",
@@ -1156,60 +1465,77 @@ async function fetchProjectImagesWithThumbs(clientOptions, opts) {
1156
1465
  results.sort((a, b) => (keyToIndex.get(a.key) ?? 0) - (keyToIndex.get(b.key) ?? 0));
1157
1466
  return { items: results, updatedAt };
1158
1467
  }
1159
-
1160
- // src/ImageManager.tsx
1161
- import * as React4 from "react";
1162
- import * as Dialog2 from "@radix-ui/react-dialog";
1163
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1164
- var IconGrid = ({ className }) => /* @__PURE__ */ jsxs4("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1165
- /* @__PURE__ */ jsx4("rect", { width: "7", height: "7", x: "3", y: "3", rx: "1" }),
1166
- /* @__PURE__ */ jsx4("rect", { width: "7", height: "7", x: "14", y: "3", rx: "1" }),
1167
- /* @__PURE__ */ jsx4("rect", { width: "7", height: "7", x: "14", y: "14", rx: "1" }),
1168
- /* @__PURE__ */ jsx4("rect", { width: "7", height: "7", x: "3", y: "14", rx: "1" })
1468
+ var IconGrid = ({ className }) => /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1469
+ /* @__PURE__ */ jsx("rect", { width: "7", height: "7", x: "3", y: "3", rx: "1" }),
1470
+ /* @__PURE__ */ jsx("rect", { width: "7", height: "7", x: "14", y: "3", rx: "1" }),
1471
+ /* @__PURE__ */ jsx("rect", { width: "7", height: "7", x: "14", y: "14", rx: "1" }),
1472
+ /* @__PURE__ */ jsx("rect", { width: "7", height: "7", x: "3", y: "14", rx: "1" })
1169
1473
  ] });
1170
- var IconUploadCloud = ({ className }) => /* @__PURE__ */ jsxs4("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1171
- /* @__PURE__ */ jsx4("path", { d: "M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" }),
1172
- /* @__PURE__ */ jsx4("path", { d: "M12 12v9" }),
1173
- /* @__PURE__ */ jsx4("path", { d: "m16 16-4-4-4 4" })
1474
+ var IconUploadCloud = ({ className }) => /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1475
+ /* @__PURE__ */ jsx("path", { d: "M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" }),
1476
+ /* @__PURE__ */ jsx("path", { d: "M12 12v9" }),
1477
+ /* @__PURE__ */ jsx("path", { d: "m16 16-4-4-4 4" })
1174
1478
  ] });
1175
- var IconFolder = ({ className }) => /* @__PURE__ */ jsx4("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", stroke: "currentColor", strokeWidth: "0", className, children: /* @__PURE__ */ jsx4("path", { d: "M19.5 21a3 3 0 0 0 3-3v-4.5a3 3 0 0 0-3-3h-15a3 3 0 0 0-3 3V18a3 3 0 0 0 3 3h15ZM1.5 10.146V6a3 3 0 0 1 3-3h5.379a2.25 2.25 0 0 1 1.59.659l2.122 2.121c.14.141.331.22.53.22H19.5a3 3 0 0 1 3 3v1.146A4.483 4.483 0 0 0 19.5 9h-15a4.483 4.483 0 0 0-3 1.146Z" }) });
1176
- var IconFolderPlus = ({ className }) => /* @__PURE__ */ jsxs4("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1177
- /* @__PURE__ */ jsx4("path", { d: "M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 2H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2Z" }),
1178
- /* @__PURE__ */ jsx4("line", { x1: "12", x2: "12", y1: "10", y2: "16" }),
1179
- /* @__PURE__ */ jsx4("line", { x1: "9", x2: "15", y1: "13", y2: "13" })
1479
+ var IconFolder = ({ className }) => /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", stroke: "currentColor", strokeWidth: "0", className, children: /* @__PURE__ */ jsx("path", { d: "M19.5 21a3 3 0 0 0 3-3v-4.5a3 3 0 0 0-3-3h-15a3 3 0 0 0-3 3V18a3 3 0 0 0 3 3h15ZM1.5 10.146V6a3 3 0 0 1 3-3h5.379a2.25 2.25 0 0 1 1.59.659l2.122 2.121c.14.141.331.22.53.22H19.5a3 3 0 0 1 3 3v1.146A4.483 4.483 0 0 0 19.5 9h-15a4.483 4.483 0 0 0-3 1.146Z" }) });
1480
+ var IconFolderPlus = ({ className }) => /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1481
+ /* @__PURE__ */ jsx("path", { d: "M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 2H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2Z" }),
1482
+ /* @__PURE__ */ jsx("line", { x1: "12", x2: "12", y1: "10", y2: "16" }),
1483
+ /* @__PURE__ */ jsx("line", { x1: "9", x2: "15", y1: "13", y2: "13" })
1180
1484
  ] });
1181
- var IconSearch = ({ className }) => /* @__PURE__ */ jsxs4("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1182
- /* @__PURE__ */ jsx4("circle", { cx: "11", cy: "11", r: "8" }),
1183
- /* @__PURE__ */ jsx4("path", { d: "m21 21-4.3-4.3" })
1485
+ var IconSearch = ({ className }) => /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1486
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "8" }),
1487
+ /* @__PURE__ */ jsx("path", { d: "m21 21-4.3-4.3" })
1184
1488
  ] });
1185
- var IconRefresh = ({ className }) => /* @__PURE__ */ jsxs4("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1186
- /* @__PURE__ */ jsx4("path", { d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }),
1187
- /* @__PURE__ */ jsx4("path", { d: "M21 3v5h-5" }),
1188
- /* @__PURE__ */ jsx4("path", { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }),
1189
- /* @__PURE__ */ jsx4("path", { d: "M8 16H3v5" })
1489
+ var IconRefresh = ({ className }) => /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1490
+ /* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }),
1491
+ /* @__PURE__ */ jsx("path", { d: "M21 3v5h-5" }),
1492
+ /* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }),
1493
+ /* @__PURE__ */ jsx("path", { d: "M8 16H3v5" })
1190
1494
  ] });
1191
- var IconChevronRight = ({ className }) => /* @__PURE__ */ jsx4("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: /* @__PURE__ */ jsx4("path", { d: "m9 18 6-6-6-6" }) });
1192
- var IconTrash = ({ className }) => /* @__PURE__ */ jsxs4("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1193
- /* @__PURE__ */ jsx4("path", { d: "M3 6h18" }),
1194
- /* @__PURE__ */ jsx4("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
1195
- /* @__PURE__ */ jsx4("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" }),
1196
- /* @__PURE__ */ jsx4("line", { x1: "10", x2: "10", y1: "11", y2: "17" }),
1197
- /* @__PURE__ */ jsx4("line", { x1: "14", x2: "14", y1: "11", y2: "17" })
1495
+ var IconChevronRight = ({ className }) => /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" }) });
1496
+ var IconTrash = ({ className }) => /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1497
+ /* @__PURE__ */ jsx("path", { d: "M3 6h18" }),
1498
+ /* @__PURE__ */ jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
1499
+ /* @__PURE__ */ jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" }),
1500
+ /* @__PURE__ */ jsx("line", { x1: "10", x2: "10", y1: "11", y2: "17" }),
1501
+ /* @__PURE__ */ jsx("line", { x1: "14", x2: "14", y1: "11", y2: "17" })
1198
1502
  ] });
1199
- var IconX = ({ className }) => /* @__PURE__ */ jsxs4("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1200
- /* @__PURE__ */ jsx4("path", { d: "M18 6 6 18" }),
1201
- /* @__PURE__ */ jsx4("path", { d: "m6 6 12 12" })
1503
+ var IconX = ({ className }) => /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1504
+ /* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }),
1505
+ /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })
1202
1506
  ] });
1203
- var IconHome = ({ className }) => /* @__PURE__ */ jsxs4("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1204
- /* @__PURE__ */ jsx4("path", { d: "m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }),
1205
- /* @__PURE__ */ jsx4("polyline", { points: "9 22 9 12 15 12 15 22" })
1507
+ var IconHome = ({ className }) => /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1508
+ /* @__PURE__ */ jsx("path", { d: "m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }),
1509
+ /* @__PURE__ */ jsx("polyline", { points: "9 22 9 12 15 12 15 22" })
1206
1510
  ] });
1207
- var IconImage = ({ className }) => /* @__PURE__ */ jsxs4("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1208
- /* @__PURE__ */ jsx4("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", ry: "2" }),
1209
- /* @__PURE__ */ jsx4("circle", { cx: "9", cy: "9", r: "2" }),
1210
- /* @__PURE__ */ jsx4("path", { d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" })
1511
+ var IconImage = ({ className }) => /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1512
+ /* @__PURE__ */ jsx("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", ry: "2" }),
1513
+ /* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "2" }),
1514
+ /* @__PURE__ */ jsx("path", { d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" })
1211
1515
  ] });
1212
- var IconLoader = ({ className }) => /* @__PURE__ */ jsx4("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: /* @__PURE__ */ jsx4("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) });
1516
+ var IconLoader = ({ className }) => /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: /* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) });
1517
+ var IconLayers = ({ className }) => /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
1518
+ /* @__PURE__ */ jsx("polygon", { points: "12 2 2 7 12 12 22 7 12 2" }),
1519
+ /* @__PURE__ */ jsx("polyline", { points: "2 17 12 22 22 17" }),
1520
+ /* @__PURE__ */ jsx("polyline", { points: "2 12 12 17 22 12" })
1521
+ ] });
1522
+ var VisuallyHidden = ({ children }) => /* @__PURE__ */ jsx(
1523
+ "span",
1524
+ {
1525
+ style: {
1526
+ position: "absolute",
1527
+ width: "1px",
1528
+ height: "1px",
1529
+ padding: "0",
1530
+ margin: "-1px",
1531
+ overflow: "hidden",
1532
+ clip: "rect(0, 0, 0, 0)",
1533
+ whiteSpace: "nowrap",
1534
+ border: "0"
1535
+ },
1536
+ children
1537
+ }
1538
+ );
1213
1539
  function ImageManager(props) {
1214
1540
  const {
1215
1541
  open,
@@ -1217,6 +1543,7 @@ function ImageManager(props) {
1217
1543
  clientOptions,
1218
1544
  projectId,
1219
1545
  folderPath,
1546
+ userId,
1220
1547
  title,
1221
1548
  description,
1222
1549
  className,
@@ -1230,7 +1557,9 @@ function ImageManager(props) {
1230
1557
  maxFiles = 10,
1231
1558
  mode = "full",
1232
1559
  showDelete = true,
1233
- refreshInterval = 5e3
1560
+ refreshInterval = 5e3,
1561
+ stayOnUploadAfterComplete = false,
1562
+ thumbnailConfig
1234
1563
  } = props;
1235
1564
  const [activeView, setActiveView] = React4.useState(mode === "upload" ? "upload" : "browse");
1236
1565
  const [loading, setLoading] = React4.useState(false);
@@ -1246,8 +1575,21 @@ function ImageManager(props) {
1246
1575
  const [creatingFolder, setCreatingFolder] = React4.useState(false);
1247
1576
  const [fileToDelete, setFileToDelete] = React4.useState(null);
1248
1577
  const [thumbnailSelectorFile, setThumbnailSelectorFile] = React4.useState(null);
1578
+ const [selectedThumbnailForFile, setSelectedThumbnailForFile] = React4.useState(null);
1579
+ const [previewFile, setPreviewFile] = React4.useState(null);
1580
+ const [generatingThumbnails, setGeneratingThumbnails] = React4.useState(false);
1249
1581
  const lastUpdatedRef = React4.useRef(null);
1250
1582
  const isFetchingRef = React4.useRef(false);
1583
+ const deduplicateFiles = React4.useCallback((files2) => {
1584
+ const seen = /* @__PURE__ */ new Set();
1585
+ return files2.filter((file) => {
1586
+ if (seen.has(file.key)) {
1587
+ return false;
1588
+ }
1589
+ seen.add(file.key);
1590
+ return true;
1591
+ });
1592
+ }, []);
1251
1593
  const load = React4.useCallback(async (isPolling = false) => {
1252
1594
  if (isFetchingRef.current) return;
1253
1595
  isFetchingRef.current = true;
@@ -1258,13 +1600,14 @@ function ImageManager(props) {
1258
1600
  const currentPathPrefix = breadcrumbs.length > 1 ? breadcrumbs.slice(1).map((b) => b.name).join("/") + "/" : void 0;
1259
1601
  const effectiveFolderPath = currentPathPrefix || folderPath;
1260
1602
  const [filesData, foldersData] = await Promise.all([
1261
- fetchProjectImagesWithThumbs(clientOptions, { folderPath: effectiveFolderPath, limitConcurrent: 6 }),
1603
+ fetchProjectFiles(clientOptions, effectiveFolderPath),
1262
1604
  client.getFolders(currentFolderId)
1263
1605
  ]);
1264
- const { items: list, updatedAt } = filesData;
1606
+ const { files: rawFiles, updatedAt } = filesData;
1607
+ const list = rawFiles;
1265
1608
  if (isPolling && updatedAt && updatedAt === lastUpdatedRef.current) {
1266
1609
  }
1267
- setFiles(list);
1610
+ setFiles(deduplicateFiles(list));
1268
1611
  setFolders(foldersData);
1269
1612
  lastUpdatedRef.current = updatedAt;
1270
1613
  } catch (e) {
@@ -1287,30 +1630,6 @@ function ImageManager(props) {
1287
1630
  load();
1288
1631
  }
1289
1632
  }, [open, activeView, load, mode]);
1290
- React4.useEffect(() => {
1291
- if (!open) return;
1292
- const client = new UpfilesClient(clientOptions);
1293
- const url = client.getEventsUrl(projectId);
1294
- const eventSource = new EventSource(url);
1295
- eventSource.onmessage = (event) => {
1296
- try {
1297
- const payload = JSON.parse(event.data);
1298
- if (payload.type === "file:uploaded") {
1299
- const newFile = payload.data.file;
1300
- setFiles((prev) => {
1301
- if (prev.find((f2) => f2.key === newFile.key)) return prev;
1302
- return [newFile, ...prev];
1303
- });
1304
- } else if (payload.type === "file:deleted") {
1305
- const deletedKey = payload.data.fileKey || payload.data.file?.key;
1306
- setFiles((prev) => prev.filter((f2) => f2.key !== deletedKey));
1307
- }
1308
- } catch (e) {
1309
- console.error("SSE Error parsing message", e);
1310
- }
1311
- };
1312
- return () => eventSource.close();
1313
- }, [open, clientOptions, projectId]);
1314
1633
  const visibleFiles = React4.useMemo(() => {
1315
1634
  const q = query.trim().toLowerCase();
1316
1635
  return !q ? files : files.filter((i) => (i.originalName || "").toLowerCase().includes(q));
@@ -1334,10 +1653,18 @@ function ImageManager(props) {
1334
1653
  "content-type": "application/json",
1335
1654
  ...clientOptions?.headers || {}
1336
1655
  };
1337
- if (clientOptions?.apiKey) {
1656
+ const token = clientOptions?.getToken?.();
1657
+ if (token) {
1658
+ headers["authorization"] = `Bearer ${token}`;
1659
+ if (clientOptions.projectName) {
1660
+ headers["x-project-name"] = clientOptions.projectName;
1661
+ } else if (clientOptions.projectId) {
1662
+ headers["x-project-id"] = clientOptions.projectId;
1663
+ }
1664
+ } else if (clientOptions?.apiKey) {
1338
1665
  const keyHeader = clientOptions.apiKeyHeader || "authorization";
1339
1666
  if (keyHeader === "authorization") {
1340
- headers["authorization"] = clientOptions.apiKey.startsWith("upk_") ? `Bearer ${clientOptions.apiKey}` : clientOptions.apiKey;
1667
+ headers["authorization"] = clientOptions.apiKey.startsWith("upf_") ? `Bearer ${clientOptions.apiKey}` : clientOptions.apiKey;
1341
1668
  } else {
1342
1669
  headers[keyHeader] = clientOptions.apiKey;
1343
1670
  }
@@ -1397,65 +1724,87 @@ function ImageManager(props) {
1397
1724
  });
1398
1725
  return options;
1399
1726
  }, [breadcrumbs, currentFolderId, folders]);
1727
+ const handleThumbnailClick = React4.useCallback(async (f2) => {
1728
+ if (f2.thumbnails && f2.thumbnails.length > 0) {
1729
+ setThumbnailSelectorFile(f2);
1730
+ setSelectedThumbnailForFile(null);
1731
+ return;
1732
+ }
1733
+ setGeneratingThumbnails(true);
1734
+ setThumbnailSelectorFile(f2);
1735
+ setSelectedThumbnailForFile(null);
1736
+ try {
1737
+ const client = new UpfilesClient(clientOptions);
1738
+ const result = await client.generateThumbnails(f2.key);
1739
+ const updatedFile = { ...f2, thumbnails: result.thumbnails };
1740
+ setThumbnailSelectorFile(updatedFile);
1741
+ setFiles((prev) => prev.map((file) => file.key === f2.key ? updatedFile : file));
1742
+ } catch (e) {
1743
+ console.error("Failed to generate thumbnails:", e);
1744
+ } finally {
1745
+ setGeneratingThumbnails(false);
1746
+ }
1747
+ }, [clientOptions]);
1400
1748
  const [selectedUploadFolder, setSelectedUploadFolder] = React4.useState("current");
1401
1749
  const activeBreadcrumbPath = React4.useMemo(() => {
1402
1750
  return breadcrumbs.length > 1 ? breadcrumbs.slice(1).map((b) => b.name).join("/") + "/" : void 0;
1403
1751
  }, [breadcrumbs]);
1404
- return /* @__PURE__ */ jsxs4(Dialog2.Root, { open, onOpenChange, children: [
1405
- /* @__PURE__ */ jsxs4(Dialog2.Portal, { children: [
1406
- /* @__PURE__ */ jsx4(Dialog2.Overlay, { className: "fixed inset-0 bg-gray-900/50 backdrop-blur-sm z-50 transition-all duration-300" }),
1407
- /* @__PURE__ */ jsxs4(
1752
+ return /* @__PURE__ */ jsxs(Dialog2.Root, { open, onOpenChange, children: [
1753
+ /* @__PURE__ */ jsxs(Dialog2.Portal, { children: [
1754
+ /* @__PURE__ */ jsx(Dialog2.Overlay, { className: "fixed inset-0 bg-black/60 z-[9999]" }),
1755
+ /* @__PURE__ */ jsxs(
1408
1756
  Dialog2.Content,
1409
1757
  {
1410
- className: `fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[90vw] h-[85vh] max-w-6xl rounded-2xl bg-white dark:bg-gray-950 shadow-2xl z-50 overflow-hidden flex flex-row ${className ?? ""}`,
1758
+ className: `fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[95vw] h-[90vh] max-w-7xl min-w-[800px] min-h-[600px] rounded-2xl bg-white dark:bg-gray-950 shadow-2xl z-[10000] overflow-hidden flex flex-row ${className ?? ""}`,
1411
1759
  children: [
1412
- /* @__PURE__ */ jsxs4("div", { className: "w-64 bg-gray-50 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col flex-shrink-0", children: [
1413
- /* @__PURE__ */ jsxs4("div", { className: "p-6", children: [
1414
- /* @__PURE__ */ jsxs4("h2", { className: "text-lg font-bold text-gray-900 dark:text-white flex items-center gap-2", children: [
1415
- /* @__PURE__ */ jsx4(IconImage, { className: "text-blue-600" }),
1760
+ /* @__PURE__ */ jsx(VisuallyHidden, { children: /* @__PURE__ */ jsx(Dialog2.Title, { children: title ?? "Media Library" }) }),
1761
+ /* @__PURE__ */ jsxs("div", { className: "w-64 min-w-[256px] bg-gray-50 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col flex-shrink-0", children: [
1762
+ /* @__PURE__ */ jsxs("div", { className: "p-6", children: [
1763
+ /* @__PURE__ */ jsxs("h2", { className: "text-lg font-bold text-gray-900 dark:text-white flex items-center gap-2", children: [
1764
+ /* @__PURE__ */ jsx(IconImage, { className: "text-blue-600" }),
1416
1765
  title ?? "Media Library"
1417
1766
  ] }),
1418
- /* @__PURE__ */ jsx4("p", { className: "text-xs text-gray-500 mt-1", children: description ?? "Manage your assets" })
1767
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: description ?? "Manage your assets" })
1419
1768
  ] }),
1420
- /* @__PURE__ */ jsxs4("nav", { className: "flex-1 px-3 space-y-1", children: [
1421
- /* @__PURE__ */ jsxs4(
1769
+ /* @__PURE__ */ jsxs("nav", { className: "flex-1 px-3 space-y-1", children: [
1770
+ /* @__PURE__ */ jsxs(
1422
1771
  "button",
1423
1772
  {
1424
1773
  onClick: () => setActiveView("browse"),
1425
1774
  className: `w-full flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-lg transition-colors ${activeView === "browse" ? "bg-white dark:bg-gray-800 text-blue-600 dark:text-blue-400 shadow-sm" : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"}`,
1426
1775
  children: [
1427
- /* @__PURE__ */ jsx4(IconGrid, { className: "w-4 h-4" }),
1776
+ /* @__PURE__ */ jsx(IconGrid, { className: "w-4 h-4" }),
1428
1777
  "All Files"
1429
1778
  ]
1430
1779
  }
1431
1780
  ),
1432
- mode === "full" && /* @__PURE__ */ jsxs4(
1781
+ mode === "full" && /* @__PURE__ */ jsxs(
1433
1782
  "button",
1434
1783
  {
1435
1784
  onClick: () => setActiveView("upload"),
1436
1785
  className: `w-full flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-lg transition-colors ${activeView === "upload" ? "bg-white dark:bg-gray-800 text-blue-600 dark:text-blue-400 shadow-sm" : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"}`,
1437
1786
  children: [
1438
- /* @__PURE__ */ jsx4(IconUploadCloud, { className: "w-4 h-4" }),
1787
+ /* @__PURE__ */ jsx(IconUploadCloud, { className: "w-4 h-4" }),
1439
1788
  "Upload New"
1440
1789
  ]
1441
1790
  }
1442
1791
  )
1443
1792
  ] }),
1444
- /* @__PURE__ */ jsx4("div", { className: "p-4 border-t border-gray-200 dark:border-gray-800", children: /* @__PURE__ */ jsxs4("div", { className: "text-xs text-center text-gray-400", children: [
1793
+ /* @__PURE__ */ jsx("div", { className: "p-4 border-t border-gray-200 dark:border-gray-800", children: /* @__PURE__ */ jsxs("div", { className: "text-xs text-center text-gray-400", children: [
1445
1794
  files.length,
1446
1795
  " files \u2022 ",
1447
1796
  folders.length,
1448
1797
  " folders"
1449
1798
  ] }) })
1450
1799
  ] }),
1451
- /* @__PURE__ */ jsxs4("div", { className: "flex-1 flex flex-col bg-white dark:bg-gray-950 relative", children: [
1452
- /* @__PURE__ */ jsxs4("div", { className: "h-16 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between px-6 gap-4", children: [
1453
- /* @__PURE__ */ jsx4("div", { className: "flex items-center gap-4 flex-1", children: activeView === "browse" && /* @__PURE__ */ jsxs4(Fragment2, { children: [
1454
- /* @__PURE__ */ jsxs4("div", { className: "flex items-center text-sm text-gray-500 overflow-hidden whitespace-nowrap", children: [
1455
- /* @__PURE__ */ jsx4("button", { onClick: () => handleBreadcrumbClick(0), className: "hover:text-blue-600 transition-colors", children: /* @__PURE__ */ jsx4(IconHome, { className: "w-4 h-4" }) }),
1456
- breadcrumbs.slice(1).map((b, i) => /* @__PURE__ */ jsxs4(React4.Fragment, { children: [
1457
- /* @__PURE__ */ jsx4(IconChevronRight, { className: "w-4 h-4 text-gray-300 mx-1" }),
1458
- /* @__PURE__ */ jsx4(
1800
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col bg-white dark:bg-gray-950 relative", children: [
1801
+ /* @__PURE__ */ jsxs("div", { className: "h-16 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between px-6 gap-4", children: [
1802
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-4 flex-1", children: activeView === "browse" && /* @__PURE__ */ jsxs(Fragment, { children: [
1803
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center text-sm text-gray-500 overflow-hidden whitespace-nowrap", children: [
1804
+ /* @__PURE__ */ jsx("button", { onClick: () => handleBreadcrumbClick(0), className: "hover:text-blue-600 transition-colors", children: /* @__PURE__ */ jsx(IconHome, { className: "w-4 h-4" }) }),
1805
+ breadcrumbs.slice(1).map((b, i) => /* @__PURE__ */ jsxs(React4.Fragment, { children: [
1806
+ /* @__PURE__ */ jsx(IconChevronRight, { className: "w-4 h-4 text-gray-300 mx-1" }),
1807
+ /* @__PURE__ */ jsx(
1459
1808
  "button",
1460
1809
  {
1461
1810
  onClick: () => handleBreadcrumbClick(i + 1),
@@ -1463,23 +1812,23 @@ function ImageManager(props) {
1463
1812
  children: b.name
1464
1813
  }
1465
1814
  )
1466
- ] }, b.id || i))
1815
+ ] }, `${b.id || "breadcrumb"}-${i}`))
1467
1816
  ] }),
1468
- /* @__PURE__ */ jsx4("div", { className: "h-4 w-px bg-gray-200 dark:bg-gray-700 mx-2" }),
1469
- /* @__PURE__ */ jsx4(
1817
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-gray-200 dark:bg-gray-700 mx-2" }),
1818
+ /* @__PURE__ */ jsx(
1470
1819
  "button",
1471
1820
  {
1472
1821
  onClick: () => load(),
1473
1822
  disabled: loading,
1474
1823
  className: "p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-md transition-all disabled:opacity-50",
1475
1824
  title: "Refresh",
1476
- children: /* @__PURE__ */ jsx4(IconRefresh, { className: `w-4 h-4 ${loading ? "animate-spin" : ""}` })
1825
+ children: /* @__PURE__ */ jsx(IconRefresh, { className: `w-4 h-4 ${loading ? "animate-spin" : ""}` })
1477
1826
  }
1478
1827
  ),
1479
- /* @__PURE__ */ jsx4("div", { className: "h-4 w-px bg-gray-200 dark:bg-gray-700 mx-2" }),
1480
- /* @__PURE__ */ jsxs4("div", { className: "relative flex-1 max-w-sm group", children: [
1481
- /* @__PURE__ */ jsx4(IconSearch, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 group-focus-within:text-blue-500 transition-colors" }),
1482
- /* @__PURE__ */ jsx4(
1828
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-gray-200 dark:bg-gray-700 mx-2" }),
1829
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1 max-w-sm group", children: [
1830
+ /* @__PURE__ */ jsx(IconSearch, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 group-focus-within:text-blue-500 transition-colors" }),
1831
+ /* @__PURE__ */ jsx(
1483
1832
  "input",
1484
1833
  {
1485
1834
  value: query,
@@ -1490,45 +1839,58 @@ function ImageManager(props) {
1490
1839
  )
1491
1840
  ] })
1492
1841
  ] }) }),
1493
- /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [
1494
- activeView === "browse" && /* @__PURE__ */ jsxs4(Fragment2, { children: [
1495
- /* @__PURE__ */ jsx4("button", { onClick: () => setShowCreateFolder(true), className: "p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/10 rounded-lg transition-colors", title: "New Folder", children: /* @__PURE__ */ jsx4(IconFolderPlus, { className: "w-5 h-5" }) }),
1496
- /* @__PURE__ */ jsx4("button", { onClick: () => load(), className: `p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/10 rounded-lg transition-colors ${loading ? "animate-spin text-blue-600" : ""}`, title: "Refresh", children: /* @__PURE__ */ jsx4(IconLoader, { className: "w-5 h-5" }) })
1842
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1843
+ activeView === "browse" && /* @__PURE__ */ jsxs(Fragment, { children: [
1844
+ /* @__PURE__ */ jsx("button", { onClick: () => setShowCreateFolder(true), className: "p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/10 rounded-lg transition-colors", title: "New Folder", children: /* @__PURE__ */ jsx(IconFolderPlus, { className: "w-5 h-5" }) }),
1845
+ /* @__PURE__ */ jsx("button", { onClick: () => load(), className: `p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/10 rounded-lg transition-colors ${loading ? "animate-spin text-blue-600" : ""}`, title: "Refresh", children: /* @__PURE__ */ jsx(IconLoader, { className: "w-5 h-5" }) })
1497
1846
  ] }),
1498
- /* @__PURE__ */ jsx4(Dialog2.Close, { asChild: true, children: /* @__PURE__ */ jsx4("button", { className: "ml-2 p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/10 rounded-lg transition-colors", children: /* @__PURE__ */ jsx4(IconX, { className: "w-5 h-5" }) }) })
1847
+ /* @__PURE__ */ jsx(Dialog2.Close, { asChild: true, children: /* @__PURE__ */ jsx("button", { className: "ml-2 p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/10 rounded-lg transition-colors", children: /* @__PURE__ */ jsx(IconX, { className: "w-5 h-5" }) }) })
1499
1848
  ] })
1500
1849
  ] }),
1501
- /* @__PURE__ */ jsxs4("div", { className: "flex-1 overflow-y-auto p-6 bg-gray-50/30 dark:bg-gray-900/30", children: [
1502
- activeView === "browse" && /* @__PURE__ */ jsxs4(Fragment2, { children: [
1503
- error && /* @__PURE__ */ jsx4("div", { className: "mb-6 bg-red-50 text-red-600 p-4 rounded-lg text-sm border border-red-100 flex items-center gap-2", children: /* @__PURE__ */ jsx4("span", { children: error }) }),
1504
- /* @__PURE__ */ jsxs4("div", { className: gridClassName ?? "grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-6", children: [
1505
- visibleFolders.map((folder) => /* @__PURE__ */ jsxs4(
1850
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-6 bg-gray-50/30 dark:bg-gray-900/30", children: [
1851
+ activeView === "browse" && /* @__PURE__ */ jsxs(Fragment, { children: [
1852
+ error && /* @__PURE__ */ jsx("div", { className: "mb-6 bg-red-50 text-red-600 p-4 rounded-lg text-sm border border-red-100 flex items-center gap-2", children: /* @__PURE__ */ jsx("span", { children: error }) }),
1853
+ /* @__PURE__ */ jsxs("div", { className: gridClassName ?? "grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-6", children: [
1854
+ visibleFolders.map((folder) => /* @__PURE__ */ jsxs(
1506
1855
  "div",
1507
1856
  {
1508
1857
  onClick: () => handleNavigate(folder),
1509
1858
  className: "group flex flex-col items-center gap-3 p-4 rounded-xl border border-gray-200/60 bg-white hover:border-blue-200 hover:shadow-lg hover:shadow-blue-500/5 hover:-translate-y-1 transition-all cursor-pointer",
1510
1859
  children: [
1511
- /* @__PURE__ */ jsx4("div", { className: "w-16 h-12 bg-blue-50 rounded-lg flex items-center justify-center text-blue-500 group-hover:scale-110 transition-transform", children: /* @__PURE__ */ jsx4(IconFolder, { className: "w-8 h-8 drop-shadow-sm" }) }),
1512
- /* @__PURE__ */ jsx4("span", { className: "text-sm font-medium text-gray-700 truncate w-full text-center", children: folder.name })
1860
+ /* @__PURE__ */ jsx("div", { className: "w-16 h-12 bg-blue-50 rounded-lg flex items-center justify-center text-blue-500 group-hover:scale-110 transition-transform", children: /* @__PURE__ */ jsx(IconFolder, { className: "w-8 h-8 drop-shadow-sm" }) }),
1861
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-gray-700 truncate w-full text-center", children: folder.name })
1513
1862
  ]
1514
1863
  },
1515
1864
  folder.id
1516
1865
  )),
1517
1866
  visibleFiles.map((f2) => {
1518
- const fWithThumbs = f2;
1519
- const bestThumb = fWithThumbs.thumbnails?.sort((a, b) => a.size - b.size).find((t) => t.size >= 300) || fWithThumbs.thumbnails?.[fWithThumbs.thumbnails?.length - 1];
1520
- const thumbUrl = bestThumb?.url || f2.url;
1521
- return /* @__PURE__ */ jsxs4(
1867
+ const displayUrl = f2.url;
1868
+ const hasThumbnails = f2.thumbnails && f2.thumbnails.length > 0;
1869
+ return /* @__PURE__ */ jsxs(
1522
1870
  "div",
1523
1871
  {
1524
- className: "group relative aspect-[4/3] bg-white rounded-xl border border-gray-200 overflow-hidden shadow-sm hover:shadow-xl hover:shadow-gray-200/50 hover:border-blue-300 transition-all duration-300",
1872
+ onClick: () => setPreviewFile(f2),
1873
+ className: "group relative aspect-[4/3] bg-white rounded-xl border border-gray-200 overflow-hidden shadow-sm hover:shadow-xl hover:shadow-gray-200/50 hover:border-blue-300 transition-all duration-300 cursor-pointer",
1525
1874
  children: [
1526
- /* @__PURE__ */ jsx4("img", { src: thumbUrl, alt: "", className: "w-full h-full object-cover transition-transform duration-700 group-hover:scale-105" }),
1527
- /* @__PURE__ */ jsx4("div", { className: "absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex flex-col justify-end p-3", children: /* @__PURE__ */ jsxs4("div", { className: "flex gap-2 justify-center transform translate-y-2 group-hover:translate-y-0 transition-transform duration-300", children: [
1528
- /* @__PURE__ */ jsx4(
1875
+ /* @__PURE__ */ jsx("img", { src: displayUrl, alt: f2.originalName, className: "w-full h-full object-cover transition-transform duration-700 group-hover:scale-105" }),
1876
+ f2.contentType?.startsWith("image/") && /* @__PURE__ */ jsx(
1877
+ "button",
1878
+ {
1879
+ onClick: (e) => {
1880
+ e.stopPropagation();
1881
+ handleThumbnailClick(f2);
1882
+ },
1883
+ className: `absolute top-2 left-2 p-1.5 text-white rounded-md opacity-0 group-hover:opacity-100 transition-opacity shadow-lg ${hasThumbnails ? "bg-purple-600/90 hover:bg-purple-500" : "bg-gray-600/90 hover:bg-purple-500"}`,
1884
+ title: hasThumbnails ? "View Thumbnails" : "Generate Thumbnails",
1885
+ children: /* @__PURE__ */ jsx(IconLayers, { className: "w-4 h-4" })
1886
+ }
1887
+ ),
1888
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex flex-col justify-end p-3", children: /* @__PURE__ */ jsxs("div", { className: "flex gap-2 justify-center transform translate-y-2 group-hover:translate-y-0 transition-transform duration-300", children: [
1889
+ /* @__PURE__ */ jsx(
1529
1890
  "button",
1530
1891
  {
1531
- onClick: () => {
1892
+ onClick: (e) => {
1893
+ e.stopPropagation();
1532
1894
  onSelect({
1533
1895
  url: f2.url,
1534
1896
  key: f2.key,
@@ -1543,7 +1905,7 @@ function ImageManager(props) {
1543
1905
  children: "Select"
1544
1906
  }
1545
1907
  ),
1546
- showDelete && /* @__PURE__ */ jsx4(
1908
+ showDelete && /* @__PURE__ */ jsx(
1547
1909
  "button",
1548
1910
  {
1549
1911
  onClick: (e) => {
@@ -1551,57 +1913,70 @@ function ImageManager(props) {
1551
1913
  setFileToDelete(f2);
1552
1914
  },
1553
1915
  className: "p-1.5 bg-white/20 text-white text-xs font-semibold rounded-md hover:bg-red-500 backdrop-blur-md transition-colors",
1554
- children: /* @__PURE__ */ jsx4(IconTrash, { className: "w-4 h-4" })
1916
+ children: /* @__PURE__ */ jsx(IconTrash, { className: "w-4 h-4" })
1555
1917
  }
1556
1918
  )
1557
1919
  ] }) }),
1558
- /* @__PURE__ */ jsx4("div", { className: "absolute top-2 right-2 bg-black/50 backdrop-blur-md text-white text-[10px] px-1.5 py-0.5 rounded opacity-0 group-hover:opacity-100 transition-opacity", children: f2.originalName.split(".").pop()?.toUpperCase() })
1920
+ /* @__PURE__ */ jsx("div", { className: "absolute top-2 right-2 bg-black/50 backdrop-blur-md text-white text-[10px] px-1.5 py-0.5 rounded opacity-0 group-hover:opacity-100 transition-opacity", children: f2.originalName.split(".").pop()?.toUpperCase() })
1559
1921
  ]
1560
1922
  },
1561
1923
  f2.key
1562
1924
  );
1563
1925
  })
1564
1926
  ] }),
1565
- !loading && visibleFolders.length === 0 && visibleFiles.length === 0 && /* @__PURE__ */ jsxs4("div", { className: "h-full flex flex-col items-center justify-center text-gray-400", children: [
1566
- /* @__PURE__ */ jsx4("div", { className: "w-16 h-16 bg-gray-100 rounded-2xl flex items-center justify-center mb-4", children: /* @__PURE__ */ jsx4(IconSearch, { className: "w-8 h-8 opacity-40" }) }),
1567
- /* @__PURE__ */ jsx4("h3", { className: "text-gray-900 font-medium mb-1", children: "No Assets Found" }),
1568
- /* @__PURE__ */ jsx4("p", { className: "text-sm", children: "Try uploading a file or creating a new folder." })
1927
+ !loading && visibleFolders.length === 0 && visibleFiles.length === 0 && /* @__PURE__ */ jsxs("div", { className: "h-full flex flex-col items-center justify-center text-gray-400", children: [
1928
+ /* @__PURE__ */ jsx("div", { className: "w-16 h-16 bg-gray-100 rounded-2xl flex items-center justify-center mb-4", children: /* @__PURE__ */ jsx(IconSearch, { className: "w-8 h-8 opacity-40" }) }),
1929
+ /* @__PURE__ */ jsx("h3", { className: "text-gray-900 font-medium mb-1", children: "No Assets Found" }),
1930
+ /* @__PURE__ */ jsx("p", { className: "text-sm", children: "Try uploading a file or creating a new folder." })
1569
1931
  ] })
1570
1932
  ] }),
1571
- activeView === "upload" && /* @__PURE__ */ jsx4("div", { className: "h-full flex flex-col items-center justify-center max-w-2xl mx-auto", children: /* @__PURE__ */ jsxs4("div", { className: "w-full bg-white rounded-2xl border border-gray-200 p-8 shadow-sm", children: [
1572
- /* @__PURE__ */ jsxs4("h3", { className: "text-lg font-semibold text-gray-900 mb-6 flex items-center gap-2", children: [
1573
- /* @__PURE__ */ jsx4(IconUploadCloud, { className: "text-blue-500" }),
1933
+ activeView === "upload" && /* @__PURE__ */ jsx("div", { className: "h-full flex flex-col items-center justify-center max-w-2xl mx-auto gap-4", children: /* @__PURE__ */ jsxs("div", { className: "w-full bg-white rounded-2xl border border-gray-200 p-8 shadow-sm", children: [
1934
+ /* @__PURE__ */ jsxs("h3", { className: "text-lg font-semibold text-gray-900 mb-6 flex items-center gap-2", children: [
1935
+ /* @__PURE__ */ jsx(IconUploadCloud, { className: "text-blue-500" }),
1574
1936
  "Upload Files"
1575
1937
  ] }),
1576
- /* @__PURE__ */ jsxs4("div", { className: "mb-6", children: [
1577
- /* @__PURE__ */ jsx4("label", { className: "block text-sm font-medium text-gray-700 mb-2", children: "Target Folder" }),
1578
- /* @__PURE__ */ jsxs4("div", { className: "flex gap-2", children: [
1579
- /* @__PURE__ */ jsx4(
1938
+ /* @__PURE__ */ jsxs("div", { className: "mb-6", children: [
1939
+ /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-gray-700 mb-2", children: "Target Folder" }),
1940
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1941
+ /* @__PURE__ */ jsx(
1580
1942
  "select",
1581
1943
  {
1582
1944
  value: selectedUploadFolder,
1583
1945
  onChange: (e) => setSelectedUploadFolder(e.target.value),
1584
1946
  className: "block w-full rounded-lg border-gray-200 bg-gray-50 text-gray-900 text-sm focus:ring-blue-500 focus:border-blue-500 p-2.5",
1585
- children: uploadFoldersList.map((opt) => /* @__PURE__ */ jsx4("option", { value: opt.id, children: opt.name }, opt.id))
1947
+ children: uploadFoldersList.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.id, children: opt.name }, opt.id))
1586
1948
  }
1587
1949
  ),
1588
- /* @__PURE__ */ jsx4("button", { onClick: () => setShowCreateFolder(true), className: "px-3 bg-gray-100 hover:bg-gray-200 rounded-lg text-gray-600 transition-colors", children: /* @__PURE__ */ jsx4(IconFolderPlus, { className: "w-5 h-5" }) })
1950
+ /* @__PURE__ */ jsx("button", { onClick: () => setShowCreateFolder(true), className: "px-3 bg-gray-100 hover:bg-gray-200 rounded-lg text-gray-600 transition-colors", children: /* @__PURE__ */ jsx(IconFolderPlus, { className: "w-5 h-5" }) })
1589
1951
  ] })
1590
1952
  ] }),
1591
- /* @__PURE__ */ jsx4(
1953
+ /* @__PURE__ */ jsx(
1592
1954
  Uploader,
1593
1955
  {
1594
1956
  clientOptions,
1595
1957
  projectId,
1958
+ userId,
1596
1959
  folderPath: uploadFoldersList.find((o) => o.id === selectedUploadFolder)?.path || activeBreadcrumbPath || folderPath,
1597
1960
  accept: ["image/*"],
1598
1961
  maxFileSize,
1599
1962
  maxFiles,
1600
1963
  autoRecordToDb,
1601
- fetchThumbnails,
1602
- autoUpload: true,
1603
- onClientUploadComplete: (file) => {
1964
+ fetchThumbnails: false,
1965
+ autoUpload: false,
1966
+ thumbnailEnabled: thumbnailConfig?.enabled ?? true,
1967
+ thumbnailSizes: thumbnailConfig?.sizes,
1968
+ onClientUploadComplete: async (file) => {
1604
1969
  if (file.status === "success" && file.publicUrl && file.fileKey) {
1970
+ let thumbnails = file.thumbnails;
1971
+ if (file.thumbnailEnabled && file.thumbnailSizesSelected && file.thumbnailSizesSelected.length > 0 && file.type?.startsWith("image/")) {
1972
+ try {
1973
+ const client = new UpfilesClient(clientOptions);
1974
+ const result = await client.generateThumbnails(file.fileKey, file.thumbnailSizesSelected);
1975
+ thumbnails = result.thumbnails;
1976
+ } catch (e) {
1977
+ console.warn("Failed to generate thumbnails:", e);
1978
+ }
1979
+ }
1605
1980
  const newItem = {
1606
1981
  key: file.fileKey,
1607
1982
  originalName: file.name,
@@ -1609,26 +1984,31 @@ function ImageManager(props) {
1609
1984
  contentType: file.type,
1610
1985
  uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
1611
1986
  url: file.publicUrl,
1612
- thumbnails: file.thumbnails
1987
+ thumbnails
1613
1988
  };
1614
- setFiles((prev) => [newItem, ...prev]);
1989
+ setFiles((prev) => {
1990
+ if (prev.find((f2) => f2.key === newItem.key)) return prev;
1991
+ return deduplicateFiles([newItem, ...prev]);
1992
+ });
1615
1993
  }
1616
1994
  },
1617
1995
  onComplete: () => {
1618
- if (mode === "full") {
1996
+ if (mode === "full" && !stayOnUploadAfterComplete) {
1619
1997
  setTimeout(() => {
1620
1998
  setActiveView("browse");
1621
1999
  load();
1622
2000
  }, 1e3);
2001
+ } else {
2002
+ load();
1623
2003
  }
1624
2004
  },
1625
2005
  dropzoneClassName: "border-2 border-dashed border-gray-300 bg-gray-50 rounded-xl p-12 text-center cursor-pointer hover:border-blue-500 hover:bg-blue-50/50 transition-all",
1626
2006
  buttonClassName: "hidden",
1627
- children: /* @__PURE__ */ jsxs4("div", { className: "flex flex-col items-center gap-4", children: [
1628
- /* @__PURE__ */ jsx4("div", { className: "w-12 h-12 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center", children: /* @__PURE__ */ jsx4(IconUploadCloud, { className: "w-6 h-6" }) }),
1629
- /* @__PURE__ */ jsxs4("div", { children: [
1630
- /* @__PURE__ */ jsx4("p", { className: "text-gray-900 font-medium", children: "Click to upload or drag and drop" }),
1631
- /* @__PURE__ */ jsxs4("p", { className: "text-xs text-gray-500 mt-1", children: [
2007
+ children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-4", children: [
2008
+ /* @__PURE__ */ jsx("div", { className: "w-12 h-12 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center", children: /* @__PURE__ */ jsx(IconUploadCloud, { className: "w-6 h-6" }) }),
2009
+ /* @__PURE__ */ jsxs("div", { children: [
2010
+ /* @__PURE__ */ jsx("p", { className: "text-gray-900 font-medium", children: "Click to upload or drag and drop" }),
2011
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-500 mt-1", children: [
1632
2012
  "Supports PNG, JPG, GIF up to ",
1633
2013
  Math.round(maxFileSize / 1024 / 1024),
1634
2014
  "MB"
@@ -1644,11 +2024,12 @@ function ImageManager(props) {
1644
2024
  }
1645
2025
  )
1646
2026
  ] }),
1647
- /* @__PURE__ */ jsx4(Dialog2.Root, { open: showCreateFolder, onOpenChange: setShowCreateFolder, children: /* @__PURE__ */ jsxs4(Dialog2.Portal, { children: [
1648
- /* @__PURE__ */ jsx4(Dialog2.Overlay, { className: "fixed inset-0 bg-black/40 z-[60]" }),
1649
- /* @__PURE__ */ jsxs4(Dialog2.Content, { className: "fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-sm rounded-xl bg-white p-6 shadow-2xl z-[61] outline-none", children: [
1650
- /* @__PURE__ */ jsx4(Dialog2.Title, { className: "text-lg font-semibold text-gray-900 mb-4", children: "New Folder" }),
1651
- /* @__PURE__ */ jsx4(
2027
+ /* @__PURE__ */ jsx(Dialog2.Root, { open: showCreateFolder, onOpenChange: setShowCreateFolder, children: /* @__PURE__ */ jsxs(Dialog2.Portal, { children: [
2028
+ /* @__PURE__ */ jsx(Dialog2.Overlay, { className: "fixed inset-0 bg-black/40 z-[10010]" }),
2029
+ /* @__PURE__ */ jsxs(Dialog2.Content, { className: "fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-sm rounded-xl bg-white p-6 shadow-2xl z-[10011] outline-none", "aria-describedby": "new-folder-description", children: [
2030
+ /* @__PURE__ */ jsx(Dialog2.Title, { className: "text-lg font-semibold text-gray-900 mb-4", children: "New Folder" }),
2031
+ /* @__PURE__ */ jsx(Dialog2.Description, { id: "new-folder-description", className: "sr-only", children: "Create a new folder to organize your files" }),
2032
+ /* @__PURE__ */ jsx(
1652
2033
  "input",
1653
2034
  {
1654
2035
  autoFocus: true,
@@ -1659,9 +2040,9 @@ function ImageManager(props) {
1659
2040
  onKeyDown: (e) => e.key === "Enter" && handleCreateFolder()
1660
2041
  }
1661
2042
  ),
1662
- /* @__PURE__ */ jsxs4("div", { className: "flex justify-end gap-3", children: [
1663
- /* @__PURE__ */ jsx4("button", { onClick: () => setShowCreateFolder(false), className: "px-4 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 rounded-lg", children: "Cancel" }),
1664
- /* @__PURE__ */ jsx4(
2043
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-3", children: [
2044
+ /* @__PURE__ */ jsx("button", { onClick: () => setShowCreateFolder(false), className: "px-4 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 rounded-lg", children: "Cancel" }),
2045
+ /* @__PURE__ */ jsx(
1665
2046
  "button",
1666
2047
  {
1667
2048
  onClick: handleCreateFolder,
@@ -1673,21 +2054,21 @@ function ImageManager(props) {
1673
2054
  ] })
1674
2055
  ] })
1675
2056
  ] }) }),
1676
- /* @__PURE__ */ jsx4(Dialog2.Root, { open: !!fileToDelete, onOpenChange: (open2) => !open2 && setFileToDelete(null), children: /* @__PURE__ */ jsxs4(Dialog2.Portal, { children: [
1677
- /* @__PURE__ */ jsx4(Dialog2.Overlay, { className: "fixed inset-0 bg-black/40 z-[80]" }),
1678
- /* @__PURE__ */ jsxs4(Dialog2.Content, { className: "fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md rounded-xl bg-white p-6 shadow-2xl z-[81]", children: [
1679
- /* @__PURE__ */ jsx4(Dialog2.Title, { className: "text-xl font-semibold text-gray-900 mb-2", children: "Delete File" }),
1680
- /* @__PURE__ */ jsxs4(Dialog2.Description, { className: "text-gray-600 mb-6", children: [
2057
+ /* @__PURE__ */ jsx(Dialog2.Root, { open: !!fileToDelete, onOpenChange: (open2) => !open2 && setFileToDelete(null), children: /* @__PURE__ */ jsxs(Dialog2.Portal, { children: [
2058
+ /* @__PURE__ */ jsx(Dialog2.Overlay, { className: "fixed inset-0 bg-black/40 z-[10020]" }),
2059
+ /* @__PURE__ */ jsxs(Dialog2.Content, { className: "fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md rounded-xl bg-white p-6 shadow-2xl z-[10021]", "aria-describedby": "delete-file-description", children: [
2060
+ /* @__PURE__ */ jsx(Dialog2.Title, { className: "text-xl font-semibold text-gray-900 mb-2", children: "Delete File" }),
2061
+ /* @__PURE__ */ jsxs(Dialog2.Description, { id: "delete-file-description", className: "text-gray-600 mb-6", children: [
1681
2062
  "Are you sure you want to delete ",
1682
- /* @__PURE__ */ jsxs4("span", { className: "font-semibold text-gray-900", children: [
2063
+ /* @__PURE__ */ jsxs("span", { className: "font-semibold text-gray-900", children: [
1683
2064
  '"',
1684
2065
  fileToDelete?.originalName,
1685
2066
  '"'
1686
2067
  ] }),
1687
2068
  "?"
1688
2069
  ] }),
1689
- /* @__PURE__ */ jsxs4("div", { className: "flex justify-end gap-3", children: [
1690
- /* @__PURE__ */ jsx4(
2070
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-3", children: [
2071
+ /* @__PURE__ */ jsx(
1691
2072
  "button",
1692
2073
  {
1693
2074
  onClick: () => setFileToDelete(null),
@@ -1695,7 +2076,7 @@ function ImageManager(props) {
1695
2076
  children: "Cancel"
1696
2077
  }
1697
2078
  ),
1698
- /* @__PURE__ */ jsx4(
2079
+ /* @__PURE__ */ jsx(
1699
2080
  "button",
1700
2081
  {
1701
2082
  onClick: handleDelete,
@@ -1706,22 +2087,346 @@ function ImageManager(props) {
1706
2087
  )
1707
2088
  ] })
1708
2089
  ] })
2090
+ ] }) }),
2091
+ /* @__PURE__ */ jsx(Dialog2.Root, { open: !!thumbnailSelectorFile, onOpenChange: (open2) => !open2 && setThumbnailSelectorFile(null), children: /* @__PURE__ */ jsxs(Dialog2.Portal, { children: [
2092
+ /* @__PURE__ */ jsx(Dialog2.Overlay, { className: "fixed inset-0 bg-black/50 z-[10030]" }),
2093
+ /* @__PURE__ */ jsxs(Dialog2.Content, { className: "fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-4xl max-h-[80vh] rounded-xl bg-white shadow-2xl z-[10031] overflow-hidden", "aria-describedby": "thumbnail-selector-description", children: [
2094
+ /* @__PURE__ */ jsxs("div", { className: "p-6 border-b border-gray-200", children: [
2095
+ /* @__PURE__ */ jsx(Dialog2.Title, { className: "text-xl font-semibold text-gray-900 mb-2", children: "Select Thumbnail" }),
2096
+ /* @__PURE__ */ jsxs(Dialog2.Description, { id: "thumbnail-selector-description", className: "text-gray-600", children: [
2097
+ "Choose a thumbnail size for ",
2098
+ /* @__PURE__ */ jsxs("span", { className: "font-semibold text-gray-900", children: [
2099
+ '"',
2100
+ thumbnailSelectorFile?.originalName,
2101
+ '"'
2102
+ ] })
2103
+ ] })
2104
+ ] }),
2105
+ /* @__PURE__ */ jsxs("div", { className: "p-6 overflow-y-auto max-h-[60vh]", children: [
2106
+ generatingThumbnails && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-3 py-8 text-gray-500", children: [
2107
+ /* @__PURE__ */ jsx(IconLoader, { className: "w-5 h-5 animate-spin" }),
2108
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Generating thumbnails..." })
2109
+ ] }),
2110
+ /* @__PURE__ */ jsxs("div", { className: `grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 ${generatingThumbnails ? "opacity-50 pointer-events-none" : ""}`, children: [
2111
+ /* @__PURE__ */ jsxs(
2112
+ "div",
2113
+ {
2114
+ onClick: () => setSelectedThumbnailForFile(null),
2115
+ className: `group cursor-pointer rounded-lg border-2 overflow-hidden transition-all ${selectedThumbnailForFile === null ? "border-blue-500 ring-2 ring-blue-200" : "border-gray-200 hover:border-blue-300"}`,
2116
+ children: [
2117
+ /* @__PURE__ */ jsxs("div", { className: "aspect-square bg-gray-100 flex items-center justify-center relative", children: [
2118
+ /* @__PURE__ */ jsx(
2119
+ "img",
2120
+ {
2121
+ src: thumbnailSelectorFile?.url,
2122
+ alt: "Original",
2123
+ className: "w-full h-full object-cover"
2124
+ }
2125
+ ),
2126
+ selectedThumbnailForFile === null && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-blue-500/20 flex items-center justify-center", children: /* @__PURE__ */ jsx("div", { className: "w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center", children: /* @__PURE__ */ jsx("svg", { className: "w-4 h-4 text-white", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", clipRule: "evenodd" }) }) }) })
2127
+ ] }),
2128
+ /* @__PURE__ */ jsxs("div", { className: "p-3 bg-white", children: [
2129
+ /* @__PURE__ */ jsx("div", { className: "font-medium text-sm text-gray-900", children: "Original" }),
2130
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-gray-500", children: "Full size" })
2131
+ ] })
2132
+ ]
2133
+ }
2134
+ ),
2135
+ thumbnailSelectorFile?.thumbnails?.map((thumbnail) => /* @__PURE__ */ jsxs(
2136
+ "div",
2137
+ {
2138
+ onClick: () => setSelectedThumbnailForFile(thumbnail),
2139
+ className: `group cursor-pointer rounded-lg border-2 overflow-hidden transition-all ${selectedThumbnailForFile?.id === thumbnail.id ? "border-blue-500 ring-2 ring-blue-200" : "border-gray-200 hover:border-blue-300"}`,
2140
+ children: [
2141
+ /* @__PURE__ */ jsxs("div", { className: "aspect-square bg-gray-100 flex items-center justify-center relative", children: [
2142
+ /* @__PURE__ */ jsx(
2143
+ "img",
2144
+ {
2145
+ src: thumbnail.url,
2146
+ alt: `${thumbnail.size}px thumbnail`,
2147
+ className: "w-full h-full object-cover"
2148
+ }
2149
+ ),
2150
+ selectedThumbnailForFile?.id === thumbnail.id && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-blue-500/20 flex items-center justify-center", children: /* @__PURE__ */ jsx("div", { className: "w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center", children: /* @__PURE__ */ jsx("svg", { className: "w-4 h-4 text-white", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", clipRule: "evenodd" }) }) }) })
2151
+ ] }),
2152
+ /* @__PURE__ */ jsxs("div", { className: "p-3 bg-white", children: [
2153
+ /* @__PURE__ */ jsxs("div", { className: "font-medium text-sm text-gray-900", children: [
2154
+ thumbnail.size,
2155
+ "px"
2156
+ ] }),
2157
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-gray-500", children: thumbnail.sizeType || "Thumbnail" })
2158
+ ] })
2159
+ ]
2160
+ },
2161
+ thumbnail.id
2162
+ ))
2163
+ ] })
2164
+ ] }),
2165
+ /* @__PURE__ */ jsxs("div", { className: "p-6 border-t border-gray-200 flex justify-end gap-3", children: [
2166
+ /* @__PURE__ */ jsx(
2167
+ "button",
2168
+ {
2169
+ onClick: () => setThumbnailSelectorFile(null),
2170
+ className: "px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-lg",
2171
+ children: "Cancel"
2172
+ }
2173
+ ),
2174
+ /* @__PURE__ */ jsxs(
2175
+ "button",
2176
+ {
2177
+ onClick: () => {
2178
+ if (thumbnailSelectorFile) {
2179
+ onSelect({
2180
+ url: selectedThumbnailForFile?.url || thumbnailSelectorFile.url,
2181
+ key: thumbnailSelectorFile.key,
2182
+ originalName: thumbnailSelectorFile.originalName,
2183
+ size: thumbnailSelectorFile.size,
2184
+ contentType: thumbnailSelectorFile.contentType,
2185
+ thumbnails: thumbnailSelectorFile.thumbnails,
2186
+ selectedThumbnail: selectedThumbnailForFile || void 0
2187
+ });
2188
+ onOpenChange(false);
2189
+ setThumbnailSelectorFile(null);
2190
+ }
2191
+ },
2192
+ className: "px-4 py-2 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700",
2193
+ children: [
2194
+ "Select ",
2195
+ selectedThumbnailForFile ? `${selectedThumbnailForFile.size}px Thumbnail` : "Original"
2196
+ ]
2197
+ }
2198
+ )
2199
+ ] })
2200
+ ] })
2201
+ ] }) }),
2202
+ /* @__PURE__ */ jsx(Dialog2.Root, { open: !!previewFile, onOpenChange: (open2) => !open2 && setPreviewFile(null), children: /* @__PURE__ */ jsxs(Dialog2.Portal, { children: [
2203
+ /* @__PURE__ */ jsx(Dialog2.Overlay, { className: "fixed inset-0 bg-black/90 z-[10040]", onClick: () => setPreviewFile(null) }),
2204
+ /* @__PURE__ */ jsx(Dialog2.Content, { className: "fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[90vw] h-[90vh] z-[10041] outline-none", "aria-describedby": "image-preview-description", children: /* @__PURE__ */ jsxs("div", { className: "relative w-full h-full flex flex-col", children: [
2205
+ /* @__PURE__ */ jsx("div", { className: "absolute top-0 left-0 right-0 z-10 bg-gradient-to-b from-black/70 to-transparent p-6", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between", children: [
2206
+ /* @__PURE__ */ jsxs("div", { className: "text-white", children: [
2207
+ /* @__PURE__ */ jsx(Dialog2.Title, { className: "text-xl font-semibold mb-1", children: previewFile?.originalName }),
2208
+ /* @__PURE__ */ jsxs(Dialog2.Description, { id: "image-preview-description", className: "text-sm text-gray-300", children: [
2209
+ previewFile?.size ? `${(previewFile.size / 1024).toFixed(1)} KB` : "",
2210
+ " \u2022 ",
2211
+ previewFile?.contentType
2212
+ ] })
2213
+ ] }),
2214
+ /* @__PURE__ */ jsx(
2215
+ "button",
2216
+ {
2217
+ onClick: () => setPreviewFile(null),
2218
+ className: "p-2 bg-white/10 hover:bg-white/20 rounded-lg backdrop-blur-md transition-colors",
2219
+ children: /* @__PURE__ */ jsx(IconX, { className: "w-6 h-6 text-white" })
2220
+ }
2221
+ )
2222
+ ] }) }),
2223
+ /* @__PURE__ */ jsx("div", { className: "flex-1 flex items-center justify-center p-6", children: /* @__PURE__ */ jsx(
2224
+ "img",
2225
+ {
2226
+ src: previewFile?.url,
2227
+ alt: previewFile?.originalName,
2228
+ className: "max-w-full max-h-full object-contain",
2229
+ onError: (e) => {
2230
+ const target = e.target;
2231
+ target.style.display = "none";
2232
+ const errorDiv = document.createElement("div");
2233
+ errorDiv.className = "text-white text-center";
2234
+ errorDiv.innerHTML = `
2235
+ <div class="text-red-400 mb-2">Failed to load image</div>
2236
+ <div class="text-sm text-gray-400">URL: ${previewFile?.url}</div>
2237
+ `;
2238
+ target.parentElement?.appendChild(errorDiv);
2239
+ }
2240
+ }
2241
+ ) }),
2242
+ /* @__PURE__ */ jsx("div", { className: "absolute bottom-0 left-0 right-0 z-10 bg-gradient-to-t from-black/70 to-transparent p-6", children: /* @__PURE__ */ jsxs("div", { className: "flex gap-3 justify-center", children: [
2243
+ /* @__PURE__ */ jsx(
2244
+ "button",
2245
+ {
2246
+ onClick: (e) => {
2247
+ e.stopPropagation();
2248
+ if (previewFile) {
2249
+ onSelect({
2250
+ url: previewFile.url,
2251
+ key: previewFile.key,
2252
+ originalName: previewFile.originalName,
2253
+ size: previewFile.size,
2254
+ contentType: previewFile.contentType,
2255
+ thumbnails: previewFile.thumbnails
2256
+ });
2257
+ onOpenChange(false);
2258
+ setPreviewFile(null);
2259
+ }
2260
+ },
2261
+ className: "px-6 py-2.5 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-500 shadow-lg",
2262
+ children: "Select Image"
2263
+ }
2264
+ ),
2265
+ /* @__PURE__ */ jsx(
2266
+ "a",
2267
+ {
2268
+ href: previewFile?.url,
2269
+ target: "_blank",
2270
+ rel: "noopener noreferrer",
2271
+ onClick: (e) => e.stopPropagation(),
2272
+ className: "px-6 py-2.5 bg-white/10 text-white font-semibold rounded-lg hover:bg-white/20 backdrop-blur-md transition-colors",
2273
+ children: "Open in New Tab"
2274
+ }
2275
+ ),
2276
+ previewFile?.contentType?.startsWith("image/") && /* @__PURE__ */ jsxs(
2277
+ "button",
2278
+ {
2279
+ onClick: (e) => {
2280
+ e.stopPropagation();
2281
+ if (previewFile) {
2282
+ handleThumbnailClick(previewFile);
2283
+ setPreviewFile(null);
2284
+ }
2285
+ },
2286
+ className: "px-6 py-2.5 bg-purple-600 text-white font-semibold rounded-lg hover:bg-purple-500 shadow-lg flex items-center gap-2",
2287
+ children: [
2288
+ /* @__PURE__ */ jsx(IconLayers, { className: "w-5 h-5" }),
2289
+ "Select Thumbnail"
2290
+ ]
2291
+ }
2292
+ )
2293
+ ] }) })
2294
+ ] }) })
1709
2295
  ] }) })
1710
2296
  ] });
1711
2297
  }
1712
- export {
1713
- ConnectProjectDialog,
1714
- ImageManager,
1715
- ProjectFilesWidget,
1716
- UpfilesClient,
1717
- Uploader,
1718
- addApiKeyManually,
1719
- connectProject,
1720
- connectUsingApiKey,
1721
- createClientWithKey,
1722
- createProject,
1723
- fetchFileThumbnails,
1724
- fetchProjectFiles,
1725
- fetchProjectImagesWithThumbs,
1726
- listProjects
1727
- };
2298
+ function useUpfilesInhouse(config) {
2299
+ const {
2300
+ baseUrl,
2301
+ getToken,
2302
+ userId,
2303
+ projectName = "default",
2304
+ enabled = true
2305
+ } = config;
2306
+ const [state, setState] = useState({ clientOptions: null, projectId: null, loading: false, error: null });
2307
+ const [retryCount, setRetryCount] = useState(0);
2308
+ const [token, setToken] = useState(
2309
+ typeof getToken === "string" ? getToken : null
2310
+ );
2311
+ const getTokenFn = useCallback(() => {
2312
+ return typeof getToken === "function" ? getToken() : getToken;
2313
+ }, [getToken]);
2314
+ useEffect(() => {
2315
+ let cancelled = false;
2316
+ async function resolveToken() {
2317
+ const next = await getTokenFn();
2318
+ if (!cancelled) {
2319
+ setToken(next ?? null);
2320
+ }
2321
+ }
2322
+ resolveToken();
2323
+ window.addEventListener("focus", resolveToken);
2324
+ window.addEventListener("storage", resolveToken);
2325
+ return () => {
2326
+ cancelled = true;
2327
+ window.removeEventListener("focus", resolveToken);
2328
+ window.removeEventListener("storage", resolveToken);
2329
+ };
2330
+ }, [getTokenFn]);
2331
+ const mountedRef = useRef(true);
2332
+ useEffect(() => {
2333
+ mountedRef.current = true;
2334
+ return () => {
2335
+ mountedRef.current = false;
2336
+ };
2337
+ }, []);
2338
+ const buildClientOptions = useCallback(
2339
+ () => ({
2340
+ baseUrl,
2341
+ getToken: getTokenFn,
2342
+ projectName
2343
+ }),
2344
+ [baseUrl, getTokenFn, projectName]
2345
+ );
2346
+ useEffect(() => {
2347
+ if (!enabled || !token || !userId) {
2348
+ setState({ clientOptions: null, projectId: null, loading: false, error: null });
2349
+ return;
2350
+ }
2351
+ let cancelled = false;
2352
+ async function provision() {
2353
+ if (!mountedRef.current) return;
2354
+ setState((prev) => ({ ...prev, loading: true, error: null }));
2355
+ try {
2356
+ const connectOpts = { baseUrl, token };
2357
+ const projects = await listProjects(connectOpts);
2358
+ if (cancelled || !mountedRef.current) return;
2359
+ const existing = projects.find((p) => p.name === projectName);
2360
+ if (existing) {
2361
+ await updateProject({
2362
+ ...connectOpts,
2363
+ projectId: existing.id,
2364
+ isInHouse: true,
2365
+ freeStorageLimitBytes: 2147483648
2366
+ // 2GB
2367
+ });
2368
+ } else {
2369
+ try {
2370
+ await createProject({
2371
+ ...connectOpts,
2372
+ name: projectName,
2373
+ isInHouse: true,
2374
+ freeStorageLimitBytes: 2147483648
2375
+ // 2GB in bytes
2376
+ });
2377
+ } catch (err) {
2378
+ if (err?.message?.includes("409") || err?.message?.includes("duplicate") || err?.message?.includes("already exists")) {
2379
+ const retryProjects = await listProjects(connectOpts);
2380
+ const found = retryProjects.find((p) => p.name === projectName);
2381
+ if (!found) throw err;
2382
+ await updateProject({
2383
+ ...connectOpts,
2384
+ projectId: found.id,
2385
+ isInHouse: true,
2386
+ freeStorageLimitBytes: 2147483648
2387
+ // 2GB
2388
+ });
2389
+ } else {
2390
+ throw err;
2391
+ }
2392
+ }
2393
+ }
2394
+ if (cancelled || !mountedRef.current) return;
2395
+ setState({
2396
+ clientOptions: buildClientOptions(),
2397
+ projectId: null,
2398
+ loading: false,
2399
+ error: null
2400
+ });
2401
+ } catch (err) {
2402
+ if (cancelled || !mountedRef.current) return;
2403
+ setState({
2404
+ clientOptions: null,
2405
+ projectId: null,
2406
+ loading: false,
2407
+ error: err?.message || "Failed to auto-connect upfiles"
2408
+ });
2409
+ }
2410
+ }
2411
+ provision();
2412
+ return () => {
2413
+ cancelled = true;
2414
+ };
2415
+ }, [enabled, token, userId, baseUrl, projectName, retryCount, buildClientOptions]);
2416
+ const retry = useCallback(() => {
2417
+ setRetryCount((c) => c + 1);
2418
+ }, []);
2419
+ const clearCache = useCallback(() => {
2420
+ setState({ clientOptions: null, projectId: null, loading: false, error: null });
2421
+ }, []);
2422
+ return {
2423
+ ...state,
2424
+ isReady: !!state.clientOptions,
2425
+ retry,
2426
+ clearCache
2427
+ };
2428
+ }
2429
+
2430
+ export { ConnectProjectDialog, ImageManager, ProjectFilesWidget, StorageQuotaError, UpfilesClient, Uploader, addApiKeyManually, connectProject, connectUsingApiKey, createClientWithKey, createProject, createProjectWithApiKey, fetchFileThumbnails, fetchProjectFiles, fetchProjectImagesWithThumbs, listProjects, updateProject, useUpfilesInhouse };
2431
+ //# sourceMappingURL=index.mjs.map
2432
+ //# sourceMappingURL=index.mjs.map