@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 CHANGED
@@ -1,39 +1,224 @@
1
1
  # @vryzel/file-next-ui
2
2
 
3
- Ready-to-use file explorer for `@vryzel/file-next`. Tailwind classes. Compose the pieces, or drop in `FileExplorer`.
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 in `tailwind.config`:
9
+ Scan the package so Tailwind keeps the classes:
10
10
 
11
11
  ```js
12
- content: ["./node_modules/@vryzel/file-next-ui/dist/**/*.{js,mjs}"]
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
- ## Default explorer
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={null}
30
+ parentId={folderId}
24
31
  listFiles={listFiles}
25
32
  searchFiles={searchFiles}
26
33
  listTrash={listTrash}
27
34
  requestUpload={requestUpload}
28
- actions={actions}
29
- onOpenFolder={setFolder}
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
- Pass `labels` to override copy. Pass `className` on any piece to restyle.
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
- Put `UploadQueueProvider` on a layout if uploads should survive unmounting the page.
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
- `ExplorerToolbar`, `ExplorerListView`, `ExplorerGridView`, `ExplorerContextMenu`, `Breadcrumbs`, `EmptyState`, `ErrorState`, `FileIcon`, `FilePreviewDialog`, `CreateFolderDialog`, `ConfirmDeleteDialog`, `ExplorerSelectionToast`, `ExplorerUploadStatus`.
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.jsx("div", { className: "my-1 h-px bg-border" }),
840
- /* @__PURE__ */ jsxRuntime.jsxs(
841
- "button",
842
- {
843
- type: "button",
844
- role: "menuitem",
845
- className: cn(itemClass, "text-destructive"),
846
- onClick: () => {
847
- close();
848
- if (onRequestPurge) onRequestPurge(target.file);
849
- else void actions.purgeNode?.({ id: target.file.id });
850
- },
851
- children: [
852
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true", className: "size-4" }),
853
- labels.deleteForever
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
  ),