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