@thetechfossil/upfiles 1.0.12 → 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,9 +1,16 @@
1
1
  import * as React4 from 'react';
2
- import React4__default, { useRef, useMemo, useState, useCallback, useEffect } from 'react';
2
+ import React4__default, { useRef, useMemo, useState, useEffect, useCallback } from 'react';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
  import * as Dialog2 from '@radix-ui/react-dialog';
5
5
 
6
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
+ };
7
14
  var UpfilesClient = class {
8
15
  constructor(opts) {
9
16
  this.baseUrl = opts.baseUrl?.replace(/\/$/, "");
@@ -17,25 +24,66 @@ var UpfilesClient = class {
17
24
  this.completionPath = opts.completionPath ?? "/api/plugin/files";
18
25
  this.foldersPath = opts.foldersPath ?? "/api/plugin/folders";
19
26
  this.callCompletionEndpoint = opts.callCompletionEndpoint ?? false;
27
+ this.getToken = opts.getToken;
28
+ this.projectId = opts.projectId;
29
+ this.projectName = opts.projectName;
20
30
  }
21
- async getPresignedUrl(params) {
22
- if (!params.fileName) throw new Error("fileName is required");
23
- if (!params.fileType) throw new Error("fileType is required");
24
- if (!params.fileSize || params.fileSize <= 0) throw new Error("fileSize must be > 0");
25
- const target = this.presignUrl ? this.presignUrl : this.baseUrl ? `${this.baseUrl}${this.presignPath}` : this.presignPath;
26
- const headers = {
27
- "content-type": "application/json",
28
- ...this.headers || {}
29
- };
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
+ }
30
46
  if (this.apiKey) {
31
47
  if (this.apiKeyHeader === "authorization") {
32
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
48
+ headers["authorization"] = this.apiKey.startsWith("upf_") ? `Bearer ${this.apiKey}` : this.apiKey;
33
49
  } else if (this.apiKeyHeader === "x-api-key") {
34
50
  headers["x-api-key"] = this.apiKey;
35
51
  } else {
36
52
  headers["x-up-api-key"] = this.apiKey;
37
53
  }
38
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
+ };
39
87
  const res = await fetch(target, {
40
88
  method: "POST",
41
89
  headers,
@@ -46,8 +94,12 @@ var UpfilesClient = class {
46
94
  let msg = "Failed to get upload URL";
47
95
  try {
48
96
  const data = await res.json();
49
- msg = data.error || msg;
50
- } 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;
51
103
  }
52
104
  throw new Error(msg);
53
105
  }
@@ -83,7 +135,11 @@ var UpfilesClient = class {
83
135
  originalName: file.name,
84
136
  size: file.size,
85
137
  contentType: file.type || "application/octet-stream",
86
- 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.
87
143
  });
88
144
  } catch (error) {
89
145
  throw new Error(`Upload verification failed: ${error.message}`);
@@ -112,22 +168,16 @@ var UpfilesClient = class {
112
168
  const target = this.baseUrl ? `${this.baseUrl}${this.completionPath}` : this.completionPath;
113
169
  const headers = {
114
170
  "content-type": "application/json",
115
- ...this.headers || {}
171
+ ...await this.buildAuthHeaders()
116
172
  };
117
- if (this.apiKey) {
118
- if (this.apiKeyHeader === "authorization") {
119
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
120
- } else if (this.apiKeyHeader === "x-api-key") {
121
- headers["x-api-key"] = this.apiKey;
122
- } else {
123
- headers["x-up-api-key"] = this.apiKey;
124
- }
125
- }
126
173
  const res = await fetch(target, {
127
174
  method: "POST",
128
175
  headers,
129
176
  credentials: this.withCredentials ? "include" : "same-origin",
130
- body: JSON.stringify(params)
177
+ body: JSON.stringify({
178
+ ...params,
179
+ key: params.fileKey
180
+ })
131
181
  });
132
182
  if (!res.ok) {
133
183
  let msg = "Failed to complete upload";
@@ -143,16 +193,7 @@ var UpfilesClient = class {
143
193
  async getThumbnails(fileKey) {
144
194
  const target = this.baseUrl ? `${this.baseUrl}${this.thumbnailsPath}` : this.thumbnailsPath;
145
195
  const url = `${target}?fileKey=${encodeURIComponent(fileKey)}`;
146
- const headers = { ...this.headers || {} };
147
- if (this.apiKey) {
148
- if (this.apiKeyHeader === "authorization") {
149
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
150
- } else if (this.apiKeyHeader === "x-api-key") {
151
- headers["x-api-key"] = this.apiKey;
152
- } else {
153
- headers["x-up-api-key"] = this.apiKey;
154
- }
155
- }
196
+ const headers = await this.buildAuthHeaders();
156
197
  const res = await fetch(url, { method: "GET", headers, credentials: this.withCredentials ? "include" : "same-origin" });
157
198
  if (!res.ok) {
158
199
  let msg = "Failed to fetch thumbnails";
@@ -166,26 +207,19 @@ var UpfilesClient = class {
166
207
  const data = await res.json();
167
208
  return data?.thumbnails ?? [];
168
209
  }
169
- async generateThumbnails(fileKey) {
210
+ async generateThumbnails(fileKey, sizes) {
170
211
  const target = this.baseUrl ? `${this.baseUrl}${this.thumbnailsPath}` : this.thumbnailsPath;
171
212
  const headers = {
172
213
  "content-type": "application/json",
173
- ...this.headers || {}
214
+ ...await this.buildAuthHeaders()
174
215
  };
175
- if (this.apiKey) {
176
- if (this.apiKeyHeader === "authorization") {
177
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
178
- } else if (this.apiKeyHeader === "x-api-key") {
179
- headers["x-api-key"] = this.apiKey;
180
- } else {
181
- headers["x-up-api-key"] = this.apiKey;
182
- }
183
- }
216
+ const body = { fileKey };
217
+ if (sizes && sizes.length > 0) body.sizes = sizes;
184
218
  const res = await fetch(target, {
185
219
  method: "POST",
186
220
  headers,
187
221
  credentials: this.withCredentials ? "include" : "same-origin",
188
- body: JSON.stringify({ fileKey })
222
+ body: JSON.stringify(body)
189
223
  });
190
224
  if (!res.ok) {
191
225
  let msg = "Failed to generate thumbnails";
@@ -199,19 +233,53 @@ var UpfilesClient = class {
199
233
  const data = await res.json();
200
234
  return { masterWebp: data.masterWebp, thumbnails: data.thumbnails ?? [] };
201
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
+ }
202
279
  async getFolders(parentId) {
203
280
  const target = this.baseUrl ? `${this.baseUrl}${this.foldersPath}` : this.foldersPath;
204
281
  const url = parentId ? `${target}?parentId=${encodeURIComponent(parentId)}` : target;
205
- const headers = { ...this.headers || {} };
206
- if (this.apiKey) {
207
- if (this.apiKeyHeader === "authorization") {
208
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
209
- } else if (this.apiKeyHeader === "x-api-key") {
210
- headers["x-api-key"] = this.apiKey;
211
- } else {
212
- headers["x-up-api-key"] = this.apiKey;
213
- }
214
- }
282
+ const headers = await this.buildAuthHeaders();
215
283
  const res = await fetch(url, { method: "GET", headers, credentials: this.withCredentials ? "include" : "same-origin" });
216
284
  if (!res.ok) {
217
285
  let msg = "Failed to fetch folders";
@@ -229,17 +297,8 @@ var UpfilesClient = class {
229
297
  const target = this.baseUrl ? `${this.baseUrl}${this.foldersPath}` : this.foldersPath;
230
298
  const headers = {
231
299
  "content-type": "application/json",
232
- ...this.headers || {}
300
+ ...await this.buildAuthHeaders()
233
301
  };
234
- if (this.apiKey) {
235
- if (this.apiKeyHeader === "authorization") {
236
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
237
- } else if (this.apiKeyHeader === "x-api-key") {
238
- headers["x-api-key"] = this.apiKey;
239
- } else {
240
- headers["x-up-api-key"] = this.apiKey;
241
- }
242
- }
243
302
  const res = await fetch(target, {
244
303
  method: "POST",
245
304
  headers,
@@ -263,16 +322,7 @@ var UpfilesClient = class {
263
322
  const filesPath = `${basePath}/files`;
264
323
  const target = this.baseUrl ? `${this.baseUrl}${filesPath}` : filesPath;
265
324
  const url = params?.folderPath ? `${target}?folderPath=${encodeURIComponent(params.folderPath)}` : target;
266
- const headers = { ...this.headers || {} };
267
- if (this.apiKey) {
268
- if (this.apiKeyHeader === "authorization") {
269
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
270
- } else if (this.apiKeyHeader === "x-api-key") {
271
- headers["x-api-key"] = this.apiKey;
272
- } else {
273
- headers["x-up-api-key"] = this.apiKey;
274
- }
275
- }
325
+ const headers = await this.buildAuthHeaders();
276
326
  const res = await fetch(url, { method: "GET", headers, credentials: this.withCredentials ? "include" : "same-origin" });
277
327
  if (!res.ok) {
278
328
  let msg = "Failed to fetch files";
@@ -290,24 +340,21 @@ var UpfilesClient = class {
290
340
  };
291
341
  }
292
342
  getEventsUrl(projectId) {
293
- this.thumbnailsPath.replace(/\/thumbnails$/, "");
294
343
  const target = this.baseUrl ? `${this.baseUrl}/api/events` : "/api/events";
295
344
  const url = new URL(target, window.location.href);
296
- if (this.apiKey) {
297
- url.searchParams.set("apiKey", this.apiKey);
298
- }
299
- if (projectId) {
300
- url.searchParams.set("projectId", projectId);
345
+ if (!this.getToken && !this.apiKey && (projectId || this.projectId)) {
346
+ url.searchParams.set("projectId", projectId || this.projectId);
301
347
  }
302
348
  return url.toString();
303
349
  }
304
350
  };
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;
338
429
  });
339
- const limited = filtered.slice(0, Math.max(0, maxFiles - files.length));
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;
438
+ });
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,7 +652,7 @@ var Uploader = ({
467
652
  } finally {
468
653
  setIsUploading(false);
469
654
  }
470
- }, [files, onComplete, onError, uploadOne]);
655
+ }, [files, client, onComplete, onError, uploadOne]);
471
656
  React4__default.useEffect(() => {
472
657
  if (autoUpload && files.some((f2) => f2.status === "pending") && !isUploading) {
473
658
  const timer = setTimeout(() => {
@@ -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",
@@ -695,10 +973,18 @@ var ProjectFilesWidget = ({
695
973
  "content-type": "application/json",
696
974
  ...clientOptions?.headers || {}
697
975
  };
698
- 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) {
699
985
  const keyHeader = clientOptions.apiKeyHeader || "authorization";
700
986
  if (keyHeader === "authorization") {
701
- headers["authorization"] = clientOptions.apiKey.startsWith("upk_") ? `Bearer ${clientOptions.apiKey}` : clientOptions.apiKey;
987
+ headers["authorization"] = clientOptions.apiKey.startsWith("upf_") ? `Bearer ${clientOptions.apiKey}` : clientOptions.apiKey;
702
988
  } else {
703
989
  headers[keyHeader] = clientOptions.apiKey;
704
990
  }
@@ -819,6 +1105,19 @@ var ProjectFilesWidget = ({
819
1105
 
820
1106
  // src/projectConnect.ts
821
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
+ }
822
1121
  async function httpJson(input, init, useFetch) {
823
1122
  const doFetch = useFetch ?? f;
824
1123
  const res = await doFetch(input, init);
@@ -826,7 +1125,7 @@ async function httpJson(input, init, useFetch) {
826
1125
  let msg = `Request failed (${res.status})`;
827
1126
  try {
828
1127
  const j = await res.json();
829
- msg = j?.error || msg;
1128
+ msg = j?.error || j?.message || msg;
830
1129
  } catch {
831
1130
  }
832
1131
  throw new Error(msg);
@@ -837,7 +1136,7 @@ async function listProjects(opts) {
837
1136
  const url = `${opts.baseUrl.replace(/\/$/, "")}/api/projects`;
838
1137
  const data = await httpJson(
839
1138
  url,
840
- { method: "GET", credentials: "include" },
1139
+ buildInit(opts, { method: "GET" }),
841
1140
  opts.fetch
842
1141
  );
843
1142
  return data?.projects ?? [];
@@ -846,53 +1145,74 @@ async function createProject(opts) {
846
1145
  const base = opts.baseUrl.replace(/\/$/, "");
847
1146
  const created = await httpJson(
848
1147
  `${base}/api/projects`,
849
- {
1148
+ buildInit(opts, {
850
1149
  method: "POST",
851
- credentials: "include",
852
1150
  headers: { "content-type": "application/json" },
853
- body: JSON.stringify({ name: opts.name })
854
- },
1151
+ body: JSON.stringify({
1152
+ name: opts.name,
1153
+ isInHouse: opts.isInHouse,
1154
+ freeStorageLimitBytes: opts.freeStorageLimitBytes
1155
+ })
1156
+ }),
855
1157
  opts.fetch
856
1158
  );
857
1159
  const projectId = created?.project?.id;
858
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);
859
1166
  const keyLabel = opts.keyName || `Plugin key ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
860
1167
  const key = await httpJson(
861
1168
  `${base}/api/projects/${projectId}/keys`,
862
- {
1169
+ buildInit(opts, {
863
1170
  method: "POST",
864
- credentials: "include",
865
1171
  headers: { "content-type": "application/json" },
866
1172
  body: JSON.stringify({ name: keyLabel })
867
- },
1173
+ }),
868
1174
  opts.fetch
869
1175
  );
870
- const plaintext = key?.key?.plaintext;
1176
+ const plaintext = key?.key?.secret ?? key?.key?.plaintext;
871
1177
  if (!plaintext) throw new Error("Failed to create API key");
872
1178
  return { apiKey: plaintext, projectId };
873
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
+ }
874
1195
  async function connectProject(opts) {
875
1196
  const base = opts.baseUrl.replace(/\/$/, "");
876
1197
  const keyLabel = opts.keyName || `Plugin key ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
877
1198
  const key = await httpJson(
878
1199
  `${base}/api/projects/${opts.projectId}/keys`,
879
- {
1200
+ buildInit(opts, {
880
1201
  method: "POST",
881
- credentials: "include",
882
1202
  headers: { "content-type": "application/json" },
883
1203
  body: JSON.stringify({ name: keyLabel })
884
- },
1204
+ }),
885
1205
  opts.fetch
886
1206
  );
887
- const plaintext = key?.key?.plaintext;
1207
+ const plaintext = key?.key?.secret ?? key?.key?.plaintext;
888
1208
  if (!plaintext) throw new Error("Failed to create API key");
889
1209
  return { apiKey: plaintext };
890
1210
  }
891
1211
  function addApiKeyManually(apiKey) {
892
1212
  const k = (apiKey || "").trim();
893
1213
  if (!k) throw new Error("API key is required");
894
- if (!/^upk_[A-Za-z0-9]+/.test(k)) {
895
- 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_*");
896
1216
  }
897
1217
  return { apiKey: k };
898
1218
  }
@@ -976,7 +1296,7 @@ function ConnectProjectDialog(props) {
976
1296
  setLoading(true);
977
1297
  setError(null);
978
1298
  if (!newProjectName.trim()) throw new Error("Project name is required");
979
- const { apiKey, projectId } = await createProject({ baseUrl, name: newProjectName.trim(), fetch: fetchImpl });
1299
+ const { apiKey, projectId } = await createProjectWithApiKey({ baseUrl, name: newProjectName.trim(), fetch: fetchImpl });
980
1300
  onConnected(apiKey, { projectId, source: "new" });
981
1301
  onOpenChange(false);
982
1302
  resetState();
@@ -1199,6 +1519,23 @@ var IconLayers = ({ className }) => /* @__PURE__ */ jsxs("svg", { xmlns: "http:/
1199
1519
  /* @__PURE__ */ jsx("polyline", { points: "2 17 12 22 22 17" }),
1200
1520
  /* @__PURE__ */ jsx("polyline", { points: "2 12 12 17 22 12" })
1201
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
+ );
1202
1539
  function ImageManager(props) {
1203
1540
  const {
1204
1541
  open,
@@ -1206,6 +1543,7 @@ function ImageManager(props) {
1206
1543
  clientOptions,
1207
1544
  projectId,
1208
1545
  folderPath,
1546
+ userId,
1209
1547
  title,
1210
1548
  description,
1211
1549
  className,
@@ -1219,7 +1557,9 @@ function ImageManager(props) {
1219
1557
  maxFiles = 10,
1220
1558
  mode = "full",
1221
1559
  showDelete = true,
1222
- refreshInterval = 5e3
1560
+ refreshInterval = 5e3,
1561
+ stayOnUploadAfterComplete = false,
1562
+ thumbnailConfig
1223
1563
  } = props;
1224
1564
  const [activeView, setActiveView] = React4.useState(mode === "upload" ? "upload" : "browse");
1225
1565
  const [loading, setLoading] = React4.useState(false);
@@ -1237,6 +1577,7 @@ function ImageManager(props) {
1237
1577
  const [thumbnailSelectorFile, setThumbnailSelectorFile] = React4.useState(null);
1238
1578
  const [selectedThumbnailForFile, setSelectedThumbnailForFile] = React4.useState(null);
1239
1579
  const [previewFile, setPreviewFile] = React4.useState(null);
1580
+ const [generatingThumbnails, setGeneratingThumbnails] = React4.useState(false);
1240
1581
  const lastUpdatedRef = React4.useRef(null);
1241
1582
  const isFetchingRef = React4.useRef(false);
1242
1583
  const deduplicateFiles = React4.useCallback((files2) => {
@@ -1259,10 +1600,11 @@ function ImageManager(props) {
1259
1600
  const currentPathPrefix = breadcrumbs.length > 1 ? breadcrumbs.slice(1).map((b) => b.name).join("/") + "/" : void 0;
1260
1601
  const effectiveFolderPath = currentPathPrefix || folderPath;
1261
1602
  const [filesData, foldersData] = await Promise.all([
1262
- fetchProjectImagesWithThumbs(clientOptions, { folderPath: effectiveFolderPath, limitConcurrent: 6 }),
1603
+ fetchProjectFiles(clientOptions, effectiveFolderPath),
1263
1604
  client.getFolders(currentFolderId)
1264
1605
  ]);
1265
- const { items: list, updatedAt } = filesData;
1606
+ const { files: rawFiles, updatedAt } = filesData;
1607
+ const list = rawFiles;
1266
1608
  if (isPolling && updatedAt && updatedAt === lastUpdatedRef.current) {
1267
1609
  }
1268
1610
  setFiles(deduplicateFiles(list));
@@ -1288,30 +1630,6 @@ function ImageManager(props) {
1288
1630
  load();
1289
1631
  }
1290
1632
  }, [open, activeView, load, mode]);
1291
- React4.useEffect(() => {
1292
- if (!open) return;
1293
- const client = new UpfilesClient(clientOptions);
1294
- const url = client.getEventsUrl(projectId);
1295
- const eventSource = new EventSource(url);
1296
- eventSource.onmessage = (event) => {
1297
- try {
1298
- const payload = JSON.parse(event.data);
1299
- if (payload.type === "file:uploaded") {
1300
- const newFile = payload.data.file;
1301
- setFiles((prev) => {
1302
- if (prev.find((f2) => f2.key === newFile.key)) return prev;
1303
- return deduplicateFiles([newFile, ...prev]);
1304
- });
1305
- } else if (payload.type === "file:deleted") {
1306
- const deletedKey = payload.data.fileKey || payload.data.file?.key;
1307
- setFiles((prev) => prev.filter((f2) => f2.key !== deletedKey));
1308
- }
1309
- } catch (e) {
1310
- console.error("SSE Error parsing message", e);
1311
- }
1312
- };
1313
- return () => eventSource.close();
1314
- }, [open, clientOptions, projectId]);
1315
1633
  const visibleFiles = React4.useMemo(() => {
1316
1634
  const q = query.trim().toLowerCase();
1317
1635
  return !q ? files : files.filter((i) => (i.originalName || "").toLowerCase().includes(q));
@@ -1335,10 +1653,18 @@ function ImageManager(props) {
1335
1653
  "content-type": "application/json",
1336
1654
  ...clientOptions?.headers || {}
1337
1655
  };
1338
- 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) {
1339
1665
  const keyHeader = clientOptions.apiKeyHeader || "authorization";
1340
1666
  if (keyHeader === "authorization") {
1341
- headers["authorization"] = clientOptions.apiKey.startsWith("upk_") ? `Bearer ${clientOptions.apiKey}` : clientOptions.apiKey;
1667
+ headers["authorization"] = clientOptions.apiKey.startsWith("upf_") ? `Bearer ${clientOptions.apiKey}` : clientOptions.apiKey;
1342
1668
  } else {
1343
1669
  headers[keyHeader] = clientOptions.apiKey;
1344
1670
  }
@@ -1398,35 +1724,40 @@ function ImageManager(props) {
1398
1724
  });
1399
1725
  return options;
1400
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]);
1401
1748
  const [selectedUploadFolder, setSelectedUploadFolder] = React4.useState("current");
1402
1749
  const activeBreadcrumbPath = React4.useMemo(() => {
1403
1750
  return breadcrumbs.length > 1 ? breadcrumbs.slice(1).map((b) => b.name).join("/") + "/" : void 0;
1404
1751
  }, [breadcrumbs]);
1405
1752
  return /* @__PURE__ */ jsxs(Dialog2.Root, { open, onOpenChange, children: [
1406
1753
  /* @__PURE__ */ jsxs(Dialog2.Portal, { children: [
1407
- /* @__PURE__ */ jsx(Dialog2.Overlay, { className: "fixed inset-0 bg-black/60 backdrop-blur-sm z-50 transition-all duration-300 flex items-center justify-center" }),
1754
+ /* @__PURE__ */ jsx(Dialog2.Overlay, { className: "fixed inset-0 bg-black/60 z-[9999]" }),
1408
1755
  /* @__PURE__ */ jsxs(
1409
1756
  Dialog2.Content,
1410
1757
  {
1411
- 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-[51] 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 ?? ""}`,
1412
1759
  children: [
1413
- /* @__PURE__ */ jsx(
1414
- Dialog2.Title,
1415
- {
1416
- style: {
1417
- position: "absolute",
1418
- width: "1px",
1419
- height: "1px",
1420
- padding: "0",
1421
- margin: "-1px",
1422
- overflow: "hidden",
1423
- clip: "rect(0, 0, 0, 0)",
1424
- whiteSpace: "nowrap",
1425
- border: "0"
1426
- },
1427
- children: title ?? "Media Library"
1428
- }
1429
- ),
1760
+ /* @__PURE__ */ jsx(VisuallyHidden, { children: /* @__PURE__ */ jsx(Dialog2.Title, { children: title ?? "Media Library" }) }),
1430
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: [
1431
1762
  /* @__PURE__ */ jsxs("div", { className: "p-6", children: [
1432
1763
  /* @__PURE__ */ jsxs("h2", { className: "text-lg font-bold text-gray-900 dark:text-white flex items-center gap-2", children: [
@@ -1533,16 +1864,27 @@ function ImageManager(props) {
1533
1864
  folder.id
1534
1865
  )),
1535
1866
  visibleFiles.map((f2) => {
1536
- const fWithThumbs = f2;
1537
- const bestThumb = fWithThumbs.thumbnails?.sort((a, b) => a.size - b.size).find((t) => t.size >= 300) || fWithThumbs.thumbnails?.[fWithThumbs.thumbnails?.length - 1];
1538
- const thumbUrl = bestThumb?.url || f2.url;
1867
+ const displayUrl = f2.url;
1868
+ const hasThumbnails = f2.thumbnails && f2.thumbnails.length > 0;
1539
1869
  return /* @__PURE__ */ jsxs(
1540
1870
  "div",
1541
1871
  {
1542
1872
  onClick: () => setPreviewFile(f2),
1543
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",
1544
1874
  children: [
1545
- /* @__PURE__ */ jsx("img", { src: thumbUrl, alt: f2.originalName, className: "w-full h-full object-cover transition-transform duration-700 group-hover:scale-105" }),
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
+ ),
1546
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: [
1547
1889
  /* @__PURE__ */ jsx(
1548
1890
  "button",
@@ -1563,19 +1905,6 @@ function ImageManager(props) {
1563
1905
  children: "Select"
1564
1906
  }
1565
1907
  ),
1566
- fWithThumbs.thumbnails && fWithThumbs.thumbnails.length > 0 && /* @__PURE__ */ jsx(
1567
- "button",
1568
- {
1569
- onClick: (e) => {
1570
- e.stopPropagation();
1571
- setThumbnailSelectorFile(f2);
1572
- setSelectedThumbnailForFile(null);
1573
- },
1574
- className: "p-1.5 bg-white/20 text-white text-xs font-semibold rounded-md hover:bg-purple-500 backdrop-blur-md transition-colors",
1575
- title: "Select Thumbnail",
1576
- children: /* @__PURE__ */ jsx(IconLayers, { className: "w-4 h-4" })
1577
- }
1578
- ),
1579
1908
  showDelete && /* @__PURE__ */ jsx(
1580
1909
  "button",
1581
1910
  {
@@ -1601,7 +1930,7 @@ function ImageManager(props) {
1601
1930
  /* @__PURE__ */ jsx("p", { className: "text-sm", children: "Try uploading a file or creating a new folder." })
1602
1931
  ] })
1603
1932
  ] }),
1604
- activeView === "upload" && /* @__PURE__ */ jsx("div", { className: "h-full flex flex-col items-center justify-center max-w-2xl mx-auto", children: /* @__PURE__ */ jsxs("div", { className: "w-full bg-white rounded-2xl border border-gray-200 p-8 shadow-sm", children: [
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: [
1605
1934
  /* @__PURE__ */ jsxs("h3", { className: "text-lg font-semibold text-gray-900 mb-6 flex items-center gap-2", children: [
1606
1935
  /* @__PURE__ */ jsx(IconUploadCloud, { className: "text-blue-500" }),
1607
1936
  "Upload Files"
@@ -1626,15 +1955,28 @@ function ImageManager(props) {
1626
1955
  {
1627
1956
  clientOptions,
1628
1957
  projectId,
1958
+ userId,
1629
1959
  folderPath: uploadFoldersList.find((o) => o.id === selectedUploadFolder)?.path || activeBreadcrumbPath || folderPath,
1630
1960
  accept: ["image/*"],
1631
1961
  maxFileSize,
1632
1962
  maxFiles,
1633
1963
  autoRecordToDb,
1634
- fetchThumbnails,
1635
- autoUpload: true,
1636
- onClientUploadComplete: (file) => {
1964
+ fetchThumbnails: false,
1965
+ autoUpload: false,
1966
+ thumbnailEnabled: thumbnailConfig?.enabled ?? true,
1967
+ thumbnailSizes: thumbnailConfig?.sizes,
1968
+ onClientUploadComplete: async (file) => {
1637
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
+ }
1638
1980
  const newItem = {
1639
1981
  key: file.fileKey,
1640
1982
  originalName: file.name,
@@ -1642,7 +1984,7 @@ function ImageManager(props) {
1642
1984
  contentType: file.type,
1643
1985
  uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
1644
1986
  url: file.publicUrl,
1645
- thumbnails: file.thumbnails
1987
+ thumbnails
1646
1988
  };
1647
1989
  setFiles((prev) => {
1648
1990
  if (prev.find((f2) => f2.key === newItem.key)) return prev;
@@ -1651,11 +1993,13 @@ function ImageManager(props) {
1651
1993
  }
1652
1994
  },
1653
1995
  onComplete: () => {
1654
- if (mode === "full") {
1996
+ if (mode === "full" && !stayOnUploadAfterComplete) {
1655
1997
  setTimeout(() => {
1656
1998
  setActiveView("browse");
1657
1999
  load();
1658
2000
  }, 1e3);
2001
+ } else {
2002
+ load();
1659
2003
  }
1660
2004
  },
1661
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",
@@ -1681,8 +2025,8 @@ function ImageManager(props) {
1681
2025
  )
1682
2026
  ] }),
1683
2027
  /* @__PURE__ */ jsx(Dialog2.Root, { open: showCreateFolder, onOpenChange: setShowCreateFolder, children: /* @__PURE__ */ jsxs(Dialog2.Portal, { children: [
1684
- /* @__PURE__ */ jsx(Dialog2.Overlay, { className: "fixed inset-0 bg-black/40 z-[60]" }),
1685
- /* @__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-[61] outline-none", "aria-describedby": "new-folder-description", 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: [
1686
2030
  /* @__PURE__ */ jsx(Dialog2.Title, { className: "text-lg font-semibold text-gray-900 mb-4", children: "New Folder" }),
1687
2031
  /* @__PURE__ */ jsx(Dialog2.Description, { id: "new-folder-description", className: "sr-only", children: "Create a new folder to organize your files" }),
1688
2032
  /* @__PURE__ */ jsx(
@@ -1711,8 +2055,8 @@ function ImageManager(props) {
1711
2055
  ] })
1712
2056
  ] }) }),
1713
2057
  /* @__PURE__ */ jsx(Dialog2.Root, { open: !!fileToDelete, onOpenChange: (open2) => !open2 && setFileToDelete(null), children: /* @__PURE__ */ jsxs(Dialog2.Portal, { children: [
1714
- /* @__PURE__ */ jsx(Dialog2.Overlay, { className: "fixed inset-0 bg-black/40 z-[80]" }),
1715
- /* @__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-[81]", "aria-describedby": "delete-file-description", 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: [
1716
2060
  /* @__PURE__ */ jsx(Dialog2.Title, { className: "text-xl font-semibold text-gray-900 mb-2", children: "Delete File" }),
1717
2061
  /* @__PURE__ */ jsxs(Dialog2.Description, { id: "delete-file-description", className: "text-gray-600 mb-6", children: [
1718
2062
  "Are you sure you want to delete ",
@@ -1745,8 +2089,8 @@ function ImageManager(props) {
1745
2089
  ] })
1746
2090
  ] }) }),
1747
2091
  /* @__PURE__ */ jsx(Dialog2.Root, { open: !!thumbnailSelectorFile, onOpenChange: (open2) => !open2 && setThumbnailSelectorFile(null), children: /* @__PURE__ */ jsxs(Dialog2.Portal, { children: [
1748
- /* @__PURE__ */ jsx(Dialog2.Overlay, { className: "fixed inset-0 bg-black/50 z-[90]" }),
1749
- /* @__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-[91] overflow-hidden", "aria-describedby": "thumbnail-selector-description", 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: [
1750
2094
  /* @__PURE__ */ jsxs("div", { className: "p-6 border-b border-gray-200", children: [
1751
2095
  /* @__PURE__ */ jsx(Dialog2.Title, { className: "text-xl font-semibold text-gray-900 mb-2", children: "Select Thumbnail" }),
1752
2096
  /* @__PURE__ */ jsxs(Dialog2.Description, { id: "thumbnail-selector-description", className: "text-gray-600", children: [
@@ -1758,60 +2102,66 @@ function ImageManager(props) {
1758
2102
  ] })
1759
2103
  ] })
1760
2104
  ] }),
1761
- /* @__PURE__ */ jsx("div", { className: "p-6 overflow-y-auto max-h-[60vh]", children: /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4", children: [
1762
- /* @__PURE__ */ jsxs(
1763
- "div",
1764
- {
1765
- onClick: () => setSelectedThumbnailForFile(null),
1766
- 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"}`,
1767
- children: [
1768
- /* @__PURE__ */ jsxs("div", { className: "aspect-square bg-gray-100 flex items-center justify-center relative", children: [
1769
- /* @__PURE__ */ jsx(
1770
- "img",
1771
- {
1772
- src: thumbnailSelectorFile?.url,
1773
- alt: "Original",
1774
- className: "w-full h-full object-cover"
1775
- }
1776
- ),
1777
- 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" }) }) }) })
1778
- ] }),
1779
- /* @__PURE__ */ jsxs("div", { className: "p-3 bg-white", children: [
1780
- /* @__PURE__ */ jsx("div", { className: "font-medium text-sm text-gray-900", children: "Original" }),
1781
- /* @__PURE__ */ jsx("div", { className: "text-xs text-gray-500", children: "Full size" })
1782
- ] })
1783
- ]
1784
- }
1785
- ),
1786
- thumbnailSelectorFile?.thumbnails?.map((thumbnail) => /* @__PURE__ */ jsxs(
1787
- "div",
1788
- {
1789
- onClick: () => setSelectedThumbnailForFile(thumbnail),
1790
- 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"}`,
1791
- children: [
1792
- /* @__PURE__ */ jsxs("div", { className: "aspect-square bg-gray-100 flex items-center justify-center relative", children: [
1793
- /* @__PURE__ */ jsx(
1794
- "img",
1795
- {
1796
- src: thumbnail.url,
1797
- alt: `${thumbnail.size}px thumbnail`,
1798
- className: "w-full h-full object-cover"
1799
- }
1800
- ),
1801
- 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" }) }) }) })
1802
- ] }),
1803
- /* @__PURE__ */ jsxs("div", { className: "p-3 bg-white", children: [
1804
- /* @__PURE__ */ jsxs("div", { className: "font-medium text-sm text-gray-900", children: [
1805
- thumbnail.size,
1806
- "px"
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" }) }) }) })
1807
2127
  ] }),
1808
- /* @__PURE__ */ jsx("div", { className: "text-xs text-gray-500", children: thumbnail.sizeType || "Thumbnail" })
1809
- ] })
1810
- ]
1811
- },
1812
- thumbnail.id
1813
- ))
1814
- ] }) }),
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
+ ] }),
1815
2165
  /* @__PURE__ */ jsxs("div", { className: "p-6 border-t border-gray-200 flex justify-end gap-3", children: [
1816
2166
  /* @__PURE__ */ jsx(
1817
2167
  "button",
@@ -1850,8 +2200,8 @@ function ImageManager(props) {
1850
2200
  ] })
1851
2201
  ] }) }),
1852
2202
  /* @__PURE__ */ jsx(Dialog2.Root, { open: !!previewFile, onOpenChange: (open2) => !open2 && setPreviewFile(null), children: /* @__PURE__ */ jsxs(Dialog2.Portal, { children: [
1853
- /* @__PURE__ */ jsx(Dialog2.Overlay, { className: "fixed inset-0 bg-black/90 z-[100]", onClick: () => setPreviewFile(null) }),
1854
- /* @__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-[101] outline-none", "aria-describedby": "image-preview-description", children: /* @__PURE__ */ jsxs("div", { className: "relative w-full h-full flex flex-col", 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: [
1855
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: [
1856
2206
  /* @__PURE__ */ jsxs("div", { className: "text-white", children: [
1857
2207
  /* @__PURE__ */ jsx(Dialog2.Title, { className: "text-xl font-semibold mb-1", children: previewFile?.originalName }),
@@ -1923,14 +2273,15 @@ function ImageManager(props) {
1923
2273
  children: "Open in New Tab"
1924
2274
  }
1925
2275
  ),
1926
- previewFile?.thumbnails && previewFile.thumbnails.length > 0 && /* @__PURE__ */ jsxs(
2276
+ previewFile?.contentType?.startsWith("image/") && /* @__PURE__ */ jsxs(
1927
2277
  "button",
1928
2278
  {
1929
2279
  onClick: (e) => {
1930
2280
  e.stopPropagation();
1931
- setThumbnailSelectorFile(previewFile);
1932
- setSelectedThumbnailForFile(null);
1933
- setPreviewFile(null);
2281
+ if (previewFile) {
2282
+ handleThumbnailClick(previewFile);
2283
+ setPreviewFile(null);
2284
+ }
1934
2285
  },
1935
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",
1936
2287
  children: [
@@ -1944,7 +2295,138 @@ function ImageManager(props) {
1944
2295
  ] }) })
1945
2296
  ] });
1946
2297
  }
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
+ }
1947
2429
 
1948
- export { ConnectProjectDialog, ImageManager, ProjectFilesWidget, UpfilesClient, Uploader, addApiKeyManually, connectProject, connectUsingApiKey, createClientWithKey, createProject, fetchFileThumbnails, fetchProjectFiles, fetchProjectImagesWithThumbs, listProjects };
2430
+ export { ConnectProjectDialog, ImageManager, ProjectFilesWidget, StorageQuotaError, UpfilesClient, Uploader, addApiKeyManually, connectProject, connectUsingApiKey, createClientWithKey, createProject, createProjectWithApiKey, fetchFileThumbnails, fetchProjectFiles, fetchProjectImagesWithThumbs, listProjects, updateProject, useUpfilesInhouse };
1949
2431
  //# sourceMappingURL=index.mjs.map
1950
2432
  //# sourceMappingURL=index.mjs.map