uplofile 0.1.0 → 0.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 +230 -0
- package/dist/output.css +4 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
### Uplofile
|
|
2
|
+
|
|
3
|
+
Composable file upload component for React. Build your own UI with small, accessible primitives: context-driven Root, Trigger, Dropzone, Preview, and helpers for cancel/retry/remove — with progress, drag-and-drop, and a hidden input for form posts.
|
|
4
|
+
|
|
5
|
+
- React 16+ compatible
|
|
6
|
+
- Drag & drop, click-to-upload, and render-props APIs
|
|
7
|
+
- Progress, cancel, retry, and optimistic/strict remove flows
|
|
8
|
+
- Hidden input with JSON of successful uploads for regular form submits
|
|
9
|
+
- Style however you like
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Installation
|
|
14
|
+
|
|
15
|
+
- npm: npm install uplofile
|
|
16
|
+
- yarn: yarn add uplofile
|
|
17
|
+
- pnpm: pnpm add uplofile
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
### Quick start
|
|
22
|
+
|
|
23
|
+
Client component (e.g. in Next.js add "use client"):
|
|
24
|
+
|
|
25
|
+
### 1) Minimal usage
|
|
26
|
+
The smallest working setup with a single button that opens the file picker.
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
"use client";
|
|
30
|
+
|
|
31
|
+
import * as FileUploader from "uplofile";
|
|
32
|
+
|
|
33
|
+
export default function Basic() {
|
|
34
|
+
return (
|
|
35
|
+
<FileUploader.Root upload={async (file) => ({ url: URL.createObjectURL(file) })}>
|
|
36
|
+
<FileUploader.Trigger>
|
|
37
|
+
<button type="button">Select file</button>
|
|
38
|
+
</FileUploader.Trigger>
|
|
39
|
+
</FileUploader.Root>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- Root sets up context and takes an `upload(file, signal, setProgress?)` function.
|
|
45
|
+
- Trigger opens the native file dialog.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### 2) Multiple files and a hidden input (form friendly)
|
|
50
|
+
Add `multiple` and a `name` so successful uploads are available as JSON in a hidden input for regular form posts.
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<FileUploader.Root multiple name="images" upload={upload}>
|
|
54
|
+
<FileUploader.Trigger>
|
|
55
|
+
<button type="button">Select images</button>
|
|
56
|
+
</FileUploader.Trigger>
|
|
57
|
+
<FileUploader.HiddenInput />
|
|
58
|
+
</FileUploader.Root>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- HiddenInput contains `[ { name, url, id? } ]` of successful uploads.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
### 3) Providing a real upload function
|
|
66
|
+
Use fetch for a simple upload, or XHR to report progress. You can keep this in a separate module.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import type { UploadResult } from "uplofile";
|
|
70
|
+
|
|
71
|
+
export function makeFetchUploader(endpoint: string, fieldName = "file") {
|
|
72
|
+
return async function upload(file: File, signal: AbortSignal, setProgress?: (pct: number) => void): Promise<UploadResult> {
|
|
73
|
+
if (setProgress) {
|
|
74
|
+
// XHR branch for progress
|
|
75
|
+
const form = new FormData();
|
|
76
|
+
form.append(fieldName, file);
|
|
77
|
+
const xhr = new XMLHttpRequest();
|
|
78
|
+
const p = new Promise<UploadResult>((resolve, reject) => {
|
|
79
|
+
xhr.upload.onprogress = (evt) => {
|
|
80
|
+
if (!evt.lengthComputable) return;
|
|
81
|
+
setProgress(Math.round((evt.loaded / evt.total) * 100));
|
|
82
|
+
};
|
|
83
|
+
xhr.onload = () => {
|
|
84
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
85
|
+
try { const json = JSON.parse(xhr.responseText); resolve({ url: json.url, id: json.id }); }
|
|
86
|
+
catch { resolve({ url: xhr.responseText }); }
|
|
87
|
+
} else reject(new Error(`Upload failed (${xhr.status})`));
|
|
88
|
+
};
|
|
89
|
+
xhr.onerror = () => reject(new Error("Network error"));
|
|
90
|
+
xhr.onabort = () => reject(new DOMException("Aborted", "AbortError"));
|
|
91
|
+
xhr.open("POST", endpoint);
|
|
92
|
+
xhr.send(form);
|
|
93
|
+
});
|
|
94
|
+
signal.addEventListener("abort", () => xhr.abort());
|
|
95
|
+
return p;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Simple fetch (no progress)
|
|
99
|
+
const form = new FormData();
|
|
100
|
+
form.append(fieldName, file);
|
|
101
|
+
const res = await fetch(endpoint, { method: "POST", body: form, signal });
|
|
102
|
+
if (!res.ok) throw new Error(`Upload failed (${res.status})`);
|
|
103
|
+
try { const json = await res.json(); return { url: json.url, id: json.id }; }
|
|
104
|
+
catch { const text = await res.text(); return { url: text }; }
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const upload = makeFetchUploader("/api/upload");
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### 4) A nicer trigger with render props
|
|
114
|
+
Show live progress and counts without building a full preview.
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
<FileUploader.Trigger
|
|
118
|
+
render={({ isUploading, totalProgress, items }) => (
|
|
119
|
+
<button type="button" data-loading={isUploading || undefined}>
|
|
120
|
+
{isUploading ? `Uploading ${totalProgress ?? 0}%` : "Select images"}
|
|
121
|
+
<span> ({items.length})</span>
|
|
122
|
+
</button>
|
|
123
|
+
)}
|
|
124
|
+
/>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- `isUploading`, `totalProgress`, and `items` come from context.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### 5) Custom preview UI with actions
|
|
132
|
+
Render thumbnails, a progress bar, and actions like cancel/retry/remove.
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
<FileUploader.Preview
|
|
136
|
+
render={({ items, actions }) => (
|
|
137
|
+
<div className="grid">
|
|
138
|
+
{items.map((item) => (
|
|
139
|
+
<div key={item.uid} data-state={item.status}>
|
|
140
|
+
<img src={item.previewUrl || item.url} alt={item.name} />
|
|
141
|
+
|
|
142
|
+
{item.status === "uploading" && (
|
|
143
|
+
<div>
|
|
144
|
+
<div style={{ width: `${Math.max(0, Math.min(100, item.progress ?? 0))}%` }} />
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{item.status === "error" && <div>{item.error ?? "Upload failed"}</div>}
|
|
149
|
+
|
|
150
|
+
<div>
|
|
151
|
+
{item.status === "uploading" && (
|
|
152
|
+
<button type="button" onClick={() => actions.cancel(item.uid)}>Cancel</button>
|
|
153
|
+
)}
|
|
154
|
+
{(item.status === "error" || item.status === "canceled") && (
|
|
155
|
+
<button type="button" onClick={() => actions.retry(item.uid)}>Retry</button>
|
|
156
|
+
)}
|
|
157
|
+
<button type="button" onClick={() => actions.remove(item.uid)}>Remove</button>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
))}
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
/>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- `item` has fields like `uid, name, url?, previewUrl?, status, progress?, error?`.
|
|
167
|
+
- `actions` exposes `cancel(uid)`, `retry(uid)`, `remove(uid)`.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### 6) Drag-and-drop (Dropzone)
|
|
172
|
+
If your package includes a `Dropzone` primitive, surface it here. If not, you can skip this section.
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
<FileUploader.Dropzone>
|
|
176
|
+
<div>Drop files here or click to upload</div>
|
|
177
|
+
</FileUploader.Dropzone>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
- Works alongside `Trigger`.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### 7) Putting it together (complete example)
|
|
185
|
+
A compact, end-to-end example using multiple files, custom trigger, preview, and a hidden input for form submit.
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
"use client";
|
|
189
|
+
import "uplofile/output.css";
|
|
190
|
+
import * as FileUploader from "uplofile";
|
|
191
|
+
import { upload } from "./upload"; // from step 3
|
|
192
|
+
|
|
193
|
+
export default function Example() {
|
|
194
|
+
return (
|
|
195
|
+
<form className="max-w-md">
|
|
196
|
+
<FileUploader.Root multiple name="images" upload={upload}>
|
|
197
|
+
<FileUploader.Trigger
|
|
198
|
+
render={({ isUploading, totalProgress, items }) => (
|
|
199
|
+
<button type="button" data-loading={isUploading || undefined}>
|
|
200
|
+
{isUploading ? `Uploading ${totalProgress ?? 0}%` : "Select Images"}
|
|
201
|
+
<span> ({items.length})</span>
|
|
202
|
+
</button>
|
|
203
|
+
)}
|
|
204
|
+
/>
|
|
205
|
+
|
|
206
|
+
<FileUploader.Preview /* custom UI as in step 5 */ />
|
|
207
|
+
<FileUploader.HiddenInput />
|
|
208
|
+
</FileUploader.Root>
|
|
209
|
+
|
|
210
|
+
<button type="submit">Submit</button>
|
|
211
|
+
</form>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
### Styling
|
|
219
|
+
|
|
220
|
+
- Bring your own styles or Tailwind. The package includes a tiny CSS build with utility classes used by the default Preview.
|
|
221
|
+
- Import "uplofile/output.css".
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
### FAQ
|
|
226
|
+
|
|
227
|
+
- Can I limit the number of files? Use maxCount on Root.
|
|
228
|
+
- Can I prehydrate already-uploaded files? Pass initial=[{ id?, uid?, name, url }]. They’ll be shown as done and included in HiddenInput.
|
|
229
|
+
- How do I do strict server-side delete? Set removeMode="strict" and implement onRemove(item, signal).
|
|
230
|
+
- Do I have to use Tailwind? No. The components are unstyled besides minimal examples; you can render your own UI with the render props and actions.
|
package/dist/output.css
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
--color-black: #000;
|
|
13
13
|
--color-white: #fff;
|
|
14
14
|
--spacing: 0.25rem;
|
|
15
|
+
--container-md: 28rem;
|
|
15
16
|
--text-xs: 0.75rem;
|
|
16
17
|
--text-xs--line-height: calc(1 / 0.75);
|
|
17
18
|
--radius-xl: 0.75rem;
|
|
@@ -213,6 +214,9 @@
|
|
|
213
214
|
.w-full {
|
|
214
215
|
width: 100%;
|
|
215
216
|
}
|
|
217
|
+
.max-w-md {
|
|
218
|
+
max-width: var(--container-md);
|
|
219
|
+
}
|
|
216
220
|
.grid-cols-2 {
|
|
217
221
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
218
222
|
}
|