@blitheforge/media-library 1.0.7 → 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 +51 -15
- package/dist/client.cjs +140 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +68 -0
- package/dist/client.d.ts +68 -0
- package/dist/client.js +106 -0
- package/dist/client.js.map +1 -0
- package/dist/index.cjs +23 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +23 -39
- package/dist/index.js.map +1 -1
- package/dist/style.css +83 -1
- package/package.json +19 -7
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# @blitheforge/media-library
|
|
2
2
|
|
|
3
|
-
Production-ready React media library with
|
|
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
|
|
38
|
+
### 1. Import the bundled CSS once
|
|
38
39
|
|
|
39
|
-
|
|
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
|
|
42
|
-
|
|
43
|
+
// React — app/layout.tsx, main.tsx, etc.
|
|
43
44
|
import "@blitheforge/media-library/styles.css";
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
52
|
+
Styles are scoped under `.bfml-root`, so they will **not** override your app's `hidden`, `flex`, `lg:block`, etc.
|
|
55
53
|
|
|
56
|
-
|
|
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.
|
|
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
|
-
- "
|
|
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
|
|
126
|
+
cd package && pnpm build
|
|
91
127
|
```
|
|
92
128
|
|
|
93
129
|
---
|
package/dist/client.cjs
ADDED
|
@@ -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 };
|
package/dist/client.d.ts
ADDED
|
@@ -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":[]}
|