@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Blitheforge
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,558 @@
1
+ # @blitheforge/media-library
2
+
3
+ Production-ready React media library with nested folders, drag-and-drop upload, search, RBAC-driven UI, toast notifications, and configurable API URLs. Built with Tailwind CSS — works in Next.js, Vite, or any React 18+ app.
4
+
5
+ ## Features
6
+
7
+ - **Nested folder management** — browse, create, and delete folders
8
+ - **File upload** — click to upload or drag-and-drop; files upload one-by-one with live preview cards in the grid
9
+ - **5 MB client-side limit** — oversized files show a warning toast and are skipped (configurable via `MAX_MEDIA_UPLOAD_BYTES`)
10
+ - **Search** — filter files in the current folder
11
+ - **RBAC / capabilities** — upload, create-folder, and delete UI is driven by your API `capabilities` response
12
+ - **Delete confirmation** — custom confirm dialog (not native `confirm()`)
13
+ - **Toast notifications** — success, error, and warning toasts (top-right inside the panel/modal)
14
+ - **Responsive** — mobile folder drawer, full-screen modal on small screens
15
+ - **Theme sync** — inherits host light/dark mode via CSS variables (`theme="sync"`)
16
+ - **Three display modes** — form pickers (`MediaPicker`), modal (`MediaLibraryModal`), embedded widget (`MediaLibraryWidget`)
17
+ - **Type-safe headless client** — `createMediaLibraryClient` for custom integrations
18
+
19
+ ---
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npm install @blitheforge/media-library
25
+ ```
26
+
27
+ Peer dependencies:
28
+
29
+ ```bash
30
+ npm install react react-dom
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Setup
36
+
37
+ ### 1. Import styles once
38
+
39
+ In your app entry (e.g. `app/layout.tsx` or `main.tsx`):
40
+
41
+ ```tsx
42
+ import "@blitheforge/media-library/styles.css";
43
+ import "./globals.css";
44
+ ```
45
+
46
+ Import the library **before** your globals. Library utilities are scoped to a lower CSS layer so they will not override your app's responsive classes (e.g. `lg:block`).
47
+
48
+ Do **not** add `@source` for this package in your app Tailwind config.
49
+
50
+ ### 2. Next.js (App Router)
51
+
52
+ Add the package to `transpilePackages` in `next.config.ts`:
53
+
54
+ ```ts
55
+ const nextConfig = {
56
+ transpilePackages: ["@blitheforge/media-library"]
57
+ };
58
+ export default nextConfig;
59
+ ```
60
+
61
+ ### 3. Monorepo / workspace
62
+
63
+ Link the local package via pnpm workspace:
64
+
65
+ ```yaml
66
+ # pnpm-workspace.yaml
67
+ packages:
68
+ - "Blitheforge-media-library"
69
+ - "."
70
+ ```
71
+
72
+ ```json
73
+ // package.json
74
+ {
75
+ "dependencies": {
76
+ "@blitheforge/media-library": "workspace:*"
77
+ }
78
+ }
79
+ ```
80
+
81
+ Rebuild after package changes:
82
+
83
+ ```bash
84
+ cd Blitheforge-media-library && npm run build
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Publishing
90
+
91
+ Pushes to `main` run `.github/workflows/publish.yml`. The workflow
92
+ typechecks and builds the package, then publishes it only when the version in
93
+ `package.json` is not already present on npm.
94
+
95
+ Before the first automated publish:
96
+
97
+ 1. Create an npm granular access token with **Bypass 2FA** enabled and
98
+ **Read and write** package permission for the `@blitheforge` scope.
99
+ 2. In the GitHub repository, open **Settings > Secrets and variables >
100
+ Actions**.
101
+ 3. Add the token as a repository secret named `NPM_TOKEN`.
102
+
103
+ For each release, bump the package version and push to `main`:
104
+
105
+ ```bash
106
+ npm version patch
107
+ git push --follow-tags
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Theming
113
+
114
+ Default is **`theme="sync"`** — the library reads your app's CSS variables:
115
+
116
+ | Host variable | Used for |
117
+ |---------------|----------|
118
+ | `--background` | Page background |
119
+ | `--foreground` | Text |
120
+ | `--surface` | Panel/card background |
121
+ | `--border` | Borders |
122
+ | `--primary` | Buttons, active states |
123
+ | `--destructive` | Delete actions |
124
+ | `--accent` | Hover/secondary surfaces |
125
+ | `--warning` | Warning toasts |
126
+
127
+ When your site toggles dark mode (e.g. `[data-theme="dark"]` on `<html>`), the media library follows automatically.
128
+
129
+ **Standalone** (no shared design tokens):
130
+
131
+ ```tsx
132
+ <MediaPicker name="image" theme="light" />
133
+ <MediaPicker name="image" theme="dark" />
134
+ ```
135
+
136
+ Or via config:
137
+
138
+ ```tsx
139
+ <MediaPicker name="image" config={{ theme: "sync" }} />
140
+ ```
141
+
142
+ Theme modes: `"sync"` | `"light"` | `"dark"`
143
+
144
+ ---
145
+
146
+ ## Components
147
+
148
+ | Export | Use case |
149
+ |--------|----------|
150
+ | `MediaPicker` | Single file field for forms — opens modal, shows live preview |
151
+ | `MediaPickerMulti` | Multiple attachments with preview cards |
152
+ | `MediaLibraryModal` | Full-screen/modal picker with Done button |
153
+ | `MediaLibraryWidget` | Embedded admin panel — fixed width/height, no overlay |
154
+ | `MediaLibraryPanel` | Low-level panel (`variant="modal"` or `"embedded"`) |
155
+ | `MediaPreview` | Standalone image/PDF preview block |
156
+ | `createMediaLibraryClient` | Headless API client |
157
+
158
+ ---
159
+
160
+ ## MediaPicker — single file (forms)
161
+
162
+ Best for donation proofs, expense vouchers, profile photos, etc.
163
+
164
+ ```tsx
165
+ import { MediaPicker } from "@blitheforge/media-library";
166
+
167
+ export function DonationForm() {
168
+ return (
169
+ <form>
170
+ <MediaPicker
171
+ name="proofUrl"
172
+ label="Donation proof"
173
+ accept={["image", "pdf"]}
174
+ onChange={(path) => console.log(path)}
175
+ />
176
+ </form>
177
+ );
178
+ }
179
+ ```
180
+
181
+ Props: `name`, `label`, `value`, `defaultValue`, `onChange`, `accept`, `config`, `theme`, `title`, `description`, `className`
182
+
183
+ ---
184
+
185
+ ## MediaPickerMulti — multiple attachments
186
+
187
+ ```tsx
188
+ import { MediaPickerMulti } from "@blitheforge/media-library";
189
+
190
+ <MediaPickerMulti
191
+ name="attachments"
192
+ label="Attachments"
193
+ max={5}
194
+ accept={["image", "pdf"]}
195
+ onChange={(paths) => console.log(paths)}
196
+ />
197
+ ```
198
+
199
+ ---
200
+
201
+ ## MediaLibraryModal — picker modal
202
+
203
+ Use when you need a custom trigger but still want the picker flow (select file → Done).
204
+
205
+ ```tsx
206
+ import { useState } from "react";
207
+ import { MediaLibraryModal } from "@blitheforge/media-library";
208
+
209
+ function CustomTrigger() {
210
+ const [open, setOpen] = useState(false);
211
+
212
+ return (
213
+ <>
214
+ <button type="button" onClick={() => setOpen(true)}>Browse media</button>
215
+ <MediaLibraryModal
216
+ open={open}
217
+ onClose={() => setOpen(false)}
218
+ onSelect={(file) => {
219
+ console.log(file.url);
220
+ setOpen(false);
221
+ }}
222
+ accept={["image", "pdf"]}
223
+ />
224
+ </>
225
+ );
226
+ }
227
+ ```
228
+
229
+ Modal behavior:
230
+ - Portal overlay with backdrop blur
231
+ - Escape key closes (unless delete confirm is open)
232
+ - Mobile: full viewport height; desktop: centered dialog
233
+ - Footer with **Selected** label and **Done** button
234
+
235
+ ---
236
+
237
+ ## MediaLibraryWidget — embedded admin panel
238
+
239
+ Use for a dedicated **Media Library** admin page — no modal, no overlay. Folders and files are always visible.
240
+
241
+ ```tsx
242
+ import { MediaLibraryWidget } from "@blitheforge/media-library";
243
+
244
+ export function MediaAdminPage() {
245
+ return (
246
+ <MediaLibraryWidget
247
+ width="100%"
248
+ height="calc(100vh - 200px)"
249
+ title="Media Library"
250
+ description="Create folders, upload files, and manage media."
251
+ accept={["image", "pdf"]}
252
+ />
253
+ );
254
+ }
255
+ ```
256
+
257
+ ### Width & height props
258
+
259
+ | Prop | Type | Default | Examples |
260
+ |------|------|---------|----------|
261
+ | `width` | `string \| number` | `"100%"` | `800`, `"70vw"`, `"100%"` |
262
+ | `height` | `string \| number` | `640` | `720`, `"70vh"`, `"calc(100vh - 200px)"` |
263
+
264
+ - **Numbers** → pixels (`720` → `720px`)
265
+ - **Strings** → passed as CSS values (`"100%"`, `"70vh"`)
266
+ - **`calc()`** — use proper CSS syntax with spaces: `"calc(100vh - 200px)"`
267
+ - **Shorthand** — `"100vh-200px"` is auto-converted to `calc(100vh - 200px)`
268
+
269
+ ### Widget vs modal
270
+
271
+ | | `MediaLibraryWidget` | `MediaLibraryModal` |
272
+ |--|----------------------|---------------------|
273
+ | Display | Inline on page | Overlay / portal |
274
+ | Close button | No | Yes |
275
+ | Done footer | Hidden by default | Always shown |
276
+ | Loads | On mount | When `open={true}` |
277
+ | Best for | Admin manage page | Form file picking |
278
+
279
+ ### Selectable widget (picker in embedded mode)
280
+
281
+ ```tsx
282
+ <MediaLibraryWidget
283
+ width={900}
284
+ height={600}
285
+ selectable
286
+ onSelect={(file) => console.log(file.path)}
287
+ />
288
+ ```
289
+
290
+ ---
291
+
292
+ ## MediaLibraryPanel — low-level
293
+
294
+ Build custom layouts by composing the panel directly.
295
+
296
+ ```tsx
297
+ import { MediaLibraryPanel } from "@blitheforge/media-library";
298
+
299
+ <MediaLibraryPanel
300
+ active
301
+ variant="embedded"
302
+ selectable={false}
303
+ title="Files"
304
+ accept={["image"]}
305
+ className="h-full"
306
+ />
307
+ ```
308
+
309
+ Props: `active`, `variant` (`"modal"` | `"embedded"`), `selectable`, `onClose`, `onSelect`, `config`, `theme`, `title`, `description`, `accept`, `className`
310
+
311
+ ---
312
+
313
+ ## MediaPreview
314
+
315
+ Standalone preview for a stored path/URL:
316
+
317
+ ```tsx
318
+ import { MediaPreview } from "@blitheforge/media-library";
319
+
320
+ <MediaPreview path="/uploads/media/photo.png" alt="Donation proof" />
321
+ ```
322
+
323
+ ---
324
+
325
+ ## Configure API URLs
326
+
327
+ All components accept an optional `config` prop:
328
+
329
+ ```tsx
330
+ <MediaPicker
331
+ name="imageUrl"
332
+ config={{
333
+ listUrl: "/api/media",
334
+ uploadUrl: "/api/media/upload",
335
+ createFolderUrl: "/api/media/folders",
336
+ updateUrl: "/api/media",
337
+ deleteUrl: "/api/media",
338
+ rootLabel: "Root",
339
+ theme: "sync"
340
+ }}
341
+ />
342
+ ```
343
+
344
+ Default URLs (relative to your app):
345
+
346
+ | Key | Default |
347
+ |-----|---------|
348
+ | `listUrl` | `/api/media` |
349
+ | `uploadUrl` | `/api/media/upload` |
350
+ | `createFolderUrl` | `/api/media/folders` |
351
+ | `updateUrl` | `/api/media` |
352
+ | `deleteUrl` | `/api/media` |
353
+ | `rootLabel` | `"Root"` |
354
+
355
+ ---
356
+
357
+ ## Backend API contract
358
+
359
+ All responses must follow:
360
+
361
+ ```json
362
+ { "success": true, "data": ... }
363
+ ```
364
+
365
+ Errors:
366
+
367
+ ```json
368
+ {
369
+ "success": false,
370
+ "error": { "code": "VALIDATION_ERROR", "message": "Human readable message" }
371
+ }
372
+ ```
373
+
374
+ ### List — `GET {listUrl}?path=&q=`
375
+
376
+ Query params:
377
+ - `path` — current folder path (empty = root)
378
+ - `q` — search query (optional)
379
+
380
+ Response:
381
+
382
+ ```json
383
+ {
384
+ "success": true,
385
+ "data": {
386
+ "path": "donations",
387
+ "folders": [
388
+ { "name": "2026", "path": "donations/2026" }
389
+ ],
390
+ "files": [
391
+ {
392
+ "name": "receipt.png",
393
+ "path": "donations/receipt.png",
394
+ "url": "/uploads/media/donations/receipt.png",
395
+ "size": 12345,
396
+ "mimeType": "image/png",
397
+ "updatedAt": "2026-06-06T00:00:00.000Z"
398
+ }
399
+ ],
400
+ "capabilities": {
401
+ "view": true,
402
+ "upload": true,
403
+ "createFolder": true,
404
+ "delete": true,
405
+ "rename": true,
406
+ "select": true
407
+ }
408
+ }
409
+ }
410
+ ```
411
+
412
+ ### Capabilities (RBAC)
413
+
414
+ The UI reads `capabilities` from every list response to show or hide actions:
415
+
416
+ | Capability | Controls |
417
+ |------------|----------|
418
+ | `view` | Access to browse (required) |
419
+ | `upload` | Upload button + drag-and-drop |
420
+ | `createFolder` | Folder create form in sidebar |
421
+ | `delete` | Delete buttons on files/folders |
422
+ | `rename` | Reserved for future rename UI |
423
+ | `select` | File selection (picker flows) |
424
+
425
+ If `capabilities` is omitted, all actions default to enabled.
426
+
427
+ ### Upload — `POST {uploadUrl}`
428
+
429
+ `multipart/form-data`:
430
+ - `path` — target folder path (empty string = root)
431
+ - `files` — one or more files
432
+
433
+ The UI uploads **one file at a time** sequentially and shows a preview card per file in the grid.
434
+
435
+ Recommended server limit: **5 MB per file** (matches client-side check).
436
+
437
+ ### Create folder — `POST {createFolderUrl}`
438
+
439
+ ```json
440
+ {
441
+ "path": "donations",
442
+ "name": "2026",
443
+ "nested": true
444
+ }
445
+ ```
446
+
447
+ - `nested: true` — create inside `path`
448
+ - `nested: false` — create at root level
449
+
450
+ ### Rename — `PATCH {updateUrl}`
451
+
452
+ ```json
453
+ {
454
+ "path": "donations/old.png",
455
+ "newName": "new.png",
456
+ "type": "file"
457
+ }
458
+ ```
459
+
460
+ `type`: `"file"` | `"folder"`
461
+
462
+ ### Delete — `DELETE {deleteUrl}`
463
+
464
+ ```json
465
+ {
466
+ "path": "donations/old.png",
467
+ "type": "file"
468
+ }
469
+ ```
470
+
471
+ Deleting a folder should remove all nested content on the server.
472
+
473
+ ---
474
+
475
+ ## Headless client
476
+
477
+ Use without UI for scripts, tests, or custom interfaces:
478
+
479
+ ```tsx
480
+ import { createMediaLibraryClient } from "@blitheforge/media-library";
481
+
482
+ const client = createMediaLibraryClient({
483
+ listUrl: "/api/media",
484
+ uploadUrl: "/api/media/upload",
485
+ createFolderUrl: "/api/media/folders",
486
+ updateUrl: "/api/media",
487
+ deleteUrl: "/api/media"
488
+ });
489
+
490
+ const listing = await client.list("donations", "receipt");
491
+ await client.createFolder("", "archive", true);
492
+ await client.uploadOne("donations", file);
493
+ await client.rename("donations/old.png", "new.png", "file");
494
+ await client.remove("donations/old.png", "file");
495
+ ```
496
+
497
+ ### Utility exports
498
+
499
+ ```tsx
500
+ import {
501
+ MAX_MEDIA_UPLOAD_BYTES, // 5 * 1024 * 1024
502
+ isFileWithinUploadSizeLimit,
503
+ formatUploadSizeLimit, // "5 MB"
504
+ fileMatchesAccept,
505
+ fileMatchesAcceptForUpload,
506
+ fileNameFromPath,
507
+ isImagePath,
508
+ bfmlRootProps,
509
+ resolveThemeMode
510
+ } from "@blitheforge/media-library";
511
+ ```
512
+
513
+ ---
514
+
515
+ ## Upload behavior
516
+
517
+ 1. User selects files (click or drag-and-drop)
518
+ 2. Client validates type (`accept` prop) and size (5 MB default)
519
+ 3. Oversized files → warning toast, skipped
520
+ 4. Invalid type → error toast, skipped
521
+ 5. Valid files → preview cards appear in the file grid
522
+ 6. Each file uploads sequentially; card removed on success
523
+ 7. Folder list refreshes silently after each successful upload
524
+ 8. Success toast when batch completes
525
+
526
+ ---
527
+
528
+ ## File type filtering
529
+
530
+ Pass `accept` to restrict allowed types:
531
+
532
+ ```tsx
533
+ accept={["image"]} // images only
534
+ accept={["pdf"]} // PDF only
535
+ accept={["image", "pdf"]} // both (default for admin widget)
536
+ ```
537
+
538
+ Omit `accept` to allow all types returned by the API.
539
+
540
+ ---
541
+
542
+ ## TypeScript types
543
+
544
+ ```tsx
545
+ import type {
546
+ MediaFile,
547
+ MediaFolder,
548
+ MediaListing,
549
+ MediaCapabilities,
550
+ MediaLibraryConfig,
551
+ MediaLibraryModalProps,
552
+ MediaLibraryWidgetProps,
553
+ MediaLibraryPanelProps,
554
+ MediaPickerProps,
555
+ MediaPickerMultiProps,
556
+ MediaLibraryThemeMode
557
+ } from "@blitheforge/media-library";
558
+ ```