@blitheforge/media-library 1.0.5

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 ADDED
@@ -0,0 +1,1387 @@
1
+ "use client";
2
+
3
+ // src/types.ts
4
+ var defaultMediaCapabilities = {
5
+ view: true,
6
+ upload: true,
7
+ createFolder: true,
8
+ delete: true,
9
+ rename: true,
10
+ select: true
11
+ };
12
+ var defaultMediaLibraryConfig = {
13
+ listUrl: "/api/media",
14
+ uploadUrl: "/api/media/upload",
15
+ createFolderUrl: "/api/media/folders",
16
+ updateUrl: "/api/media",
17
+ deleteUrl: "/api/media",
18
+ rootLabel: "Root"
19
+ };
20
+
21
+ // src/client.ts
22
+ function resolveConfig(config) {
23
+ return { ...defaultMediaLibraryConfig, ...config };
24
+ }
25
+ async function parseResponse(response) {
26
+ const payload = await response.json();
27
+ if (!payload.success) throw new Error(payload.error?.message ?? "Media request failed.");
28
+ return payload.data;
29
+ }
30
+ function createMediaLibraryClient(config) {
31
+ const urls = resolveConfig(config);
32
+ return {
33
+ async list(path = "", q = "") {
34
+ const params = new URLSearchParams();
35
+ if (path) params.set("path", path);
36
+ if (q) params.set("q", q);
37
+ const response = await fetch(`${urls.listUrl}?${params.toString()}`);
38
+ return parseResponse(response);
39
+ },
40
+ async createFolder(path, name, nested = true) {
41
+ const response = await fetch(urls.createFolderUrl, {
42
+ method: "POST",
43
+ headers: { "content-type": "application/json" },
44
+ body: JSON.stringify({ path, name, nested })
45
+ });
46
+ return parseResponse(response);
47
+ },
48
+ async upload(path, files) {
49
+ const form = new FormData();
50
+ form.set("path", path);
51
+ files.forEach((file) => form.append("files", file));
52
+ const response = await fetch(urls.uploadUrl, { method: "POST", body: form });
53
+ return parseResponse(response);
54
+ },
55
+ async uploadOne(path, file) {
56
+ const uploaded = await this.upload(path, [file]);
57
+ return uploaded[0];
58
+ },
59
+ async rename(path, newName, type) {
60
+ const response = await fetch(urls.updateUrl, {
61
+ method: "PATCH",
62
+ headers: { "content-type": "application/json" },
63
+ body: JSON.stringify({ path, newName, type })
64
+ });
65
+ return parseResponse(response);
66
+ },
67
+ async remove(path, type) {
68
+ const response = await fetch(urls.deleteUrl, {
69
+ method: "DELETE",
70
+ headers: { "content-type": "application/json" },
71
+ body: JSON.stringify({ path, type })
72
+ });
73
+ return parseResponse(response);
74
+ }
75
+ };
76
+ }
77
+ var MAX_MEDIA_UPLOAD_BYTES = 5 * 1024 * 1024;
78
+ function isFileWithinUploadSizeLimit(file, maxBytes = MAX_MEDIA_UPLOAD_BYTES) {
79
+ return file.size <= maxBytes;
80
+ }
81
+ function formatUploadSizeLimit(maxBytes = MAX_MEDIA_UPLOAD_BYTES) {
82
+ return `${Math.round(maxBytes / (1024 * 1024))} MB`;
83
+ }
84
+ function fileMatchesAccept(file, accept) {
85
+ if (!accept || accept.length === 0) return true;
86
+ const isImage = file.mimeType.startsWith("image/");
87
+ const isPdf = file.mimeType === "application/pdf";
88
+ if (accept.includes("image") && isImage) return true;
89
+ if (accept.includes("pdf") && isPdf) return true;
90
+ return false;
91
+ }
92
+ function fileMatchesAcceptForUpload(file, accept) {
93
+ if (!accept || accept.length === 0) return true;
94
+ const isImage = file.type.startsWith("image/");
95
+ const isPdf = file.type === "application/pdf";
96
+ if (accept.includes("image") && isImage) return true;
97
+ if (accept.includes("pdf") && isPdf) return true;
98
+ return false;
99
+ }
100
+ function fileNameFromPath(path) {
101
+ return path.split("/").pop() ?? path;
102
+ }
103
+ function isImagePath(path) {
104
+ return /\.(png|jpe?g|webp|gif)$/i.test(path);
105
+ }
106
+
107
+ // src/components/media-library-modal.tsx
108
+ import { createPortal as createPortal2 } from "react-dom";
109
+
110
+ // src/utils/cn.ts
111
+ import { clsx } from "clsx";
112
+ import { twMerge } from "tailwind-merge";
113
+ function cn(...inputs) {
114
+ return twMerge(clsx(inputs));
115
+ }
116
+
117
+ // src/utils/bfml-theme.ts
118
+ function bfmlRootProps(theme = "sync") {
119
+ return {
120
+ className: "bfml-root",
121
+ ...theme !== "sync" ? { "data-theme": theme } : {}
122
+ };
123
+ }
124
+ function resolveThemeMode(theme) {
125
+ return theme ?? "sync";
126
+ }
127
+
128
+ // src/components/media-library-panel.tsx
129
+ import { useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
130
+ import { ChevronRight, Folder, ImagePlus, LoaderCircle as LoaderCircle2, PanelLeft, Search, Trash2, Upload, X as X2 } from "lucide-react";
131
+
132
+ // src/components/ui/button.tsx
133
+ import { jsx } from "react/jsx-runtime";
134
+ var variants = {
135
+ primary: "bg-[var(--bfml-primary)] text-[var(--bfml-primary-foreground)] shadow-[var(--bfml-shadow)] hover:brightness-95",
136
+ secondary: "border border-[var(--bfml-border)] bg-[var(--bfml-surface)] text-[var(--bfml-foreground)] shadow-[var(--bfml-shadow)] hover:bg-[var(--bfml-surface-soft)]",
137
+ danger: "bg-[var(--bfml-destructive)] text-[var(--bfml-primary-foreground)] hover:brightness-95",
138
+ ghost: "bg-transparent text-[var(--bfml-muted-foreground)] hover:bg-[var(--bfml-surface-soft)] hover:text-[var(--bfml-foreground)]"
139
+ };
140
+ function Button({ className, variant = "primary", disabled, children, ...props }) {
141
+ return /* @__PURE__ */ jsx(
142
+ "button",
143
+ {
144
+ className: cn(
145
+ "inline-flex h-10 items-center justify-center gap-2 rounded-lg px-4 text-sm font-semibold transition disabled:pointer-events-none disabled:opacity-50",
146
+ variants[variant],
147
+ className
148
+ ),
149
+ disabled,
150
+ ...props,
151
+ children
152
+ }
153
+ );
154
+ }
155
+
156
+ // src/components/ui/confirm-dialog.tsx
157
+ import { createPortal } from "react-dom";
158
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
159
+ function ConfirmDialog({
160
+ open,
161
+ title,
162
+ description,
163
+ confirmLabel = "Delete",
164
+ cancelLabel = "Cancel",
165
+ loading = false,
166
+ theme = "sync",
167
+ onCancel,
168
+ onConfirm
169
+ }) {
170
+ if (!open || typeof document === "undefined") return null;
171
+ const rootProps = bfmlRootProps(theme);
172
+ return createPortal(
173
+ /* @__PURE__ */ jsx2(
174
+ "div",
175
+ {
176
+ ...rootProps,
177
+ className: cn(rootProps.className, "fixed inset-0 z-[10001] flex items-end justify-center p-0 backdrop-blur-sm sm:items-center sm:p-4"),
178
+ style: { backgroundColor: "var(--bfml-overlay)" },
179
+ role: "presentation",
180
+ onClick: onCancel,
181
+ children: /* @__PURE__ */ jsxs(
182
+ "section",
183
+ {
184
+ role: "alertdialog",
185
+ "aria-modal": "true",
186
+ "aria-labelledby": "bfml-confirm-title",
187
+ "aria-describedby": "bfml-confirm-description",
188
+ className: "w-full max-w-md rounded-t-2xl border border-[var(--bfml-border)] bg-[var(--bfml-surface)] p-4 shadow-[var(--bfml-shadow-lg)] sm:rounded-2xl sm:p-6",
189
+ onClick: (event) => event.stopPropagation(),
190
+ children: [
191
+ /* @__PURE__ */ jsx2("h3", { id: "bfml-confirm-title", className: "text-lg font-semibold text-[var(--bfml-foreground)]", children: title }),
192
+ /* @__PURE__ */ jsx2("p", { id: "bfml-confirm-description", className: "mt-2 text-sm text-[var(--bfml-muted-foreground)]", children: description }),
193
+ /* @__PURE__ */ jsxs("div", { className: "mt-5 flex flex-col-reverse gap-2 sm:mt-6 sm:flex-row sm:justify-end", children: [
194
+ /* @__PURE__ */ jsx2(Button, { type: "button", variant: "secondary", disabled: loading, className: "w-full sm:w-auto", onClick: onCancel, children: cancelLabel }),
195
+ /* @__PURE__ */ jsx2(Button, { type: "button", variant: "danger", disabled: loading, className: "w-full sm:w-auto", onClick: onConfirm, children: loading ? "Deleting..." : confirmLabel })
196
+ ] })
197
+ ]
198
+ }
199
+ )
200
+ }
201
+ ),
202
+ document.body
203
+ );
204
+ }
205
+
206
+ // src/components/ui/input.tsx
207
+ import * as React from "react";
208
+ import { jsx as jsx3 } from "react/jsx-runtime";
209
+ var Input = React.forwardRef(function Input2({ className, ...props }, ref) {
210
+ return /* @__PURE__ */ jsx3(
211
+ "input",
212
+ {
213
+ ref,
214
+ className: cn(
215
+ "h-11 w-full rounded-lg border border-[var(--bfml-border)] bg-[var(--bfml-surface)] px-4 text-sm text-[var(--bfml-foreground)] shadow-[var(--bfml-shadow)] outline-none transition placeholder:text-[var(--bfml-muted-foreground)] focus:border-[var(--bfml-primary-border)] focus:ring-4 focus:ring-[var(--bfml-primary-soft)]",
216
+ className
217
+ ),
218
+ ...props
219
+ }
220
+ );
221
+ });
222
+
223
+ // src/components/ui/toast-container.tsx
224
+ import { useEffect, useState } from "react";
225
+ import { CheckCircle2, TriangleAlert, X, XCircle } from "lucide-react";
226
+
227
+ // src/utils/toast-store.ts
228
+ var listeners = /* @__PURE__ */ new Set();
229
+ var toasts = [];
230
+ function emit() {
231
+ listeners.forEach((listener) => listener([...toasts]));
232
+ }
233
+ function createId() {
234
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
235
+ return crypto.randomUUID();
236
+ }
237
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
238
+ }
239
+ function dismissToast(id) {
240
+ toasts = toasts.filter((toast) => toast.id !== id);
241
+ emit();
242
+ }
243
+ function showToast(type, message, durationMs = 3500) {
244
+ const id = createId();
245
+ toasts = [...toasts, { id, type, message }];
246
+ emit();
247
+ window.setTimeout(() => dismissToast(id), durationMs);
248
+ }
249
+ function subscribeToasts(listener) {
250
+ listeners.add(listener);
251
+ listener([...toasts]);
252
+ return () => {
253
+ listeners.delete(listener);
254
+ };
255
+ }
256
+ function toastSuccess(message) {
257
+ showToast("success", message);
258
+ }
259
+ function toastError(message) {
260
+ showToast("error", message);
261
+ }
262
+ function toastWarning(message) {
263
+ showToast("warning", message);
264
+ }
265
+
266
+ // src/components/ui/toast-container.tsx
267
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
268
+ function ToastCard({ toast }) {
269
+ const isSuccess = toast.type === "success";
270
+ const isWarning = toast.type === "warning";
271
+ return /* @__PURE__ */ jsxs2(
272
+ "div",
273
+ {
274
+ role: "status",
275
+ className: cn(
276
+ "pointer-events-auto flex w-full max-w-sm items-start gap-3 rounded-xl border px-4 py-3 shadow-[var(--bfml-shadow-lg)] transition",
277
+ isSuccess ? "border-[var(--bfml-success)]/30 bg-[var(--bfml-success-soft)] text-[var(--bfml-success-foreground)]" : isWarning ? "border-[var(--bfml-warning)]/30 bg-[var(--bfml-warning-soft)] text-[var(--bfml-warning-foreground)]" : "border-[var(--bfml-destructive)]/30 bg-[var(--bfml-destructive-soft)] text-[var(--bfml-foreground)]"
278
+ ),
279
+ children: [
280
+ isSuccess ? /* @__PURE__ */ jsx4(CheckCircle2, { className: "mt-0.5 h-4 w-4 shrink-0 text-[var(--bfml-success)]", "aria-hidden": "true" }) : isWarning ? /* @__PURE__ */ jsx4(TriangleAlert, { className: "mt-0.5 h-4 w-4 shrink-0 text-[var(--bfml-warning)]", "aria-hidden": "true" }) : /* @__PURE__ */ jsx4(XCircle, { className: "mt-0.5 h-4 w-4 shrink-0 text-[var(--bfml-destructive)]", "aria-hidden": "true" }),
281
+ /* @__PURE__ */ jsx4("p", { className: "min-w-0 flex-1 text-sm font-medium leading-5", children: toast.message }),
282
+ /* @__PURE__ */ jsx4(
283
+ "button",
284
+ {
285
+ type: "button",
286
+ className: "rounded-md p-0.5 opacity-70 transition hover:opacity-100",
287
+ "aria-label": "Dismiss notification",
288
+ onClick: () => dismissToast(toast.id),
289
+ children: /* @__PURE__ */ jsx4(X, { className: "h-4 w-4" })
290
+ }
291
+ )
292
+ ]
293
+ }
294
+ );
295
+ }
296
+ function ToastContainer({ theme = "sync" }) {
297
+ const [items, setItems] = useState([]);
298
+ const rootProps = bfmlRootProps(theme);
299
+ useEffect(() => {
300
+ return subscribeToasts(setItems);
301
+ }, []);
302
+ if (items.length === 0) return null;
303
+ return /* @__PURE__ */ jsx4(
304
+ "div",
305
+ {
306
+ ...rootProps,
307
+ className: cn(rootProps.className, "pointer-events-none absolute right-3 top-3 z-[45] flex w-[min(100%,20rem)] flex-col items-end gap-2 sm:right-4 sm:top-4"),
308
+ children: items.map((toast) => /* @__PURE__ */ jsx4(ToastCard, { toast }, toast.id))
309
+ }
310
+ );
311
+ }
312
+
313
+ // src/components/upload-preview.tsx
314
+ import { CheckCircle2 as CheckCircle22, LoaderCircle, XCircle as XCircle2 } from "lucide-react";
315
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
316
+ function UploadPreviewCard({ item }) {
317
+ const isImage = item.file.type.startsWith("image/");
318
+ return /* @__PURE__ */ jsxs3(
319
+ "div",
320
+ {
321
+ className: cn(
322
+ "relative overflow-hidden rounded-xl border bg-[var(--bfml-surface)] p-2 sm:p-3",
323
+ item.status === "done" ? "border-[var(--bfml-success)]/40" : item.status === "error" ? "border-[var(--bfml-destructive)]/40" : "border-[var(--bfml-border)]"
324
+ ),
325
+ children: [
326
+ /* @__PURE__ */ jsxs3("div", { className: "relative flex h-20 items-center justify-center overflow-hidden rounded-lg bg-[var(--bfml-surface-soft)] sm:h-28", children: [
327
+ isImage ? /* @__PURE__ */ jsx5("img", { src: item.previewUrl, alt: item.file.name, className: "h-full w-full object-contain opacity-80" }) : /* @__PURE__ */ jsx5("div", { className: "text-xs font-semibold uppercase text-[var(--bfml-muted-foreground)]", children: "PDF" }),
328
+ item.status === "pending" || item.status === "uploading" ? /* @__PURE__ */ jsxs3(
329
+ "div",
330
+ {
331
+ className: "absolute inset-0 flex flex-col items-center justify-center gap-2 px-2 text-[var(--bfml-primary-foreground)]",
332
+ style: { backgroundColor: "var(--bfml-overlay)" },
333
+ children: [
334
+ /* @__PURE__ */ jsx5(LoaderCircle, { className: "h-6 w-6 animate-spin" }),
335
+ /* @__PURE__ */ jsx5("span", { className: "text-xs font-medium", children: item.status === "uploading" ? "Uploading..." : "Waiting..." })
336
+ ]
337
+ }
338
+ ) : null,
339
+ item.status === "done" ? /* @__PURE__ */ jsx5(
340
+ "div",
341
+ {
342
+ className: "absolute inset-0 flex items-center justify-center",
343
+ style: { backgroundColor: "color-mix(in srgb, var(--bfml-success) 25%, transparent)" },
344
+ children: /* @__PURE__ */ jsx5(CheckCircle22, { className: "h-8 w-8 text-[var(--bfml-success)]" })
345
+ }
346
+ ) : null,
347
+ item.status === "error" ? /* @__PURE__ */ jsx5(
348
+ "div",
349
+ {
350
+ className: "absolute inset-0 flex items-center justify-center",
351
+ style: { backgroundColor: "color-mix(in srgb, var(--bfml-destructive) 25%, transparent)" },
352
+ children: /* @__PURE__ */ jsx5(XCircle2, { className: "h-8 w-8 text-[var(--bfml-destructive)]" })
353
+ }
354
+ ) : null
355
+ ] }),
356
+ /* @__PURE__ */ jsx5("p", { className: "mt-2 truncate text-sm font-medium text-[var(--bfml-foreground)]", children: item.file.name }),
357
+ /* @__PURE__ */ jsx5("p", { className: "truncate text-xs text-[var(--bfml-muted-foreground)]", children: item.status === "error" ? item.error ?? "Upload failed" : item.status === "done" ? "Uploaded" : "In queue" })
358
+ ]
359
+ }
360
+ );
361
+ }
362
+
363
+ // src/utils/media-library-utils.ts
364
+ function parentPath(path) {
365
+ const segments = path.split("/").filter(Boolean);
366
+ segments.pop();
367
+ return segments.join("/");
368
+ }
369
+ function isPathInside(path, folderPath) {
370
+ return path === folderPath || path.startsWith(`${folderPath}/`);
371
+ }
372
+ function buildBreadcrumb(path, rootLabel) {
373
+ const segments = path ? path.split("/").filter(Boolean) : [];
374
+ const crumbs = [{ label: rootLabel, path: "" }];
375
+ segments.forEach((segment, index) => {
376
+ crumbs.push({ label: segment, path: segments.slice(0, index + 1).join("/") });
377
+ });
378
+ return crumbs;
379
+ }
380
+ function createQueueId() {
381
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
382
+ return crypto.randomUUID();
383
+ }
384
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
385
+ }
386
+ function toCssSize(value) {
387
+ if (value === void 0) return void 0;
388
+ if (typeof value === "number") return `${value}px`;
389
+ const trimmed = value.trim();
390
+ if (/^calc\s*\(/i.test(trimmed) || /^var\s*\(/i.test(trimmed)) return trimmed;
391
+ if (/^[\d.]+\s*(vh|vw|vmin|vmax|%|px|rem|em)\s*[-+]\s*[\d.]+/i.test(trimmed)) {
392
+ const normalized = trimmed.replace(/\s*([+-])\s*/g, " $1 ");
393
+ return `calc(${normalized})`;
394
+ }
395
+ return trimmed;
396
+ }
397
+
398
+ // src/components/media-library-panel.tsx
399
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
400
+ function MediaLibraryPanel({
401
+ active = true,
402
+ config,
403
+ theme,
404
+ title = "Media Library",
405
+ description = "Create folders, upload files, and choose media.",
406
+ accept,
407
+ variant = "embedded",
408
+ selectable = false,
409
+ closeOnSelect = true,
410
+ selectionMode = "single",
411
+ maxSelections,
412
+ autoSelectUploads = false,
413
+ onClose,
414
+ onSelect,
415
+ onSelectMany,
416
+ className
417
+ }) {
418
+ const client = useMemo(() => createMediaLibraryClient(config), [config]);
419
+ const resolved = useMemo(() => ({ ...defaultMediaLibraryConfig, ...config }), [config]);
420
+ const themeMode = resolveThemeMode(theme ?? resolved.theme);
421
+ const rootProps = bfmlRootProps(themeMode);
422
+ const uploadInputRef = useRef(null);
423
+ const dragCounterRef = useRef(0);
424
+ const [currentPath, setCurrentPath] = useState2("");
425
+ const [search, setSearch] = useState2("");
426
+ const [folderName, setFolderName] = useState2("");
427
+ const [nestedFolder, setNestedFolder] = useState2(true);
428
+ const [loading, setLoading] = useState2(false);
429
+ const [uploading, setUploading] = useState2(false);
430
+ const [dragActive, setDragActive] = useState2(false);
431
+ const [capabilities, setCapabilities] = useState2(defaultMediaCapabilities);
432
+ const [uploadQueue, setUploadQueue] = useState2([]);
433
+ const [folders, setFolders] = useState2([]);
434
+ const [files, setFiles] = useState2([]);
435
+ const [selected, setSelected] = useState2(null);
436
+ const [selectedFiles, setSelectedFiles] = useState2([]);
437
+ const [deleteTarget, setDeleteTarget] = useState2(null);
438
+ const [deleting, setDeleting] = useState2(false);
439
+ const [sidebarOpen, setSidebarOpen] = useState2(false);
440
+ async function load(path = currentPath, q = search, options) {
441
+ if (!options?.silent) setLoading(true);
442
+ try {
443
+ const listing = await client.list(path, q);
444
+ setCurrentPath(listing.path);
445
+ setFolders(listing.folders);
446
+ setFiles(listing.files.filter((file) => fileMatchesAccept(file, accept)));
447
+ setCapabilities({ ...defaultMediaCapabilities, ...listing.capabilities });
448
+ } catch (caught) {
449
+ toastError(caught instanceof Error ? caught.message : "Failed to load media.");
450
+ } finally {
451
+ if (!options?.silent) setLoading(false);
452
+ }
453
+ }
454
+ function clearUploadQueue() {
455
+ setUploadQueue((current) => {
456
+ current.forEach((item) => URL.revokeObjectURL(item.previewUrl));
457
+ return [];
458
+ });
459
+ }
460
+ useEffect2(() => {
461
+ if (!active) return;
462
+ setSelected(null);
463
+ setSelectedFiles([]);
464
+ setDeleteTarget(null);
465
+ setSidebarOpen(false);
466
+ clearUploadQueue();
467
+ setDragActive(false);
468
+ dragCounterRef.current = 0;
469
+ setSearch("");
470
+ void load("", "");
471
+ }, [active]);
472
+ useEffect2(() => {
473
+ if (!active || variant !== "modal" || !onClose) return;
474
+ const handleClose = onClose;
475
+ function onKeyDown(event) {
476
+ if (event.key !== "Escape") return;
477
+ if (deleteTarget && !deleting) {
478
+ setDeleteTarget(null);
479
+ return;
480
+ }
481
+ handleClose();
482
+ }
483
+ document.body.style.overflow = "hidden";
484
+ window.addEventListener("keydown", onKeyDown);
485
+ return () => {
486
+ document.body.style.overflow = "";
487
+ window.removeEventListener("keydown", onKeyDown);
488
+ };
489
+ }, [active, deleteTarget, deleting, onClose, variant]);
490
+ async function createFolder() {
491
+ if (!capabilities.createFolder) {
492
+ toastError("You do not have permission to create folders.");
493
+ return;
494
+ }
495
+ const name = folderName.trim();
496
+ if (!name) {
497
+ toastError("Enter a folder name.");
498
+ return;
499
+ }
500
+ try {
501
+ await client.createFolder(currentPath, name, nestedFolder);
502
+ setFolderName("");
503
+ await load(currentPath, search);
504
+ toastSuccess(`Folder "${name}" created.`);
505
+ } catch (caught) {
506
+ toastError(caught instanceof Error ? caught.message : "Failed to create folder.");
507
+ }
508
+ }
509
+ async function processFiles(incoming) {
510
+ if (!capabilities.upload || incoming.length === 0 || uploading) return;
511
+ const sizeLimit = formatUploadSizeLimit();
512
+ const accepted = [];
513
+ let invalidTypeCount = 0;
514
+ for (const file of incoming) {
515
+ if (!fileMatchesAcceptForUpload(file, accept)) {
516
+ invalidTypeCount += 1;
517
+ continue;
518
+ }
519
+ if (!isFileWithinUploadSizeLimit(file)) {
520
+ toastWarning(`"${file.name}" exceeds ${sizeLimit} and was skipped.`);
521
+ continue;
522
+ }
523
+ accepted.push(file);
524
+ }
525
+ if (invalidTypeCount > 0) {
526
+ toastError(`${invalidTypeCount} file(s) were skipped due to type restrictions.`);
527
+ }
528
+ if (accepted.length === 0) return;
529
+ const queue = accepted.map((file) => ({
530
+ id: createQueueId(),
531
+ file,
532
+ previewUrl: URL.createObjectURL(file),
533
+ status: "pending"
534
+ }));
535
+ setUploadQueue(queue);
536
+ setUploading(true);
537
+ const uploadedFiles = [];
538
+ let successCount = 0;
539
+ for (const item of queue) {
540
+ setUploadQueue(
541
+ (current) => current.map((entry) => entry.id === item.id ? { ...entry, status: "uploading" } : entry)
542
+ );
543
+ try {
544
+ const uploaded = await client.uploadOne(currentPath, item.file);
545
+ uploadedFiles.push(uploaded);
546
+ successCount += 1;
547
+ URL.revokeObjectURL(item.previewUrl);
548
+ setUploadQueue((current) => current.filter((entry) => entry.id !== item.id));
549
+ await load(currentPath, search, { silent: true });
550
+ } catch (caught) {
551
+ const message = caught instanceof Error ? caught.message : "Upload failed.";
552
+ URL.revokeObjectURL(item.previewUrl);
553
+ setUploadQueue((current) => current.filter((entry) => entry.id !== item.id));
554
+ if (/5\s*mb|too large|file size/i.test(message)) {
555
+ toastWarning(`"${item.file.name}" exceeds ${sizeLimit} and was skipped.`);
556
+ } else {
557
+ toastError(`${item.file.name}: ${message}`);
558
+ }
559
+ }
560
+ }
561
+ if (successCount > 0) {
562
+ toastSuccess(successCount === 1 ? "File uploaded successfully." : `${successCount} files uploaded successfully.`);
563
+ if (autoSelectUploads && onSelectMany && uploadedFiles.length > 0) {
564
+ const limit = maxSelections ?? uploadedFiles.length;
565
+ onSelectMany(uploadedFiles.slice(0, limit));
566
+ onClose?.();
567
+ }
568
+ }
569
+ setUploading(false);
570
+ if (uploadInputRef.current) uploadInputRef.current.value = "";
571
+ }
572
+ async function uploadFiles(fileList) {
573
+ if (!fileList?.length) return;
574
+ await processFiles(Array.from(fileList));
575
+ }
576
+ function handleDragEnter(event) {
577
+ event.preventDefault();
578
+ if (!capabilities.upload) return;
579
+ dragCounterRef.current += 1;
580
+ setDragActive(true);
581
+ }
582
+ function handleDragLeave(event) {
583
+ event.preventDefault();
584
+ dragCounterRef.current -= 1;
585
+ if (dragCounterRef.current <= 0) {
586
+ dragCounterRef.current = 0;
587
+ setDragActive(false);
588
+ }
589
+ }
590
+ function handleDragOver(event) {
591
+ event.preventDefault();
592
+ }
593
+ function handleDrop(event) {
594
+ event.preventDefault();
595
+ dragCounterRef.current = 0;
596
+ setDragActive(false);
597
+ if (!capabilities.upload) return;
598
+ void processFiles(Array.from(event.dataTransfer.files ?? []));
599
+ }
600
+ async function confirmDelete() {
601
+ if (!deleteTarget) return;
602
+ setDeleting(true);
603
+ try {
604
+ await client.remove(deleteTarget.path, deleteTarget.type);
605
+ if (deleteTarget.type === "file" && selected?.path === deleteTarget.path) {
606
+ setSelected(null);
607
+ }
608
+ const reloadPath = deleteTarget.type === "folder" && isPathInside(currentPath, deleteTarget.path) ? parentPath(deleteTarget.path) : currentPath;
609
+ const deletedName = deleteTarget.name;
610
+ const deletedType = deleteTarget.type;
611
+ setDeleteTarget(null);
612
+ await load(reloadPath, search);
613
+ toastSuccess(deletedType === "folder" ? `Folder "${deletedName}" deleted.` : `File "${deletedName}" deleted.`);
614
+ } catch (caught) {
615
+ toastError(caught instanceof Error ? caught.message : "Delete failed.");
616
+ } finally {
617
+ setDeleting(false);
618
+ }
619
+ }
620
+ function requestDelete(target, event) {
621
+ if (!capabilities.delete) {
622
+ toastError("You do not have permission to delete media.");
623
+ return;
624
+ }
625
+ event?.preventDefault();
626
+ event?.stopPropagation();
627
+ setDeleteTarget(target);
628
+ }
629
+ function navigateTo(path) {
630
+ void load(path, search);
631
+ setSidebarOpen(false);
632
+ }
633
+ function toggleFileSelection(file) {
634
+ setSelectedFiles((current) => {
635
+ const exists = current.some((item) => item.path === file.path);
636
+ if (exists) {
637
+ return current.filter((item) => item.path !== file.path);
638
+ }
639
+ const limit = maxSelections ?? Number.POSITIVE_INFINITY;
640
+ if (current.length >= limit) {
641
+ toastWarning(`You can add up to ${limit} file${limit === 1 ? "" : "s"} at a time.`);
642
+ return current;
643
+ }
644
+ return [...current, file];
645
+ });
646
+ setSelected(file);
647
+ }
648
+ function confirmSelection() {
649
+ if (selectionMode === "multi" && onSelectMany) {
650
+ if (selectedFiles.length === 0) return;
651
+ onSelectMany(selectedFiles);
652
+ onClose?.();
653
+ return;
654
+ }
655
+ if (!selected) return;
656
+ onSelect?.(selected);
657
+ if (closeOnSelect) onClose?.();
658
+ else setSelected(null);
659
+ }
660
+ function handleFileClick(file) {
661
+ if (selectionMode === "multi" && onSelectMany) {
662
+ toggleFileSelection(file);
663
+ return;
664
+ }
665
+ setSelected(file);
666
+ if (!selectable && onSelect) {
667
+ onSelect(file);
668
+ }
669
+ }
670
+ function handleFileDoubleClick(file) {
671
+ if (selectionMode === "multi" && onSelectMany) {
672
+ onSelectMany([file]);
673
+ onClose?.();
674
+ return;
675
+ }
676
+ setSelected(file);
677
+ if (selectable) {
678
+ onSelect?.(file);
679
+ if (closeOnSelect) onClose?.();
680
+ else setSelected(null);
681
+ } else if (onSelect) {
682
+ onSelect(file);
683
+ }
684
+ }
685
+ if (!active) return null;
686
+ const crumbs = buildBreadcrumb(currentPath, resolved.rootLabel ?? "Root");
687
+ const sidebarFolders = [{ name: resolved.rootLabel ?? "Root", path: "" }, ...folders];
688
+ const showFooter = selectable;
689
+ const isMultiSelect = selectionMode === "multi" && Boolean(onSelectMany);
690
+ const footerSelectionCount = isMultiSelect ? selectedFiles.length : selected ? 1 : 0;
691
+ const footerCanConfirm = isMultiSelect ? selectedFiles.length > 0 : Boolean(selected);
692
+ const showCloseButton = variant === "modal" && Boolean(onClose);
693
+ const sidebarFolderList = /* @__PURE__ */ jsx6("div", { className: "mt-4 space-y-1 lg:mt-5", children: sidebarFolders.map((folder) => {
694
+ const folderActive = folder.path === currentPath;
695
+ const canDelete = Boolean(folder.path) && capabilities.delete;
696
+ return /* @__PURE__ */ jsxs4(
697
+ "div",
698
+ {
699
+ className: cn(
700
+ "flex items-center gap-1 rounded-lg transition",
701
+ folderActive ? "bg-[var(--bfml-primary)]" : "hover:bg-[var(--bfml-surface)]"
702
+ ),
703
+ children: [
704
+ /* @__PURE__ */ jsxs4(
705
+ "button",
706
+ {
707
+ type: "button",
708
+ onClick: () => navigateTo(folder.path),
709
+ className: cn(
710
+ "flex min-w-0 flex-1 items-center gap-2 px-3 py-2.5 text-left text-sm font-medium transition",
711
+ folderActive ? "text-[var(--bfml-primary-foreground)]" : "text-[var(--bfml-foreground)]"
712
+ ),
713
+ children: [
714
+ /* @__PURE__ */ jsx6(Folder, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }),
715
+ /* @__PURE__ */ jsx6("span", { className: "truncate", children: folder.name })
716
+ ]
717
+ }
718
+ ),
719
+ canDelete ? /* @__PURE__ */ jsx6(
720
+ "button",
721
+ {
722
+ type: "button",
723
+ title: "Delete folder",
724
+ className: cn(
725
+ "mr-1 rounded-md p-1.5 transition",
726
+ folderActive ? "text-[var(--bfml-primary-foreground)] hover:bg-[var(--bfml-primary-foreground)]/15" : "text-[var(--bfml-destructive)] hover:bg-[var(--bfml-destructive-soft)]"
727
+ ),
728
+ onClick: (event) => requestDelete({ path: folder.path, name: folder.name, type: "folder" }, event),
729
+ children: /* @__PURE__ */ jsx6(Trash2, { className: "h-3.5 w-3.5" })
730
+ }
731
+ ) : null
732
+ ]
733
+ },
734
+ folder.path || "root"
735
+ );
736
+ }) });
737
+ return /* @__PURE__ */ jsxs4(
738
+ "section",
739
+ {
740
+ ...rootProps,
741
+ className: cn(
742
+ rootProps.className,
743
+ "relative flex h-full min-h-0 flex-col overflow-hidden bg-[var(--bfml-surface)]",
744
+ variant === "modal" && "h-[100dvh] w-full max-w-none border-0 shadow-[var(--bfml-shadow-lg)] sm:h-[min(92vh,760px)] sm:max-w-6xl sm:rounded-2xl sm:border sm:border-[var(--bfml-border)]",
745
+ variant === "embedded" && "rounded-none border-0 shadow-none",
746
+ className
747
+ ),
748
+ children: [
749
+ /* @__PURE__ */ jsxs4("header", { className: "flex shrink-0 items-start justify-between gap-3 border-b border-[var(--bfml-border)] px-4 py-3 sm:gap-4 sm:px-6 sm:py-5", children: [
750
+ /* @__PURE__ */ jsxs4("div", { className: "min-w-0 pr-2", children: [
751
+ /* @__PURE__ */ jsx6("h2", { className: "truncate text-base font-semibold text-[var(--bfml-foreground)] sm:text-lg", children: title }),
752
+ description ? /* @__PURE__ */ jsx6("p", { className: "mt-1 hidden text-sm text-[var(--bfml-muted-foreground)] sm:block", children: description }) : null
753
+ ] }),
754
+ showCloseButton ? /* @__PURE__ */ jsx6(Button, { type: "button", variant: "ghost", className: "h-10 w-10 shrink-0 px-0", onClick: onClose, "aria-label": "Close media library", children: /* @__PURE__ */ jsx6(X2, { className: "h-4 w-4", "aria-hidden": "true" }) }) : null
755
+ ] }),
756
+ /* @__PURE__ */ jsx6(ToastContainer, { theme: themeMode }),
757
+ /* @__PURE__ */ jsxs4("div", { className: "relative flex min-h-0 flex-1 flex-col overflow-hidden lg:grid lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,240px)_1fr] lg:grid-rows-1", children: [
758
+ sidebarOpen ? /* @__PURE__ */ jsx6(
759
+ "button",
760
+ {
761
+ type: "button",
762
+ className: "absolute inset-0 z-20 lg:hidden",
763
+ style: { backgroundColor: "var(--bfml-overlay)" },
764
+ "aria-label": "Close folders panel",
765
+ onClick: () => setSidebarOpen(false)
766
+ }
767
+ ) : null,
768
+ /* @__PURE__ */ jsxs4(
769
+ "aside",
770
+ {
771
+ className: cn(
772
+ "absolute inset-y-0 left-0 z-30 flex w-[min(88vw,280px)] flex-col overflow-y-auto border-r border-[var(--bfml-border)] bg-[var(--bfml-surface-soft)] p-4 shadow-xl transition-transform duration-200 ease-out lg:static lg:z-auto lg:h-full lg:min-h-0 lg:w-auto lg:translate-x-0 lg:shadow-none",
773
+ sidebarOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"
774
+ ),
775
+ children: [
776
+ /* @__PURE__ */ jsxs4("div", { className: "mb-3 flex items-center justify-between lg:hidden", children: [
777
+ /* @__PURE__ */ jsx6("p", { className: "text-sm font-semibold text-[var(--bfml-foreground)]", children: "Folders" }),
778
+ /* @__PURE__ */ jsx6(Button, { type: "button", variant: "ghost", className: "h-9 w-9 px-0", onClick: () => setSidebarOpen(false), "aria-label": "Close folders panel", children: /* @__PURE__ */ jsx6(X2, { className: "h-4 w-4" }) })
779
+ ] }),
780
+ capabilities.createFolder ? /* @__PURE__ */ jsxs4("div", { className: "space-y-3", children: [
781
+ /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2 sm:flex-row", children: [
782
+ /* @__PURE__ */ jsx6(
783
+ Input,
784
+ {
785
+ value: folderName,
786
+ onChange: (event) => setFolderName(event.target.value),
787
+ placeholder: "new-folder",
788
+ className: "min-w-0"
789
+ }
790
+ ),
791
+ /* @__PURE__ */ jsx6(Button, { type: "button", variant: "secondary", className: "shrink-0 sm:px-4", onClick: createFolder, children: "Add" })
792
+ ] }),
793
+ /* @__PURE__ */ jsxs4("label", { className: "flex items-start gap-2 text-xs leading-5 text-[var(--bfml-muted-foreground)]", children: [
794
+ /* @__PURE__ */ jsx6(
795
+ "input",
796
+ {
797
+ type: "checkbox",
798
+ className: "mt-0.5",
799
+ checked: nestedFolder,
800
+ onChange: (event) => setNestedFolder(event.target.checked)
801
+ }
802
+ ),
803
+ /* @__PURE__ */ jsx6("span", { children: "Create as nested folder of current path" })
804
+ ] })
805
+ ] }) : null,
806
+ sidebarFolderList
807
+ ]
808
+ }
809
+ ),
810
+ /* @__PURE__ */ jsxs4("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden p-3 sm:p-5 lg:h-full lg:min-h-0", children: [
811
+ /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-2 sm:gap-3", children: [
812
+ /* @__PURE__ */ jsxs4(
813
+ Button,
814
+ {
815
+ type: "button",
816
+ variant: "secondary",
817
+ className: "shrink-0 lg:hidden",
818
+ onClick: () => setSidebarOpen(true),
819
+ "aria-label": "Open folders panel",
820
+ children: [
821
+ /* @__PURE__ */ jsx6(PanelLeft, { className: "h-4 w-4" }),
822
+ /* @__PURE__ */ jsx6("span", { className: "hidden sm:inline", children: "Folders" })
823
+ ]
824
+ }
825
+ ),
826
+ /* @__PURE__ */ jsxs4("div", { className: "relative min-w-0 flex-1 basis-[180px]", children: [
827
+ /* @__PURE__ */ jsx6(Search, { className: "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--bfml-muted-foreground)]" }),
828
+ /* @__PURE__ */ jsx6(
829
+ Input,
830
+ {
831
+ className: "pl-9",
832
+ value: search,
833
+ onChange: (event) => setSearch(event.target.value),
834
+ onKeyDown: (event) => {
835
+ if (event.key === "Enter") void load(currentPath, search);
836
+ },
837
+ placeholder: "Search files"
838
+ }
839
+ )
840
+ ] }),
841
+ /* @__PURE__ */ jsx6(
842
+ "input",
843
+ {
844
+ ref: uploadInputRef,
845
+ type: "file",
846
+ multiple: true,
847
+ className: "hidden",
848
+ accept: accept?.includes("pdf") ? "image/*,application/pdf" : "image/*",
849
+ onChange: (event) => void uploadFiles(event.target.files)
850
+ }
851
+ ),
852
+ capabilities.upload ? /* @__PURE__ */ jsxs4(
853
+ Button,
854
+ {
855
+ type: "button",
856
+ disabled: uploading,
857
+ className: "w-full shrink-0 sm:w-auto",
858
+ onClick: () => uploadInputRef.current?.click(),
859
+ "aria-label": "Upload files",
860
+ children: [
861
+ uploading ? /* @__PURE__ */ jsx6(LoaderCircle2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx6(Upload, { className: "h-4 w-4" }),
862
+ /* @__PURE__ */ jsx6("span", { className: "sm:inline", children: "Upload" })
863
+ ]
864
+ }
865
+ ) : null
866
+ ] }),
867
+ /* @__PURE__ */ jsx6("div", { className: "-mx-1 mt-3 flex items-center gap-1 overflow-x-auto px-1 pb-1 text-sm sm:mt-4", children: crumbs.map((crumb, index) => /* @__PURE__ */ jsxs4("div", { className: "flex shrink-0 items-center gap-1", children: [
868
+ /* @__PURE__ */ jsx6(
869
+ "button",
870
+ {
871
+ type: "button",
872
+ className: cn(
873
+ "max-w-[9rem] truncate rounded px-1.5 py-1 font-medium transition sm:max-w-none",
874
+ index === crumbs.length - 1 ? "text-[var(--bfml-primary)]" : "text-[var(--bfml-muted-foreground)] hover:text-[var(--bfml-foreground)]"
875
+ ),
876
+ onClick: () => navigateTo(crumb.path),
877
+ children: crumb.label
878
+ }
879
+ ),
880
+ index < crumbs.length - 1 ? /* @__PURE__ */ jsx6(ChevronRight, { className: "h-4 w-4 shrink-0 text-[var(--bfml-muted-foreground)]" }) : null
881
+ ] }, crumb.path)) }),
882
+ /* @__PURE__ */ jsxs4(
883
+ "div",
884
+ {
885
+ className: cn(
886
+ "relative mt-3 min-h-0 flex-1 overflow-y-auto sm:mt-4",
887
+ dragActive && capabilities.upload && "rounded-xl ring-2 ring-[var(--bfml-primary)] ring-offset-2 ring-offset-[var(--bfml-surface)]"
888
+ ),
889
+ onDragEnter: handleDragEnter,
890
+ onDragLeave: handleDragLeave,
891
+ onDragOver: handleDragOver,
892
+ onDrop: handleDrop,
893
+ children: [
894
+ dragActive && capabilities.upload ? /* @__PURE__ */ jsx6("div", { className: "pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-xl border-2 border-dashed border-[var(--bfml-primary)] bg-[var(--bfml-primary-soft)]/80 px-4 text-center", children: /* @__PURE__ */ jsxs4("div", { children: [
895
+ /* @__PURE__ */ jsx6(Upload, { className: "mx-auto mb-2 h-8 w-8 text-[var(--bfml-primary)]" }),
896
+ /* @__PURE__ */ jsx6("p", { className: "text-sm font-semibold text-[var(--bfml-foreground)]", children: "Drop files to upload" }),
897
+ /* @__PURE__ */ jsx6("p", { className: "mt-1 text-xs text-[var(--bfml-muted-foreground)]", children: "Files upload one by one" })
898
+ ] }) }) : null,
899
+ loading && uploadQueue.length === 0 ? /* @__PURE__ */ jsxs4("div", { className: "flex h-32 items-center justify-center text-sm text-[var(--bfml-muted-foreground)] sm:h-40", children: [
900
+ /* @__PURE__ */ jsx6(LoaderCircle2, { className: "mr-2 h-4 w-4 animate-spin" }),
901
+ "Loading media..."
902
+ ] }) : /* @__PURE__ */ jsxs4("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4", children: [
903
+ uploadQueue.map((item) => /* @__PURE__ */ jsx6(UploadPreviewCard, { item }, item.id)),
904
+ folders.map((folder) => /* @__PURE__ */ jsxs4(
905
+ "div",
906
+ {
907
+ className: "group relative rounded-xl border border-[var(--bfml-border)] bg-[var(--bfml-surface-soft)] transition hover:border-[var(--bfml-primary-border)]",
908
+ children: [
909
+ /* @__PURE__ */ jsxs4(
910
+ "button",
911
+ {
912
+ type: "button",
913
+ onDoubleClick: () => navigateTo(folder.path),
914
+ onClick: () => navigateTo(folder.path),
915
+ className: "block w-full p-3 text-left sm:p-4",
916
+ children: [
917
+ /* @__PURE__ */ jsx6(Folder, { className: "h-7 w-7 text-[var(--bfml-primary)] sm:h-8 sm:w-8" }),
918
+ /* @__PURE__ */ jsx6("p", { className: "mt-2 truncate text-sm font-medium text-[var(--bfml-foreground)] sm:mt-3", children: folder.name }),
919
+ /* @__PURE__ */ jsx6("p", { className: "text-xs text-[var(--bfml-muted-foreground)]", children: "Folder" })
920
+ ]
921
+ }
922
+ ),
923
+ capabilities.delete ? /* @__PURE__ */ jsx6(
924
+ "button",
925
+ {
926
+ type: "button",
927
+ title: "Delete folder",
928
+ className: "absolute right-1.5 top-1.5 rounded-md border border-[var(--bfml-border)] bg-[var(--bfml-surface)] p-1.5 shadow-sm transition hover:bg-[var(--bfml-destructive-soft)] sm:right-2 sm:top-2",
929
+ onClick: (event) => requestDelete({ path: folder.path, name: folder.name, type: "folder" }, event),
930
+ children: /* @__PURE__ */ jsx6(Trash2, { className: "h-3.5 w-3.5 text-[var(--bfml-destructive)] sm:h-4 sm:w-4" })
931
+ }
932
+ ) : null
933
+ ]
934
+ },
935
+ folder.path
936
+ )),
937
+ files.map((file) => {
938
+ const fileActive = isMultiSelect ? selectedFiles.some((item) => item.path === file.path) : selected?.path === file.path;
939
+ const isImage = file.mimeType.startsWith("image/");
940
+ return /* @__PURE__ */ jsxs4(
941
+ "div",
942
+ {
943
+ className: cn(
944
+ "group relative overflow-hidden rounded-xl border bg-[var(--bfml-surface)] transition",
945
+ fileActive && selectable ? "border-[var(--bfml-primary)] ring-2 ring-[var(--bfml-primary-soft)]" : "border-[var(--bfml-border)] hover:border-[var(--bfml-primary-border)]"
946
+ ),
947
+ children: [
948
+ /* @__PURE__ */ jsxs4(
949
+ "button",
950
+ {
951
+ type: "button",
952
+ className: "block w-full p-2 text-left sm:p-3",
953
+ onClick: () => handleFileClick(file),
954
+ onDoubleClick: () => handleFileDoubleClick(file),
955
+ children: [
956
+ /* @__PURE__ */ jsx6("div", { className: "flex h-20 items-center justify-center overflow-hidden rounded-lg bg-[var(--bfml-surface-soft)] sm:h-28", children: isImage ? /* @__PURE__ */ jsx6("img", { src: file.url, alt: file.name, className: "h-full w-full object-contain" }) : /* @__PURE__ */ jsx6("div", { className: "text-xs font-semibold uppercase text-[var(--bfml-muted-foreground)]", children: "PDF" }) }),
957
+ /* @__PURE__ */ jsx6("p", { className: "mt-2 truncate text-sm font-medium text-[var(--bfml-foreground)] sm:mt-3", children: file.name }),
958
+ /* @__PURE__ */ jsx6("p", { className: "text-xs text-[var(--bfml-muted-foreground)]", children: "File" })
959
+ ]
960
+ }
961
+ ),
962
+ capabilities.delete ? /* @__PURE__ */ jsx6(
963
+ "button",
964
+ {
965
+ type: "button",
966
+ title: "Delete file",
967
+ className: "absolute right-1.5 top-1.5 rounded-md border border-[var(--bfml-border)] bg-[var(--bfml-surface)] p-1.5 shadow-sm transition hover:bg-[var(--bfml-destructive-soft)] sm:right-2 sm:top-2",
968
+ onClick: (event) => requestDelete({ path: file.path, name: file.name, type: "file" }, event),
969
+ children: /* @__PURE__ */ jsx6(Trash2, { className: "h-3.5 w-3.5 text-[var(--bfml-destructive)] sm:h-4 sm:w-4" })
970
+ }
971
+ ) : null
972
+ ]
973
+ },
974
+ file.path
975
+ );
976
+ })
977
+ ] }),
978
+ !loading && folders.length === 0 && files.length === 0 && uploadQueue.length === 0 ? /* @__PURE__ */ jsxs4("div", { className: "flex h-32 flex-col items-center justify-center rounded-xl border border-dashed border-[var(--bfml-border)] px-4 text-center text-sm text-[var(--bfml-muted-foreground)] sm:h-40", children: [
979
+ /* @__PURE__ */ jsx6(ImagePlus, { className: "mb-2 h-6 w-6" }),
980
+ "No files in this folder yet.",
981
+ capabilities.upload ? /* @__PURE__ */ jsx6("p", { className: "mt-2 text-xs", children: "Drag and drop files here or use Upload." }) : null
982
+ ] }) : null
983
+ ]
984
+ }
985
+ )
986
+ ] })
987
+ ] }),
988
+ showFooter ? /* @__PURE__ */ jsxs4("footer", { className: "flex shrink-0 flex-col-reverse gap-2 border-t border-[var(--bfml-border)] px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4 sm:px-6 sm:py-4", children: [
989
+ /* @__PURE__ */ jsx6("p", { className: "truncate text-center text-sm text-[var(--bfml-muted-foreground)] sm:text-left", children: isMultiSelect ? footerSelectionCount === 0 ? "Select one or more files" : `${footerSelectionCount} file${footerSelectionCount === 1 ? "" : "s"} selected` : `Selected: ${selected ? selected.name : "None"}` }),
990
+ /* @__PURE__ */ jsx6(Button, { type: "button", className: "w-full sm:w-auto", disabled: !footerCanConfirm, onClick: confirmSelection, children: isMultiSelect ? footerSelectionCount > 0 ? `Add ${footerSelectionCount} file${footerSelectionCount === 1 ? "" : "s"}` : "Add files" : closeOnSelect ? "Done" : "Add" })
991
+ ] }) : null,
992
+ /* @__PURE__ */ jsx6(
993
+ ConfirmDialog,
994
+ {
995
+ open: Boolean(deleteTarget),
996
+ title: deleteTarget?.type === "folder" ? "Delete folder?" : "Delete file?",
997
+ description: deleteTarget ? deleteTarget.type === "folder" ? `Are you sure you want to delete the folder "${deleteTarget.name}" and everything inside it? This action cannot be undone.` : `Are you sure you want to delete "${deleteTarget.name}"? This action cannot be undone.` : "",
998
+ confirmLabel: "Delete",
999
+ loading: deleting,
1000
+ onCancel: () => {
1001
+ if (!deleting) setDeleteTarget(null);
1002
+ },
1003
+ onConfirm: () => void confirmDelete(),
1004
+ theme: themeMode
1005
+ }
1006
+ )
1007
+ ]
1008
+ }
1009
+ );
1010
+ }
1011
+
1012
+ // src/components/media-library-modal.tsx
1013
+ import { jsx as jsx7 } from "react/jsx-runtime";
1014
+ function MediaLibraryModal({
1015
+ open,
1016
+ onClose,
1017
+ onSelect,
1018
+ onSelectMany,
1019
+ closeOnSelect = true,
1020
+ selectionMode = "single",
1021
+ maxSelections,
1022
+ autoSelectUploads = false,
1023
+ config,
1024
+ theme,
1025
+ title = "Media Library",
1026
+ description = "Create folders, upload files, and choose media.",
1027
+ accept
1028
+ }) {
1029
+ const resolved = { ...defaultMediaLibraryConfig, ...config };
1030
+ const themeMode = resolveThemeMode(theme ?? resolved.theme);
1031
+ const rootProps = bfmlRootProps(themeMode);
1032
+ if (!open || typeof document === "undefined") return null;
1033
+ return createPortal2(
1034
+ /* @__PURE__ */ jsx7(
1035
+ "div",
1036
+ {
1037
+ ...rootProps,
1038
+ className: cn(
1039
+ rootProps.className,
1040
+ "fixed inset-0 z-[9999] flex items-stretch justify-center p-0 backdrop-blur-sm sm:items-center sm:p-2 md:p-4"
1041
+ ),
1042
+ style: { backgroundColor: "var(--bfml-overlay)" },
1043
+ children: /* @__PURE__ */ jsx7(
1044
+ MediaLibraryPanel,
1045
+ {
1046
+ active: open,
1047
+ variant: "modal",
1048
+ selectable: true,
1049
+ config,
1050
+ theme,
1051
+ title,
1052
+ description,
1053
+ accept,
1054
+ onClose,
1055
+ onSelect,
1056
+ onSelectMany,
1057
+ closeOnSelect,
1058
+ selectionMode,
1059
+ maxSelections,
1060
+ autoSelectUploads
1061
+ }
1062
+ )
1063
+ }
1064
+ ),
1065
+ document.body
1066
+ );
1067
+ }
1068
+ function MediaPreview({ path, alt }) {
1069
+ const isImage = /\.(png|jpe?g|webp|gif)$/i.test(path);
1070
+ if (!path) {
1071
+ return /* @__PURE__ */ jsx7("div", { className: "flex h-32 items-center justify-center rounded-xl border border-dashed border-[var(--bfml-border)] bg-[var(--bfml-surface-soft)] px-4 text-center text-sm text-[var(--bfml-muted-foreground)] sm:h-40", children: "No media selected" });
1072
+ }
1073
+ if (isImage) {
1074
+ return /* @__PURE__ */ jsx7("div", { className: "overflow-hidden rounded-xl border border-[var(--bfml-border)] bg-[var(--bfml-surface-soft)]", children: /* @__PURE__ */ jsx7("img", { src: path, alt: alt ?? fileNameFromPath(path), className: "h-32 w-full object-contain sm:h-40" }) });
1075
+ }
1076
+ return /* @__PURE__ */ jsx7("div", { className: "flex h-32 items-center justify-center rounded-xl border border-[var(--bfml-border)] bg-[var(--bfml-surface-soft)] px-4 text-center text-sm font-medium text-[var(--bfml-foreground)] sm:h-40", children: fileNameFromPath(path) });
1077
+ }
1078
+
1079
+ // src/components/media-library-widget.tsx
1080
+ import { jsx as jsx8 } from "react/jsx-runtime";
1081
+ function MediaLibraryWidget({
1082
+ width = "100%",
1083
+ height = 640,
1084
+ config,
1085
+ theme,
1086
+ title = "Media Library",
1087
+ description = "Create folders, upload files, and manage media.",
1088
+ accept,
1089
+ selectable = false,
1090
+ onSelect,
1091
+ className
1092
+ }) {
1093
+ const resolved = { ...defaultMediaLibraryConfig, ...config };
1094
+ const themeMode = resolveThemeMode(theme ?? resolved.theme);
1095
+ const rootProps = bfmlRootProps(themeMode);
1096
+ return /* @__PURE__ */ jsx8(
1097
+ "div",
1098
+ {
1099
+ ...rootProps,
1100
+ className: cn(
1101
+ rootProps.className,
1102
+ "overflow-hidden rounded-2xl border border-[var(--bfml-border)] bg-[var(--bfml-surface)] shadow-[var(--bfml-shadow-lg)]",
1103
+ className
1104
+ ),
1105
+ style: {
1106
+ width: toCssSize(width),
1107
+ height: toCssSize(height)
1108
+ },
1109
+ children: /* @__PURE__ */ jsx8(
1110
+ MediaLibraryPanel,
1111
+ {
1112
+ active: true,
1113
+ variant: "embedded",
1114
+ config,
1115
+ theme,
1116
+ title,
1117
+ description,
1118
+ accept,
1119
+ selectable,
1120
+ onSelect,
1121
+ className: "h-full"
1122
+ }
1123
+ )
1124
+ }
1125
+ );
1126
+ }
1127
+
1128
+ // src/components/media-picker.tsx
1129
+ import { useEffect as useEffect3, useState as useState3 } from "react";
1130
+ import { Upload as Upload2 } from "lucide-react";
1131
+
1132
+ // src/components/picker-thumbnail.tsx
1133
+ import { FileText, ImagePlus as ImagePlus2 } from "lucide-react";
1134
+ import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
1135
+ var sizeClasses = {
1136
+ sm: "h-9 w-9",
1137
+ md: "h-10 w-10 sm:h-12 sm:w-12",
1138
+ grid: "h-full w-full"
1139
+ };
1140
+ function PickerThumbnail({
1141
+ path,
1142
+ alt,
1143
+ size = "md",
1144
+ shape = "circle",
1145
+ className
1146
+ }) {
1147
+ const isImage = Boolean(path && isImagePath(path));
1148
+ const rounded = shape === "circle" ? "rounded-full" : "rounded-lg";
1149
+ return /* @__PURE__ */ jsx9(
1150
+ "span",
1151
+ {
1152
+ className: cn(
1153
+ "flex shrink-0 items-center justify-center overflow-hidden border border-[var(--bfml-border)] bg-[var(--bfml-surface)]",
1154
+ size !== "grid" && sizeClasses[size],
1155
+ rounded,
1156
+ !path && "text-[var(--bfml-primary)]",
1157
+ className
1158
+ ),
1159
+ children: !path ? /* @__PURE__ */ jsx9(ImagePlus2, { className: size === "sm" ? "h-4 w-4" : "h-5 w-5", "aria-hidden": "true" }) : isImage ? /* @__PURE__ */ jsx9("img", { src: path, alt: alt ?? "Selected media", className: "h-full w-full object-cover" }) : /* @__PURE__ */ jsx9(FileText, { className: size === "sm" ? "h-4 w-4" : "h-5 w-5", "aria-hidden": "true" })
1160
+ }
1161
+ );
1162
+ }
1163
+ function PickerThumbnailStack({ paths }) {
1164
+ if (paths.length === 0) {
1165
+ return /* @__PURE__ */ jsx9("span", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[var(--bfml-surface)] text-[var(--bfml-primary)] sm:h-12 sm:w-12", children: /* @__PURE__ */ jsx9(ImagePlus2, { className: "h-5 w-5", "aria-hidden": "true" }) });
1166
+ }
1167
+ if (paths.length === 1) {
1168
+ return /* @__PURE__ */ jsx9(PickerThumbnail, { path: paths[0], alt: "Selected media" });
1169
+ }
1170
+ const visible = paths.slice(0, 3);
1171
+ return /* @__PURE__ */ jsxs5("span", { className: "relative flex h-10 w-10 shrink-0 items-center sm:h-12 sm:w-12", children: [
1172
+ visible.map((path, index) => /* @__PURE__ */ jsx9(
1173
+ "span",
1174
+ {
1175
+ className: cn(
1176
+ "absolute overflow-hidden rounded-full border-2 border-[var(--bfml-surface-soft)] bg-[var(--bfml-surface)]",
1177
+ index === 0 && "left-0 top-0 z-30 h-7 w-7 sm:h-8 sm:w-8",
1178
+ index === 1 && "left-3 top-1 z-20 h-7 w-7 sm:left-4 sm:h-8 sm:w-8",
1179
+ index === 2 && "left-1 top-3 z-10 h-7 w-7 sm:top-4 sm:h-8 sm:w-8"
1180
+ ),
1181
+ children: /* @__PURE__ */ jsx9(PickerThumbnail, { path, size: "grid", shape: "circle", className: "h-full w-full border-0" })
1182
+ },
1183
+ `${path}-${index}`
1184
+ )),
1185
+ paths.length > 3 ? /* @__PURE__ */ jsxs5("span", { className: "absolute -bottom-0.5 -right-0.5 z-40 rounded-full bg-[var(--bfml-primary)] px-1.5 py-0.5 text-[10px] font-semibold leading-none text-[var(--bfml-primary-foreground)]", children: [
1186
+ "+",
1187
+ paths.length - 3
1188
+ ] }) : null
1189
+ ] });
1190
+ }
1191
+
1192
+ // src/components/media-picker.tsx
1193
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
1194
+ function MediaPicker({
1195
+ name,
1196
+ label = "Choose image",
1197
+ title = "Media Library",
1198
+ description = "Create folders, upload files, and choose media.",
1199
+ value,
1200
+ defaultValue = "",
1201
+ onChange,
1202
+ config,
1203
+ theme,
1204
+ accept = ["image"],
1205
+ className
1206
+ }) {
1207
+ const themeMode = resolveThemeMode(theme ?? config?.theme);
1208
+ const rootProps = bfmlRootProps(themeMode);
1209
+ const [open, setOpen] = useState3(false);
1210
+ const [selectedPath, setSelectedPath] = useState3(value ?? defaultValue);
1211
+ useEffect3(() => {
1212
+ if (value !== void 0) setSelectedPath(value);
1213
+ }, [value]);
1214
+ function handleSelect(file) {
1215
+ setSelectedPath(file.url);
1216
+ onChange?.(file.url);
1217
+ setOpen(false);
1218
+ }
1219
+ const currentValue = value ?? selectedPath;
1220
+ const fileName = currentValue ? fileNameFromPath(currentValue) : null;
1221
+ return /* @__PURE__ */ jsxs6("div", { ...rootProps, className: cn(rootProps.className, "space-y-2", className), children: [
1222
+ label ? /* @__PURE__ */ jsx10("label", { className: "text-sm font-medium text-[var(--bfml-foreground)]", children: label }) : null,
1223
+ /* @__PURE__ */ jsxs6(
1224
+ "button",
1225
+ {
1226
+ type: "button",
1227
+ onClick: () => setOpen(true),
1228
+ className: "flex w-full items-center gap-3 rounded-xl border border-[var(--bfml-border)] bg-[var(--bfml-surface-soft)] px-3 py-3 text-left transition hover:border-[var(--bfml-primary-border)] hover:bg-[var(--bfml-surface)] sm:gap-4 sm:px-4 sm:py-4",
1229
+ children: [
1230
+ /* @__PURE__ */ jsx10(PickerThumbnail, { path: currentValue, alt: fileName ?? label }),
1231
+ /* @__PURE__ */ jsxs6("span", { className: "min-w-0 flex-1", children: [
1232
+ /* @__PURE__ */ jsx10("span", { className: "block truncate text-sm font-semibold text-[var(--bfml-foreground)]", children: label }),
1233
+ /* @__PURE__ */ jsx10("span", { className: "mt-0.5 block truncate text-xs text-[var(--bfml-muted-foreground)]", children: fileName ?? "Select from folders or upload new" })
1234
+ ] }),
1235
+ /* @__PURE__ */ jsx10(Upload2, { className: "hidden h-5 w-5 shrink-0 text-[var(--bfml-muted-foreground)] sm:block", "aria-hidden": "true" })
1236
+ ]
1237
+ }
1238
+ ),
1239
+ /* @__PURE__ */ jsx10("input", { type: "hidden", name, value: currentValue }),
1240
+ /* @__PURE__ */ jsx10(
1241
+ MediaLibraryModal,
1242
+ {
1243
+ open,
1244
+ onClose: () => setOpen(false),
1245
+ onSelect: handleSelect,
1246
+ config,
1247
+ theme: themeMode,
1248
+ title,
1249
+ description,
1250
+ accept
1251
+ }
1252
+ )
1253
+ ] });
1254
+ }
1255
+
1256
+ // src/components/media-picker-multi.tsx
1257
+ import { useEffect as useEffect4, useState as useState4 } from "react";
1258
+ import { Upload as Upload3, X as X3 } from "lucide-react";
1259
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
1260
+ function MediaPickerMulti({
1261
+ name,
1262
+ label = "Choose attachments",
1263
+ title = "Media Library",
1264
+ description = "Create folders, upload files, and choose one or more attachments.",
1265
+ max = 10,
1266
+ values,
1267
+ defaultValues = [],
1268
+ onChange,
1269
+ config,
1270
+ theme,
1271
+ accept = ["image", "pdf"],
1272
+ className
1273
+ }) {
1274
+ const themeMode = resolveThemeMode(theme ?? config?.theme);
1275
+ const rootProps = bfmlRootProps(themeMode);
1276
+ const [open, setOpen] = useState4(false);
1277
+ const [selectedPaths, setSelectedPaths] = useState4(values ?? defaultValues);
1278
+ useEffect4(() => {
1279
+ if (values !== void 0) setSelectedPaths(values);
1280
+ }, [values]);
1281
+ const currentValues = values ?? selectedPaths;
1282
+ const atMax = currentValues.length >= max;
1283
+ function handleSelectMany(files) {
1284
+ setSelectedPaths((current) => {
1285
+ const next = [...current];
1286
+ for (const file of files) {
1287
+ if (next.includes(file.url) || next.length >= max) continue;
1288
+ next.push(file.url);
1289
+ }
1290
+ onChange?.(next);
1291
+ return next;
1292
+ });
1293
+ setOpen(false);
1294
+ }
1295
+ function removeAt(index) {
1296
+ setSelectedPaths((current) => {
1297
+ const next = current.filter((_, itemIndex) => itemIndex !== index);
1298
+ onChange?.(next);
1299
+ return next;
1300
+ });
1301
+ }
1302
+ return /* @__PURE__ */ jsxs7("div", { ...rootProps, className: cn(rootProps.className, "space-y-2", className), children: [
1303
+ label ? /* @__PURE__ */ jsx11("label", { className: "text-sm font-medium text-[var(--bfml-foreground)]", children: label }) : null,
1304
+ /* @__PURE__ */ jsxs7("div", { className: "overflow-hidden rounded-xl border border-[var(--bfml-border)] bg-[var(--bfml-surface-soft)]", children: [
1305
+ /* @__PURE__ */ jsxs7(
1306
+ "button",
1307
+ {
1308
+ type: "button",
1309
+ onClick: () => setOpen(true),
1310
+ disabled: atMax,
1311
+ className: "flex w-full items-center gap-3 px-3 py-3 text-left transition hover:bg-[var(--bfml-surface)] disabled:cursor-not-allowed disabled:opacity-60 sm:gap-4 sm:px-4 sm:py-4",
1312
+ children: [
1313
+ /* @__PURE__ */ jsx11(PickerThumbnailStack, { paths: currentValues }),
1314
+ /* @__PURE__ */ jsxs7("span", { className: "min-w-0 flex-1", children: [
1315
+ /* @__PURE__ */ jsx11("span", { className: "block truncate text-sm font-semibold text-[var(--bfml-foreground)]", children: label }),
1316
+ /* @__PURE__ */ jsx11("span", { className: "mt-0.5 block text-xs text-[var(--bfml-muted-foreground)]", children: currentValues.length > 0 ? `${currentValues.length} selected \xB7 click to add more (${currentValues.length}/${max})` : `Select from folders or upload new (0/${max})` })
1317
+ ] }),
1318
+ /* @__PURE__ */ jsx11(Upload3, { className: "hidden h-5 w-5 shrink-0 text-[var(--bfml-muted-foreground)] sm:block", "aria-hidden": "true" })
1319
+ ]
1320
+ }
1321
+ ),
1322
+ currentValues.length > 0 ? /* @__PURE__ */ jsx11("div", { className: "grid grid-cols-3 gap-2 border-t border-[var(--bfml-border)] bg-[var(--bfml-surface)] p-3 sm:grid-cols-4 sm:p-4", children: currentValues.map((path, index) => {
1323
+ const fileName = fileNameFromPath(path);
1324
+ const isImage = isImagePath(path);
1325
+ return /* @__PURE__ */ jsxs7(
1326
+ "div",
1327
+ {
1328
+ className: "group relative aspect-square overflow-hidden rounded-lg border border-[var(--bfml-border)] bg-[var(--bfml-surface-soft)]",
1329
+ children: [
1330
+ isImage ? /* @__PURE__ */ jsx11("img", { src: path, alt: fileName, className: "h-full w-full object-cover" }) : /* @__PURE__ */ jsx11("div", { className: "flex h-full w-full flex-col items-center justify-center gap-1 px-1 text-center text-[var(--bfml-muted-foreground)]", children: /* @__PURE__ */ jsx11("span", { className: "text-[10px] font-semibold uppercase", children: "PDF" }) }),
1331
+ /* @__PURE__ */ jsx11(
1332
+ "button",
1333
+ {
1334
+ type: "button",
1335
+ className: "absolute right-1 top-1 rounded-md border border-[var(--bfml-border)] bg-[var(--bfml-surface)] p-1 shadow-sm transition hover:bg-[var(--bfml-destructive-soft)]",
1336
+ onClick: () => removeAt(index),
1337
+ title: "Remove attachment",
1338
+ children: /* @__PURE__ */ jsx11(X3, { className: "h-3 w-3 text-[var(--bfml-destructive)]" })
1339
+ }
1340
+ ),
1341
+ /* @__PURE__ */ jsx11("div", { className: "absolute inset-x-0 bottom-0 bg-[var(--bfml-overlay)] px-1.5 py-1", children: /* @__PURE__ */ jsx11("p", { className: "truncate text-[10px] font-medium text-[var(--bfml-primary-foreground)]", children: fileName }) })
1342
+ ]
1343
+ },
1344
+ `${path}-${index}`
1345
+ );
1346
+ }) }) : null
1347
+ ] }),
1348
+ currentValues.map((path, index) => /* @__PURE__ */ jsx11("input", { type: "hidden", name: `${name}[${index}]`, value: path }, `${path}-${index}`)),
1349
+ /* @__PURE__ */ jsx11(
1350
+ MediaLibraryModal,
1351
+ {
1352
+ open,
1353
+ onClose: () => setOpen(false),
1354
+ onSelectMany: handleSelectMany,
1355
+ selectionMode: "multi",
1356
+ maxSelections: Math.max(0, max - currentValues.length),
1357
+ autoSelectUploads: true,
1358
+ config,
1359
+ theme: themeMode,
1360
+ title,
1361
+ description,
1362
+ accept
1363
+ }
1364
+ )
1365
+ ] });
1366
+ }
1367
+ export {
1368
+ MAX_MEDIA_UPLOAD_BYTES,
1369
+ MediaLibraryModal,
1370
+ MediaLibraryPanel,
1371
+ MediaLibraryWidget,
1372
+ MediaPicker,
1373
+ MediaPickerMulti,
1374
+ MediaPreview,
1375
+ bfmlRootProps,
1376
+ createMediaLibraryClient,
1377
+ defaultMediaCapabilities,
1378
+ defaultMediaLibraryConfig,
1379
+ fileMatchesAccept,
1380
+ fileMatchesAcceptForUpload,
1381
+ fileNameFromPath,
1382
+ formatUploadSizeLimit,
1383
+ isFileWithinUploadSizeLimit,
1384
+ isImagePath,
1385
+ resolveThemeMode
1386
+ };
1387
+ //# sourceMappingURL=index.js.map