@vryzel/file-next-ui 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 +196 -11
- package/dist/index.cjs +23 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +23 -21
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,39 +1,224 @@
|
|
|
1
1
|
# @vryzel/file-next-ui
|
|
2
2
|
|
|
3
|
-
Ready-
|
|
3
|
+
Ready-made file explorer for Next.js. Import `FileExplorer`, pass server callbacks, done. Or compose the pieces.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
pnpm add @vryzel/file-next-ui
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
Scan the package
|
|
9
|
+
Scan the package so Tailwind keeps the classes:
|
|
10
10
|
|
|
11
11
|
```js
|
|
12
|
-
|
|
12
|
+
// tailwind.config.ts
|
|
13
|
+
content: [
|
|
14
|
+
"./app/**/*.{ts,tsx}",
|
|
15
|
+
"./node_modules/@vryzel/file-next-ui/dist/**/*.{js,mjs}",
|
|
16
|
+
]
|
|
13
17
|
```
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
Your theme must define the usual shadcn CSS variables (`--background`, `--foreground`, `--border`, `--primary`, `--muted`, `--destructive`, `--card`, `--ring`, …).
|
|
20
|
+
|
|
21
|
+
## Quick path
|
|
16
22
|
|
|
17
23
|
```tsx
|
|
24
|
+
"use client";
|
|
18
25
|
import { FileExplorer } from "@vryzel/file-next-ui";
|
|
19
26
|
|
|
20
27
|
<FileExplorer
|
|
21
|
-
className="h-[70vh] rounded-[10px] border border-border"
|
|
28
|
+
className="h-[70vh] overflow-hidden rounded-[10px] border border-border bg-card"
|
|
22
29
|
tenantId="acme"
|
|
23
|
-
parentId={
|
|
30
|
+
parentId={folderId}
|
|
24
31
|
listFiles={listFiles}
|
|
25
32
|
searchFiles={searchFiles}
|
|
26
33
|
listTrash={listTrash}
|
|
27
34
|
requestUpload={requestUpload}
|
|
28
|
-
actions={
|
|
29
|
-
|
|
35
|
+
actions={{
|
|
36
|
+
deleteFile,
|
|
37
|
+
moveFile,
|
|
38
|
+
copyFile,
|
|
39
|
+
renameFile,
|
|
40
|
+
createFolder,
|
|
41
|
+
restoreNode,
|
|
42
|
+
createShare,
|
|
43
|
+
}}
|
|
44
|
+
onOpenFolder={(folder) => setFolderId(folder.id)}
|
|
45
|
+
breadcrumbs={crumbs}
|
|
46
|
+
onBreadcrumbNavigate={(seg) => setFolderId(seg.id === "root" ? null : seg.id)}
|
|
47
|
+
/>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`listFiles` / `searchFiles` / `listTrash` return:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
Promise<
|
|
54
|
+
| { ok: true; value: { items: FileNode[]; nextCursor?: string } }
|
|
55
|
+
| { ok: false; error: { code: string; message: string } }
|
|
56
|
+
>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Revive dates if they crossed the server action boundary (`new Date(node.updatedAt)`). `FileExplorer` also revives them.
|
|
60
|
+
|
|
61
|
+
Keep callback identities stable (`useCallback` or module scope).
|
|
62
|
+
|
|
63
|
+
## Use cases
|
|
64
|
+
|
|
65
|
+
### 1. Default explorer (batteries)
|
|
66
|
+
|
|
67
|
+
What you get with `FileExplorer`:
|
|
68
|
+
|
|
69
|
+
- List / grid, sort, persisted view (`persistViewKey`)
|
|
70
|
+
- Search, trash, upload (multi-file + OS drop)
|
|
71
|
+
- New folder / rename dialogs (not `window.prompt`)
|
|
72
|
+
- Multi-select, shift-click, ⌘A, copy/paste, drag onto folders
|
|
73
|
+
- Pagination (`limit` 50 + Load more)
|
|
74
|
+
- Quota footer, selection toast, upload queue
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
<FileExplorer
|
|
78
|
+
persistViewKey="my-app.files.view"
|
|
79
|
+
usedBytes={used}
|
|
80
|
+
quotaBytes={quota}
|
|
81
|
+
refreshKey={refreshKey}
|
|
82
|
+
onPreview={openPreview}
|
|
83
|
+
onDownload={download}
|
|
84
|
+
extraFileAction={{ label: "Create quote", onSelect: (f) => router.push(`/quotes/new?file=${f.id}`) }}
|
|
85
|
+
protectedIds={linkedIds} // cannot move to trash
|
|
86
|
+
labels={{ search: "Buscar", emptyFolder: "Carpeta vacía" }}
|
|
87
|
+
{...props}
|
|
88
|
+
/>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 2. Uploads that survive navigation
|
|
92
|
+
|
|
93
|
+
Put the provider on a layout. Unmounting the files page will not kill XHR.
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
// app/(dashboard)/layout.tsx
|
|
97
|
+
import { UploadQueueProvider } from "@vryzel/file-next-ui";
|
|
98
|
+
|
|
99
|
+
<UploadQueueProvider requestUpload={requestUpload} confirmUpload={onUploaded}>
|
|
100
|
+
{children}
|
|
101
|
+
</UploadQueueProvider>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`FileExplorer` uses the nearest provider; if none exists it creates its own.
|
|
105
|
+
|
|
106
|
+
### 3. Preview dialog
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { FileExplorer, FilePreviewDialog, canPreviewFile } from "@vryzel/file-next-ui";
|
|
110
|
+
|
|
111
|
+
const [preview, setPreview] = useState<{ file: FileNode; src: string } | null>(null);
|
|
112
|
+
|
|
113
|
+
<FileExplorer
|
|
114
|
+
onPreview={async (file) => {
|
|
115
|
+
const src = await signedUrl(file);
|
|
116
|
+
if (canPreviewFile(file)) setPreview({ file, src });
|
|
117
|
+
else window.open(src, "_blank");
|
|
118
|
+
}}
|
|
119
|
+
onDownload={async (file) => window.open(await signedUrl(file), "_blank")}
|
|
120
|
+
/>
|
|
121
|
+
<FilePreviewDialog
|
|
122
|
+
file={preview?.file ?? null}
|
|
123
|
+
src={preview?.src ?? ""}
|
|
124
|
+
onClose={() => setPreview(null)}
|
|
125
|
+
onDownload={(file) => void signedUrl(file).then((src) => window.open(src, "_blank"))}
|
|
30
126
|
/>
|
|
31
127
|
```
|
|
32
128
|
|
|
33
|
-
|
|
129
|
+
`src` is yours (presigned GET). The dialog does not call S3.
|
|
130
|
+
|
|
131
|
+
### 4. Compose your own shell
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
import {
|
|
135
|
+
Breadcrumbs,
|
|
136
|
+
ExplorerToolbar,
|
|
137
|
+
ExplorerListView,
|
|
138
|
+
ExplorerGridView,
|
|
139
|
+
EmptyState,
|
|
140
|
+
ErrorState,
|
|
141
|
+
FileIcon,
|
|
142
|
+
} from "@vryzel/file-next-ui";
|
|
143
|
+
import { useFileExplorer } from "@vryzel/file-next-headless";
|
|
144
|
+
|
|
145
|
+
const explorer = useFileExplorer({ listFiles, tenantId, parentId });
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="flex h-[70vh] flex-col rounded-[10px] border">
|
|
149
|
+
<Breadcrumbs segments={crumbs} onNavigate={go} className="px-4 py-3" />
|
|
150
|
+
<ExplorerToolbar
|
|
151
|
+
view={explorer.view}
|
|
152
|
+
onViewChange={explorer.setView}
|
|
153
|
+
query={query}
|
|
154
|
+
onQueryChange={setQuery}
|
|
155
|
+
sortKey={sortKey}
|
|
156
|
+
sortDirection={sortDir}
|
|
157
|
+
onSortChange={setSort}
|
|
158
|
+
/>
|
|
159
|
+
{explorer.status === "error" ? (
|
|
160
|
+
<ErrorState error={explorer.error!} onRetry={explorer.refetch} />
|
|
161
|
+
) : explorer.files.length === 0 ? (
|
|
162
|
+
<EmptyState title="Empty folder" description="Drop files here." />
|
|
163
|
+
) : explorer.view === "list" ? (
|
|
164
|
+
<ExplorerListView files={explorer.files} {...selectionAndDrag} />
|
|
165
|
+
) : (
|
|
166
|
+
<ExplorerGridView files={explorer.files} {...selectionAndDrag} />
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Every piece takes `className`.
|
|
34
173
|
|
|
35
|
-
|
|
174
|
+
### 5. Copy / i18n
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
<FileExplorer
|
|
178
|
+
labels={{
|
|
179
|
+
search: "Buscar",
|
|
180
|
+
upload: "Subir",
|
|
181
|
+
newFolder: "Nueva carpeta",
|
|
182
|
+
emptyFolder: "Esta carpeta está vacía",
|
|
183
|
+
moveToTrash: "Mover a la papelera",
|
|
184
|
+
}}
|
|
185
|
+
/>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Partial overlay on English defaults. Or wrap several pieces:
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
import { ExplorerLabelsProvider, defaultLabels } from "@vryzel/file-next-ui";
|
|
192
|
+
|
|
193
|
+
<ExplorerLabelsProvider labels={{ search: "Buscar" }}>
|
|
194
|
+
<ExplorerToolbar ... />
|
|
195
|
+
</ExplorerLabelsProvider>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### 6. Icons only
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
import { FileIcon } from "@vryzel/file-next-ui";
|
|
202
|
+
<FileIcon kind={file.kind} mimeType={file.mimeType} className="size-6" />
|
|
203
|
+
```
|
|
36
204
|
|
|
37
205
|
## Pieces
|
|
38
206
|
|
|
39
|
-
|
|
207
|
+
| Export | Role |
|
|
208
|
+
|---|---|
|
|
209
|
+
| `FileExplorer` | Default composed explorer |
|
|
210
|
+
| `ExplorerToolbar` | Search, upload, new folder, sort, trash, list/grid |
|
|
211
|
+
| `ExplorerListView` / `ExplorerGridView` | Rows / cards + drag |
|
|
212
|
+
| `ExplorerContextMenu` | Right-click / overflow menu |
|
|
213
|
+
| `Breadcrumbs` | Path |
|
|
214
|
+
| `EmptyState` / `ErrorState` | Empty / error |
|
|
215
|
+
| `FileIcon` | Mime icon |
|
|
216
|
+
| `FilePreviewDialog` | Image/pdf/video/text overlay |
|
|
217
|
+
| `CreateFolderDialog` / `ConfirmDeleteDialog` | Modals |
|
|
218
|
+
| `ExplorerSelectionToast` / `ExplorerClipboardToast` | Multi-select / copy |
|
|
219
|
+
| `UploadQueueProvider` / `ExplorerUploadStatus` | Upload queue |
|
|
220
|
+
| `defaultLabels` | English copy |
|
|
221
|
+
|
|
222
|
+
## Not this package
|
|
223
|
+
|
|
224
|
+
Server wiring → `@vryzel/file-next`. Hooks without UI → `@vryzel/file-next-headless`.
|
package/dist/index.cjs
CHANGED
|
@@ -836,24 +836,26 @@ function ExplorerContextMenu(props) {
|
|
|
836
836
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { "aria-hidden": "true", className: "size-4" }),
|
|
837
837
|
labels.restore
|
|
838
838
|
] }),
|
|
839
|
-
/* @__PURE__ */ jsxRuntime.
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
839
|
+
onRequestPurge || actions.purgeNode ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
840
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "my-1 h-px bg-border" }),
|
|
841
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
842
|
+
"button",
|
|
843
|
+
{
|
|
844
|
+
type: "button",
|
|
845
|
+
role: "menuitem",
|
|
846
|
+
className: cn(itemClass, "text-destructive"),
|
|
847
|
+
onClick: () => {
|
|
848
|
+
close();
|
|
849
|
+
if (onRequestPurge) onRequestPurge(target.file);
|
|
850
|
+
else void actions.purgeNode?.({ id: target.file.id });
|
|
851
|
+
},
|
|
852
|
+
children: [
|
|
853
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true", className: "size-4" }),
|
|
854
|
+
labels.deleteForever
|
|
855
|
+
]
|
|
856
|
+
}
|
|
857
|
+
)
|
|
858
|
+
] }) : null
|
|
857
859
|
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
858
860
|
target.file.kind === "file" && onPreview ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "menuitem", className: itemClass, onClick: () => {
|
|
859
861
|
close();
|
|
@@ -1954,7 +1956,7 @@ function FileExplorerInner(props) {
|
|
|
1954
1956
|
if ((event.key === "Delete" || event.key === "Backspace") && selectedFiles.length > 0) {
|
|
1955
1957
|
event.preventDefault();
|
|
1956
1958
|
if (trashOpen) {
|
|
1957
|
-
setPurgeTargets(selectedFiles);
|
|
1959
|
+
if (actions.purgeNode) setPurgeTargets(selectedFiles);
|
|
1958
1960
|
return;
|
|
1959
1961
|
}
|
|
1960
1962
|
if (selectedFiles.some((file) => protectedSet.has(file.id))) return;
|
|
@@ -2157,7 +2159,7 @@ function FileExplorerInner(props) {
|
|
|
2157
2159
|
onDownload,
|
|
2158
2160
|
onRename: setRenameTarget,
|
|
2159
2161
|
onRequestDelete: (file) => setDeleteTargets([file]),
|
|
2160
|
-
onRequestPurge: (file) => setPurgeTargets([file]),
|
|
2162
|
+
onRequestPurge: actions.purgeNode ? (file) => setPurgeTargets([file]) : void 0,
|
|
2161
2163
|
onCopy: (file) => copyToClipboard([file]),
|
|
2162
2164
|
onPaste: clipboard.length > 0 && !trashOpen ? (folder) => void pasteClipboard(folder ? folder.id : parentId) : void 0,
|
|
2163
2165
|
onNewFolder: actions.createFolder ? () => setFolderOpen(true) : void 0,
|
|
@@ -2186,7 +2188,7 @@ function FileExplorerInner(props) {
|
|
|
2186
2188
|
clearSelection();
|
|
2187
2189
|
await reload();
|
|
2188
2190
|
} : void 0,
|
|
2189
|
-
onPurge: () => setPurgeTargets(selectedFiles),
|
|
2191
|
+
onPurge: actions.purgeNode ? () => setPurgeTargets(selectedFiles) : void 0,
|
|
2190
2192
|
onClear: clearSelection
|
|
2191
2193
|
}
|
|
2192
2194
|
),
|