@blitheforge/media-library 1.0.8 → 1.1.1

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/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  # @blitheforge/media-library
2
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.
3
+ Production-ready React media library with **pre-built, scoped CSS** no Tailwind required in your app. Works in Next.js, Vite, plain React, or vanilla JavaScript (headless API).
4
4
 
5
5
  ## Features
6
6
 
7
+ - **Self-contained CSS** — import one file; styles are scoped to `.bfml-root` and won't break your app layout
7
8
  - **Nested folder management** — browse, create, and delete folders
8
9
  - **File upload** — click to upload or drag-and-drop; files upload one-by-one with live preview cards in the grid
9
10
  - **5 MB client-side limit** — oversized files show a warning toast and are skipped (configurable via `MAX_MEDIA_UPLOAD_BYTES`)
@@ -34,26 +35,25 @@ npm install react react-dom
34
35
 
35
36
  ## Setup
36
37
 
37
- ### 1. Import styles once
38
+ ### 1. Import the bundled CSS once
38
39
 
39
- In your app entry (e.g. `app/layout.tsx` or `main.tsx`):
40
+ The package ships a complete CSS file built from Tailwind at publish time. **Your app does not need Tailwind.**
40
41
 
41
42
  ```tsx
43
+ // React — app/layout.tsx, main.tsx, etc.
42
44
  import "@blitheforge/media-library/styles.css";
43
- import "./globals.css";
44
45
  ```
45
46
 
46
- Import the library **before** your globals. Library utilities live in a lower CSS layer (`bfml`) so they will not override your app's responsive classes (e.g. `hidden lg:block` on sidebars).
47
-
48
- **Required for npm installs** — add this to your `globals.css` so Tailwind generates library classes in your app's utilities layer:
49
-
50
- ```css
51
- @source "../../node_modules/@blitheforge/media-library/dist/index.js";
47
+ ```html
48
+ <!-- Vanilla HTML / any framework -->
49
+ <link rel="stylesheet" href="/node_modules/@blitheforge/media-library/dist/style.css" />
52
50
  ```
53
51
 
54
- Do **not** add `@source` for this package in your app Tailwind config.
52
+ Styles are scoped under `.bfml-root`, so they will **not** override your app's `hidden`, `flex`, `lg:block`, etc.
55
53
 
56
- ### 2. Next.js (App Router)
54
+ If your app also uses Tailwind, import the library CSS **before or after** your globals — both work with scoped styles. You do **not** need `@source` for this package.
55
+
56
+ ### 2. React (Next.js App Router)
57
57
 
58
58
  Add the package to `transpilePackages` in `next.config.ts`:
59
59
 
@@ -64,14 +64,50 @@ const nextConfig = {
64
64
  export default nextConfig;
65
65
  ```
66
66
 
67
- ### 3. Monorepo / workspace
67
+ ### 3. Vanilla JavaScript (no React)
68
+
69
+ Use the headless client for upload/list/delete from any JS project:
70
+
71
+ ```html
72
+ <link rel="stylesheet" href="https://unpkg.com/@blitheforge/media-library/dist/style.css" />
73
+ <script type="module">
74
+ import { createMediaLibraryClient } from "https://unpkg.com/@blitheforge/media-library/dist/client.js";
75
+
76
+ const client = createMediaLibraryClient({
77
+ listUrl: "/api/media",
78
+ uploadUrl: "/api/media/upload",
79
+ createFolderUrl: "/api/media/folders",
80
+ deleteUrl: "/api/media"
81
+ });
82
+
83
+ const listing = await client.list("");
84
+ console.log(listing.files);
85
+
86
+ // Upload from a file input
87
+ document.querySelector("#files").addEventListener("change", async (event) => {
88
+ const files = [...event.target.files];
89
+ const uploaded = await client.upload("", files);
90
+ console.log(uploaded);
91
+ });
92
+ </script>
93
+ ```
94
+
95
+ Node / bundler:
96
+
97
+ ```js
98
+ import { createMediaLibraryClient } from "@blitheforge/media-library/client";
99
+ ```
100
+
101
+ React UI components (`MediaPicker`, `MediaLibraryModal`, etc.) still require React. The `/client` export has **no React dependency**.
102
+
103
+ ### 4. Monorepo / workspace
68
104
 
69
105
  Link the local package via pnpm workspace:
70
106
 
71
107
  ```yaml
72
108
  # pnpm-workspace.yaml
73
109
  packages:
74
- - "Blitheforge-media-library"
110
+ - "package"
75
111
  - "."
76
112
  ```
77
113
 
@@ -87,7 +123,7 @@ packages:
87
123
  Rebuild after package changes:
88
124
 
89
125
  ```bash
90
- cd Blitheforge-media-library && npm run build
126
+ cd package && pnpm build
91
127
  ```
92
128
 
93
129
  ---
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/client.ts
21
+ var client_exports = {};
22
+ __export(client_exports, {
23
+ MAX_MEDIA_UPLOAD_BYTES: () => MAX_MEDIA_UPLOAD_BYTES,
24
+ createMediaLibraryClient: () => createMediaLibraryClient,
25
+ fileMatchesAccept: () => fileMatchesAccept,
26
+ fileMatchesAcceptForUpload: () => fileMatchesAcceptForUpload,
27
+ fileNameFromPath: () => fileNameFromPath,
28
+ formatUploadSizeLimit: () => formatUploadSizeLimit,
29
+ isFileWithinUploadSizeLimit: () => isFileWithinUploadSizeLimit,
30
+ isImagePath: () => isImagePath
31
+ });
32
+ module.exports = __toCommonJS(client_exports);
33
+
34
+ // src/types.ts
35
+ var defaultMediaLibraryConfig = {
36
+ listUrl: "/api/media",
37
+ uploadUrl: "/api/media/upload",
38
+ createFolderUrl: "/api/media/folders",
39
+ updateUrl: "/api/media",
40
+ deleteUrl: "/api/media",
41
+ rootLabel: "Root"
42
+ };
43
+
44
+ // src/client.ts
45
+ function resolveConfig(config) {
46
+ return { ...defaultMediaLibraryConfig, ...config };
47
+ }
48
+ async function parseResponse(response) {
49
+ const payload = await response.json();
50
+ if (!payload.success) throw new Error(payload.error?.message ?? "Media request failed.");
51
+ return payload.data;
52
+ }
53
+ function createMediaLibraryClient(config) {
54
+ const urls = resolveConfig(config);
55
+ return {
56
+ async list(path = "", q = "") {
57
+ const params = new URLSearchParams();
58
+ if (path) params.set("path", path);
59
+ if (q) params.set("q", q);
60
+ const response = await fetch(`${urls.listUrl}?${params.toString()}`);
61
+ return parseResponse(response);
62
+ },
63
+ async createFolder(path, name, nested = true) {
64
+ const response = await fetch(urls.createFolderUrl, {
65
+ method: "POST",
66
+ headers: { "content-type": "application/json" },
67
+ body: JSON.stringify({ path, name, nested })
68
+ });
69
+ return parseResponse(response);
70
+ },
71
+ async upload(path, files) {
72
+ const form = new FormData();
73
+ form.set("path", path);
74
+ files.forEach((file) => form.append("files", file));
75
+ const response = await fetch(urls.uploadUrl, { method: "POST", body: form });
76
+ return parseResponse(response);
77
+ },
78
+ async uploadOne(path, file) {
79
+ const uploaded = await this.upload(path, [file]);
80
+ return uploaded[0];
81
+ },
82
+ async rename(path, newName, type) {
83
+ const response = await fetch(urls.updateUrl, {
84
+ method: "PATCH",
85
+ headers: { "content-type": "application/json" },
86
+ body: JSON.stringify({ path, newName, type })
87
+ });
88
+ return parseResponse(response);
89
+ },
90
+ async remove(path, type) {
91
+ const response = await fetch(urls.deleteUrl, {
92
+ method: "DELETE",
93
+ headers: { "content-type": "application/json" },
94
+ body: JSON.stringify({ path, type })
95
+ });
96
+ return parseResponse(response);
97
+ }
98
+ };
99
+ }
100
+ var MAX_MEDIA_UPLOAD_BYTES = 5 * 1024 * 1024;
101
+ function isFileWithinUploadSizeLimit(file, maxBytes = MAX_MEDIA_UPLOAD_BYTES) {
102
+ return file.size <= maxBytes;
103
+ }
104
+ function formatUploadSizeLimit(maxBytes = MAX_MEDIA_UPLOAD_BYTES) {
105
+ return `${Math.round(maxBytes / (1024 * 1024))} MB`;
106
+ }
107
+ function fileMatchesAccept(file, accept) {
108
+ if (!accept || accept.length === 0) return true;
109
+ const isImage = file.mimeType.startsWith("image/");
110
+ const isPdf = file.mimeType === "application/pdf";
111
+ if (accept.includes("image") && isImage) return true;
112
+ if (accept.includes("pdf") && isPdf) return true;
113
+ return false;
114
+ }
115
+ function fileMatchesAcceptForUpload(file, accept) {
116
+ if (!accept || accept.length === 0) return true;
117
+ const isImage = file.type.startsWith("image/");
118
+ const isPdf = file.type === "application/pdf";
119
+ if (accept.includes("image") && isImage) return true;
120
+ if (accept.includes("pdf") && isPdf) return true;
121
+ return false;
122
+ }
123
+ function fileNameFromPath(path) {
124
+ return path.split("/").pop() ?? path;
125
+ }
126
+ function isImagePath(path) {
127
+ return /\.(png|jpe?g|webp|gif)$/i.test(path);
128
+ }
129
+ // Annotate the CommonJS export names for ESM import in node:
130
+ 0 && (module.exports = {
131
+ MAX_MEDIA_UPLOAD_BYTES,
132
+ createMediaLibraryClient,
133
+ fileMatchesAccept,
134
+ fileMatchesAcceptForUpload,
135
+ fileNameFromPath,
136
+ formatUploadSizeLimit,
137
+ isFileWithinUploadSizeLimit,
138
+ isImagePath
139
+ });
140
+ //# sourceMappingURL=client.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts","../src/types.ts"],"sourcesContent":["import type { MediaFile, MediaLibraryConfig, MediaListing } from \"./types\";\nimport { defaultMediaLibraryConfig } from \"./types\";\n\ntype ApiPayload<T> = { success: boolean; data?: T; error?: { message?: string } };\n\nfunction resolveConfig(config?: Partial<MediaLibraryConfig>): MediaLibraryConfig {\n return { ...defaultMediaLibraryConfig, ...config };\n}\n\nasync function parseResponse<T>(response: Response): Promise<T> {\n const payload = (await response.json()) as ApiPayload<T>;\n if (!payload.success) throw new Error(payload.error?.message ?? \"Media request failed.\");\n return payload.data as T;\n}\n\nexport function createMediaLibraryClient(config?: Partial<MediaLibraryConfig>) {\n const urls = resolveConfig(config);\n\n return {\n async list(path = \"\", q = \"\") {\n const params = new URLSearchParams();\n if (path) params.set(\"path\", path);\n if (q) params.set(\"q\", q);\n const response = await fetch(`${urls.listUrl}?${params.toString()}`);\n return parseResponse<MediaListing>(response);\n },\n\n async createFolder(path: string, name: string, nested = true) {\n const response = await fetch(urls.createFolderUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ path, name, nested })\n });\n return parseResponse<{ name: string; path: string }>(response);\n },\n\n async upload(path: string, files: File[]) {\n const form = new FormData();\n form.set(\"path\", path);\n files.forEach((file) => form.append(\"files\", file));\n const response = await fetch(urls.uploadUrl, { method: \"POST\", body: form });\n return parseResponse<MediaFile[]>(response);\n },\n\n async uploadOne(path: string, file: File) {\n const uploaded = await this.upload(path, [file]);\n return uploaded[0];\n },\n\n async rename(path: string, newName: string, type: \"file\" | \"folder\") {\n const response = await fetch(urls.updateUrl, {\n method: \"PATCH\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ path, newName, type })\n });\n return parseResponse<MediaFile | { name: string; path: string }>(response);\n },\n\n async remove(path: string, type: \"file\" | \"folder\") {\n const response = await fetch(urls.deleteUrl, {\n method: \"DELETE\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ path, type })\n });\n return parseResponse<{ path: string }>(response);\n }\n };\n}\n\nexport const MAX_MEDIA_UPLOAD_BYTES = 5 * 1024 * 1024;\n\nexport function isFileWithinUploadSizeLimit(file: File, maxBytes = MAX_MEDIA_UPLOAD_BYTES) {\n return file.size <= maxBytes;\n}\n\nexport function formatUploadSizeLimit(maxBytes = MAX_MEDIA_UPLOAD_BYTES) {\n return `${Math.round(maxBytes / (1024 * 1024))} MB`;\n}\n\nexport function fileMatchesAccept(file: MediaFile, accept?: Array<\"image\" | \"pdf\">) {\n if (!accept || accept.length === 0) return true;\n const isImage = file.mimeType.startsWith(\"image/\");\n const isPdf = file.mimeType === \"application/pdf\";\n if (accept.includes(\"image\") && isImage) return true;\n if (accept.includes(\"pdf\") && isPdf) return true;\n return false;\n}\n\nexport function fileMatchesAcceptForUpload(file: File, accept?: Array<\"image\" | \"pdf\">) {\n if (!accept || accept.length === 0) return true;\n const isImage = file.type.startsWith(\"image/\");\n const isPdf = file.type === \"application/pdf\";\n if (accept.includes(\"image\") && isImage) return true;\n if (accept.includes(\"pdf\") && isPdf) return true;\n return false;\n}\n\nexport function fileNameFromPath(path: string) {\n return path.split(\"/\").pop() ?? path;\n}\n\nexport function isImagePath(path: string) {\n return /\\.(png|jpe?g|webp|gif)$/i.test(path);\n}\n","import type { MediaLibraryThemeMode } from \"./theme\";\n\nexport type MediaFile = {\n name: string;\n path: string;\n url: string;\n size: number;\n mimeType: string;\n updatedAt: string;\n};\n\nexport type MediaFolder = {\n name: string;\n path: string;\n};\n\nexport type MediaCapabilities = {\n view: boolean;\n upload: boolean;\n createFolder: boolean;\n delete: boolean;\n rename: boolean;\n select: boolean;\n};\n\nexport const defaultMediaCapabilities: MediaCapabilities = {\n view: true,\n upload: true,\n createFolder: true,\n delete: true,\n rename: true,\n select: true\n};\n\nexport type MediaListing = {\n path: string;\n folders: MediaFolder[];\n files: MediaFile[];\n capabilities?: MediaCapabilities;\n};\n\n/**\n * Configure API endpoints for your backend.\n * See README.md for the required request/response contract.\n */\nexport type MediaLibraryConfig = {\n listUrl: string;\n uploadUrl: string;\n createFolderUrl: string;\n updateUrl: string;\n deleteUrl: string;\n rootLabel?: string;\n /** `sync` inherits host CSS variables (default). Use `light` or `dark` for standalone theming. */\n theme?: MediaLibraryThemeMode;\n};\n\nexport const defaultMediaLibraryConfig: MediaLibraryConfig = {\n listUrl: \"/api/media\",\n uploadUrl: \"/api/media/upload\",\n createFolderUrl: \"/api/media/folders\",\n updateUrl: \"/api/media\",\n deleteUrl: \"/api/media\",\n rootLabel: \"Root\"\n};\n\nexport type MediaLibraryPanelProps = {\n /** When false, the panel does not load or render. */\n active?: boolean;\n config?: Partial<MediaLibraryConfig>;\n theme?: MediaLibraryThemeMode;\n title?: string;\n description?: string;\n accept?: Array<\"image\" | \"pdf\">;\n variant?: \"modal\" | \"embedded\";\n /** Show selection footer with Done button (picker flow). */\n selectable?: boolean;\n /** Close modal after selecting a file. Default true. */\n closeOnSelect?: boolean;\n /** `multi` allows selecting several files before confirming. Default `single`. */\n selectionMode?: \"single\" | \"multi\";\n /** Max files that can be added in one modal session (multi mode). */\n maxSelections?: number;\n /** After upload completes, add uploaded files and close (multi picker). */\n autoSelectUploads?: boolean;\n onClose?: () => void;\n onSelect?: (file: MediaFile) => void;\n onSelectMany?: (files: MediaFile[]) => void;\n className?: string;\n};\n\nexport type MediaLibraryWidgetProps = {\n /** CSS width, e.g. `\"100%\"`, `800`, or `\"70vw\"`. Default `\"100%\"`. */\n width?: string | number;\n /** CSS height, e.g. `640`, `\"600px\"`, or `\"70vh\"`. Default `640`. */\n height?: string | number;\n config?: Partial<MediaLibraryConfig>;\n theme?: MediaLibraryThemeMode;\n title?: string;\n description?: string;\n accept?: Array<\"image\" | \"pdf\">;\n selectable?: boolean;\n onSelect?: (file: MediaFile) => void;\n className?: string;\n};\n\nexport type MediaLibraryModalProps = {\n open: boolean;\n onClose: () => void;\n onSelect?: (file: MediaFile) => void;\n onSelectMany?: (files: MediaFile[]) => void;\n closeOnSelect?: boolean;\n selectionMode?: \"single\" | \"multi\";\n maxSelections?: number;\n autoSelectUploads?: boolean;\n config?: Partial<MediaLibraryConfig>;\n theme?: MediaLibraryThemeMode;\n title?: string;\n description?: string;\n accept?: Array<\"image\" | \"pdf\">;\n};\n\nexport type MediaPickerProps = {\n name: string;\n label?: string;\n title?: string;\n description?: string;\n /** @deprecated Preview is shown inline in the picker button. */\n previewTitle?: string;\n /** @deprecated Preview is shown inline in the picker button. */\n previewDescription?: string;\n value?: string;\n defaultValue?: string;\n onChange?: (path: string) => void;\n config?: Partial<MediaLibraryConfig>;\n theme?: MediaLibraryThemeMode;\n accept?: Array<\"image\" | \"pdf\">;\n className?: string;\n};\n\nexport type MediaPickerMultiProps = {\n name: string;\n label?: string;\n title?: string;\n description?: string;\n max?: number;\n values?: string[];\n defaultValues?: string[];\n onChange?: (paths: string[]) => void;\n config?: Partial<MediaLibraryConfig>;\n theme?: MediaLibraryThemeMode;\n accept?: Array<\"image\" | \"pdf\">;\n className?: string;\n};\n"],"mappings":"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","names":[]}
@@ -0,0 +1,68 @@
1
+ type MediaLibraryThemeMode = "sync" | "light" | "dark";
2
+
3
+ type MediaFile = {
4
+ name: string;
5
+ path: string;
6
+ url: string;
7
+ size: number;
8
+ mimeType: string;
9
+ updatedAt: string;
10
+ };
11
+ type MediaFolder = {
12
+ name: string;
13
+ path: string;
14
+ };
15
+ type MediaCapabilities = {
16
+ view: boolean;
17
+ upload: boolean;
18
+ createFolder: boolean;
19
+ delete: boolean;
20
+ rename: boolean;
21
+ select: boolean;
22
+ };
23
+ type MediaListing = {
24
+ path: string;
25
+ folders: MediaFolder[];
26
+ files: MediaFile[];
27
+ capabilities?: MediaCapabilities;
28
+ };
29
+ /**
30
+ * Configure API endpoints for your backend.
31
+ * See README.md for the required request/response contract.
32
+ */
33
+ type MediaLibraryConfig = {
34
+ listUrl: string;
35
+ uploadUrl: string;
36
+ createFolderUrl: string;
37
+ updateUrl: string;
38
+ deleteUrl: string;
39
+ rootLabel?: string;
40
+ /** `sync` inherits host CSS variables (default). Use `light` or `dark` for standalone theming. */
41
+ theme?: MediaLibraryThemeMode;
42
+ };
43
+
44
+ declare function createMediaLibraryClient(config?: Partial<MediaLibraryConfig>): {
45
+ list(path?: string, q?: string): Promise<MediaListing>;
46
+ createFolder(path: string, name: string, nested?: boolean): Promise<{
47
+ name: string;
48
+ path: string;
49
+ }>;
50
+ upload(path: string, files: File[]): Promise<MediaFile[]>;
51
+ uploadOne(path: string, file: File): Promise<MediaFile>;
52
+ rename(path: string, newName: string, type: "file" | "folder"): Promise<MediaFile | {
53
+ name: string;
54
+ path: string;
55
+ }>;
56
+ remove(path: string, type: "file" | "folder"): Promise<{
57
+ path: string;
58
+ }>;
59
+ };
60
+ declare const MAX_MEDIA_UPLOAD_BYTES: number;
61
+ declare function isFileWithinUploadSizeLimit(file: File, maxBytes?: number): boolean;
62
+ declare function formatUploadSizeLimit(maxBytes?: number): string;
63
+ declare function fileMatchesAccept(file: MediaFile, accept?: Array<"image" | "pdf">): boolean;
64
+ declare function fileMatchesAcceptForUpload(file: File, accept?: Array<"image" | "pdf">): boolean;
65
+ declare function fileNameFromPath(path: string): string;
66
+ declare function isImagePath(path: string): boolean;
67
+
68
+ export { MAX_MEDIA_UPLOAD_BYTES, createMediaLibraryClient, fileMatchesAccept, fileMatchesAcceptForUpload, fileNameFromPath, formatUploadSizeLimit, isFileWithinUploadSizeLimit, isImagePath };
@@ -0,0 +1,68 @@
1
+ type MediaLibraryThemeMode = "sync" | "light" | "dark";
2
+
3
+ type MediaFile = {
4
+ name: string;
5
+ path: string;
6
+ url: string;
7
+ size: number;
8
+ mimeType: string;
9
+ updatedAt: string;
10
+ };
11
+ type MediaFolder = {
12
+ name: string;
13
+ path: string;
14
+ };
15
+ type MediaCapabilities = {
16
+ view: boolean;
17
+ upload: boolean;
18
+ createFolder: boolean;
19
+ delete: boolean;
20
+ rename: boolean;
21
+ select: boolean;
22
+ };
23
+ type MediaListing = {
24
+ path: string;
25
+ folders: MediaFolder[];
26
+ files: MediaFile[];
27
+ capabilities?: MediaCapabilities;
28
+ };
29
+ /**
30
+ * Configure API endpoints for your backend.
31
+ * See README.md for the required request/response contract.
32
+ */
33
+ type MediaLibraryConfig = {
34
+ listUrl: string;
35
+ uploadUrl: string;
36
+ createFolderUrl: string;
37
+ updateUrl: string;
38
+ deleteUrl: string;
39
+ rootLabel?: string;
40
+ /** `sync` inherits host CSS variables (default). Use `light` or `dark` for standalone theming. */
41
+ theme?: MediaLibraryThemeMode;
42
+ };
43
+
44
+ declare function createMediaLibraryClient(config?: Partial<MediaLibraryConfig>): {
45
+ list(path?: string, q?: string): Promise<MediaListing>;
46
+ createFolder(path: string, name: string, nested?: boolean): Promise<{
47
+ name: string;
48
+ path: string;
49
+ }>;
50
+ upload(path: string, files: File[]): Promise<MediaFile[]>;
51
+ uploadOne(path: string, file: File): Promise<MediaFile>;
52
+ rename(path: string, newName: string, type: "file" | "folder"): Promise<MediaFile | {
53
+ name: string;
54
+ path: string;
55
+ }>;
56
+ remove(path: string, type: "file" | "folder"): Promise<{
57
+ path: string;
58
+ }>;
59
+ };
60
+ declare const MAX_MEDIA_UPLOAD_BYTES: number;
61
+ declare function isFileWithinUploadSizeLimit(file: File, maxBytes?: number): boolean;
62
+ declare function formatUploadSizeLimit(maxBytes?: number): string;
63
+ declare function fileMatchesAccept(file: MediaFile, accept?: Array<"image" | "pdf">): boolean;
64
+ declare function fileMatchesAcceptForUpload(file: File, accept?: Array<"image" | "pdf">): boolean;
65
+ declare function fileNameFromPath(path: string): string;
66
+ declare function isImagePath(path: string): boolean;
67
+
68
+ export { MAX_MEDIA_UPLOAD_BYTES, createMediaLibraryClient, fileMatchesAccept, fileMatchesAcceptForUpload, fileNameFromPath, formatUploadSizeLimit, isFileWithinUploadSizeLimit, isImagePath };
package/dist/client.js ADDED
@@ -0,0 +1,106 @@
1
+ // src/types.ts
2
+ var defaultMediaLibraryConfig = {
3
+ listUrl: "/api/media",
4
+ uploadUrl: "/api/media/upload",
5
+ createFolderUrl: "/api/media/folders",
6
+ updateUrl: "/api/media",
7
+ deleteUrl: "/api/media",
8
+ rootLabel: "Root"
9
+ };
10
+
11
+ // src/client.ts
12
+ function resolveConfig(config) {
13
+ return { ...defaultMediaLibraryConfig, ...config };
14
+ }
15
+ async function parseResponse(response) {
16
+ const payload = await response.json();
17
+ if (!payload.success) throw new Error(payload.error?.message ?? "Media request failed.");
18
+ return payload.data;
19
+ }
20
+ function createMediaLibraryClient(config) {
21
+ const urls = resolveConfig(config);
22
+ return {
23
+ async list(path = "", q = "") {
24
+ const params = new URLSearchParams();
25
+ if (path) params.set("path", path);
26
+ if (q) params.set("q", q);
27
+ const response = await fetch(`${urls.listUrl}?${params.toString()}`);
28
+ return parseResponse(response);
29
+ },
30
+ async createFolder(path, name, nested = true) {
31
+ const response = await fetch(urls.createFolderUrl, {
32
+ method: "POST",
33
+ headers: { "content-type": "application/json" },
34
+ body: JSON.stringify({ path, name, nested })
35
+ });
36
+ return parseResponse(response);
37
+ },
38
+ async upload(path, files) {
39
+ const form = new FormData();
40
+ form.set("path", path);
41
+ files.forEach((file) => form.append("files", file));
42
+ const response = await fetch(urls.uploadUrl, { method: "POST", body: form });
43
+ return parseResponse(response);
44
+ },
45
+ async uploadOne(path, file) {
46
+ const uploaded = await this.upload(path, [file]);
47
+ return uploaded[0];
48
+ },
49
+ async rename(path, newName, type) {
50
+ const response = await fetch(urls.updateUrl, {
51
+ method: "PATCH",
52
+ headers: { "content-type": "application/json" },
53
+ body: JSON.stringify({ path, newName, type })
54
+ });
55
+ return parseResponse(response);
56
+ },
57
+ async remove(path, type) {
58
+ const response = await fetch(urls.deleteUrl, {
59
+ method: "DELETE",
60
+ headers: { "content-type": "application/json" },
61
+ body: JSON.stringify({ path, type })
62
+ });
63
+ return parseResponse(response);
64
+ }
65
+ };
66
+ }
67
+ var MAX_MEDIA_UPLOAD_BYTES = 5 * 1024 * 1024;
68
+ function isFileWithinUploadSizeLimit(file, maxBytes = MAX_MEDIA_UPLOAD_BYTES) {
69
+ return file.size <= maxBytes;
70
+ }
71
+ function formatUploadSizeLimit(maxBytes = MAX_MEDIA_UPLOAD_BYTES) {
72
+ return `${Math.round(maxBytes / (1024 * 1024))} MB`;
73
+ }
74
+ function fileMatchesAccept(file, accept) {
75
+ if (!accept || accept.length === 0) return true;
76
+ const isImage = file.mimeType.startsWith("image/");
77
+ const isPdf = file.mimeType === "application/pdf";
78
+ if (accept.includes("image") && isImage) return true;
79
+ if (accept.includes("pdf") && isPdf) return true;
80
+ return false;
81
+ }
82
+ function fileMatchesAcceptForUpload(file, accept) {
83
+ if (!accept || accept.length === 0) return true;
84
+ const isImage = file.type.startsWith("image/");
85
+ const isPdf = file.type === "application/pdf";
86
+ if (accept.includes("image") && isImage) return true;
87
+ if (accept.includes("pdf") && isPdf) return true;
88
+ return false;
89
+ }
90
+ function fileNameFromPath(path) {
91
+ return path.split("/").pop() ?? path;
92
+ }
93
+ function isImagePath(path) {
94
+ return /\.(png|jpe?g|webp|gif)$/i.test(path);
95
+ }
96
+ export {
97
+ MAX_MEDIA_UPLOAD_BYTES,
98
+ createMediaLibraryClient,
99
+ fileMatchesAccept,
100
+ fileMatchesAcceptForUpload,
101
+ fileNameFromPath,
102
+ formatUploadSizeLimit,
103
+ isFileWithinUploadSizeLimit,
104
+ isImagePath
105
+ };
106
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/client.ts"],"sourcesContent":["import type { MediaLibraryThemeMode } from \"./theme\";\n\nexport type MediaFile = {\n name: string;\n path: string;\n url: string;\n size: number;\n mimeType: string;\n updatedAt: string;\n};\n\nexport type MediaFolder = {\n name: string;\n path: string;\n};\n\nexport type MediaCapabilities = {\n view: boolean;\n upload: boolean;\n createFolder: boolean;\n delete: boolean;\n rename: boolean;\n select: boolean;\n};\n\nexport const defaultMediaCapabilities: MediaCapabilities = {\n view: true,\n upload: true,\n createFolder: true,\n delete: true,\n rename: true,\n select: true\n};\n\nexport type MediaListing = {\n path: string;\n folders: MediaFolder[];\n files: MediaFile[];\n capabilities?: MediaCapabilities;\n};\n\n/**\n * Configure API endpoints for your backend.\n * See README.md for the required request/response contract.\n */\nexport type MediaLibraryConfig = {\n listUrl: string;\n uploadUrl: string;\n createFolderUrl: string;\n updateUrl: string;\n deleteUrl: string;\n rootLabel?: string;\n /** `sync` inherits host CSS variables (default). Use `light` or `dark` for standalone theming. */\n theme?: MediaLibraryThemeMode;\n};\n\nexport const defaultMediaLibraryConfig: MediaLibraryConfig = {\n listUrl: \"/api/media\",\n uploadUrl: \"/api/media/upload\",\n createFolderUrl: \"/api/media/folders\",\n updateUrl: \"/api/media\",\n deleteUrl: \"/api/media\",\n rootLabel: \"Root\"\n};\n\nexport type MediaLibraryPanelProps = {\n /** When false, the panel does not load or render. */\n active?: boolean;\n config?: Partial<MediaLibraryConfig>;\n theme?: MediaLibraryThemeMode;\n title?: string;\n description?: string;\n accept?: Array<\"image\" | \"pdf\">;\n variant?: \"modal\" | \"embedded\";\n /** Show selection footer with Done button (picker flow). */\n selectable?: boolean;\n /** Close modal after selecting a file. Default true. */\n closeOnSelect?: boolean;\n /** `multi` allows selecting several files before confirming. Default `single`. */\n selectionMode?: \"single\" | \"multi\";\n /** Max files that can be added in one modal session (multi mode). */\n maxSelections?: number;\n /** After upload completes, add uploaded files and close (multi picker). */\n autoSelectUploads?: boolean;\n onClose?: () => void;\n onSelect?: (file: MediaFile) => void;\n onSelectMany?: (files: MediaFile[]) => void;\n className?: string;\n};\n\nexport type MediaLibraryWidgetProps = {\n /** CSS width, e.g. `\"100%\"`, `800`, or `\"70vw\"`. Default `\"100%\"`. */\n width?: string | number;\n /** CSS height, e.g. `640`, `\"600px\"`, or `\"70vh\"`. Default `640`. */\n height?: string | number;\n config?: Partial<MediaLibraryConfig>;\n theme?: MediaLibraryThemeMode;\n title?: string;\n description?: string;\n accept?: Array<\"image\" | \"pdf\">;\n selectable?: boolean;\n onSelect?: (file: MediaFile) => void;\n className?: string;\n};\n\nexport type MediaLibraryModalProps = {\n open: boolean;\n onClose: () => void;\n onSelect?: (file: MediaFile) => void;\n onSelectMany?: (files: MediaFile[]) => void;\n closeOnSelect?: boolean;\n selectionMode?: \"single\" | \"multi\";\n maxSelections?: number;\n autoSelectUploads?: boolean;\n config?: Partial<MediaLibraryConfig>;\n theme?: MediaLibraryThemeMode;\n title?: string;\n description?: string;\n accept?: Array<\"image\" | \"pdf\">;\n};\n\nexport type MediaPickerProps = {\n name: string;\n label?: string;\n title?: string;\n description?: string;\n /** @deprecated Preview is shown inline in the picker button. */\n previewTitle?: string;\n /** @deprecated Preview is shown inline in the picker button. */\n previewDescription?: string;\n value?: string;\n defaultValue?: string;\n onChange?: (path: string) => void;\n config?: Partial<MediaLibraryConfig>;\n theme?: MediaLibraryThemeMode;\n accept?: Array<\"image\" | \"pdf\">;\n className?: string;\n};\n\nexport type MediaPickerMultiProps = {\n name: string;\n label?: string;\n title?: string;\n description?: string;\n max?: number;\n values?: string[];\n defaultValues?: string[];\n onChange?: (paths: string[]) => void;\n config?: Partial<MediaLibraryConfig>;\n theme?: MediaLibraryThemeMode;\n accept?: Array<\"image\" | \"pdf\">;\n className?: string;\n};\n","import type { MediaFile, MediaLibraryConfig, MediaListing } from \"./types\";\nimport { defaultMediaLibraryConfig } from \"./types\";\n\ntype ApiPayload<T> = { success: boolean; data?: T; error?: { message?: string } };\n\nfunction resolveConfig(config?: Partial<MediaLibraryConfig>): MediaLibraryConfig {\n return { ...defaultMediaLibraryConfig, ...config };\n}\n\nasync function parseResponse<T>(response: Response): Promise<T> {\n const payload = (await response.json()) as ApiPayload<T>;\n if (!payload.success) throw new Error(payload.error?.message ?? \"Media request failed.\");\n return payload.data as T;\n}\n\nexport function createMediaLibraryClient(config?: Partial<MediaLibraryConfig>) {\n const urls = resolveConfig(config);\n\n return {\n async list(path = \"\", q = \"\") {\n const params = new URLSearchParams();\n if (path) params.set(\"path\", path);\n if (q) params.set(\"q\", q);\n const response = await fetch(`${urls.listUrl}?${params.toString()}`);\n return parseResponse<MediaListing>(response);\n },\n\n async createFolder(path: string, name: string, nested = true) {\n const response = await fetch(urls.createFolderUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ path, name, nested })\n });\n return parseResponse<{ name: string; path: string }>(response);\n },\n\n async upload(path: string, files: File[]) {\n const form = new FormData();\n form.set(\"path\", path);\n files.forEach((file) => form.append(\"files\", file));\n const response = await fetch(urls.uploadUrl, { method: \"POST\", body: form });\n return parseResponse<MediaFile[]>(response);\n },\n\n async uploadOne(path: string, file: File) {\n const uploaded = await this.upload(path, [file]);\n return uploaded[0];\n },\n\n async rename(path: string, newName: string, type: \"file\" | \"folder\") {\n const response = await fetch(urls.updateUrl, {\n method: \"PATCH\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ path, newName, type })\n });\n return parseResponse<MediaFile | { name: string; path: string }>(response);\n },\n\n async remove(path: string, type: \"file\" | \"folder\") {\n const response = await fetch(urls.deleteUrl, {\n method: \"DELETE\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ path, type })\n });\n return parseResponse<{ path: string }>(response);\n }\n };\n}\n\nexport const MAX_MEDIA_UPLOAD_BYTES = 5 * 1024 * 1024;\n\nexport function isFileWithinUploadSizeLimit(file: File, maxBytes = MAX_MEDIA_UPLOAD_BYTES) {\n return file.size <= maxBytes;\n}\n\nexport function formatUploadSizeLimit(maxBytes = MAX_MEDIA_UPLOAD_BYTES) {\n return `${Math.round(maxBytes / (1024 * 1024))} MB`;\n}\n\nexport function fileMatchesAccept(file: MediaFile, accept?: Array<\"image\" | \"pdf\">) {\n if (!accept || accept.length === 0) return true;\n const isImage = file.mimeType.startsWith(\"image/\");\n const isPdf = file.mimeType === \"application/pdf\";\n if (accept.includes(\"image\") && isImage) return true;\n if (accept.includes(\"pdf\") && isPdf) return true;\n return false;\n}\n\nexport function fileMatchesAcceptForUpload(file: File, accept?: Array<\"image\" | \"pdf\">) {\n if (!accept || accept.length === 0) return true;\n const isImage = file.type.startsWith(\"image/\");\n const isPdf = file.type === \"application/pdf\";\n if (accept.includes(\"image\") && isImage) return true;\n if (accept.includes(\"pdf\") && isPdf) return true;\n return false;\n}\n\nexport function fileNameFromPath(path: string) {\n return path.split(\"/\").pop() ?? path;\n}\n\nexport function isImagePath(path: string) {\n return /\\.(png|jpe?g|webp|gif)$/i.test(path);\n}\n"],"mappings":"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","names":[]}