@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.js CHANGED
@@ -26,6 +26,13 @@ var React4__namespace = /*#__PURE__*/_interopNamespace(React4);
26
26
  var Dialog2__namespace = /*#__PURE__*/_interopNamespace(Dialog2);
27
27
 
28
28
  // src/client.ts
29
+ var StorageQuotaError = class extends Error {
30
+ constructor(message, details) {
31
+ super(message);
32
+ this.name = "StorageQuotaError";
33
+ this.details = details;
34
+ }
35
+ };
29
36
  var UpfilesClient = class {
30
37
  constructor(opts) {
31
38
  this.baseUrl = opts.baseUrl?.replace(/\/$/, "");
@@ -39,25 +46,66 @@ var UpfilesClient = class {
39
46
  this.completionPath = opts.completionPath ?? "/api/plugin/files";
40
47
  this.foldersPath = opts.foldersPath ?? "/api/plugin/folders";
41
48
  this.callCompletionEndpoint = opts.callCompletionEndpoint ?? false;
49
+ this.getToken = opts.getToken;
50
+ this.projectId = opts.projectId;
51
+ this.projectName = opts.projectName;
42
52
  }
43
- async getPresignedUrl(params) {
44
- if (!params.fileName) throw new Error("fileName is required");
45
- if (!params.fileType) throw new Error("fileType is required");
46
- if (!params.fileSize || params.fileSize <= 0) throw new Error("fileSize must be > 0");
47
- const target = this.presignUrl ? this.presignUrl : this.baseUrl ? `${this.baseUrl}${this.presignPath}` : this.presignPath;
48
- const headers = {
49
- "content-type": "application/json",
50
- ...this.headers || {}
51
- };
53
+ /** Build auth headers for API requests. Priority: getToken (JWT) > apiKey > static headers */
54
+ async buildAuthHeaders() {
55
+ const headers = { ...this.headers || {} };
56
+ if (this.getToken) {
57
+ const token = await this.getToken();
58
+ if (token) {
59
+ headers["authorization"] = `Bearer ${token}`;
60
+ }
61
+ if (this.projectName) {
62
+ headers["x-project-name"] = this.projectName;
63
+ } else if (this.projectId) {
64
+ headers["x-project-id"] = this.projectId;
65
+ }
66
+ return headers;
67
+ }
52
68
  if (this.apiKey) {
53
69
  if (this.apiKeyHeader === "authorization") {
54
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
70
+ headers["authorization"] = this.apiKey.startsWith("upf_") ? `Bearer ${this.apiKey}` : this.apiKey;
55
71
  } else if (this.apiKeyHeader === "x-api-key") {
56
72
  headers["x-api-key"] = this.apiKey;
57
73
  } else {
58
74
  headers["x-up-api-key"] = this.apiKey;
59
75
  }
60
76
  }
77
+ return headers;
78
+ }
79
+ /** Get storage quota info for the project */
80
+ async getQuota() {
81
+ const basePath = this.presignPath.replace(/\/upload$/, "");
82
+ const target = this.baseUrl ? `${this.baseUrl}${basePath}/quota` : `${basePath}/quota`;
83
+ const headers = await this.buildAuthHeaders();
84
+ const res = await fetch(target, {
85
+ method: "GET",
86
+ headers,
87
+ credentials: this.withCredentials ? "include" : "same-origin"
88
+ });
89
+ if (!res.ok) {
90
+ let msg = "Failed to get storage quota";
91
+ try {
92
+ const data = await res.json();
93
+ msg = data.message || data.error || msg;
94
+ } catch {
95
+ }
96
+ throw new Error(msg);
97
+ }
98
+ return res.json();
99
+ }
100
+ async getPresignedUrl(params) {
101
+ if (!params.fileName) throw new Error("fileName is required");
102
+ if (!params.fileType) throw new Error("fileType is required");
103
+ if (!params.fileSize || params.fileSize <= 0) throw new Error("fileSize must be > 0");
104
+ const target = this.presignUrl ? this.presignUrl : this.baseUrl ? `${this.baseUrl}${this.presignPath}` : this.presignPath;
105
+ const headers = {
106
+ "content-type": "application/json",
107
+ ...await this.buildAuthHeaders()
108
+ };
61
109
  const res = await fetch(target, {
62
110
  method: "POST",
63
111
  headers,
@@ -68,8 +116,12 @@ var UpfilesClient = class {
68
116
  let msg = "Failed to get upload URL";
69
117
  try {
70
118
  const data = await res.json();
71
- msg = data.error || msg;
72
- } catch {
119
+ if (res.status === 413 && data.error === "STORAGE_QUOTA_EXCEEDED") {
120
+ throw new StorageQuotaError(data.message || "Storage quota exceeded", data.details);
121
+ }
122
+ msg = data.message || data.error || msg;
123
+ } catch (e) {
124
+ if (e instanceof StorageQuotaError) throw e;
73
125
  }
74
126
  throw new Error(msg);
75
127
  }
@@ -105,7 +157,11 @@ var UpfilesClient = class {
105
157
  originalName: file.name,
106
158
  size: file.size,
107
159
  contentType: file.type || "application/octet-stream",
108
- projectId: extras?.projectId
160
+ projectId: extras?.projectId,
161
+ url: presign.publicUrl,
162
+ userId: extras?.userId
163
+ // 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.
164
+ // 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.
109
165
  });
110
166
  } catch (error) {
111
167
  throw new Error(`Upload verification failed: ${error.message}`);
@@ -134,22 +190,16 @@ var UpfilesClient = class {
134
190
  const target = this.baseUrl ? `${this.baseUrl}${this.completionPath}` : this.completionPath;
135
191
  const headers = {
136
192
  "content-type": "application/json",
137
- ...this.headers || {}
193
+ ...await this.buildAuthHeaders()
138
194
  };
139
- if (this.apiKey) {
140
- if (this.apiKeyHeader === "authorization") {
141
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
142
- } else if (this.apiKeyHeader === "x-api-key") {
143
- headers["x-api-key"] = this.apiKey;
144
- } else {
145
- headers["x-up-api-key"] = this.apiKey;
146
- }
147
- }
148
195
  const res = await fetch(target, {
149
196
  method: "POST",
150
197
  headers,
151
198
  credentials: this.withCredentials ? "include" : "same-origin",
152
- body: JSON.stringify(params)
199
+ body: JSON.stringify({
200
+ ...params,
201
+ key: params.fileKey
202
+ })
153
203
  });
154
204
  if (!res.ok) {
155
205
  let msg = "Failed to complete upload";
@@ -165,16 +215,7 @@ var UpfilesClient = class {
165
215
  async getThumbnails(fileKey) {
166
216
  const target = this.baseUrl ? `${this.baseUrl}${this.thumbnailsPath}` : this.thumbnailsPath;
167
217
  const url = `${target}?fileKey=${encodeURIComponent(fileKey)}`;
168
- const headers = { ...this.headers || {} };
169
- if (this.apiKey) {
170
- if (this.apiKeyHeader === "authorization") {
171
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
172
- } else if (this.apiKeyHeader === "x-api-key") {
173
- headers["x-api-key"] = this.apiKey;
174
- } else {
175
- headers["x-up-api-key"] = this.apiKey;
176
- }
177
- }
218
+ const headers = await this.buildAuthHeaders();
178
219
  const res = await fetch(url, { method: "GET", headers, credentials: this.withCredentials ? "include" : "same-origin" });
179
220
  if (!res.ok) {
180
221
  let msg = "Failed to fetch thumbnails";
@@ -188,26 +229,19 @@ var UpfilesClient = class {
188
229
  const data = await res.json();
189
230
  return data?.thumbnails ?? [];
190
231
  }
191
- async generateThumbnails(fileKey) {
232
+ async generateThumbnails(fileKey, sizes) {
192
233
  const target = this.baseUrl ? `${this.baseUrl}${this.thumbnailsPath}` : this.thumbnailsPath;
193
234
  const headers = {
194
235
  "content-type": "application/json",
195
- ...this.headers || {}
236
+ ...await this.buildAuthHeaders()
196
237
  };
197
- if (this.apiKey) {
198
- if (this.apiKeyHeader === "authorization") {
199
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
200
- } else if (this.apiKeyHeader === "x-api-key") {
201
- headers["x-api-key"] = this.apiKey;
202
- } else {
203
- headers["x-up-api-key"] = this.apiKey;
204
- }
205
- }
238
+ const body = { fileKey };
239
+ if (sizes && sizes.length > 0) body.sizes = sizes;
206
240
  const res = await fetch(target, {
207
241
  method: "POST",
208
242
  headers,
209
243
  credentials: this.withCredentials ? "include" : "same-origin",
210
- body: JSON.stringify({ fileKey })
244
+ body: JSON.stringify(body)
211
245
  });
212
246
  if (!res.ok) {
213
247
  let msg = "Failed to generate thumbnails";
@@ -221,19 +255,53 @@ var UpfilesClient = class {
221
255
  const data = await res.json();
222
256
  return { masterWebp: data.masterWebp, thumbnails: data.thumbnails ?? [] };
223
257
  }
258
+ /** Get thumbnail settings for the project (requires API key) */
259
+ async getThumbnailSettings() {
260
+ const basePath = this.presignPath.replace(/\/upload$/, "");
261
+ const target = this.baseUrl ? `${this.baseUrl}${basePath}/settings/thumbnails` : `${basePath}/settings/thumbnails`;
262
+ const headers = await this.buildAuthHeaders();
263
+ const res = await fetch(target, {
264
+ method: "GET",
265
+ headers,
266
+ credentials: this.withCredentials ? "include" : "same-origin"
267
+ });
268
+ if (!res.ok) {
269
+ let msg = "Failed to get 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
+ }
279
+ /** Update thumbnail settings for the project (requires API key with write scope) */
280
+ async updateThumbnailSettings(settings) {
281
+ const basePath = this.presignPath.replace(/\/upload$/, "");
282
+ const target = this.baseUrl ? `${this.baseUrl}${basePath}/settings/thumbnails` : `${basePath}/settings/thumbnails`;
283
+ const headers = { "content-type": "application/json", ...await this.buildAuthHeaders() };
284
+ const res = await fetch(target, {
285
+ method: "PATCH",
286
+ headers,
287
+ credentials: this.withCredentials ? "include" : "same-origin",
288
+ body: JSON.stringify(settings)
289
+ });
290
+ if (!res.ok) {
291
+ let msg = "Failed to update thumbnail settings";
292
+ try {
293
+ const d = await res.json();
294
+ msg = d.message || msg;
295
+ } catch {
296
+ }
297
+ throw new Error(msg);
298
+ }
299
+ return res.json();
300
+ }
224
301
  async getFolders(parentId) {
225
302
  const target = this.baseUrl ? `${this.baseUrl}${this.foldersPath}` : this.foldersPath;
226
303
  const url = parentId ? `${target}?parentId=${encodeURIComponent(parentId)}` : target;
227
- const headers = { ...this.headers || {} };
228
- if (this.apiKey) {
229
- if (this.apiKeyHeader === "authorization") {
230
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
231
- } else if (this.apiKeyHeader === "x-api-key") {
232
- headers["x-api-key"] = this.apiKey;
233
- } else {
234
- headers["x-up-api-key"] = this.apiKey;
235
- }
236
- }
304
+ const headers = await this.buildAuthHeaders();
237
305
  const res = await fetch(url, { method: "GET", headers, credentials: this.withCredentials ? "include" : "same-origin" });
238
306
  if (!res.ok) {
239
307
  let msg = "Failed to fetch folders";
@@ -251,17 +319,8 @@ var UpfilesClient = class {
251
319
  const target = this.baseUrl ? `${this.baseUrl}${this.foldersPath}` : this.foldersPath;
252
320
  const headers = {
253
321
  "content-type": "application/json",
254
- ...this.headers || {}
322
+ ...await this.buildAuthHeaders()
255
323
  };
256
- if (this.apiKey) {
257
- if (this.apiKeyHeader === "authorization") {
258
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
259
- } else if (this.apiKeyHeader === "x-api-key") {
260
- headers["x-api-key"] = this.apiKey;
261
- } else {
262
- headers["x-up-api-key"] = this.apiKey;
263
- }
264
- }
265
324
  const res = await fetch(target, {
266
325
  method: "POST",
267
326
  headers,
@@ -285,16 +344,7 @@ var UpfilesClient = class {
285
344
  const filesPath = `${basePath}/files`;
286
345
  const target = this.baseUrl ? `${this.baseUrl}${filesPath}` : filesPath;
287
346
  const url = params?.folderPath ? `${target}?folderPath=${encodeURIComponent(params.folderPath)}` : target;
288
- const headers = { ...this.headers || {} };
289
- if (this.apiKey) {
290
- if (this.apiKeyHeader === "authorization") {
291
- headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
292
- } else if (this.apiKeyHeader === "x-api-key") {
293
- headers["x-api-key"] = this.apiKey;
294
- } else {
295
- headers["x-up-api-key"] = this.apiKey;
296
- }
297
- }
347
+ const headers = await this.buildAuthHeaders();
298
348
  const res = await fetch(url, { method: "GET", headers, credentials: this.withCredentials ? "include" : "same-origin" });
299
349
  if (!res.ok) {
300
350
  let msg = "Failed to fetch files";
@@ -312,24 +362,21 @@ var UpfilesClient = class {
312
362
  };
313
363
  }
314
364
  getEventsUrl(projectId) {
315
- this.thumbnailsPath.replace(/\/thumbnails$/, "");
316
365
  const target = this.baseUrl ? `${this.baseUrl}/api/events` : "/api/events";
317
366
  const url = new URL(target, window.location.href);
318
- if (this.apiKey) {
319
- url.searchParams.set("apiKey", this.apiKey);
320
- }
321
- if (projectId) {
322
- url.searchParams.set("projectId", projectId);
367
+ if (!this.getToken && !this.apiKey && (projectId || this.projectId)) {
368
+ url.searchParams.set("projectId", projectId || this.projectId);
323
369
  }
324
370
  return url.toString();
325
371
  }
326
372
  };
373
+ var THUMBNAIL_SIZE_OPTIONS = [64, 128, 256, 512];
327
374
  var Uploader = ({
328
375
  clientOptions,
329
376
  multiple = true,
330
377
  accept = ["image/*", "video/*", "audio/*", "application/pdf", "text/plain", "application/json"],
331
- maxFileSize = 100 * 1024 * 1024,
332
- maxFiles = 10,
378
+ maxFileSize = 0,
379
+ maxFiles = 0,
333
380
  onComplete,
334
381
  onError,
335
382
  onUploadBegin,
@@ -340,36 +387,124 @@ var Uploader = ({
340
387
  dropzoneClassName,
341
388
  children,
342
389
  projectId,
390
+ userId,
343
391
  folderPath,
344
392
  fetchThumbnails,
345
393
  autoRecordToDb = true,
346
394
  recordUrl = "/api/files",
347
- autoUpload = false
395
+ autoUpload = false,
396
+ thumbnailEnabled,
397
+ thumbnailSizes
348
398
  }) => {
349
399
  const inputRef = React4.useRef(null);
350
400
  const client = React4.useMemo(() => new UpfilesClient(clientOptions ?? {}), [clientOptions]);
351
401
  const [files, setFiles] = React4.useState([]);
352
402
  const [isUploading, setIsUploading] = React4.useState(false);
353
403
  const [isDragOver, setIsDragOver] = React4.useState(false);
354
- const addFiles = React4.useCallback((incoming) => {
355
- const filtered = incoming.filter((f2) => {
356
- if (f2.size > maxFileSize) {
357
- return false;
358
- }
359
- return true;
404
+ const [quotaError, setQuotaError] = React4.useState(null);
405
+ const showThumbnailUI = thumbnailEnabled !== void 0;
406
+ const [thumbEnabled, setThumbEnabled] = React4.useState(thumbnailEnabled ?? true);
407
+ const [thumbSizes, setThumbSizes] = React4.useState(thumbnailSizes ?? [256, 512]);
408
+ const [thumbFileIds, setThumbFileIds] = React4.useState(/* @__PURE__ */ new Set());
409
+ const thumbEnabledRef = React4.useRef(thumbnailEnabled ?? true);
410
+ const thumbSizesRef = React4.useRef(thumbnailSizes ?? [256, 512]);
411
+ const thumbFileIdsRef = React4.useRef(/* @__PURE__ */ new Set());
412
+ React4.useEffect(() => {
413
+ thumbEnabledRef.current = thumbEnabled;
414
+ }, [thumbEnabled]);
415
+ React4.useEffect(() => {
416
+ thumbSizesRef.current = thumbSizes;
417
+ }, [thumbSizes]);
418
+ React4.useEffect(() => {
419
+ thumbFileIdsRef.current = thumbFileIds;
420
+ }, [thumbFileIds]);
421
+ const pendingImages = React4.useMemo(
422
+ () => files.filter((f2) => f2.type.startsWith("image/") && f2.status === "pending"),
423
+ [files]
424
+ );
425
+ const thumbSelectedCount = React4.useMemo(
426
+ () => pendingImages.filter((f2) => thumbFileIds.has(f2.id)).length,
427
+ [pendingImages, thumbFileIds]
428
+ );
429
+ const handleThumbEnabledToggle = React4.useCallback(() => {
430
+ const newVal = !thumbEnabledRef.current;
431
+ thumbEnabledRef.current = newVal;
432
+ setThumbEnabled(newVal);
433
+ if (newVal) {
434
+ setFiles((prev) => {
435
+ const ids = prev.filter((f2) => f2.type.startsWith("image/") && f2.status === "pending").map((f2) => f2.id);
436
+ if (ids.length > 0) {
437
+ const next = new Set(thumbFileIdsRef.current);
438
+ ids.forEach((id) => next.add(id));
439
+ thumbFileIdsRef.current = next;
440
+ setThumbFileIds(next);
441
+ }
442
+ return prev;
443
+ });
444
+ }
445
+ }, []);
446
+ const toggleThumbSize = React4.useCallback((size) => {
447
+ setThumbSizes((prev) => {
448
+ const next = prev.includes(size) ? prev.filter((s) => s !== size) : [...prev, size].sort((a, b) => a - b);
449
+ thumbSizesRef.current = next;
450
+ return next;
360
451
  });
361
- const limited = filtered.slice(0, Math.max(0, maxFiles - files.length));
452
+ }, []);
453
+ const toggleFileThumb = React4.useCallback((fileId) => {
454
+ setThumbFileIds((prev) => {
455
+ const next = new Set(prev);
456
+ if (next.has(fileId)) next.delete(fileId);
457
+ else next.add(fileId);
458
+ thumbFileIdsRef.current = next;
459
+ return next;
460
+ });
461
+ }, []);
462
+ const selectAllThumbs = React4.useCallback(() => {
463
+ const ids = pendingImages.map((f2) => f2.id);
464
+ const next = new Set(thumbFileIdsRef.current);
465
+ ids.forEach((id) => next.add(id));
466
+ thumbFileIdsRef.current = next;
467
+ setThumbFileIds(next);
468
+ }, [pendingImages]);
469
+ const deselectAllThumbs = React4.useCallback(() => {
470
+ const pendingSet = new Set(pendingImages.map((f2) => f2.id));
471
+ const next = new Set([...thumbFileIdsRef.current].filter((id) => !pendingSet.has(id)));
472
+ thumbFileIdsRef.current = next;
473
+ setThumbFileIds(next);
474
+ }, [pendingImages]);
475
+ const addFiles = React4.useCallback((incoming) => {
476
+ setQuotaError(null);
477
+ const filtered = maxFileSize > 0 ? incoming.filter((f2) => f2.size <= maxFileSize) : incoming;
478
+ const limited = maxFiles > 0 ? filtered.slice(0, Math.max(0, maxFiles - files.length)) : filtered;
362
479
  const mapped = limited.map((f2) => ({
363
480
  id: Math.random().toString(36).slice(2),
364
481
  file: f2,
365
482
  name: f2.name,
366
483
  size: f2.size,
367
- type: f2.type,
484
+ type: f2.type || "application/octet-stream",
368
485
  progress: 0,
369
486
  status: "pending"
370
487
  }));
371
488
  setFiles((prev) => [...prev, ...mapped]);
372
- }, [files.length, maxFileSize, maxFiles]);
489
+ if (showThumbnailUI && thumbEnabledRef.current) {
490
+ const imageIds = mapped.filter((f2) => f2.type.startsWith("image/")).map((f2) => f2.id);
491
+ if (imageIds.length > 0) {
492
+ const next = new Set(thumbFileIdsRef.current);
493
+ imageIds.forEach((id) => next.add(id));
494
+ thumbFileIdsRef.current = next;
495
+ setThumbFileIds(next);
496
+ }
497
+ }
498
+ }, [files.length, maxFileSize, maxFiles, showThumbnailUI]);
499
+ const removeFile = React4.useCallback((fileId) => {
500
+ setFiles((prev) => prev.filter((f2) => f2.id !== fileId));
501
+ if (showThumbnailUI) {
502
+ const next = new Set(thumbFileIdsRef.current);
503
+ next.delete(fileId);
504
+ thumbFileIdsRef.current = next;
505
+ setThumbFileIds(next);
506
+ }
507
+ }, [showThumbnailUI]);
373
508
  const onInputChange = React4.useCallback((e) => {
374
509
  const fs = Array.from(e.target.files || []);
375
510
  addFiles(fs);
@@ -390,7 +525,7 @@ var Uploader = ({
390
525
  update({ status: "uploading", progress: 1 });
391
526
  const presign = await client.getPresignedUrl({
392
527
  fileName: item.name,
393
- fileType: item.type,
528
+ fileType: item.type || "application/octet-stream",
394
529
  fileSize: item.size,
395
530
  projectId,
396
531
  folderPath
@@ -417,14 +552,17 @@ var Uploader = ({
417
552
  xhr.send(item.file);
418
553
  });
419
554
  let completionResult = null;
420
- if (autoRecordToDb && projectId) {
555
+ const canRecordToDb = autoRecordToDb && (projectId || clientOptions?.projectName || clientOptions?.projectId || clientOptions?.apiKey);
556
+ if (canRecordToDb) {
421
557
  try {
422
558
  completionResult = await client.completeUpload({
423
559
  fileKey: presign.fileKey,
424
560
  originalName: item.name,
425
561
  size: item.size,
426
- contentType: item.type,
427
- projectId
562
+ contentType: item.type || "application/octet-stream",
563
+ projectId,
564
+ url: presign.publicUrl,
565
+ userId
428
566
  });
429
567
  } catch (e) {
430
568
  console.error("Failed to complete upload:", e);
@@ -448,39 +586,86 @@ var Uploader = ({
448
586
  }
449
587
  const finalFileKey = completionResult?.file?.key || presign.fileKey;
450
588
  const finalPublicUrl = completionResult?.file?.url || presign.publicUrl;
589
+ const fileThumbEnabled = showThumbnailUI && thumbEnabledRef.current && thumbFileIdsRef.current.has(item.id);
451
590
  const result = {
452
591
  ...item,
453
592
  status: "success",
454
593
  progress: 100,
455
- fileKey: presign.fileKey,
456
- publicUrl: presign.publicUrl,
594
+ fileKey: finalFileKey,
595
+ publicUrl: finalPublicUrl,
457
596
  projectId: presign.projectId,
458
597
  apiKeyId: presign.apiKeyId,
598
+ thumbnailEnabled: showThumbnailUI ? fileThumbEnabled : void 0,
599
+ thumbnailSizesSelected: showThumbnailUI ? fileThumbEnabled ? [...thumbSizesRef.current] : [] : void 0,
459
600
  // @ts-ignore - allow downstream to access thumbs if present
460
601
  thumbnails: thumbs,
461
602
  // @ts-ignore - allow downstream to access masterWebp if present
462
603
  masterWebp
463
604
  };
464
- update({ status: "success", progress: 100, fileKey: presign.fileKey, publicUrl: presign.publicUrl });
605
+ update({ status: "success", progress: 100, fileKey: finalFileKey, publicUrl: finalPublicUrl });
465
606
  onClientUploadComplete?.(result);
466
607
  return result;
467
608
  } catch (err) {
468
- const message = err instanceof Error ? err.message : "Upload failed";
609
+ const message = err instanceof StorageQuotaError ? err.message : err instanceof Error ? err.message : "Upload failed";
469
610
  update({ status: "error", error: message });
470
611
  throw err;
471
612
  }
472
- }, [client, projectId, folderPath, fetchThumbnails, autoRecordToDb, recordUrl, clientOptions, onUploadBegin, onUploadProgress, onClientUploadComplete]);
613
+ }, [client, projectId, userId, folderPath, fetchThumbnails, autoRecordToDb, recordUrl, clientOptions, onUploadBegin, onUploadProgress, onClientUploadComplete, showThumbnailUI]);
473
614
  const uploadAll = React4.useCallback(async () => {
474
615
  const targets = files.filter((f2) => f2.status === "pending");
475
616
  if (!targets.length) return;
617
+ setQuotaError(null);
476
618
  setIsUploading(true);
477
619
  try {
620
+ try {
621
+ const quota = await client.getQuota();
622
+ if (quota.limitBytes > 0) {
623
+ const totalPendingSize = targets.reduce((sum, f2) => sum + f2.size, 0);
624
+ const remaining = quota.limitBytes - quota.usedBytes;
625
+ if (totalPendingSize > remaining) {
626
+ const fmt = (n) => {
627
+ if (n >= 1024 * 1024 * 1024) return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
628
+ if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(2)} MB`;
629
+ if (n >= 1024) return `${(n / 1024).toFixed(2)} KB`;
630
+ return `${n} B`;
631
+ };
632
+ const errMsg = `Storage quota exceeded. Remaining: ${fmt(remaining)}, Upload size: ${fmt(totalPendingSize)}`;
633
+ setQuotaError(errMsg);
634
+ targets.forEach((t) => {
635
+ setFiles((prev) => prev.map((f2) => f2.id === t.id ? { ...f2, status: "error", error: errMsg } : f2));
636
+ });
637
+ onError?.(new StorageQuotaError(errMsg, {
638
+ limit: quota.limitBytes,
639
+ used: quota.usedBytes,
640
+ remaining,
641
+ fileSize: totalPendingSize
642
+ }));
643
+ return;
644
+ }
645
+ }
646
+ } catch (e) {
647
+ if (e instanceof StorageQuotaError) {
648
+ setQuotaError(e.message);
649
+ targets.forEach((t) => {
650
+ setFiles((prev) => prev.map((f2) => f2.id === t.id ? { ...f2, status: "error", error: e.message } : f2));
651
+ });
652
+ onError?.(e);
653
+ return;
654
+ }
655
+ }
478
656
  const results = [];
479
657
  for (const f2 of targets) {
480
658
  try {
481
659
  const done = await uploadOne(f2);
482
660
  results.push(done);
483
661
  } catch (e) {
662
+ if (e instanceof StorageQuotaError) {
663
+ setQuotaError(e.message);
664
+ setFiles((prev) => prev.map(
665
+ (file) => file.status === "pending" ? { ...file, status: "error", error: e.message } : file
666
+ ));
667
+ break;
668
+ }
484
669
  }
485
670
  }
486
671
  onComplete?.(results.filter((f2) => f2.status === "success"));
@@ -489,7 +674,7 @@ var Uploader = ({
489
674
  } finally {
490
675
  setIsUploading(false);
491
676
  }
492
- }, [files, onComplete, onError, uploadOne]);
677
+ }, [files, client, onComplete, onError, uploadOne]);
493
678
  React4__namespace.default.useEffect(() => {
494
679
  if (autoUpload && files.some((f2) => f2.status === "pending") && !isUploading) {
495
680
  const timer = setTimeout(() => {
@@ -537,15 +722,32 @@ var Uploader = ({
537
722
  children ?? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
538
723
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100", children: "Drop files here or click to browse" }),
539
724
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: [
540
- multiple ? `Up to ${maxFiles} files` : "Single file",
541
- " \u2022 Max ",
542
- (maxFileSize / (1024 * 1024)).toFixed(0),
543
- "MB"
725
+ !multiple ? "Single file" : maxFiles > 0 ? `Up to ${maxFiles} files` : "Multiple files",
726
+ maxFileSize > 0 ? ` \u2022 Max ${(maxFileSize / (1024 * 1024)).toFixed(0)}MB per file` : ""
544
727
  ] })
545
728
  ] })
546
729
  ]
547
730
  }
548
731
  ),
732
+ quotaError && /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
733
+ /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.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: [
734
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10" }),
735
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
736
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
737
+ ] }) }),
738
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-red-800 dark:text-red-300", children: quotaError }) }),
739
+ /* @__PURE__ */ jsxRuntime.jsx(
740
+ "button",
741
+ {
742
+ onClick: () => setQuotaError(null),
743
+ className: "text-red-400 hover:text-red-600 dark:hover:text-red-300 transition-colors",
744
+ children: /* @__PURE__ */ jsxRuntime.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: [
745
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
746
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
747
+ ] })
748
+ }
749
+ )
750
+ ] }) }),
549
751
  files.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-8 space-y-4", children: [
550
752
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 dark:border-gray-800 pb-4", children: [
551
753
  /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "font-semibold text-gray-900 dark:text-gray-100", children: "Selected Files" }),
@@ -570,12 +772,76 @@ var Uploader = ({
570
772
  {
571
773
  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",
572
774
  disabled: isUploading,
573
- onClick: () => setFiles([]),
775
+ onClick: () => {
776
+ setFiles([]);
777
+ if (showThumbnailUI) {
778
+ const empty = /* @__PURE__ */ new Set();
779
+ thumbFileIdsRef.current = empty;
780
+ setThumbFileIds(empty);
781
+ }
782
+ },
574
783
  children: "Clear"
575
784
  }
576
785
  )
577
786
  ] })
578
787
  ] }),
788
+ showThumbnailUI && pendingImages.length > 0 && /* @__PURE__ */ jsxRuntime.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: [
789
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-3", children: [
790
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
791
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2", children: [
792
+ /* @__PURE__ */ jsxRuntime.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: [
793
+ /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "12 2 2 7 12 12 22 7 12 2" }),
794
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "2 17 12 22 22 17" }),
795
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "2 12 12 17 22 12" })
796
+ ] }),
797
+ "Generate Thumbnails"
798
+ ] }),
799
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: thumbEnabled ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
800
+ thumbSelectedCount,
801
+ " of ",
802
+ pendingImages.length,
803
+ " image",
804
+ pendingImages.length !== 1 ? "s" : "",
805
+ " selected",
806
+ " \xB7 ",
807
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: selectAllThumbs, className: "text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300", children: "Select all" }),
808
+ " \xB7 ",
809
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: deselectAllThumbs, className: "text-gray-500 hover:text-gray-600 dark:text-gray-400 dark:hover:text-gray-300", children: "Deselect all" })
810
+ ] }) : "Disabled for all images" })
811
+ ] }),
812
+ /* @__PURE__ */ jsxRuntime.jsx(
813
+ "button",
814
+ {
815
+ type: "button",
816
+ onClick: handleThumbEnabledToggle,
817
+ 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"}`,
818
+ role: "switch",
819
+ "aria-checked": thumbEnabled,
820
+ children: /* @__PURE__ */ jsxRuntime.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"}` })
821
+ }
822
+ )
823
+ ] }),
824
+ thumbEnabled && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
825
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-medium text-gray-600 dark:text-gray-400 mb-2", children: "Sizes:" }),
826
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-2 flex-wrap", children: THUMBNAIL_SIZE_OPTIONS.map((size) => {
827
+ const active = thumbSizes.includes(size);
828
+ return /* @__PURE__ */ jsxRuntime.jsxs(
829
+ "button",
830
+ {
831
+ type: "button",
832
+ onClick: () => toggleThumbSize(size),
833
+ 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"}`,
834
+ children: [
835
+ size,
836
+ "px"
837
+ ]
838
+ },
839
+ size
840
+ );
841
+ }) }),
842
+ thumbSizes.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-amber-600 dark:text-amber-400 mt-2", children: "Select at least one size." })
843
+ ] })
844
+ ] }),
579
845
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid gap-3", children: files.map((f2) => /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4", children: [
580
846
  /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.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: [
581
847
  /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
@@ -599,7 +865,19 @@ var Uploader = ({
599
865
  className: `h-full transition-all duration-300 ${f2.status === "success" ? "bg-green-500" : "bg-blue-500"}`,
600
866
  style: { width: `${f2.progress}%` }
601
867
  }
602
- ) })
868
+ ) }),
869
+ showThumbnailUI && thumbEnabled && f2.type.startsWith("image/") && f2.status === "pending" && /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "mt-1.5 inline-flex items-center gap-1.5 cursor-pointer select-none", children: [
870
+ /* @__PURE__ */ jsxRuntime.jsx(
871
+ "input",
872
+ {
873
+ type: "checkbox",
874
+ checked: thumbFileIds.has(f2.id),
875
+ onChange: () => toggleFileThumb(f2.id),
876
+ className: "w-3.5 h-3.5 rounded accent-purple-600 cursor-pointer"
877
+ }
878
+ ),
879
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: "Generate thumbnail" })
880
+ ] })
603
881
  ] }),
604
882
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-shrink-0 ml-4", children: [
605
883
  f2.status === "success" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
@@ -633,7 +911,7 @@ var Uploader = ({
633
911
  {
634
912
  onClick: (e) => {
635
913
  e.stopPropagation();
636
- setFiles((prev) => prev.filter((file) => file.id !== f2.id));
914
+ removeFile(f2.id);
637
915
  },
638
916
  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",
639
917
  title: "Remove",
@@ -717,10 +995,18 @@ var ProjectFilesWidget = ({
717
995
  "content-type": "application/json",
718
996
  ...clientOptions?.headers || {}
719
997
  };
720
- if (clientOptions?.apiKey) {
998
+ const token = clientOptions?.getToken?.();
999
+ if (token) {
1000
+ headers["authorization"] = `Bearer ${token}`;
1001
+ if (clientOptions.projectName) {
1002
+ headers["x-project-name"] = clientOptions.projectName;
1003
+ } else if (clientOptions.projectId) {
1004
+ headers["x-project-id"] = clientOptions.projectId;
1005
+ }
1006
+ } else if (clientOptions?.apiKey) {
721
1007
  const keyHeader = clientOptions.apiKeyHeader || "authorization";
722
1008
  if (keyHeader === "authorization") {
723
- headers["authorization"] = clientOptions.apiKey.startsWith("upk_") ? `Bearer ${clientOptions.apiKey}` : clientOptions.apiKey;
1009
+ headers["authorization"] = clientOptions.apiKey.startsWith("upf_") ? `Bearer ${clientOptions.apiKey}` : clientOptions.apiKey;
724
1010
  } else {
725
1011
  headers[keyHeader] = clientOptions.apiKey;
726
1012
  }
@@ -841,6 +1127,19 @@ var ProjectFilesWidget = ({
841
1127
 
842
1128
  // src/projectConnect.ts
843
1129
  var f = (globalThis.fetch || fetch).bind(globalThis);
1130
+ function buildInit(opts, extra) {
1131
+ const headers = {
1132
+ ...extra?.headers || {}
1133
+ };
1134
+ if (opts.token) {
1135
+ headers["authorization"] = `Bearer ${opts.token}`;
1136
+ }
1137
+ return {
1138
+ ...extra,
1139
+ headers,
1140
+ credentials: opts.token ? "same-origin" : "include"
1141
+ };
1142
+ }
844
1143
  async function httpJson(input, init, useFetch) {
845
1144
  const doFetch = useFetch ?? f;
846
1145
  const res = await doFetch(input, init);
@@ -848,7 +1147,7 @@ async function httpJson(input, init, useFetch) {
848
1147
  let msg = `Request failed (${res.status})`;
849
1148
  try {
850
1149
  const j = await res.json();
851
- msg = j?.error || msg;
1150
+ msg = j?.error || j?.message || msg;
852
1151
  } catch {
853
1152
  }
854
1153
  throw new Error(msg);
@@ -859,7 +1158,7 @@ async function listProjects(opts) {
859
1158
  const url = `${opts.baseUrl.replace(/\/$/, "")}/api/projects`;
860
1159
  const data = await httpJson(
861
1160
  url,
862
- { method: "GET", credentials: "include" },
1161
+ buildInit(opts, { method: "GET" }),
863
1162
  opts.fetch
864
1163
  );
865
1164
  return data?.projects ?? [];
@@ -868,53 +1167,74 @@ async function createProject(opts) {
868
1167
  const base = opts.baseUrl.replace(/\/$/, "");
869
1168
  const created = await httpJson(
870
1169
  `${base}/api/projects`,
871
- {
1170
+ buildInit(opts, {
872
1171
  method: "POST",
873
- credentials: "include",
874
1172
  headers: { "content-type": "application/json" },
875
- body: JSON.stringify({ name: opts.name })
876
- },
1173
+ body: JSON.stringify({
1174
+ name: opts.name,
1175
+ isInHouse: opts.isInHouse,
1176
+ freeStorageLimitBytes: opts.freeStorageLimitBytes
1177
+ })
1178
+ }),
877
1179
  opts.fetch
878
1180
  );
879
1181
  const projectId = created?.project?.id;
880
1182
  if (!projectId) throw new Error("Failed to create project");
1183
+ return { projectId, project: created?.project };
1184
+ }
1185
+ async function createProjectWithApiKey(opts) {
1186
+ const base = opts.baseUrl.replace(/\/$/, "");
1187
+ const { projectId } = await createProject(opts);
881
1188
  const keyLabel = opts.keyName || `Plugin key ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
882
1189
  const key = await httpJson(
883
1190
  `${base}/api/projects/${projectId}/keys`,
884
- {
1191
+ buildInit(opts, {
885
1192
  method: "POST",
886
- credentials: "include",
887
1193
  headers: { "content-type": "application/json" },
888
1194
  body: JSON.stringify({ name: keyLabel })
889
- },
1195
+ }),
890
1196
  opts.fetch
891
1197
  );
892
- const plaintext = key?.key?.plaintext;
1198
+ const plaintext = key?.key?.secret ?? key?.key?.plaintext;
893
1199
  if (!plaintext) throw new Error("Failed to create API key");
894
1200
  return { apiKey: plaintext, projectId };
895
1201
  }
1202
+ async function updateProject(opts) {
1203
+ const base = opts.baseUrl.replace(/\/$/, "");
1204
+ const body = {};
1205
+ if (opts.isInHouse !== void 0) body.isInHouse = opts.isInHouse;
1206
+ if (opts.freeStorageLimitBytes !== void 0) body.freeStorageLimitBytes = opts.freeStorageLimitBytes;
1207
+ await httpJson(
1208
+ `${base}/api/projects/${opts.projectId}`,
1209
+ buildInit(opts, {
1210
+ method: "PUT",
1211
+ headers: { "content-type": "application/json" },
1212
+ body: JSON.stringify(body)
1213
+ }),
1214
+ opts.fetch
1215
+ );
1216
+ }
896
1217
  async function connectProject(opts) {
897
1218
  const base = opts.baseUrl.replace(/\/$/, "");
898
1219
  const keyLabel = opts.keyName || `Plugin key ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
899
1220
  const key = await httpJson(
900
1221
  `${base}/api/projects/${opts.projectId}/keys`,
901
- {
1222
+ buildInit(opts, {
902
1223
  method: "POST",
903
- credentials: "include",
904
1224
  headers: { "content-type": "application/json" },
905
1225
  body: JSON.stringify({ name: keyLabel })
906
- },
1226
+ }),
907
1227
  opts.fetch
908
1228
  );
909
- const plaintext = key?.key?.plaintext;
1229
+ const plaintext = key?.key?.secret ?? key?.key?.plaintext;
910
1230
  if (!plaintext) throw new Error("Failed to create API key");
911
1231
  return { apiKey: plaintext };
912
1232
  }
913
1233
  function addApiKeyManually(apiKey) {
914
1234
  const k = (apiKey || "").trim();
915
1235
  if (!k) throw new Error("API key is required");
916
- if (!/^upk_[A-Za-z0-9]+/.test(k)) {
917
- console.warn("Provided API key does not match expected format upk_*");
1236
+ if (!/^upf_[A-Za-z0-9_-]+/.test(k)) {
1237
+ console.warn("Provided API key does not match expected format upf_*");
918
1238
  }
919
1239
  return { apiKey: k };
920
1240
  }
@@ -998,7 +1318,7 @@ function ConnectProjectDialog(props) {
998
1318
  setLoading(true);
999
1319
  setError(null);
1000
1320
  if (!newProjectName.trim()) throw new Error("Project name is required");
1001
- const { apiKey, projectId } = await createProject({ baseUrl, name: newProjectName.trim(), fetch: fetchImpl });
1321
+ const { apiKey, projectId } = await createProjectWithApiKey({ baseUrl, name: newProjectName.trim(), fetch: fetchImpl });
1002
1322
  onConnected(apiKey, { projectId, source: "new" });
1003
1323
  onOpenChange(false);
1004
1324
  resetState();
@@ -1221,6 +1541,23 @@ var IconLayers = ({ className }) => /* @__PURE__ */ jsxRuntime.jsxs("svg", { xml
1221
1541
  /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "2 17 12 22 22 17" }),
1222
1542
  /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "2 12 12 17 22 12" })
1223
1543
  ] });
1544
+ var VisuallyHidden = ({ children }) => /* @__PURE__ */ jsxRuntime.jsx(
1545
+ "span",
1546
+ {
1547
+ style: {
1548
+ position: "absolute",
1549
+ width: "1px",
1550
+ height: "1px",
1551
+ padding: "0",
1552
+ margin: "-1px",
1553
+ overflow: "hidden",
1554
+ clip: "rect(0, 0, 0, 0)",
1555
+ whiteSpace: "nowrap",
1556
+ border: "0"
1557
+ },
1558
+ children
1559
+ }
1560
+ );
1224
1561
  function ImageManager(props) {
1225
1562
  const {
1226
1563
  open,
@@ -1228,6 +1565,7 @@ function ImageManager(props) {
1228
1565
  clientOptions,
1229
1566
  projectId,
1230
1567
  folderPath,
1568
+ userId,
1231
1569
  title,
1232
1570
  description,
1233
1571
  className,
@@ -1241,7 +1579,9 @@ function ImageManager(props) {
1241
1579
  maxFiles = 10,
1242
1580
  mode = "full",
1243
1581
  showDelete = true,
1244
- refreshInterval = 5e3
1582
+ refreshInterval = 5e3,
1583
+ stayOnUploadAfterComplete = false,
1584
+ thumbnailConfig
1245
1585
  } = props;
1246
1586
  const [activeView, setActiveView] = React4__namespace.useState(mode === "upload" ? "upload" : "browse");
1247
1587
  const [loading, setLoading] = React4__namespace.useState(false);
@@ -1259,6 +1599,7 @@ function ImageManager(props) {
1259
1599
  const [thumbnailSelectorFile, setThumbnailSelectorFile] = React4__namespace.useState(null);
1260
1600
  const [selectedThumbnailForFile, setSelectedThumbnailForFile] = React4__namespace.useState(null);
1261
1601
  const [previewFile, setPreviewFile] = React4__namespace.useState(null);
1602
+ const [generatingThumbnails, setGeneratingThumbnails] = React4__namespace.useState(false);
1262
1603
  const lastUpdatedRef = React4__namespace.useRef(null);
1263
1604
  const isFetchingRef = React4__namespace.useRef(false);
1264
1605
  const deduplicateFiles = React4__namespace.useCallback((files2) => {
@@ -1281,10 +1622,11 @@ function ImageManager(props) {
1281
1622
  const currentPathPrefix = breadcrumbs.length > 1 ? breadcrumbs.slice(1).map((b) => b.name).join("/") + "/" : void 0;
1282
1623
  const effectiveFolderPath = currentPathPrefix || folderPath;
1283
1624
  const [filesData, foldersData] = await Promise.all([
1284
- fetchProjectImagesWithThumbs(clientOptions, { folderPath: effectiveFolderPath, limitConcurrent: 6 }),
1625
+ fetchProjectFiles(clientOptions, effectiveFolderPath),
1285
1626
  client.getFolders(currentFolderId)
1286
1627
  ]);
1287
- const { items: list, updatedAt } = filesData;
1628
+ const { files: rawFiles, updatedAt } = filesData;
1629
+ const list = rawFiles;
1288
1630
  if (isPolling && updatedAt && updatedAt === lastUpdatedRef.current) {
1289
1631
  }
1290
1632
  setFiles(deduplicateFiles(list));
@@ -1310,30 +1652,6 @@ function ImageManager(props) {
1310
1652
  load();
1311
1653
  }
1312
1654
  }, [open, activeView, load, mode]);
1313
- React4__namespace.useEffect(() => {
1314
- if (!open) return;
1315
- const client = new UpfilesClient(clientOptions);
1316
- const url = client.getEventsUrl(projectId);
1317
- const eventSource = new EventSource(url);
1318
- eventSource.onmessage = (event) => {
1319
- try {
1320
- const payload = JSON.parse(event.data);
1321
- if (payload.type === "file:uploaded") {
1322
- const newFile = payload.data.file;
1323
- setFiles((prev) => {
1324
- if (prev.find((f2) => f2.key === newFile.key)) return prev;
1325
- return deduplicateFiles([newFile, ...prev]);
1326
- });
1327
- } else if (payload.type === "file:deleted") {
1328
- const deletedKey = payload.data.fileKey || payload.data.file?.key;
1329
- setFiles((prev) => prev.filter((f2) => f2.key !== deletedKey));
1330
- }
1331
- } catch (e) {
1332
- console.error("SSE Error parsing message", e);
1333
- }
1334
- };
1335
- return () => eventSource.close();
1336
- }, [open, clientOptions, projectId]);
1337
1655
  const visibleFiles = React4__namespace.useMemo(() => {
1338
1656
  const q = query.trim().toLowerCase();
1339
1657
  return !q ? files : files.filter((i) => (i.originalName || "").toLowerCase().includes(q));
@@ -1357,10 +1675,18 @@ function ImageManager(props) {
1357
1675
  "content-type": "application/json",
1358
1676
  ...clientOptions?.headers || {}
1359
1677
  };
1360
- if (clientOptions?.apiKey) {
1678
+ const token = clientOptions?.getToken?.();
1679
+ if (token) {
1680
+ headers["authorization"] = `Bearer ${token}`;
1681
+ if (clientOptions.projectName) {
1682
+ headers["x-project-name"] = clientOptions.projectName;
1683
+ } else if (clientOptions.projectId) {
1684
+ headers["x-project-id"] = clientOptions.projectId;
1685
+ }
1686
+ } else if (clientOptions?.apiKey) {
1361
1687
  const keyHeader = clientOptions.apiKeyHeader || "authorization";
1362
1688
  if (keyHeader === "authorization") {
1363
- headers["authorization"] = clientOptions.apiKey.startsWith("upk_") ? `Bearer ${clientOptions.apiKey}` : clientOptions.apiKey;
1689
+ headers["authorization"] = clientOptions.apiKey.startsWith("upf_") ? `Bearer ${clientOptions.apiKey}` : clientOptions.apiKey;
1364
1690
  } else {
1365
1691
  headers[keyHeader] = clientOptions.apiKey;
1366
1692
  }
@@ -1420,35 +1746,40 @@ function ImageManager(props) {
1420
1746
  });
1421
1747
  return options;
1422
1748
  }, [breadcrumbs, currentFolderId, folders]);
1749
+ const handleThumbnailClick = React4__namespace.useCallback(async (f2) => {
1750
+ if (f2.thumbnails && f2.thumbnails.length > 0) {
1751
+ setThumbnailSelectorFile(f2);
1752
+ setSelectedThumbnailForFile(null);
1753
+ return;
1754
+ }
1755
+ setGeneratingThumbnails(true);
1756
+ setThumbnailSelectorFile(f2);
1757
+ setSelectedThumbnailForFile(null);
1758
+ try {
1759
+ const client = new UpfilesClient(clientOptions);
1760
+ const result = await client.generateThumbnails(f2.key);
1761
+ const updatedFile = { ...f2, thumbnails: result.thumbnails };
1762
+ setThumbnailSelectorFile(updatedFile);
1763
+ setFiles((prev) => prev.map((file) => file.key === f2.key ? updatedFile : file));
1764
+ } catch (e) {
1765
+ console.error("Failed to generate thumbnails:", e);
1766
+ } finally {
1767
+ setGeneratingThumbnails(false);
1768
+ }
1769
+ }, [clientOptions]);
1423
1770
  const [selectedUploadFolder, setSelectedUploadFolder] = React4__namespace.useState("current");
1424
1771
  const activeBreadcrumbPath = React4__namespace.useMemo(() => {
1425
1772
  return breadcrumbs.length > 1 ? breadcrumbs.slice(1).map((b) => b.name).join("/") + "/" : void 0;
1426
1773
  }, [breadcrumbs]);
1427
1774
  return /* @__PURE__ */ jsxRuntime.jsxs(Dialog2__namespace.Root, { open, onOpenChange, children: [
1428
1775
  /* @__PURE__ */ jsxRuntime.jsxs(Dialog2__namespace.Portal, { children: [
1429
- /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Overlay, { className: "fixed inset-0 bg-black/60 backdrop-blur-sm z-50 transition-all duration-300 flex items-center justify-center" }),
1776
+ /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Overlay, { className: "fixed inset-0 bg-black/60 z-[9999]" }),
1430
1777
  /* @__PURE__ */ jsxRuntime.jsxs(
1431
1778
  Dialog2__namespace.Content,
1432
1779
  {
1433
- 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 ?? ""}`,
1780
+ 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 ?? ""}`,
1434
1781
  children: [
1435
- /* @__PURE__ */ jsxRuntime.jsx(
1436
- Dialog2__namespace.Title,
1437
- {
1438
- style: {
1439
- position: "absolute",
1440
- width: "1px",
1441
- height: "1px",
1442
- padding: "0",
1443
- margin: "-1px",
1444
- overflow: "hidden",
1445
- clip: "rect(0, 0, 0, 0)",
1446
- whiteSpace: "nowrap",
1447
- border: "0"
1448
- },
1449
- children: title ?? "Media Library"
1450
- }
1451
- ),
1782
+ /* @__PURE__ */ jsxRuntime.jsx(VisuallyHidden, { children: /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Title, { children: title ?? "Media Library" }) }),
1452
1783
  /* @__PURE__ */ jsxRuntime.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: [
1453
1784
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-6", children: [
1454
1785
  /* @__PURE__ */ jsxRuntime.jsxs("h2", { className: "text-lg font-bold text-gray-900 dark:text-white flex items-center gap-2", children: [
@@ -1555,16 +1886,27 @@ function ImageManager(props) {
1555
1886
  folder.id
1556
1887
  )),
1557
1888
  visibleFiles.map((f2) => {
1558
- const fWithThumbs = f2;
1559
- const bestThumb = fWithThumbs.thumbnails?.sort((a, b) => a.size - b.size).find((t) => t.size >= 300) || fWithThumbs.thumbnails?.[fWithThumbs.thumbnails?.length - 1];
1560
- const thumbUrl = bestThumb?.url || f2.url;
1889
+ const displayUrl = f2.url;
1890
+ const hasThumbnails = f2.thumbnails && f2.thumbnails.length > 0;
1561
1891
  return /* @__PURE__ */ jsxRuntime.jsxs(
1562
1892
  "div",
1563
1893
  {
1564
1894
  onClick: () => setPreviewFile(f2),
1565
1895
  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",
1566
1896
  children: [
1567
- /* @__PURE__ */ jsxRuntime.jsx("img", { src: thumbUrl, alt: f2.originalName, className: "w-full h-full object-cover transition-transform duration-700 group-hover:scale-105" }),
1897
+ /* @__PURE__ */ jsxRuntime.jsx("img", { src: displayUrl, alt: f2.originalName, className: "w-full h-full object-cover transition-transform duration-700 group-hover:scale-105" }),
1898
+ f2.contentType?.startsWith("image/") && /* @__PURE__ */ jsxRuntime.jsx(
1899
+ "button",
1900
+ {
1901
+ onClick: (e) => {
1902
+ e.stopPropagation();
1903
+ handleThumbnailClick(f2);
1904
+ },
1905
+ 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"}`,
1906
+ title: hasThumbnails ? "View Thumbnails" : "Generate Thumbnails",
1907
+ children: /* @__PURE__ */ jsxRuntime.jsx(IconLayers, { className: "w-4 h-4" })
1908
+ }
1909
+ ),
1568
1910
  /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsxs("div", { className: "flex gap-2 justify-center transform translate-y-2 group-hover:translate-y-0 transition-transform duration-300", children: [
1569
1911
  /* @__PURE__ */ jsxRuntime.jsx(
1570
1912
  "button",
@@ -1585,19 +1927,6 @@ function ImageManager(props) {
1585
1927
  children: "Select"
1586
1928
  }
1587
1929
  ),
1588
- fWithThumbs.thumbnails && fWithThumbs.thumbnails.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
1589
- "button",
1590
- {
1591
- onClick: (e) => {
1592
- e.stopPropagation();
1593
- setThumbnailSelectorFile(f2);
1594
- setSelectedThumbnailForFile(null);
1595
- },
1596
- className: "p-1.5 bg-white/20 text-white text-xs font-semibold rounded-md hover:bg-purple-500 backdrop-blur-md transition-colors",
1597
- title: "Select Thumbnail",
1598
- children: /* @__PURE__ */ jsxRuntime.jsx(IconLayers, { className: "w-4 h-4" })
1599
- }
1600
- ),
1601
1930
  showDelete && /* @__PURE__ */ jsxRuntime.jsx(
1602
1931
  "button",
1603
1932
  {
@@ -1623,7 +1952,7 @@ function ImageManager(props) {
1623
1952
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: "Try uploading a file or creating a new folder." })
1624
1953
  ] })
1625
1954
  ] }),
1626
- activeView === "upload" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full flex flex-col items-center justify-center max-w-2xl mx-auto", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full bg-white rounded-2xl border border-gray-200 p-8 shadow-sm", children: [
1955
+ activeView === "upload" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full flex flex-col items-center justify-center max-w-2xl mx-auto gap-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full bg-white rounded-2xl border border-gray-200 p-8 shadow-sm", children: [
1627
1956
  /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "text-lg font-semibold text-gray-900 mb-6 flex items-center gap-2", children: [
1628
1957
  /* @__PURE__ */ jsxRuntime.jsx(IconUploadCloud, { className: "text-blue-500" }),
1629
1958
  "Upload Files"
@@ -1648,15 +1977,28 @@ function ImageManager(props) {
1648
1977
  {
1649
1978
  clientOptions,
1650
1979
  projectId,
1980
+ userId,
1651
1981
  folderPath: uploadFoldersList.find((o) => o.id === selectedUploadFolder)?.path || activeBreadcrumbPath || folderPath,
1652
1982
  accept: ["image/*"],
1653
1983
  maxFileSize,
1654
1984
  maxFiles,
1655
1985
  autoRecordToDb,
1656
- fetchThumbnails,
1657
- autoUpload: true,
1658
- onClientUploadComplete: (file) => {
1986
+ fetchThumbnails: false,
1987
+ autoUpload: false,
1988
+ thumbnailEnabled: thumbnailConfig?.enabled ?? true,
1989
+ thumbnailSizes: thumbnailConfig?.sizes,
1990
+ onClientUploadComplete: async (file) => {
1659
1991
  if (file.status === "success" && file.publicUrl && file.fileKey) {
1992
+ let thumbnails = file.thumbnails;
1993
+ if (file.thumbnailEnabled && file.thumbnailSizesSelected && file.thumbnailSizesSelected.length > 0 && file.type?.startsWith("image/")) {
1994
+ try {
1995
+ const client = new UpfilesClient(clientOptions);
1996
+ const result = await client.generateThumbnails(file.fileKey, file.thumbnailSizesSelected);
1997
+ thumbnails = result.thumbnails;
1998
+ } catch (e) {
1999
+ console.warn("Failed to generate thumbnails:", e);
2000
+ }
2001
+ }
1660
2002
  const newItem = {
1661
2003
  key: file.fileKey,
1662
2004
  originalName: file.name,
@@ -1664,7 +2006,7 @@ function ImageManager(props) {
1664
2006
  contentType: file.type,
1665
2007
  uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
1666
2008
  url: file.publicUrl,
1667
- thumbnails: file.thumbnails
2009
+ thumbnails
1668
2010
  };
1669
2011
  setFiles((prev) => {
1670
2012
  if (prev.find((f2) => f2.key === newItem.key)) return prev;
@@ -1673,11 +2015,13 @@ function ImageManager(props) {
1673
2015
  }
1674
2016
  },
1675
2017
  onComplete: () => {
1676
- if (mode === "full") {
2018
+ if (mode === "full" && !stayOnUploadAfterComplete) {
1677
2019
  setTimeout(() => {
1678
2020
  setActiveView("browse");
1679
2021
  load();
1680
2022
  }, 1e3);
2023
+ } else {
2024
+ load();
1681
2025
  }
1682
2026
  },
1683
2027
  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",
@@ -1703,8 +2047,8 @@ function ImageManager(props) {
1703
2047
  )
1704
2048
  ] }),
1705
2049
  /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Root, { open: showCreateFolder, onOpenChange: setShowCreateFolder, children: /* @__PURE__ */ jsxRuntime.jsxs(Dialog2__namespace.Portal, { children: [
1706
- /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Overlay, { className: "fixed inset-0 bg-black/40 z-[60]" }),
1707
- /* @__PURE__ */ jsxRuntime.jsxs(Dialog2__namespace.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: [
2050
+ /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Overlay, { className: "fixed inset-0 bg-black/40 z-[10010]" }),
2051
+ /* @__PURE__ */ jsxRuntime.jsxs(Dialog2__namespace.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: [
1708
2052
  /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Title, { className: "text-lg font-semibold text-gray-900 mb-4", children: "New Folder" }),
1709
2053
  /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Description, { id: "new-folder-description", className: "sr-only", children: "Create a new folder to organize your files" }),
1710
2054
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1733,8 +2077,8 @@ function ImageManager(props) {
1733
2077
  ] })
1734
2078
  ] }) }),
1735
2079
  /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Root, { open: !!fileToDelete, onOpenChange: (open2) => !open2 && setFileToDelete(null), children: /* @__PURE__ */ jsxRuntime.jsxs(Dialog2__namespace.Portal, { children: [
1736
- /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Overlay, { className: "fixed inset-0 bg-black/40 z-[80]" }),
1737
- /* @__PURE__ */ jsxRuntime.jsxs(Dialog2__namespace.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: [
2080
+ /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Overlay, { className: "fixed inset-0 bg-black/40 z-[10020]" }),
2081
+ /* @__PURE__ */ jsxRuntime.jsxs(Dialog2__namespace.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: [
1738
2082
  /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Title, { className: "text-xl font-semibold text-gray-900 mb-2", children: "Delete File" }),
1739
2083
  /* @__PURE__ */ jsxRuntime.jsxs(Dialog2__namespace.Description, { id: "delete-file-description", className: "text-gray-600 mb-6", children: [
1740
2084
  "Are you sure you want to delete ",
@@ -1767,8 +2111,8 @@ function ImageManager(props) {
1767
2111
  ] })
1768
2112
  ] }) }),
1769
2113
  /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Root, { open: !!thumbnailSelectorFile, onOpenChange: (open2) => !open2 && setThumbnailSelectorFile(null), children: /* @__PURE__ */ jsxRuntime.jsxs(Dialog2__namespace.Portal, { children: [
1770
- /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Overlay, { className: "fixed inset-0 bg-black/50 z-[90]" }),
1771
- /* @__PURE__ */ jsxRuntime.jsxs(Dialog2__namespace.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: [
2114
+ /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Overlay, { className: "fixed inset-0 bg-black/50 z-[10030]" }),
2115
+ /* @__PURE__ */ jsxRuntime.jsxs(Dialog2__namespace.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: [
1772
2116
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-6 border-b border-gray-200", children: [
1773
2117
  /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Title, { className: "text-xl font-semibold text-gray-900 mb-2", children: "Select Thumbnail" }),
1774
2118
  /* @__PURE__ */ jsxRuntime.jsxs(Dialog2__namespace.Description, { id: "thumbnail-selector-description", className: "text-gray-600", children: [
@@ -1780,60 +2124,66 @@ function ImageManager(props) {
1780
2124
  ] })
1781
2125
  ] })
1782
2126
  ] }),
1783
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6 overflow-y-auto max-h-[60vh]", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4", children: [
1784
- /* @__PURE__ */ jsxRuntime.jsxs(
1785
- "div",
1786
- {
1787
- onClick: () => setSelectedThumbnailForFile(null),
1788
- 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"}`,
1789
- children: [
1790
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "aspect-square bg-gray-100 flex items-center justify-center relative", children: [
1791
- /* @__PURE__ */ jsxRuntime.jsx(
1792
- "img",
1793
- {
1794
- src: thumbnailSelectorFile?.url,
1795
- alt: "Original",
1796
- className: "w-full h-full object-cover"
1797
- }
1798
- ),
1799
- selectedThumbnailForFile === null && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-blue-500/20 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-4 h-4 text-white", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsxRuntime.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" }) }) }) })
1800
- ] }),
1801
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 bg-white", children: [
1802
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium text-sm text-gray-900", children: "Original" }),
1803
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-500", children: "Full size" })
1804
- ] })
1805
- ]
1806
- }
1807
- ),
1808
- thumbnailSelectorFile?.thumbnails?.map((thumbnail) => /* @__PURE__ */ jsxRuntime.jsxs(
1809
- "div",
1810
- {
1811
- onClick: () => setSelectedThumbnailForFile(thumbnail),
1812
- 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"}`,
1813
- children: [
1814
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "aspect-square bg-gray-100 flex items-center justify-center relative", children: [
1815
- /* @__PURE__ */ jsxRuntime.jsx(
1816
- "img",
1817
- {
1818
- src: thumbnail.url,
1819
- alt: `${thumbnail.size}px thumbnail`,
1820
- className: "w-full h-full object-cover"
1821
- }
1822
- ),
1823
- selectedThumbnailForFile?.id === thumbnail.id && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-blue-500/20 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-4 h-4 text-white", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsxRuntime.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" }) }) }) })
1824
- ] }),
1825
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 bg-white", children: [
1826
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "font-medium text-sm text-gray-900", children: [
1827
- thumbnail.size,
1828
- "px"
2127
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-6 overflow-y-auto max-h-[60vh]", children: [
2128
+ generatingThumbnails && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-3 py-8 text-gray-500", children: [
2129
+ /* @__PURE__ */ jsxRuntime.jsx(IconLoader, { className: "w-5 h-5 animate-spin" }),
2130
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: "Generating thumbnails..." })
2131
+ ] }),
2132
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 ${generatingThumbnails ? "opacity-50 pointer-events-none" : ""}`, children: [
2133
+ /* @__PURE__ */ jsxRuntime.jsxs(
2134
+ "div",
2135
+ {
2136
+ onClick: () => setSelectedThumbnailForFile(null),
2137
+ 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"}`,
2138
+ children: [
2139
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "aspect-square bg-gray-100 flex items-center justify-center relative", children: [
2140
+ /* @__PURE__ */ jsxRuntime.jsx(
2141
+ "img",
2142
+ {
2143
+ src: thumbnailSelectorFile?.url,
2144
+ alt: "Original",
2145
+ className: "w-full h-full object-cover"
2146
+ }
2147
+ ),
2148
+ selectedThumbnailForFile === null && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-blue-500/20 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-4 h-4 text-white", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsxRuntime.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" }) }) }) })
1829
2149
  ] }),
1830
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-500", children: thumbnail.sizeType || "Thumbnail" })
1831
- ] })
1832
- ]
1833
- },
1834
- thumbnail.id
1835
- ))
1836
- ] }) }),
2150
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 bg-white", children: [
2151
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium text-sm text-gray-900", children: "Original" }),
2152
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-500", children: "Full size" })
2153
+ ] })
2154
+ ]
2155
+ }
2156
+ ),
2157
+ thumbnailSelectorFile?.thumbnails?.map((thumbnail) => /* @__PURE__ */ jsxRuntime.jsxs(
2158
+ "div",
2159
+ {
2160
+ onClick: () => setSelectedThumbnailForFile(thumbnail),
2161
+ 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"}`,
2162
+ children: [
2163
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "aspect-square bg-gray-100 flex items-center justify-center relative", children: [
2164
+ /* @__PURE__ */ jsxRuntime.jsx(
2165
+ "img",
2166
+ {
2167
+ src: thumbnail.url,
2168
+ alt: `${thumbnail.size}px thumbnail`,
2169
+ className: "w-full h-full object-cover"
2170
+ }
2171
+ ),
2172
+ selectedThumbnailForFile?.id === thumbnail.id && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-blue-500/20 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-4 h-4 text-white", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsxRuntime.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" }) }) }) })
2173
+ ] }),
2174
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 bg-white", children: [
2175
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "font-medium text-sm text-gray-900", children: [
2176
+ thumbnail.size,
2177
+ "px"
2178
+ ] }),
2179
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-500", children: thumbnail.sizeType || "Thumbnail" })
2180
+ ] })
2181
+ ]
2182
+ },
2183
+ thumbnail.id
2184
+ ))
2185
+ ] })
2186
+ ] }),
1837
2187
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-6 border-t border-gray-200 flex justify-end gap-3", children: [
1838
2188
  /* @__PURE__ */ jsxRuntime.jsx(
1839
2189
  "button",
@@ -1872,8 +2222,8 @@ function ImageManager(props) {
1872
2222
  ] })
1873
2223
  ] }) }),
1874
2224
  /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Root, { open: !!previewFile, onOpenChange: (open2) => !open2 && setPreviewFile(null), children: /* @__PURE__ */ jsxRuntime.jsxs(Dialog2__namespace.Portal, { children: [
1875
- /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Overlay, { className: "fixed inset-0 bg-black/90 z-[100]", onClick: () => setPreviewFile(null) }),
1876
- /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.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__ */ jsxRuntime.jsxs("div", { className: "relative w-full h-full flex flex-col", children: [
2225
+ /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Overlay, { className: "fixed inset-0 bg-black/90 z-[10040]", onClick: () => setPreviewFile(null) }),
2226
+ /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.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__ */ jsxRuntime.jsxs("div", { className: "relative w-full h-full flex flex-col", children: [
1877
2227
  /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between", children: [
1878
2228
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-white", children: [
1879
2229
  /* @__PURE__ */ jsxRuntime.jsx(Dialog2__namespace.Title, { className: "text-xl font-semibold mb-1", children: previewFile?.originalName }),
@@ -1945,14 +2295,15 @@ function ImageManager(props) {
1945
2295
  children: "Open in New Tab"
1946
2296
  }
1947
2297
  ),
1948
- previewFile?.thumbnails && previewFile.thumbnails.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
2298
+ previewFile?.contentType?.startsWith("image/") && /* @__PURE__ */ jsxRuntime.jsxs(
1949
2299
  "button",
1950
2300
  {
1951
2301
  onClick: (e) => {
1952
2302
  e.stopPropagation();
1953
- setThumbnailSelectorFile(previewFile);
1954
- setSelectedThumbnailForFile(null);
1955
- setPreviewFile(null);
2303
+ if (previewFile) {
2304
+ handleThumbnailClick(previewFile);
2305
+ setPreviewFile(null);
2306
+ }
1956
2307
  },
1957
2308
  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",
1958
2309
  children: [
@@ -1966,10 +2317,142 @@ function ImageManager(props) {
1966
2317
  ] }) })
1967
2318
  ] });
1968
2319
  }
2320
+ function useUpfilesInhouse(config) {
2321
+ const {
2322
+ baseUrl,
2323
+ getToken,
2324
+ userId,
2325
+ projectName = "default",
2326
+ enabled = true
2327
+ } = config;
2328
+ const [state, setState] = React4.useState({ clientOptions: null, projectId: null, loading: false, error: null });
2329
+ const [retryCount, setRetryCount] = React4.useState(0);
2330
+ const [token, setToken] = React4.useState(
2331
+ typeof getToken === "string" ? getToken : null
2332
+ );
2333
+ const getTokenFn = React4.useCallback(() => {
2334
+ return typeof getToken === "function" ? getToken() : getToken;
2335
+ }, [getToken]);
2336
+ React4.useEffect(() => {
2337
+ let cancelled = false;
2338
+ async function resolveToken() {
2339
+ const next = await getTokenFn();
2340
+ if (!cancelled) {
2341
+ setToken(next ?? null);
2342
+ }
2343
+ }
2344
+ resolveToken();
2345
+ window.addEventListener("focus", resolveToken);
2346
+ window.addEventListener("storage", resolveToken);
2347
+ return () => {
2348
+ cancelled = true;
2349
+ window.removeEventListener("focus", resolveToken);
2350
+ window.removeEventListener("storage", resolveToken);
2351
+ };
2352
+ }, [getTokenFn]);
2353
+ const mountedRef = React4.useRef(true);
2354
+ React4.useEffect(() => {
2355
+ mountedRef.current = true;
2356
+ return () => {
2357
+ mountedRef.current = false;
2358
+ };
2359
+ }, []);
2360
+ const buildClientOptions = React4.useCallback(
2361
+ () => ({
2362
+ baseUrl,
2363
+ getToken: getTokenFn,
2364
+ projectName
2365
+ }),
2366
+ [baseUrl, getTokenFn, projectName]
2367
+ );
2368
+ React4.useEffect(() => {
2369
+ if (!enabled || !token || !userId) {
2370
+ setState({ clientOptions: null, projectId: null, loading: false, error: null });
2371
+ return;
2372
+ }
2373
+ let cancelled = false;
2374
+ async function provision() {
2375
+ if (!mountedRef.current) return;
2376
+ setState((prev) => ({ ...prev, loading: true, error: null }));
2377
+ try {
2378
+ const connectOpts = { baseUrl, token };
2379
+ const projects = await listProjects(connectOpts);
2380
+ if (cancelled || !mountedRef.current) return;
2381
+ const existing = projects.find((p) => p.name === projectName);
2382
+ if (existing) {
2383
+ await updateProject({
2384
+ ...connectOpts,
2385
+ projectId: existing.id,
2386
+ isInHouse: true,
2387
+ freeStorageLimitBytes: 2147483648
2388
+ // 2GB
2389
+ });
2390
+ } else {
2391
+ try {
2392
+ await createProject({
2393
+ ...connectOpts,
2394
+ name: projectName,
2395
+ isInHouse: true,
2396
+ freeStorageLimitBytes: 2147483648
2397
+ // 2GB in bytes
2398
+ });
2399
+ } catch (err) {
2400
+ if (err?.message?.includes("409") || err?.message?.includes("duplicate") || err?.message?.includes("already exists")) {
2401
+ const retryProjects = await listProjects(connectOpts);
2402
+ const found = retryProjects.find((p) => p.name === projectName);
2403
+ if (!found) throw err;
2404
+ await updateProject({
2405
+ ...connectOpts,
2406
+ projectId: found.id,
2407
+ isInHouse: true,
2408
+ freeStorageLimitBytes: 2147483648
2409
+ // 2GB
2410
+ });
2411
+ } else {
2412
+ throw err;
2413
+ }
2414
+ }
2415
+ }
2416
+ if (cancelled || !mountedRef.current) return;
2417
+ setState({
2418
+ clientOptions: buildClientOptions(),
2419
+ projectId: null,
2420
+ loading: false,
2421
+ error: null
2422
+ });
2423
+ } catch (err) {
2424
+ if (cancelled || !mountedRef.current) return;
2425
+ setState({
2426
+ clientOptions: null,
2427
+ projectId: null,
2428
+ loading: false,
2429
+ error: err?.message || "Failed to auto-connect upfiles"
2430
+ });
2431
+ }
2432
+ }
2433
+ provision();
2434
+ return () => {
2435
+ cancelled = true;
2436
+ };
2437
+ }, [enabled, token, userId, baseUrl, projectName, retryCount, buildClientOptions]);
2438
+ const retry = React4.useCallback(() => {
2439
+ setRetryCount((c) => c + 1);
2440
+ }, []);
2441
+ const clearCache = React4.useCallback(() => {
2442
+ setState({ clientOptions: null, projectId: null, loading: false, error: null });
2443
+ }, []);
2444
+ return {
2445
+ ...state,
2446
+ isReady: !!state.clientOptions,
2447
+ retry,
2448
+ clearCache
2449
+ };
2450
+ }
1969
2451
 
1970
2452
  exports.ConnectProjectDialog = ConnectProjectDialog;
1971
2453
  exports.ImageManager = ImageManager;
1972
2454
  exports.ProjectFilesWidget = ProjectFilesWidget;
2455
+ exports.StorageQuotaError = StorageQuotaError;
1973
2456
  exports.UpfilesClient = UpfilesClient;
1974
2457
  exports.Uploader = Uploader;
1975
2458
  exports.addApiKeyManually = addApiKeyManually;
@@ -1977,9 +2460,12 @@ exports.connectProject = connectProject;
1977
2460
  exports.connectUsingApiKey = connectUsingApiKey;
1978
2461
  exports.createClientWithKey = createClientWithKey;
1979
2462
  exports.createProject = createProject;
2463
+ exports.createProjectWithApiKey = createProjectWithApiKey;
1980
2464
  exports.fetchFileThumbnails = fetchFileThumbnails;
1981
2465
  exports.fetchProjectFiles = fetchProjectFiles;
1982
2466
  exports.fetchProjectImagesWithThumbs = fetchProjectImagesWithThumbs;
1983
2467
  exports.listProjects = listProjects;
2468
+ exports.updateProject = updateProject;
2469
+ exports.useUpfilesInhouse = useUpfilesInhouse;
1984
2470
  //# sourceMappingURL=index.js.map
1985
2471
  //# sourceMappingURL=index.js.map