@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 CHANGED
@@ -1,3 +1,123 @@
1
- # Temporary Holding Version
1
+ # @medialit/react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ });
@@ -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 };
@@ -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
+ };
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }