@vryzel/file-next-headless 0.4.0 → 0.4.2
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 +166 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,18 +1,176 @@
|
|
|
1
1
|
# @vryzel/file-next-headless
|
|
2
2
|
|
|
3
|
-
Headless React hooks for
|
|
3
|
+
Headless React hooks for file-next. No UI, no Tailwind, no Radix. You inject server callbacks; the hook owns state.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
pnpm add @vryzel/file-next-headless
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Peer: `react`, `@vryzel/file-next`. Safe in Client Components — it never imports `server-only`.
|
|
10
|
+
|
|
11
|
+
## Quick path
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
"use client";
|
|
15
|
+
import { useFileBrowser } from "@vryzel/file-next-headless";
|
|
16
|
+
import { asTenantId } from "@vryzel/file-next";
|
|
17
|
+
import { listFiles } from "./actions"; // "use server"
|
|
18
|
+
|
|
19
|
+
export function Folder({ parentId }: { parentId: string | null }) {
|
|
20
|
+
const { status, files, error, refetch } = useFileBrowser({
|
|
21
|
+
listFiles,
|
|
22
|
+
tenantId: asTenantId("acme"),
|
|
23
|
+
parentId,
|
|
24
|
+
autoFetch: true,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (status === "loading") return <p>Loading…</p>;
|
|
28
|
+
if (status === "error") return <p>{error?.message}</p>;
|
|
29
|
+
return (
|
|
30
|
+
<ul>
|
|
31
|
+
{files.map((f) => (
|
|
32
|
+
<li key={f.id}>{f.name}</li>
|
|
33
|
+
))}
|
|
34
|
+
</ul>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`listFiles` must be a **stable** function (module-level or `useCallback`). A new arrow every render refetches.
|
|
40
|
+
|
|
41
|
+
## Hooks
|
|
42
|
+
|
|
43
|
+
| Hook | Use when |
|
|
10
44
|
|---|---|
|
|
11
|
-
| `useFileBrowser` |
|
|
12
|
-
| `useFileExplorer` |
|
|
13
|
-
| `useUploader` | XHR upload with
|
|
45
|
+
| `useFileBrowser` | You need a list + loading/empty/error |
|
|
46
|
+
| `useFileExplorer` | List + view mode + selection + drag |
|
|
47
|
+
| `useUploader` | XHR upload with `%` and cancel |
|
|
14
48
|
| `useFileActions` | Optimistic delete/move/copy/rename |
|
|
15
|
-
| `useFileUrl` |
|
|
16
|
-
| `useDownloadProgress` |
|
|
49
|
+
| `useFileUrl` | Resolve a presigned download URL |
|
|
50
|
+
| `useDownloadProgress` | Browser download with progress |
|
|
51
|
+
|
|
52
|
+
If you want the full explorer UI, use `@vryzel/file-next-ui` instead of assembling these yourself.
|
|
53
|
+
|
|
54
|
+
## Use cases
|
|
55
|
+
|
|
56
|
+
### 1. List a folder — `useFileBrowser`
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
const { status, files, error, refetch } = useFileBrowser({
|
|
60
|
+
listFiles, // (input) => Promise<Result<{ items }, FileSystemError>>
|
|
61
|
+
tenantId,
|
|
62
|
+
parentId, // null = root
|
|
63
|
+
autoFetch: true,
|
|
64
|
+
limit: 50,
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Statuses: `idle` → `loading` → `success` | `error`.
|
|
69
|
+
|
|
70
|
+
### 2. Custom explorer chrome — `useFileExplorer`
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
const explorer = useFileExplorer({
|
|
74
|
+
listFiles,
|
|
75
|
+
tenantId,
|
|
76
|
+
parentId,
|
|
77
|
+
initialView: "list",
|
|
78
|
+
onMove: async ({ itemIds, destinationFolderId }) => {
|
|
79
|
+
await moveFiles({ itemIds, destinationFolderId });
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
explorer.view; // "list" | "grid"
|
|
84
|
+
explorer.setView("grid");
|
|
85
|
+
explorer.selectedId;
|
|
86
|
+
explorer.beginDrag(id);
|
|
87
|
+
explorer.commitDrop(destinationFolderId);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 3. Upload with progress — `useUploader`
|
|
91
|
+
|
|
92
|
+
Uses **XHR**, not `fetch`, so `progress` is real.
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
const uploader = useUploader({
|
|
96
|
+
requestUpload: async (file) => ({
|
|
97
|
+
url: `/api/upload?name=${encodeURIComponent(file.name)}`,
|
|
98
|
+
method: "PUT",
|
|
99
|
+
headers: { "Content-Type": file.type || "application/octet-stream" },
|
|
100
|
+
}),
|
|
101
|
+
confirmUpload: async () => {
|
|
102
|
+
await refetch();
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
<input
|
|
107
|
+
type="file"
|
|
108
|
+
onChange={(e) => {
|
|
109
|
+
const file = e.target.files?.[0];
|
|
110
|
+
if (!file) return;
|
|
111
|
+
uploader.upload({
|
|
112
|
+
name: file.name,
|
|
113
|
+
size: file.size,
|
|
114
|
+
type: file.type,
|
|
115
|
+
content: file,
|
|
116
|
+
});
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
<button type="button" onClick={uploader.cancel} disabled={uploader.status !== "uploading"}>
|
|
120
|
+
Cancel {uploader.progress}%
|
|
121
|
+
</button>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
For a queue that survives leaving the page, use `UploadQueueProvider` from `@vryzel/file-next-ui`.
|
|
125
|
+
|
|
126
|
+
### 4. Optimistic mutations — `useFileActions`
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
const [files, setFiles] = useState(listed);
|
|
130
|
+
const { deleteFile, moveFile, copyFile, renameFile, isPending, error } =
|
|
131
|
+
useFileActions({
|
|
132
|
+
files,
|
|
133
|
+
setFiles,
|
|
134
|
+
actions: {
|
|
135
|
+
deleteFile: (input) => deleteFileAction(input),
|
|
136
|
+
moveFile: (input) => moveFileAction(input),
|
|
137
|
+
copyFile: (input) => copyFileAction(input),
|
|
138
|
+
renameFile: async (id, newName) => {
|
|
139
|
+
await moveFileAction({ id, newParentId: parentId, newName });
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
On failure the previous list is restored.
|
|
146
|
+
|
|
147
|
+
### 5. Preview / download URL — `useFileUrl`
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
const { url, status } = useFileUrl({
|
|
151
|
+
getDownloadUrl: ({ key }) => getDownloadUrlAction({ key }),
|
|
152
|
+
key: file.s3Key,
|
|
153
|
+
});
|
|
154
|
+
return url ? <img src={url} alt={file.name} /> : null;
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 6. Download with progress — `useDownloadProgress`
|
|
158
|
+
|
|
159
|
+
Starts when `url` is set. Status: `idle` | `loading` | `success` | `error` | `aborted`.
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
const dl = useDownloadProgress({ url: signedUrl });
|
|
163
|
+
<p>{dl.status === "loading" ? `${dl.progress}%` : dl.status}</p>
|
|
164
|
+
<button type="button" onClick={dl.cancel}>Cancel</button>
|
|
165
|
+
{dl.blob ? <a href={URL.createObjectURL(dl.blob)} download>Save</a> : null}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Contract
|
|
169
|
+
|
|
170
|
+
Callbacks return the same `Result` as the server:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
|
|
174
|
+
```
|
|
17
175
|
|
|
18
|
-
|
|
176
|
+
Do not import `@vryzel/file-next/server` from a file that also imports these hooks.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vryzel/file-next-headless",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Headless React hooks for file-next: behavior-only state machines for file browsers, uploaders, file actions, presigned URL resolution, and download progress. No UI, no Tailwind, no Radix. State is local per-hook (useReducer); server actions are dependency-injected.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"react-dom": "^18.3.1",
|
|
42
42
|
"tsup": "^8.3.5",
|
|
43
43
|
"vitest": "^2.1.5",
|
|
44
|
-
"@vryzel/file-next": "0.4.
|
|
44
|
+
"@vryzel/file-next": "0.4.2"
|
|
45
45
|
},
|
|
46
46
|
"keywords": [
|
|
47
47
|
"react",
|