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 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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uplofile",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Composable file upload component for React.",
5
5
  "license": "MIT",
6
6
  "author": "Chris Josh <KristofaJosh>",