@medialit/react 0.0.0-stage → 0.1.0
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 +122 -2
- package/dist/index.cjs +385 -0
- package/dist/index.d.cts +53 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.mjs +363 -0
- package/dist/styles.css +222 -0
- package/package.json +56 -4
package/README.md
CHANGED
|
@@ -1,3 +1,123 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @medialit/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A drop-in upload component and hook for [MediaLit](https://medialit.cloud). Files go straight from the browser to MediaLit over the resumable tus protocol, so your API key stays on your server.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @medialit/react medialit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 1. Add a signature route
|
|
12
|
+
|
|
13
|
+
The browser needs a short-lived upload signature from your backend. With Next.js:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// app/api/medialit/signature/route.ts
|
|
17
|
+
import { createSignatureHandler } from "medialit";
|
|
18
|
+
import { getSession } from "@/lib/auth"; // your app's auth
|
|
19
|
+
|
|
20
|
+
export const POST = createSignatureHandler({
|
|
21
|
+
// Reads MEDIALIT_API_KEY and MEDIALIT_ENDPOINT from the environment.
|
|
22
|
+
// Anyone who passes this check can upload to your MediaLit app.
|
|
23
|
+
authorize: async (request) => !!(await getSession(request)),
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Return `{ group: "..." }` from `authorize` to put the files in a group.
|
|
28
|
+
|
|
29
|
+
## 2. Render the uploader
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
"use client";
|
|
33
|
+
|
|
34
|
+
import { MediaLitUploader } from "@medialit/react";
|
|
35
|
+
import "@medialit/react/styles.css";
|
|
36
|
+
|
|
37
|
+
export function AvatarUpload() {
|
|
38
|
+
return (
|
|
39
|
+
<MediaLitUploader
|
|
40
|
+
signatureEndpoint="/api/medialit/signature"
|
|
41
|
+
access="public"
|
|
42
|
+
accept="image/*"
|
|
43
|
+
maxFileSize={5 * 1024 * 1024}
|
|
44
|
+
onUploadComplete={(media) => saveAvatar(media.mediaId)}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 3. Seal the upload
|
|
51
|
+
|
|
52
|
+
Uploads are temporary and MediaLit deletes them after a while unless they are sealed. Seal on your server when the user saves, so files from abandoned forms are cleaned up for you:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { MediaLit } from "medialit";
|
|
56
|
+
|
|
57
|
+
await new MediaLit().seal(mediaId);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Props
|
|
61
|
+
|
|
62
|
+
| Prop | Type | Description |
|
|
63
|
+
| ------------------- | ---------------------------------------- | -------------------------------------------------------- |
|
|
64
|
+
| `signatureEndpoint` | `string` | Your route that returns `{ signature, endpoint }`. |
|
|
65
|
+
| `getSignature` | `() => Promise<{ signature, endpoint }>` | Use instead of `signatureEndpoint` to fetch it yourself. |
|
|
66
|
+
| `access` | `"public" \| "private"` | Defaults to `private`. |
|
|
67
|
+
| `caption` | `string` | Stored with the file. |
|
|
68
|
+
| `accept` | `string` | Same format as `<input accept>`. Also checked for drops. |
|
|
69
|
+
| `maxFileSize` | `number` | Bytes. MediaLit enforces your plan's limit as well. |
|
|
70
|
+
| `chunkSize` | `number` | Bytes per request. Defaults to one request. |
|
|
71
|
+
| `onUploadComplete` | `(media) => void` | Called with the uploaded media. |
|
|
72
|
+
| `onUploadError` | `(error) => void` | Called with a `MediaLitUploadError`. |
|
|
73
|
+
| `labels` | `Partial<MediaLitUploaderLabels>` | Replace any text, for example to translate it. |
|
|
74
|
+
| `disabled` | `boolean` | |
|
|
75
|
+
| `className` | `string` | Added to the root element. |
|
|
76
|
+
|
|
77
|
+
## Styling
|
|
78
|
+
|
|
79
|
+
`styles.css` is optional. It styles the `.medialit-uploader` classes and follows the system's light or dark mode. Override its variables to match your app:
|
|
80
|
+
|
|
81
|
+
```css
|
|
82
|
+
.medialit-uploader {
|
|
83
|
+
--medialit-accent: #0f766e;
|
|
84
|
+
--medialit-radius: 6px;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The root element has `data-status` set to `idle`, `uploading`, `success`, or `error`.
|
|
89
|
+
|
|
90
|
+
## Build your own UI
|
|
91
|
+
|
|
92
|
+
`useMediaLitUpload` has the same upload logic without any markup:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import { useMediaLitUpload } from "@medialit/react";
|
|
96
|
+
|
|
97
|
+
function UploadButton() {
|
|
98
|
+
const { upload, cancel, status, progress, error } = useMediaLitUpload({
|
|
99
|
+
signatureEndpoint: "/api/medialit/signature",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (status === "uploading") {
|
|
103
|
+
return (
|
|
104
|
+
<button onClick={cancel}>Cancel ({Math.round(progress)}%)</button>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return (
|
|
108
|
+
<>
|
|
109
|
+
<input
|
|
110
|
+
type="file"
|
|
111
|
+
onChange={(e) =>
|
|
112
|
+
e.target.files?.[0] && upload(e.target.files[0])
|
|
113
|
+
}
|
|
114
|
+
/>
|
|
115
|
+
{error && <p>{error.message}</p>}
|
|
116
|
+
</>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`upload(file)` resolves to the media, or `null` if the upload failed or was cancelled. It never rejects.
|
|
122
|
+
|
|
123
|
+
For a framework other than React, use [`@medialit/uploader`](../uploader).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
MediaLitUploadError: () => import_uploader2.MediaLitUploadError,
|
|
25
|
+
MediaLitUploader: () => MediaLitUploader,
|
|
26
|
+
isUploadAbortedError: () => import_uploader2.isUploadAbortedError,
|
|
27
|
+
uploadFile: () => import_uploader2.uploadFile,
|
|
28
|
+
useMediaLitUpload: () => useMediaLitUpload
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/medialit-uploader.tsx
|
|
33
|
+
var import_react2 = require("react");
|
|
34
|
+
|
|
35
|
+
// src/use-medialit-upload.ts
|
|
36
|
+
var import_react = require("react");
|
|
37
|
+
var import_uploader = require("@medialit/uploader");
|
|
38
|
+
var idle = {
|
|
39
|
+
status: "idle",
|
|
40
|
+
progress: 0,
|
|
41
|
+
file: null,
|
|
42
|
+
media: null,
|
|
43
|
+
error: null
|
|
44
|
+
};
|
|
45
|
+
function useMediaLitUpload(options) {
|
|
46
|
+
const [state, setState] = (0, import_react.useState)(idle);
|
|
47
|
+
const optionsRef = (0, import_react.useRef)(options);
|
|
48
|
+
const controllerRef = (0, import_react.useRef)(null);
|
|
49
|
+
(0, import_react.useEffect)(() => {
|
|
50
|
+
optionsRef.current = options;
|
|
51
|
+
});
|
|
52
|
+
(0, import_react.useEffect)(() => () => controllerRef.current?.abort(), []);
|
|
53
|
+
const upload = (0, import_react.useCallback)(
|
|
54
|
+
async (file, overrides = {}) => {
|
|
55
|
+
controllerRef.current?.abort();
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
controllerRef.current = controller;
|
|
58
|
+
const isCurrent = () => controllerRef.current === controller;
|
|
59
|
+
const { onUploadComplete, onUploadError, ...uploadOptions } = {
|
|
60
|
+
...optionsRef.current,
|
|
61
|
+
...overrides
|
|
62
|
+
};
|
|
63
|
+
setState({ ...idle, status: "uploading", file });
|
|
64
|
+
try {
|
|
65
|
+
const media = await (0, import_uploader.uploadFile)(file, {
|
|
66
|
+
...uploadOptions,
|
|
67
|
+
signal: controller.signal,
|
|
68
|
+
onProgress: ({ percentage }) => {
|
|
69
|
+
if (isCurrent()) {
|
|
70
|
+
setState((s) => ({ ...s, progress: percentage }));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
if (!isCurrent()) return null;
|
|
75
|
+
controllerRef.current = null;
|
|
76
|
+
setState({
|
|
77
|
+
...idle,
|
|
78
|
+
status: "success",
|
|
79
|
+
progress: 100,
|
|
80
|
+
file,
|
|
81
|
+
media
|
|
82
|
+
});
|
|
83
|
+
onUploadComplete?.(media);
|
|
84
|
+
return media;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if ((0, import_uploader.isUploadAbortedError)(err) || !isCurrent()) return null;
|
|
87
|
+
controllerRef.current = null;
|
|
88
|
+
const error = err instanceof import_uploader.MediaLitUploadError ? err : new import_uploader.MediaLitUploadError(
|
|
89
|
+
err instanceof Error ? err.message : "Upload failed",
|
|
90
|
+
{ cause: err }
|
|
91
|
+
);
|
|
92
|
+
setState((s) => ({ ...s, status: "error", error }));
|
|
93
|
+
onUploadError?.(error);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
[]
|
|
98
|
+
);
|
|
99
|
+
const cancel = (0, import_react.useCallback)(() => {
|
|
100
|
+
controllerRef.current?.abort();
|
|
101
|
+
controllerRef.current = null;
|
|
102
|
+
setState(idle);
|
|
103
|
+
}, []);
|
|
104
|
+
return { ...state, upload, cancel, reset: cancel };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/files.ts
|
|
108
|
+
function matchesAccept(file, accept) {
|
|
109
|
+
const rules = (accept || "").split(",").map((rule) => rule.trim().toLowerCase()).filter(Boolean);
|
|
110
|
+
if (!rules.length) return true;
|
|
111
|
+
const name = file.name.toLowerCase();
|
|
112
|
+
const type = file.type.toLowerCase();
|
|
113
|
+
return rules.some((rule) => {
|
|
114
|
+
if (rule.startsWith(".")) return name.endsWith(rule);
|
|
115
|
+
if (rule.endsWith("/*")) return type.startsWith(rule.slice(0, -1));
|
|
116
|
+
return type === rule;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function formatBytes(bytes) {
|
|
120
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
121
|
+
let value = bytes;
|
|
122
|
+
let unit = 0;
|
|
123
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
124
|
+
value /= 1024;
|
|
125
|
+
unit++;
|
|
126
|
+
}
|
|
127
|
+
const rounded = value >= 10 || unit === 0 ? Math.round(value) : value.toFixed(1);
|
|
128
|
+
return `${rounded} ${units[unit]}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/medialit-uploader.tsx
|
|
132
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
133
|
+
var defaultLabels = {
|
|
134
|
+
prompt: "Drop a file here or click to browse",
|
|
135
|
+
cancel: "Cancel",
|
|
136
|
+
uploaded: "Uploaded",
|
|
137
|
+
uploadAnother: "Upload another",
|
|
138
|
+
retry: "Try again",
|
|
139
|
+
chooseAnother: "Choose another file",
|
|
140
|
+
invalidType: "This file type is not allowed",
|
|
141
|
+
tooLarge: (maxSize) => `Files must be ${maxSize} or smaller`
|
|
142
|
+
};
|
|
143
|
+
function MediaLitUploader({
|
|
144
|
+
accept,
|
|
145
|
+
maxFileSize,
|
|
146
|
+
disabled = false,
|
|
147
|
+
className,
|
|
148
|
+
labels: labelOverrides,
|
|
149
|
+
...uploadOptions
|
|
150
|
+
}) {
|
|
151
|
+
const labels = { ...defaultLabels, ...labelOverrides };
|
|
152
|
+
const { status, progress, file, media, error, upload, cancel, reset } = useMediaLitUpload(uploadOptions);
|
|
153
|
+
const inputRef = (0, import_react2.useRef)(null);
|
|
154
|
+
const [dragging, setDragging] = (0, import_react2.useState)(false);
|
|
155
|
+
const [invalid, setInvalid] = (0, import_react2.useState)("");
|
|
156
|
+
const hint = labels.hint ?? [accept, maxFileSize ? `Up to ${formatBytes(maxFileSize)}` : ""].filter(Boolean).join(" \xB7 ");
|
|
157
|
+
const start = (selected) => {
|
|
158
|
+
if (!selected || disabled) return;
|
|
159
|
+
if (!matchesAccept(selected, accept)) {
|
|
160
|
+
return setInvalid(labels.invalidType);
|
|
161
|
+
}
|
|
162
|
+
if (maxFileSize && selected.size > maxFileSize) {
|
|
163
|
+
return setInvalid(labels.tooLarge(formatBytes(maxFileSize)));
|
|
164
|
+
}
|
|
165
|
+
setInvalid("");
|
|
166
|
+
upload(selected);
|
|
167
|
+
};
|
|
168
|
+
const onDrop = (e) => {
|
|
169
|
+
e.preventDefault();
|
|
170
|
+
setDragging(false);
|
|
171
|
+
start(e.dataTransfer.files[0]);
|
|
172
|
+
};
|
|
173
|
+
const chooseFile = () => {
|
|
174
|
+
reset();
|
|
175
|
+
inputRef.current?.click();
|
|
176
|
+
};
|
|
177
|
+
const percent = Math.round(progress);
|
|
178
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
179
|
+
"div",
|
|
180
|
+
{
|
|
181
|
+
className: ["medialit-uploader", className].filter(Boolean).join(" "),
|
|
182
|
+
"data-status": status,
|
|
183
|
+
children: [
|
|
184
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
185
|
+
"input",
|
|
186
|
+
{
|
|
187
|
+
ref: inputRef,
|
|
188
|
+
type: "file",
|
|
189
|
+
accept,
|
|
190
|
+
hidden: true,
|
|
191
|
+
disabled,
|
|
192
|
+
onChange: (e) => {
|
|
193
|
+
start(e.target.files?.[0]);
|
|
194
|
+
e.target.value = "";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
),
|
|
198
|
+
status === "idle" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
199
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
200
|
+
"button",
|
|
201
|
+
{
|
|
202
|
+
type: "button",
|
|
203
|
+
className: "medialit-uploader__dropzone",
|
|
204
|
+
"data-dragging": dragging || void 0,
|
|
205
|
+
disabled,
|
|
206
|
+
onClick: () => inputRef.current?.click(),
|
|
207
|
+
onDragOver: (e) => {
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
if (!disabled) setDragging(true);
|
|
210
|
+
},
|
|
211
|
+
onDragLeave: () => setDragging(false),
|
|
212
|
+
onDrop,
|
|
213
|
+
children: [
|
|
214
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(UploadIcon, {}),
|
|
215
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "medialit-uploader__prompt", children: labels.prompt }),
|
|
216
|
+
hint && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "medialit-uploader__hint", children: hint })
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
),
|
|
220
|
+
invalid && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "medialit-uploader__error", role: "alert", children: invalid })
|
|
221
|
+
] }),
|
|
222
|
+
status === "uploading" && file && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "medialit-uploader__panel", children: [
|
|
223
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "medialit-uploader__row", children: [
|
|
224
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "medialit-uploader__name", children: file.name }),
|
|
225
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "medialit-uploader__percent", children: [
|
|
226
|
+
percent,
|
|
227
|
+
"%"
|
|
228
|
+
] })
|
|
229
|
+
] }),
|
|
230
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
231
|
+
"div",
|
|
232
|
+
{
|
|
233
|
+
className: "medialit-uploader__bar",
|
|
234
|
+
role: "progressbar",
|
|
235
|
+
"aria-label": file.name,
|
|
236
|
+
"aria-valuemin": 0,
|
|
237
|
+
"aria-valuemax": 100,
|
|
238
|
+
"aria-valuenow": percent,
|
|
239
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
240
|
+
"div",
|
|
241
|
+
{
|
|
242
|
+
className: "medialit-uploader__fill",
|
|
243
|
+
style: { width: `${progress}%` }
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
),
|
|
248
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "medialit-uploader__actions", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
249
|
+
"button",
|
|
250
|
+
{
|
|
251
|
+
type: "button",
|
|
252
|
+
className: "medialit-uploader__button",
|
|
253
|
+
onClick: cancel,
|
|
254
|
+
children: labels.cancel
|
|
255
|
+
}
|
|
256
|
+
) })
|
|
257
|
+
] }),
|
|
258
|
+
status === "success" && media && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "medialit-uploader__panel", children: [
|
|
259
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "medialit-uploader__row", children: [
|
|
260
|
+
media.thumbnail ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
261
|
+
"img",
|
|
262
|
+
{
|
|
263
|
+
className: "medialit-uploader__thumbnail",
|
|
264
|
+
src: media.thumbnail,
|
|
265
|
+
alt: ""
|
|
266
|
+
}
|
|
267
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CheckIcon, {}),
|
|
268
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "medialit-uploader__details", children: [
|
|
269
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "medialit-uploader__name", children: media.originalFileName }),
|
|
270
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "medialit-uploader__hint", children: [
|
|
271
|
+
labels.uploaded,
|
|
272
|
+
" \xB7 ",
|
|
273
|
+
formatBytes(media.size)
|
|
274
|
+
] })
|
|
275
|
+
] })
|
|
276
|
+
] }),
|
|
277
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "medialit-uploader__actions", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
278
|
+
"button",
|
|
279
|
+
{
|
|
280
|
+
type: "button",
|
|
281
|
+
className: "medialit-uploader__button",
|
|
282
|
+
onClick: chooseFile,
|
|
283
|
+
disabled,
|
|
284
|
+
children: labels.uploadAnother
|
|
285
|
+
}
|
|
286
|
+
) })
|
|
287
|
+
] }),
|
|
288
|
+
status === "error" && file && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "medialit-uploader__panel", children: [
|
|
289
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "medialit-uploader__row", children: [
|
|
290
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertIcon, {}),
|
|
291
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "medialit-uploader__details", children: [
|
|
292
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "medialit-uploader__name", children: file.name }),
|
|
293
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
294
|
+
"span",
|
|
295
|
+
{
|
|
296
|
+
className: "medialit-uploader__error",
|
|
297
|
+
role: "alert",
|
|
298
|
+
children: error?.message
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
] })
|
|
302
|
+
] }),
|
|
303
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "medialit-uploader__actions", children: [
|
|
304
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
305
|
+
"button",
|
|
306
|
+
{
|
|
307
|
+
type: "button",
|
|
308
|
+
className: "medialit-uploader__button medialit-uploader__button--primary",
|
|
309
|
+
onClick: () => upload(file),
|
|
310
|
+
disabled,
|
|
311
|
+
children: labels.retry
|
|
312
|
+
}
|
|
313
|
+
),
|
|
314
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
315
|
+
"button",
|
|
316
|
+
{
|
|
317
|
+
type: "button",
|
|
318
|
+
className: "medialit-uploader__button",
|
|
319
|
+
onClick: chooseFile,
|
|
320
|
+
disabled,
|
|
321
|
+
children: labels.chooseAnother
|
|
322
|
+
}
|
|
323
|
+
)
|
|
324
|
+
] })
|
|
325
|
+
] })
|
|
326
|
+
]
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
var iconProps = {
|
|
331
|
+
width: 24,
|
|
332
|
+
height: 24,
|
|
333
|
+
viewBox: "0 0 24 24",
|
|
334
|
+
fill: "none",
|
|
335
|
+
stroke: "currentColor",
|
|
336
|
+
strokeWidth: 2,
|
|
337
|
+
strokeLinecap: "round",
|
|
338
|
+
strokeLinejoin: "round",
|
|
339
|
+
"aria-hidden": true
|
|
340
|
+
};
|
|
341
|
+
function UploadIcon() {
|
|
342
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { ...iconProps, className: "medialit-uploader__icon", children: [
|
|
343
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
|
|
344
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M17 8l-5-5-5 5" }),
|
|
345
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 3v12" })
|
|
346
|
+
] });
|
|
347
|
+
}
|
|
348
|
+
function CheckIcon() {
|
|
349
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
350
|
+
"svg",
|
|
351
|
+
{
|
|
352
|
+
...iconProps,
|
|
353
|
+
className: "medialit-uploader__icon medialit-uploader__icon--success",
|
|
354
|
+
children: [
|
|
355
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
|
|
356
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M8 12l3 3 5-6" })
|
|
357
|
+
]
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
function AlertIcon() {
|
|
362
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
363
|
+
"svg",
|
|
364
|
+
{
|
|
365
|
+
...iconProps,
|
|
366
|
+
className: "medialit-uploader__icon medialit-uploader__icon--error",
|
|
367
|
+
children: [
|
|
368
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
|
|
369
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 8v4" }),
|
|
370
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 16h.01" })
|
|
371
|
+
]
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/index.ts
|
|
377
|
+
var import_uploader2 = require("@medialit/uploader");
|
|
378
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
379
|
+
0 && (module.exports = {
|
|
380
|
+
MediaLitUploadError,
|
|
381
|
+
MediaLitUploader,
|
|
382
|
+
isUploadAbortedError,
|
|
383
|
+
uploadFile,
|
|
384
|
+
useMediaLitUpload
|
|
385
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { UploadFileOptions, UploadedMedia, MediaLitUploadError } from '@medialit/uploader';
|
|
3
|
+
export { Access, MediaLitUploadError, UploadProgress, UploadSignature, UploadedMedia, isUploadAbortedError, uploadFile } from '@medialit/uploader';
|
|
4
|
+
|
|
5
|
+
type UploadStatus = "idle" | "uploading" | "success" | "error";
|
|
6
|
+
interface UseMediaLitUploadOptions extends Omit<UploadFileOptions, "signal" | "onProgress" | "fileName"> {
|
|
7
|
+
onUploadComplete?: (media: UploadedMedia) => void;
|
|
8
|
+
onUploadError?: (error: MediaLitUploadError) => void;
|
|
9
|
+
}
|
|
10
|
+
interface UploadState {
|
|
11
|
+
status: UploadStatus;
|
|
12
|
+
/** 0 to 100. */
|
|
13
|
+
progress: number;
|
|
14
|
+
file: File | null;
|
|
15
|
+
media: UploadedMedia | null;
|
|
16
|
+
error: MediaLitUploadError | null;
|
|
17
|
+
}
|
|
18
|
+
declare function useMediaLitUpload(options: UseMediaLitUploadOptions): {
|
|
19
|
+
upload: (file: File, overrides?: Partial<UseMediaLitUploadOptions>) => Promise<UploadedMedia | null>;
|
|
20
|
+
cancel: () => void;
|
|
21
|
+
reset: () => void;
|
|
22
|
+
status: UploadStatus;
|
|
23
|
+
/** 0 to 100. */
|
|
24
|
+
progress: number;
|
|
25
|
+
file: File | null;
|
|
26
|
+
media: UploadedMedia | null;
|
|
27
|
+
error: MediaLitUploadError | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
interface MediaLitUploaderLabels {
|
|
31
|
+
prompt: string;
|
|
32
|
+
/** Shown under the prompt. Defaults to the accepted types and size limit. */
|
|
33
|
+
hint?: string;
|
|
34
|
+
cancel: string;
|
|
35
|
+
uploaded: string;
|
|
36
|
+
uploadAnother: string;
|
|
37
|
+
retry: string;
|
|
38
|
+
chooseAnother: string;
|
|
39
|
+
invalidType: string;
|
|
40
|
+
tooLarge: (maxSize: string) => string;
|
|
41
|
+
}
|
|
42
|
+
interface MediaLitUploaderProps extends UseMediaLitUploadOptions {
|
|
43
|
+
/** Same format as the `accept` attribute of `<input type="file">`. */
|
|
44
|
+
accept?: string;
|
|
45
|
+
/** Bytes. MediaLit enforces its own limit as well. */
|
|
46
|
+
maxFileSize?: number;
|
|
47
|
+
disabled?: boolean;
|
|
48
|
+
className?: string;
|
|
49
|
+
labels?: Partial<MediaLitUploaderLabels>;
|
|
50
|
+
}
|
|
51
|
+
declare function MediaLitUploader({ accept, maxFileSize, disabled, className, labels: labelOverrides, ...uploadOptions }: MediaLitUploaderProps): react_jsx_runtime.JSX.Element;
|
|
52
|
+
|
|
53
|
+
export { MediaLitUploader, type MediaLitUploaderLabels, type MediaLitUploaderProps, type UploadState, type UploadStatus, type UseMediaLitUploadOptions, useMediaLitUpload };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { UploadFileOptions, UploadedMedia, MediaLitUploadError } from '@medialit/uploader';
|
|
3
|
+
export { Access, MediaLitUploadError, UploadProgress, UploadSignature, UploadedMedia, isUploadAbortedError, uploadFile } from '@medialit/uploader';
|
|
4
|
+
|
|
5
|
+
type UploadStatus = "idle" | "uploading" | "success" | "error";
|
|
6
|
+
interface UseMediaLitUploadOptions extends Omit<UploadFileOptions, "signal" | "onProgress" | "fileName"> {
|
|
7
|
+
onUploadComplete?: (media: UploadedMedia) => void;
|
|
8
|
+
onUploadError?: (error: MediaLitUploadError) => void;
|
|
9
|
+
}
|
|
10
|
+
interface UploadState {
|
|
11
|
+
status: UploadStatus;
|
|
12
|
+
/** 0 to 100. */
|
|
13
|
+
progress: number;
|
|
14
|
+
file: File | null;
|
|
15
|
+
media: UploadedMedia | null;
|
|
16
|
+
error: MediaLitUploadError | null;
|
|
17
|
+
}
|
|
18
|
+
declare function useMediaLitUpload(options: UseMediaLitUploadOptions): {
|
|
19
|
+
upload: (file: File, overrides?: Partial<UseMediaLitUploadOptions>) => Promise<UploadedMedia | null>;
|
|
20
|
+
cancel: () => void;
|
|
21
|
+
reset: () => void;
|
|
22
|
+
status: UploadStatus;
|
|
23
|
+
/** 0 to 100. */
|
|
24
|
+
progress: number;
|
|
25
|
+
file: File | null;
|
|
26
|
+
media: UploadedMedia | null;
|
|
27
|
+
error: MediaLitUploadError | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
interface MediaLitUploaderLabels {
|
|
31
|
+
prompt: string;
|
|
32
|
+
/** Shown under the prompt. Defaults to the accepted types and size limit. */
|
|
33
|
+
hint?: string;
|
|
34
|
+
cancel: string;
|
|
35
|
+
uploaded: string;
|
|
36
|
+
uploadAnother: string;
|
|
37
|
+
retry: string;
|
|
38
|
+
chooseAnother: string;
|
|
39
|
+
invalidType: string;
|
|
40
|
+
tooLarge: (maxSize: string) => string;
|
|
41
|
+
}
|
|
42
|
+
interface MediaLitUploaderProps extends UseMediaLitUploadOptions {
|
|
43
|
+
/** Same format as the `accept` attribute of `<input type="file">`. */
|
|
44
|
+
accept?: string;
|
|
45
|
+
/** Bytes. MediaLit enforces its own limit as well. */
|
|
46
|
+
maxFileSize?: number;
|
|
47
|
+
disabled?: boolean;
|
|
48
|
+
className?: string;
|
|
49
|
+
labels?: Partial<MediaLitUploaderLabels>;
|
|
50
|
+
}
|
|
51
|
+
declare function MediaLitUploader({ accept, maxFileSize, disabled, className, labels: labelOverrides, ...uploadOptions }: MediaLitUploaderProps): react_jsx_runtime.JSX.Element;
|
|
52
|
+
|
|
53
|
+
export { MediaLitUploader, type MediaLitUploaderLabels, type MediaLitUploaderProps, type UploadState, type UploadStatus, type UseMediaLitUploadOptions, useMediaLitUpload };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/medialit-uploader.tsx
|
|
4
|
+
import { useRef as useRef2, useState as useState2 } from "react";
|
|
5
|
+
|
|
6
|
+
// src/use-medialit-upload.ts
|
|
7
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
8
|
+
import {
|
|
9
|
+
uploadFile,
|
|
10
|
+
isUploadAbortedError,
|
|
11
|
+
MediaLitUploadError
|
|
12
|
+
} from "@medialit/uploader";
|
|
13
|
+
var idle = {
|
|
14
|
+
status: "idle",
|
|
15
|
+
progress: 0,
|
|
16
|
+
file: null,
|
|
17
|
+
media: null,
|
|
18
|
+
error: null
|
|
19
|
+
};
|
|
20
|
+
function useMediaLitUpload(options) {
|
|
21
|
+
const [state, setState] = useState(idle);
|
|
22
|
+
const optionsRef = useRef(options);
|
|
23
|
+
const controllerRef = useRef(null);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
optionsRef.current = options;
|
|
26
|
+
});
|
|
27
|
+
useEffect(() => () => controllerRef.current?.abort(), []);
|
|
28
|
+
const upload = useCallback(
|
|
29
|
+
async (file, overrides = {}) => {
|
|
30
|
+
controllerRef.current?.abort();
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
controllerRef.current = controller;
|
|
33
|
+
const isCurrent = () => controllerRef.current === controller;
|
|
34
|
+
const { onUploadComplete, onUploadError, ...uploadOptions } = {
|
|
35
|
+
...optionsRef.current,
|
|
36
|
+
...overrides
|
|
37
|
+
};
|
|
38
|
+
setState({ ...idle, status: "uploading", file });
|
|
39
|
+
try {
|
|
40
|
+
const media = await uploadFile(file, {
|
|
41
|
+
...uploadOptions,
|
|
42
|
+
signal: controller.signal,
|
|
43
|
+
onProgress: ({ percentage }) => {
|
|
44
|
+
if (isCurrent()) {
|
|
45
|
+
setState((s) => ({ ...s, progress: percentage }));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
if (!isCurrent()) return null;
|
|
50
|
+
controllerRef.current = null;
|
|
51
|
+
setState({
|
|
52
|
+
...idle,
|
|
53
|
+
status: "success",
|
|
54
|
+
progress: 100,
|
|
55
|
+
file,
|
|
56
|
+
media
|
|
57
|
+
});
|
|
58
|
+
onUploadComplete?.(media);
|
|
59
|
+
return media;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (isUploadAbortedError(err) || !isCurrent()) return null;
|
|
62
|
+
controllerRef.current = null;
|
|
63
|
+
const error = err instanceof MediaLitUploadError ? err : new MediaLitUploadError(
|
|
64
|
+
err instanceof Error ? err.message : "Upload failed",
|
|
65
|
+
{ cause: err }
|
|
66
|
+
);
|
|
67
|
+
setState((s) => ({ ...s, status: "error", error }));
|
|
68
|
+
onUploadError?.(error);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
[]
|
|
73
|
+
);
|
|
74
|
+
const cancel = useCallback(() => {
|
|
75
|
+
controllerRef.current?.abort();
|
|
76
|
+
controllerRef.current = null;
|
|
77
|
+
setState(idle);
|
|
78
|
+
}, []);
|
|
79
|
+
return { ...state, upload, cancel, reset: cancel };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/files.ts
|
|
83
|
+
function matchesAccept(file, accept) {
|
|
84
|
+
const rules = (accept || "").split(",").map((rule) => rule.trim().toLowerCase()).filter(Boolean);
|
|
85
|
+
if (!rules.length) return true;
|
|
86
|
+
const name = file.name.toLowerCase();
|
|
87
|
+
const type = file.type.toLowerCase();
|
|
88
|
+
return rules.some((rule) => {
|
|
89
|
+
if (rule.startsWith(".")) return name.endsWith(rule);
|
|
90
|
+
if (rule.endsWith("/*")) return type.startsWith(rule.slice(0, -1));
|
|
91
|
+
return type === rule;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function formatBytes(bytes) {
|
|
95
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
96
|
+
let value = bytes;
|
|
97
|
+
let unit = 0;
|
|
98
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
99
|
+
value /= 1024;
|
|
100
|
+
unit++;
|
|
101
|
+
}
|
|
102
|
+
const rounded = value >= 10 || unit === 0 ? Math.round(value) : value.toFixed(1);
|
|
103
|
+
return `${rounded} ${units[unit]}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/medialit-uploader.tsx
|
|
107
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
108
|
+
var defaultLabels = {
|
|
109
|
+
prompt: "Drop a file here or click to browse",
|
|
110
|
+
cancel: "Cancel",
|
|
111
|
+
uploaded: "Uploaded",
|
|
112
|
+
uploadAnother: "Upload another",
|
|
113
|
+
retry: "Try again",
|
|
114
|
+
chooseAnother: "Choose another file",
|
|
115
|
+
invalidType: "This file type is not allowed",
|
|
116
|
+
tooLarge: (maxSize) => `Files must be ${maxSize} or smaller`
|
|
117
|
+
};
|
|
118
|
+
function MediaLitUploader({
|
|
119
|
+
accept,
|
|
120
|
+
maxFileSize,
|
|
121
|
+
disabled = false,
|
|
122
|
+
className,
|
|
123
|
+
labels: labelOverrides,
|
|
124
|
+
...uploadOptions
|
|
125
|
+
}) {
|
|
126
|
+
const labels = { ...defaultLabels, ...labelOverrides };
|
|
127
|
+
const { status, progress, file, media, error, upload, cancel, reset } = useMediaLitUpload(uploadOptions);
|
|
128
|
+
const inputRef = useRef2(null);
|
|
129
|
+
const [dragging, setDragging] = useState2(false);
|
|
130
|
+
const [invalid, setInvalid] = useState2("");
|
|
131
|
+
const hint = labels.hint ?? [accept, maxFileSize ? `Up to ${formatBytes(maxFileSize)}` : ""].filter(Boolean).join(" \xB7 ");
|
|
132
|
+
const start = (selected) => {
|
|
133
|
+
if (!selected || disabled) return;
|
|
134
|
+
if (!matchesAccept(selected, accept)) {
|
|
135
|
+
return setInvalid(labels.invalidType);
|
|
136
|
+
}
|
|
137
|
+
if (maxFileSize && selected.size > maxFileSize) {
|
|
138
|
+
return setInvalid(labels.tooLarge(formatBytes(maxFileSize)));
|
|
139
|
+
}
|
|
140
|
+
setInvalid("");
|
|
141
|
+
upload(selected);
|
|
142
|
+
};
|
|
143
|
+
const onDrop = (e) => {
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
setDragging(false);
|
|
146
|
+
start(e.dataTransfer.files[0]);
|
|
147
|
+
};
|
|
148
|
+
const chooseFile = () => {
|
|
149
|
+
reset();
|
|
150
|
+
inputRef.current?.click();
|
|
151
|
+
};
|
|
152
|
+
const percent = Math.round(progress);
|
|
153
|
+
return /* @__PURE__ */ jsxs(
|
|
154
|
+
"div",
|
|
155
|
+
{
|
|
156
|
+
className: ["medialit-uploader", className].filter(Boolean).join(" "),
|
|
157
|
+
"data-status": status,
|
|
158
|
+
children: [
|
|
159
|
+
/* @__PURE__ */ jsx(
|
|
160
|
+
"input",
|
|
161
|
+
{
|
|
162
|
+
ref: inputRef,
|
|
163
|
+
type: "file",
|
|
164
|
+
accept,
|
|
165
|
+
hidden: true,
|
|
166
|
+
disabled,
|
|
167
|
+
onChange: (e) => {
|
|
168
|
+
start(e.target.files?.[0]);
|
|
169
|
+
e.target.value = "";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
),
|
|
173
|
+
status === "idle" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
174
|
+
/* @__PURE__ */ jsxs(
|
|
175
|
+
"button",
|
|
176
|
+
{
|
|
177
|
+
type: "button",
|
|
178
|
+
className: "medialit-uploader__dropzone",
|
|
179
|
+
"data-dragging": dragging || void 0,
|
|
180
|
+
disabled,
|
|
181
|
+
onClick: () => inputRef.current?.click(),
|
|
182
|
+
onDragOver: (e) => {
|
|
183
|
+
e.preventDefault();
|
|
184
|
+
if (!disabled) setDragging(true);
|
|
185
|
+
},
|
|
186
|
+
onDragLeave: () => setDragging(false),
|
|
187
|
+
onDrop,
|
|
188
|
+
children: [
|
|
189
|
+
/* @__PURE__ */ jsx(UploadIcon, {}),
|
|
190
|
+
/* @__PURE__ */ jsx("span", { className: "medialit-uploader__prompt", children: labels.prompt }),
|
|
191
|
+
hint && /* @__PURE__ */ jsx("span", { className: "medialit-uploader__hint", children: hint })
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
),
|
|
195
|
+
invalid && /* @__PURE__ */ jsx("p", { className: "medialit-uploader__error", role: "alert", children: invalid })
|
|
196
|
+
] }),
|
|
197
|
+
status === "uploading" && file && /* @__PURE__ */ jsxs("div", { className: "medialit-uploader__panel", children: [
|
|
198
|
+
/* @__PURE__ */ jsxs("div", { className: "medialit-uploader__row", children: [
|
|
199
|
+
/* @__PURE__ */ jsx("span", { className: "medialit-uploader__name", children: file.name }),
|
|
200
|
+
/* @__PURE__ */ jsxs("span", { className: "medialit-uploader__percent", children: [
|
|
201
|
+
percent,
|
|
202
|
+
"%"
|
|
203
|
+
] })
|
|
204
|
+
] }),
|
|
205
|
+
/* @__PURE__ */ jsx(
|
|
206
|
+
"div",
|
|
207
|
+
{
|
|
208
|
+
className: "medialit-uploader__bar",
|
|
209
|
+
role: "progressbar",
|
|
210
|
+
"aria-label": file.name,
|
|
211
|
+
"aria-valuemin": 0,
|
|
212
|
+
"aria-valuemax": 100,
|
|
213
|
+
"aria-valuenow": percent,
|
|
214
|
+
children: /* @__PURE__ */ jsx(
|
|
215
|
+
"div",
|
|
216
|
+
{
|
|
217
|
+
className: "medialit-uploader__fill",
|
|
218
|
+
style: { width: `${progress}%` }
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
),
|
|
223
|
+
/* @__PURE__ */ jsx("div", { className: "medialit-uploader__actions", children: /* @__PURE__ */ jsx(
|
|
224
|
+
"button",
|
|
225
|
+
{
|
|
226
|
+
type: "button",
|
|
227
|
+
className: "medialit-uploader__button",
|
|
228
|
+
onClick: cancel,
|
|
229
|
+
children: labels.cancel
|
|
230
|
+
}
|
|
231
|
+
) })
|
|
232
|
+
] }),
|
|
233
|
+
status === "success" && media && /* @__PURE__ */ jsxs("div", { className: "medialit-uploader__panel", children: [
|
|
234
|
+
/* @__PURE__ */ jsxs("div", { className: "medialit-uploader__row", children: [
|
|
235
|
+
media.thumbnail ? /* @__PURE__ */ jsx(
|
|
236
|
+
"img",
|
|
237
|
+
{
|
|
238
|
+
className: "medialit-uploader__thumbnail",
|
|
239
|
+
src: media.thumbnail,
|
|
240
|
+
alt: ""
|
|
241
|
+
}
|
|
242
|
+
) : /* @__PURE__ */ jsx(CheckIcon, {}),
|
|
243
|
+
/* @__PURE__ */ jsxs("div", { className: "medialit-uploader__details", children: [
|
|
244
|
+
/* @__PURE__ */ jsx("span", { className: "medialit-uploader__name", children: media.originalFileName }),
|
|
245
|
+
/* @__PURE__ */ jsxs("span", { className: "medialit-uploader__hint", children: [
|
|
246
|
+
labels.uploaded,
|
|
247
|
+
" \xB7 ",
|
|
248
|
+
formatBytes(media.size)
|
|
249
|
+
] })
|
|
250
|
+
] })
|
|
251
|
+
] }),
|
|
252
|
+
/* @__PURE__ */ jsx("div", { className: "medialit-uploader__actions", children: /* @__PURE__ */ jsx(
|
|
253
|
+
"button",
|
|
254
|
+
{
|
|
255
|
+
type: "button",
|
|
256
|
+
className: "medialit-uploader__button",
|
|
257
|
+
onClick: chooseFile,
|
|
258
|
+
disabled,
|
|
259
|
+
children: labels.uploadAnother
|
|
260
|
+
}
|
|
261
|
+
) })
|
|
262
|
+
] }),
|
|
263
|
+
status === "error" && file && /* @__PURE__ */ jsxs("div", { className: "medialit-uploader__panel", children: [
|
|
264
|
+
/* @__PURE__ */ jsxs("div", { className: "medialit-uploader__row", children: [
|
|
265
|
+
/* @__PURE__ */ jsx(AlertIcon, {}),
|
|
266
|
+
/* @__PURE__ */ jsxs("div", { className: "medialit-uploader__details", children: [
|
|
267
|
+
/* @__PURE__ */ jsx("span", { className: "medialit-uploader__name", children: file.name }),
|
|
268
|
+
/* @__PURE__ */ jsx(
|
|
269
|
+
"span",
|
|
270
|
+
{
|
|
271
|
+
className: "medialit-uploader__error",
|
|
272
|
+
role: "alert",
|
|
273
|
+
children: error?.message
|
|
274
|
+
}
|
|
275
|
+
)
|
|
276
|
+
] })
|
|
277
|
+
] }),
|
|
278
|
+
/* @__PURE__ */ jsxs("div", { className: "medialit-uploader__actions", children: [
|
|
279
|
+
/* @__PURE__ */ jsx(
|
|
280
|
+
"button",
|
|
281
|
+
{
|
|
282
|
+
type: "button",
|
|
283
|
+
className: "medialit-uploader__button medialit-uploader__button--primary",
|
|
284
|
+
onClick: () => upload(file),
|
|
285
|
+
disabled,
|
|
286
|
+
children: labels.retry
|
|
287
|
+
}
|
|
288
|
+
),
|
|
289
|
+
/* @__PURE__ */ jsx(
|
|
290
|
+
"button",
|
|
291
|
+
{
|
|
292
|
+
type: "button",
|
|
293
|
+
className: "medialit-uploader__button",
|
|
294
|
+
onClick: chooseFile,
|
|
295
|
+
disabled,
|
|
296
|
+
children: labels.chooseAnother
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
] })
|
|
300
|
+
] })
|
|
301
|
+
]
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
var iconProps = {
|
|
306
|
+
width: 24,
|
|
307
|
+
height: 24,
|
|
308
|
+
viewBox: "0 0 24 24",
|
|
309
|
+
fill: "none",
|
|
310
|
+
stroke: "currentColor",
|
|
311
|
+
strokeWidth: 2,
|
|
312
|
+
strokeLinecap: "round",
|
|
313
|
+
strokeLinejoin: "round",
|
|
314
|
+
"aria-hidden": true
|
|
315
|
+
};
|
|
316
|
+
function UploadIcon() {
|
|
317
|
+
return /* @__PURE__ */ jsxs("svg", { ...iconProps, className: "medialit-uploader__icon", children: [
|
|
318
|
+
/* @__PURE__ */ jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
|
|
319
|
+
/* @__PURE__ */ jsx("path", { d: "M17 8l-5-5-5 5" }),
|
|
320
|
+
/* @__PURE__ */ jsx("path", { d: "M12 3v12" })
|
|
321
|
+
] });
|
|
322
|
+
}
|
|
323
|
+
function CheckIcon() {
|
|
324
|
+
return /* @__PURE__ */ jsxs(
|
|
325
|
+
"svg",
|
|
326
|
+
{
|
|
327
|
+
...iconProps,
|
|
328
|
+
className: "medialit-uploader__icon medialit-uploader__icon--success",
|
|
329
|
+
children: [
|
|
330
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
331
|
+
/* @__PURE__ */ jsx("path", { d: "M8 12l3 3 5-6" })
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
function AlertIcon() {
|
|
337
|
+
return /* @__PURE__ */ jsxs(
|
|
338
|
+
"svg",
|
|
339
|
+
{
|
|
340
|
+
...iconProps,
|
|
341
|
+
className: "medialit-uploader__icon medialit-uploader__icon--error",
|
|
342
|
+
children: [
|
|
343
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
344
|
+
/* @__PURE__ */ jsx("path", { d: "M12 8v4" }),
|
|
345
|
+
/* @__PURE__ */ jsx("path", { d: "M12 16h.01" })
|
|
346
|
+
]
|
|
347
|
+
}
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/index.ts
|
|
352
|
+
import {
|
|
353
|
+
uploadFile as uploadFile2,
|
|
354
|
+
MediaLitUploadError as MediaLitUploadError2,
|
|
355
|
+
isUploadAbortedError as isUploadAbortedError2
|
|
356
|
+
} from "@medialit/uploader";
|
|
357
|
+
export {
|
|
358
|
+
MediaLitUploadError2 as MediaLitUploadError,
|
|
359
|
+
MediaLitUploader,
|
|
360
|
+
isUploadAbortedError2 as isUploadAbortedError,
|
|
361
|
+
uploadFile2 as uploadFile,
|
|
362
|
+
useMediaLitUpload
|
|
363
|
+
};
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Override any of these variables on .medialit-uploader or an ancestor to
|
|
3
|
+
* match your app.
|
|
4
|
+
*/
|
|
5
|
+
.medialit-uploader {
|
|
6
|
+
--medialit-accent: #4f46e5;
|
|
7
|
+
--medialit-accent-contrast: #ffffff;
|
|
8
|
+
--medialit-text: #18181b;
|
|
9
|
+
--medialit-muted: #71717a;
|
|
10
|
+
--medialit-border: #d4d4d8;
|
|
11
|
+
--medialit-surface: #ffffff;
|
|
12
|
+
--medialit-surface-hover: #f4f4f5;
|
|
13
|
+
--medialit-track: #e4e4e7;
|
|
14
|
+
--medialit-success: #16a34a;
|
|
15
|
+
--medialit-error: #dc2626;
|
|
16
|
+
--medialit-radius: 12px;
|
|
17
|
+
--medialit-font: inherit;
|
|
18
|
+
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
width: 100%;
|
|
21
|
+
color: var(--medialit-text);
|
|
22
|
+
font-family: var(--medialit-font);
|
|
23
|
+
font-size: 14px;
|
|
24
|
+
line-height: 1.4;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@media (prefers-color-scheme: dark) {
|
|
28
|
+
.medialit-uploader {
|
|
29
|
+
--medialit-accent: #818cf8;
|
|
30
|
+
--medialit-accent-contrast: #18181b;
|
|
31
|
+
--medialit-text: #f4f4f5;
|
|
32
|
+
--medialit-muted: #a1a1aa;
|
|
33
|
+
--medialit-border: #3f3f46;
|
|
34
|
+
--medialit-surface: #18181b;
|
|
35
|
+
--medialit-surface-hover: #27272a;
|
|
36
|
+
--medialit-track: #3f3f46;
|
|
37
|
+
--medialit-success: #4ade80;
|
|
38
|
+
--medialit-error: #f87171;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.medialit-uploader *,
|
|
43
|
+
.medialit-uploader *::before,
|
|
44
|
+
.medialit-uploader *::after {
|
|
45
|
+
box-sizing: inherit;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.medialit-uploader__dropzone {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
align-items: center;
|
|
52
|
+
justify-content: center;
|
|
53
|
+
gap: 6px;
|
|
54
|
+
width: 100%;
|
|
55
|
+
min-height: 144px;
|
|
56
|
+
padding: 24px;
|
|
57
|
+
border: 2px dashed var(--medialit-border);
|
|
58
|
+
border-radius: var(--medialit-radius);
|
|
59
|
+
background: var(--medialit-surface);
|
|
60
|
+
color: inherit;
|
|
61
|
+
font: inherit;
|
|
62
|
+
text-align: center;
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
transition:
|
|
65
|
+
border-color 0.15s,
|
|
66
|
+
background-color 0.15s;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.medialit-uploader__dropzone:hover:not(:disabled),
|
|
70
|
+
.medialit-uploader__dropzone[data-dragging] {
|
|
71
|
+
border-color: var(--medialit-accent);
|
|
72
|
+
background: var(--medialit-surface-hover);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.medialit-uploader__dropzone:focus-visible,
|
|
76
|
+
.medialit-uploader__button:focus-visible {
|
|
77
|
+
outline: 2px solid var(--medialit-accent);
|
|
78
|
+
outline-offset: 2px;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.medialit-uploader__dropzone:disabled {
|
|
82
|
+
cursor: not-allowed;
|
|
83
|
+
opacity: 0.6;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.medialit-uploader__icon {
|
|
87
|
+
flex: none;
|
|
88
|
+
width: 24px;
|
|
89
|
+
height: 24px;
|
|
90
|
+
color: var(--medialit-muted);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.medialit-uploader__icon--success {
|
|
94
|
+
color: var(--medialit-success);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.medialit-uploader__icon--error {
|
|
98
|
+
color: var(--medialit-error);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.medialit-uploader__prompt {
|
|
102
|
+
font-weight: 500;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.medialit-uploader__hint {
|
|
106
|
+
color: var(--medialit-muted);
|
|
107
|
+
font-size: 13px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.medialit-uploader__error {
|
|
111
|
+
margin: 8px 0 0;
|
|
112
|
+
color: var(--medialit-error);
|
|
113
|
+
font-size: 13px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.medialit-uploader__details .medialit-uploader__error {
|
|
117
|
+
margin: 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.medialit-uploader__panel {
|
|
121
|
+
display: flex;
|
|
122
|
+
flex-direction: column;
|
|
123
|
+
gap: 12px;
|
|
124
|
+
padding: 16px;
|
|
125
|
+
border: 1px solid var(--medialit-border);
|
|
126
|
+
border-radius: var(--medialit-radius);
|
|
127
|
+
background: var(--medialit-surface);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.medialit-uploader__row {
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
gap: 12px;
|
|
134
|
+
min-width: 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.medialit-uploader__details {
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-direction: column;
|
|
140
|
+
gap: 2px;
|
|
141
|
+
min-width: 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.medialit-uploader__name {
|
|
145
|
+
flex: 1;
|
|
146
|
+
min-width: 0;
|
|
147
|
+
overflow: hidden;
|
|
148
|
+
font-weight: 500;
|
|
149
|
+
text-overflow: ellipsis;
|
|
150
|
+
white-space: nowrap;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.medialit-uploader__percent {
|
|
154
|
+
color: var(--medialit-muted);
|
|
155
|
+
font-variant-numeric: tabular-nums;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.medialit-uploader__bar {
|
|
159
|
+
height: 6px;
|
|
160
|
+
overflow: hidden;
|
|
161
|
+
border-radius: 999px;
|
|
162
|
+
background: var(--medialit-track);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.medialit-uploader__fill {
|
|
166
|
+
height: 100%;
|
|
167
|
+
border-radius: inherit;
|
|
168
|
+
background: var(--medialit-accent);
|
|
169
|
+
transition: width 0.2s;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.medialit-uploader__thumbnail {
|
|
173
|
+
flex: none;
|
|
174
|
+
width: 48px;
|
|
175
|
+
height: 48px;
|
|
176
|
+
border-radius: calc(var(--medialit-radius) / 2);
|
|
177
|
+
object-fit: cover;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.medialit-uploader__actions {
|
|
181
|
+
display: flex;
|
|
182
|
+
flex-wrap: wrap;
|
|
183
|
+
gap: 8px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.medialit-uploader__button {
|
|
187
|
+
padding: 6px 12px;
|
|
188
|
+
border: 1px solid var(--medialit-border);
|
|
189
|
+
border-radius: calc(var(--medialit-radius) / 2);
|
|
190
|
+
background: var(--medialit-surface);
|
|
191
|
+
color: inherit;
|
|
192
|
+
font: inherit;
|
|
193
|
+
font-weight: 500;
|
|
194
|
+
cursor: pointer;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.medialit-uploader__button:hover:not(:disabled) {
|
|
198
|
+
background: var(--medialit-surface-hover);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.medialit-uploader__button:disabled {
|
|
202
|
+
cursor: not-allowed;
|
|
203
|
+
opacity: 0.6;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.medialit-uploader__button--primary {
|
|
207
|
+
border-color: var(--medialit-accent);
|
|
208
|
+
background: var(--medialit-accent);
|
|
209
|
+
color: var(--medialit-accent-contrast);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.medialit-uploader__button--primary:hover:not(:disabled) {
|
|
213
|
+
background: var(--medialit-accent);
|
|
214
|
+
opacity: 0.9;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
@media (prefers-reduced-motion: reduce) {
|
|
218
|
+
.medialit-uploader__dropzone,
|
|
219
|
+
.medialit-uploader__fill {
|
|
220
|
+
transition: none;
|
|
221
|
+
}
|
|
222
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medialit/react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React hook and drop-in upload component for MediaLit",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "dist/index.mjs",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./styles.css": "./dist/styles.css"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": [
|
|
21
|
+
"**/*.css"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup && cp src/styles.css dist/styles.css",
|
|
25
|
+
"test": "node --import tsx --test '__tests__/**/*.test.ts'",
|
|
26
|
+
"prepublishOnly": "bun run build"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"medialit",
|
|
30
|
+
"react",
|
|
31
|
+
"upload",
|
|
32
|
+
"file upload",
|
|
33
|
+
"dropzone"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/codelitdev/medialit"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@medialit/uploader": "^0.1.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"react": ">=18"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^20.0.0",
|
|
48
|
+
"@types/react": "^19",
|
|
49
|
+
"react": "^19.0.0",
|
|
50
|
+
"tsup": "^8.0.0",
|
|
51
|
+
"tsx": "^4.19.3",
|
|
52
|
+
"typescript": "^5.0.0"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"packageManager": "bun@1.4.1"
|
|
58
|
+
}
|