@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.
Files changed (2) hide show
  1. package/README.md +166 -8
  2. 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 `@vryzel/file-next`. No UI, no Tailwind, no Radix. Server work is injected as callbacks.
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
- | Hook | Role |
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` | List + loading/empty/error |
12
- | `useFileExplorer` | View mode, selection, drag on top of the list |
13
- | `useUploader` | XHR upload with progress and cancel |
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` | Presigned download URL |
16
- | `useDownloadProgress` | Fetch download with progress |
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
- Public on npmjs. See the repository README.
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.0",
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.0"
44
+ "@vryzel/file-next": "0.4.2"
45
45
  },
46
46
  "keywords": [
47
47
  "react",