vfs-kit 1.0.1 → 1.0.3

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 (61) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +618 -35
  3. package/dist/VfsAdapter-BWjniD9Y.d.mts +57 -0
  4. package/dist/VfsAdapter-DOBt_TyL.d.ts +57 -0
  5. package/dist/VfsEngine-B6nhgyjQ.d.mts +152 -0
  6. package/dist/VfsEngine-DLx0iUpi.d.ts +152 -0
  7. package/dist/VfsNode-D10gxL5W.d.mts +48 -0
  8. package/dist/VfsNode-D10gxL5W.d.ts +48 -0
  9. package/dist/adapters/index.d.mts +201 -0
  10. package/dist/adapters/index.d.ts +201 -0
  11. package/dist/adapters/index.js +1159 -0
  12. package/dist/adapters/index.js.map +1 -0
  13. package/dist/adapters/index.mjs +1159 -0
  14. package/dist/adapters/index.mjs.map +1 -0
  15. package/dist/chunk-2FEJBM4N.js +60 -0
  16. package/dist/chunk-2FEJBM4N.js.map +1 -0
  17. package/dist/chunk-7OQI6PNM.mjs +60 -0
  18. package/dist/chunk-7OQI6PNM.mjs.map +1 -0
  19. package/dist/chunk-ALWOZGZI.mjs +23 -0
  20. package/dist/chunk-ALWOZGZI.mjs.map +1 -0
  21. package/dist/chunk-POSVS4C7.mjs +531 -0
  22. package/dist/chunk-POSVS4C7.mjs.map +1 -0
  23. package/dist/chunk-R3ROYAMW.js +23 -0
  24. package/dist/chunk-R3ROYAMW.js.map +1 -0
  25. package/dist/chunk-SWRBVSS6.mjs +16 -0
  26. package/dist/chunk-SWRBVSS6.mjs.map +1 -0
  27. package/dist/chunk-U2CKTXY7.js +16 -0
  28. package/dist/chunk-U2CKTXY7.js.map +1 -0
  29. package/dist/chunk-WZVVI3HX.js +531 -0
  30. package/dist/chunk-WZVVI3HX.js.map +1 -0
  31. package/dist/components/index.d.mts +193 -0
  32. package/dist/components/index.d.ts +193 -0
  33. package/dist/components/index.js +1197 -0
  34. package/dist/components/index.js.map +1 -0
  35. package/dist/components/index.mjs +1197 -0
  36. package/dist/components/index.mjs.map +1 -0
  37. package/dist/hooks/index.d.mts +120 -0
  38. package/dist/hooks/index.d.ts +120 -0
  39. package/dist/hooks/index.js +51 -0
  40. package/dist/hooks/index.js.map +1 -0
  41. package/dist/hooks/index.mjs +51 -0
  42. package/dist/hooks/index.mjs.map +1 -0
  43. package/dist/index.d.mts +42 -0
  44. package/dist/index.d.ts +38 -3
  45. package/dist/index.js +528 -13
  46. package/dist/index.js.map +1 -0
  47. package/dist/index.mjs +530 -0
  48. package/dist/index.mjs.map +1 -0
  49. package/dist/useVfsTabs-ZHDaLrM1.d.mts +39 -0
  50. package/dist/useVfsTabs-ZHDaLrM1.d.ts +39 -0
  51. package/package.json +59 -61
  52. package/dist/index.cjs +0 -43
  53. package/dist/index.d.cts +0 -7
  54. package/index.js +0 -7
  55. package/src/components/TreeView.tsx +0 -5
  56. package/src/components/index.ts +0 -1
  57. package/src/hooks/index.ts +0 -1
  58. package/src/hooks/useVfs.ts +0 -3
  59. package/src/index.ts +0 -2
  60. package/tsconfig.json +0 -44
  61. package/tsup.config.ts +0 -10
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 bengru07
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bengru07
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,35 +1,618 @@
1
- # vfs-kit
2
- A headless-first Virtual File System for React. Complete with drag-and-drop hierarchy, tab management and pluggable storage adapters.
3
-
4
- ## Building the library
5
-
6
- ```bash
7
- npm install
8
- npm run build
9
- ```
10
-
11
- The package is authored in TypeScript and exports ESM modules with declaration files. React, Tailwind, and any Shadcn UI components are declared as
12
- peer dependencies – a consuming project must provide them.
13
-
14
- ## Using locally
15
-
16
- While developing you can link the package into a demo app:
17
-
18
- ```bash
19
- cd ./*/vfs-kit-test
20
- npm link vfs-kit
21
- ```
22
-
23
- Import components in your app:
24
-
25
- ```tsx
26
- import { TreeView, useVfs } from "vfs-kit";
27
- ```
28
-
29
- ## Notes
30
-
31
- The current release only includes a simple placeholder component. Real
32
- Shadcn imports can be added once you’re ready; you may need to supply a
33
- local `declare module '@shadcn/ui';` or update tsconfig `moduleResolution` to
34
- `nodenext`.
35
-
1
+ # vfs-kit
2
+
3
+ A headless virtual filesystem engine for React. Provides a fully typed adapter model, a caching engine, and a suite of hooks and renderless components for building file explorers, editors, and asset managers — with no opinions on styling.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [Quick Start](#quick-start)
11
+ - [Adapters](#adapters)
12
+ - [InMemoryAdapter](#inmemoryadapter)
13
+ - [IndexedDBAdapter](#indexeddbadapter)
14
+ - [RestAdapter](#restadapter)
15
+ - [Writing a Custom Adapter](#writing-a-custom-adapter)
16
+ - [Provider](#provider)
17
+ - [Hooks](#hooks)
18
+ - [useVfs](#usevfs)
19
+ - [useVfsEngine](#usevfsengine)
20
+ - [useVfsTabs](#usevfstabs)
21
+ - [useVfsHistory](#usevfshistory)
22
+ - [useVfsExpanded](#usevfsexpanded)
23
+ - [useVfsSelection](#usevfsselection)
24
+ - [useSortableTab](#usesoratabletab)
25
+ - [Components](#components)
26
+ - [FileTree](#filetree)
27
+ - [FileRenderer](#filerenderer)
28
+ - [TabList / TabListBase](#tablist--tablistbase)
29
+ - [InlineInput](#inlineinput)
30
+ - [Core Types](#core-types)
31
+ - [Roadmap](#roadmap)
32
+
33
+ ---
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ npm install vfs-kit
39
+ ```
40
+
41
+ Peer dependencies: `react`, `react-dom`, `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`.
42
+
43
+ For `IndexedDBAdapter`: `idb`.
44
+
45
+ ---
46
+
47
+ ## Quick Start
48
+
49
+ ```tsx
50
+ import { VfsProvider } from 'vfs-kit';
51
+ import { InMemoryAdapter } from 'vfs-kit/adapters';
52
+ import { FileTree } from 'vfs-kit/components';
53
+ import { useVfsEngine } from 'vfs-kit/hooks';
54
+
55
+ const adapter = new InMemoryAdapter();
56
+
57
+ export default function App() {
58
+ return (
59
+ <VfsProvider
60
+ workspaces={[{ id: 'main', name: 'My Files', adapter }]}
61
+ config={{ sessionId: 'user-1' }}
62
+ >
63
+ <Explorer />
64
+ </VfsProvider>
65
+ );
66
+ }
67
+
68
+ function Explorer() {
69
+ const { fs } = useVfsEngine('main');
70
+
71
+ return (
72
+ <FileTree
73
+ workspaceId="main"
74
+ renderNode={({ flatNode, onClick, dragHandleProps, style }) => (
75
+ <div {...dragHandleProps} onClick={onClick} style={style}>
76
+ {flatNode.node.name}
77
+ </div>
78
+ )}
79
+ />
80
+ );
81
+ }
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Adapters
87
+
88
+ Adapters implement the `VfsAdapter` abstract class and provide the storage backend. Three are included; you can write your own.
89
+
90
+ ### InMemoryAdapter
91
+
92
+ Stores everything in `Map`s. No setup required. Ideal for prototyping, testing, and ephemeral state.
93
+
94
+ ```ts
95
+ import { InMemoryAdapter } from 'vfs-kit/adapters';
96
+
97
+ const adapter = new InMemoryAdapter();
98
+ ```
99
+
100
+ Supports history (`supportsHistory = true`). Data is lost on page refresh.
101
+
102
+ You can pre-seed the adapter directly before passing it to `VfsProvider`:
103
+
104
+ ```ts
105
+ const root = await adapter.createFolder({ parentId: null, name: 'src' });
106
+ const file = await adapter.createFile({ parentId: root.id, name: 'index.ts', mimeType: 'text/typescript' });
107
+ await adapter.writeFile(file.id, new TextEncoder().encode('export {}'));
108
+ ```
109
+
110
+ ### IndexedDBAdapter
111
+
112
+ Persists to the browser's IndexedDB via [`idb`](https://github.com/jakearchibald/idb). Survives page refresh. Supports optional cross-tab sync via `BroadcastChannel`.
113
+
114
+ ```ts
115
+ import { IndexedDBAdapter } from 'vfs-kit/adapters';
116
+
117
+ const adapter = new IndexedDBAdapter({
118
+ dbName: 'my-app-fs',
119
+ dbVersion: 1, // optional, default 1
120
+ isolation: 'shared', // 'private' (default) | 'shared' — enables BroadcastChannel sync
121
+ });
122
+
123
+ await adapter.open(); // must be called before passing to VfsProvider
124
+ ```
125
+
126
+ Call `adapter.close()` on unmount if managing lifecycle manually.
127
+
128
+ Supports history (`supportsHistory = true`). Enforces a 50-snapshot cap per file, evicting the oldest automatically.
129
+
130
+ ### RestAdapter
131
+
132
+ Delegates all operations to a remote HTTP API. Connects to a server-sent events (SSE) endpoint for real-time change propagation, with an optional fallback for non-SSE environments.
133
+
134
+ ```ts
135
+ import { RestAdapter } from 'vfs-kit/adapters';
136
+
137
+ const adapter = new RestAdapter({
138
+ baseUrl: 'https://api.example.com/fs',
139
+ supportsHistory: true, // default true
140
+ fetch: customFetch, // optional — defaults to globalThis.fetch
141
+ endpoints: { ... }, // optional — override individual endpoint URLs
142
+ realtimeFallback: (onMessage, onError) => {
143
+ // e.g. set up a WebSocket and return a cleanup function
144
+ return () => { /* disconnect */ };
145
+ },
146
+ });
147
+
148
+ await adapter.open();
149
+ ```
150
+
151
+ **Default endpoint conventions** (all relative to `baseUrl`):
152
+
153
+ | Operation | Method | Path |
154
+ |---|---|---|
155
+ | Get node | GET | `/nodes/:id` |
156
+ | Get children | GET | `/nodes/:parentId/children` |
157
+ | Create file | POST | `/nodes/file` |
158
+ | Create folder | POST | `/nodes/folder` |
159
+ | Write content | PUT | `/content/:id` |
160
+ | Move (batch) | POST | `/nodes/batch/move` |
161
+ | Get snapshots | GET | `/snapshots/:fileId` |
162
+ | Save snapshot | POST | `/snapshots/:fileId` |
163
+ | Restore snapshot | POST | `/snapshots/:fileId/:index/restore` |
164
+ | Delete snapshot | DELETE | `/snapshots/:fileId/:index` |
165
+ | SSE stream | GET | `/events` |
166
+
167
+ Any endpoint can be overridden individually via the `endpoints` option.
168
+
169
+ ### Writing a Custom Adapter
170
+
171
+ Extend `VfsAdapter` and implement all abstract methods. Set `supportsHistory = true` and implement the four history methods (`getSnapshots`, `saveSnapshot`, `restoreSnapshot`, `deleteSnapshot`) if your backend supports it.
172
+
173
+ ```ts
174
+ import { VfsAdapter } from 'vfs-kit/core';
175
+
176
+ export class MyAdapter extends VfsAdapter {
177
+ readonly supportsHistory = false;
178
+ // implement abstract methods...
179
+ }
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Provider
185
+
186
+ `VfsProvider` initialises the engine for each workspace and makes them available via context.
187
+
188
+ ```tsx
189
+ <VfsProvider
190
+ workspaces={[
191
+ { id: 'ws-a', name: 'Source', adapter: adapterA },
192
+ { id: 'ws-b', name: 'Target', adapter: adapterB },
193
+ ]}
194
+ config={{
195
+ sessionId: 'user-123', // required — used for node locking
196
+ allowDuplicateNames: false, // default false
197
+ duplicateResolution: 'throw', // 'throw' | 'rename' — default 'throw'
198
+ history: {
199
+ maxSnapshots: 50, // default 50
200
+ autosave: {
201
+ enabled: false, // default false
202
+ intervalMs: 30_000,
203
+ },
204
+ },
205
+ }}
206
+ cacheStrategy="hybrid" // 'eager' | 'lazy' | 'hybrid' — default 'hybrid'
207
+ tabPersistence={{ strategy: 'session' }} // 'session' | 'none' — default 'none'
208
+ fallback={<div>Loading…</div>}
209
+ >
210
+ {children}
211
+ </VfsProvider>
212
+ ```
213
+
214
+ **Cache strategies:**
215
+ - `eager` — loads all nodes on init
216
+ - `lazy` — loads nodes on demand
217
+ - `hybrid` — loads root children on init, everything else on demand
218
+
219
+ ---
220
+
221
+ ## Hooks
222
+
223
+ ### useVfs
224
+
225
+ The all-in-one hook. Combines engine, tabs, selection, and expanded state in a single call. Use this when you want everything wired together; use the individual hooks when you need fine-grained control.
226
+
227
+ ```ts
228
+ import { useVfs } from 'vfs-kit/hooks';
229
+
230
+ const {
231
+ fs, // VfsFsApi — write operations
232
+ tree, // VfsTreeApi — read operations
233
+ status, // VfsStatusApi
234
+ tabs, // VfsTabsApi
235
+ selection, // VfsSelectionApi
236
+ expanded, // VfsExpandedApi
237
+ } = useVfs({
238
+ workspaceId: 'ws-a', // optional — defaults to active workspace
239
+ tabs: { workspaceIds: ['ws-a'] },
240
+ selection: { multiSelect: true, rangeSelect: true },
241
+ expanded: { persistKey: 'sidebar-expanded' },
242
+ });
243
+ ```
244
+
245
+ ### useVfsEngine
246
+
247
+ Returns only the `fs` / `tree` / `status` surfaces for a workspace. The most commonly used hook when you don't need tabs or selection.
248
+
249
+ ```ts
250
+ const { fs, tree, status } = useVfsEngine('ws-a');
251
+ ```
252
+
253
+ **`fs` — write operations:**
254
+
255
+ ```ts
256
+ fs.createFile(parentId, name, { mimeType?, meta? })
257
+ fs.createFolder(parentId, name, { meta? })
258
+ fs.rename(id, newName)
259
+ fs.delete(ids, permanent?) // soft-delete by default
260
+ fs.restore(ids)
261
+ fs.purge(ids) // permanent delete
262
+ fs.move(ids, newParentId)
263
+ fs.write(id, content) // Uint8Array
264
+ fs.lock(ids)
265
+ fs.unlock(ids)
266
+ fs.reorder(parentId, orderedIds)
267
+ fs.snapshot(fileId, label?)
268
+ fs.execute(command) // raw VfsCommand escape hatch
269
+ ```
270
+
271
+ **`tree` — read operations:**
272
+
273
+ ```ts
274
+ tree.getNode(id) // Promise<VfsNode | null>
275
+ tree.getChildren(parentId, opts?) // Promise<VfsNode[]>
276
+ tree.getPath(id) // Promise<string> e.g. '/src/index.ts'
277
+ tree.readFile(id) // Promise<Uint8Array>
278
+ tree.readJSON<T>(id) // Promise<T>
279
+ tree.writeJSON<T>(id, data) // Promise<void>
280
+ tree.search(query, opts?)
281
+ tree.getTrashed()
282
+ tree.getSnapshots(fileId) // Promise<VfsFileSnapshot[]>
283
+ ```
284
+
285
+ **`status`:**
286
+
287
+ ```ts
288
+ status.pending // boolean — true while a command is in flight
289
+ status.error // Error | null — last command error
290
+ status.version // number — increments on every change, useful as a cache key
291
+ ```
292
+
293
+ ### useVfsTabs
294
+
295
+ Manages open file tabs. Supports dirty tracking, locking, drag-to-reorder, and optional session persistence.
296
+
297
+ ```ts
298
+ import { useVfsTabs } from 'vfs-kit/hooks';
299
+
300
+ const tabs = useVfsTabs({
301
+ workspaceIds: ['ws-a'], // optional
302
+ dirtyChecker: ({ savedContent, currentContent }) => ..., // optional custom checker
303
+ });
304
+ ```
305
+
306
+ ```ts
307
+ tabs.tabs // VfsTab[]
308
+ tabs.activeTabId // string | null
309
+ tabs.activeTab // VfsTab | null
310
+
311
+ tabs.open(nodeId, workspaceId) // opens or focuses a tab
312
+ tabs.close(tabId)
313
+ tabs.closeOthers(tabId)
314
+ tabs.closeAll(workspaceId?)
315
+ tabs.setActive(tabId)
316
+ tabs.reorder(activeId, overId)
317
+ tabs.lock(tabId)
318
+ tabs.unlock(tabId)
319
+ tabs.markDirty(tabId, currentContent)
320
+ tabs.markSaved(tabId)
321
+ ```
322
+
323
+ **`VfsTab` shape:**
324
+
325
+ ```ts
326
+ interface VfsTab {
327
+ id: string;
328
+ nodeId: string;
329
+ workspaceId: string;
330
+ title: string;
331
+ isDirty: boolean;
332
+ isLocked: boolean;
333
+ savedContent: Uint8Array | null;
334
+ currentContent: Uint8Array | null;
335
+ lastSavedAt: number | null;
336
+ }
337
+ ```
338
+
339
+ > **Important:** when using a custom tab UI, use `TabListBase` and pass your `useVfsTabs` instance into it directly. Using `TabList` creates its own internal `useVfsTabs` instance which will be disconnected from yours.
340
+
341
+ ### useVfsHistory
342
+
343
+ Per-file snapshot manager. Loads snapshots for the active file and keeps the list up to date as snapshots are taken. Scoped to the current render — history is discarded when the component unmounts.
344
+
345
+ ```ts
346
+ import { useVfsHistory } from 'vfs-kit/hooks';
347
+
348
+ const { snapshots, takeSnapshot, restore, remove, loading } = useVfsHistory({
349
+ workspaceId: 'ws-a',
350
+ nodeId: 'file-id', // null clears the list
351
+ onRestore: (bytes) => { // called after a restore so you can sync editor state
352
+ setContent(new TextDecoder().decode(bytes));
353
+ },
354
+ });
355
+ ```
356
+
357
+ ```ts
358
+ takeSnapshot(label?) // saves current file content as a new snapshot
359
+ restore(index) // writes snapshot content back to the file and calls onRestore
360
+ remove(index) // removes a snapshot
361
+ loading // true while the initial snapshot list is loading
362
+ snapshots // VfsFileSnapshot[] — most-recent first
363
+ ```
364
+
365
+ **`VfsFileSnapshot` shape:**
366
+
367
+ ```ts
368
+ interface VfsFileSnapshot {
369
+ id: string;
370
+ fileId: string;
371
+ index: number;
372
+ content: Uint8Array;
373
+ createdAt: number;
374
+ label?: string;
375
+ }
376
+ ```
377
+
378
+ Only available when the adapter has `supportsHistory = true` (`InMemoryAdapter` and `IndexedDBAdapter` both do; `RestAdapter` depends on your server).
379
+
380
+ ### useVfsExpanded
381
+
382
+ Manages folder expanded state in a file tree. Persists to `sessionStorage` when a `persistKey` is provided. Automatically collapses deleted folders.
383
+
384
+ ```ts
385
+ const expanded = useVfsExpanded({
386
+ workspaceIds: ['ws-a'], // optional
387
+ defaultExpanded: ['folder-id'], // optional
388
+ persistKey: 'sidebar-expanded', // optional — enables sessionStorage persistence
389
+ });
390
+ ```
391
+
392
+ ```ts
393
+ expanded.expandedIds // Set<string>
394
+ expanded.isExpanded(id) // boolean
395
+ expanded.expand(id)
396
+ expanded.collapse(id)
397
+ expanded.toggle(id)
398
+ expanded.expandAll(ids)
399
+ expanded.collapseAll()
400
+ expanded.expandToNode(id, workspaceId?) // expands all ancestors of a node
401
+ ```
402
+
403
+ ### useVfsSelection
404
+
405
+ Manages selected node state. Supports single select, multi-select, and shift-range select.
406
+
407
+ ```ts
408
+ const selection = useVfsSelection({
409
+ multiSelect: true, // default true
410
+ rangeSelect: true, // default true
411
+ });
412
+ ```
413
+
414
+ ```ts
415
+ selection.selection // string[]
416
+ selection.lastSelectedId // string | null
417
+ selection.isSelected(id) // boolean
418
+ selection.select(id) // replaces selection
419
+ selection.toggle(id) // adds/removes from selection
420
+ selection.selectRange(orderedIds, anchorId, targetId)
421
+ selection.deselect(id)
422
+ selection.deselectAll()
423
+ selection.selectAll(ids)
424
+ ```
425
+
426
+ ### useSortableTab
427
+
428
+ A thin wrapper around `@dnd-kit/sortable`'s `useSortable` for tab drag-to-reorder. Returns `ref`, `style`, and `dragHandleProps` ready to spread onto a tab element.
429
+
430
+ ```ts
431
+ import { useSortableTab } from 'vfs-kit/hooks';
432
+
433
+ function MyTab({ tab }) {
434
+ const { ref, style, dragHandleProps } = useSortableTab(tab.id);
435
+ return (
436
+ <div ref={ref} style={style} {...dragHandleProps}>
437
+ {tab.title}
438
+ </div>
439
+ );
440
+ }
441
+ ```
442
+
443
+ Used internally by `TabListBase` when `sortable` is true. Use it directly when building a fully custom tab strip.
444
+
445
+ ---
446
+
447
+ ## Components
448
+
449
+ All components are headless by default — they manage behaviour and delegate rendering to your own render props.
450
+
451
+ ### FileTree
452
+
453
+ Renders a flat virtualised list of nodes for a workspace. Handles expand/collapse, drag-and-drop reordering, inline creation, and drop-zone overlays.
454
+
455
+ ```tsx
456
+ import { FileTree } from 'vfs-kit/components';
457
+
458
+ <FileTree
459
+ workspaceId="ws-a"
460
+ draggable // enables drag-and-drop
461
+ renderNode={({ flatNode, isSelected, isActive, isDragging, isDragSession,
462
+ isRenaming, setRenaming, dragHandleProps, style, onClick }) => (
463
+ <div {...dragHandleProps} onClick={onClick} style={style}>
464
+ {flatNode.node.name}
465
+ </div>
466
+ )}
467
+ inlineCreate={{ kind: 'file' }} // pass null to cancel
468
+ onCancelCreate={() => {}}
469
+ folderOverlay={{
470
+ mode: 'full', // 'full' | 'none'
471
+ renderOverlay: ({ isBlocked, isEmpty }) => <div />,
472
+ }}
473
+ />
474
+ ```
475
+
476
+ **`NodeRenderProps`:**
477
+
478
+ ```ts
479
+ interface NodeRenderProps<TMeta> {
480
+ flatNode: FlatNode<TMeta>; // { node, level, isOpen }
481
+ isSelected: boolean;
482
+ isActive: boolean;
483
+ isDragging: boolean;
484
+ isDragSession: boolean;
485
+ isRenaming: boolean;
486
+ setRenaming: (v: boolean) => void;
487
+ dragHandleProps: Record<string, unknown>;
488
+ style: React.CSSProperties;
489
+ onClick: (e: React.MouseEvent) => void;
490
+ }
491
+ ```
492
+
493
+ > When a file node is clicked, `FileTree`'s internal `onClick` handles selection and expand — it does not open tabs. Wire `tabs.open(node.id, workspaceId)` inside your `renderNode` click handler if you want tab behaviour.
494
+
495
+ ### FileRenderer
496
+
497
+ Loads and renders the content of the active file. Re-runs when `activeNodeId` changes, handles loading and error states, and calls `onLoad` once content is available.
498
+
499
+ ```tsx
500
+ import { FileRenderer } from 'vfs-kit/components';
501
+
502
+ <FileRenderer
503
+ workspaceId="ws-a"
504
+ activeNodeId={tabs.activeTab?.nodeId ?? null}
505
+ onLoad={(node, bytes) => setContent(new TextDecoder().decode(bytes))}
506
+ onError={(node, error) => console.error(error)}
507
+ fallback={<div>No file open</div>}
508
+ >
509
+ {({ node, content, loading, error }) => (
510
+ <textarea value={new TextDecoder().decode(content ?? new Uint8Array())} />
511
+ )}
512
+ </FileRenderer>
513
+ ```
514
+
515
+ > Key the child component on `node.id` if it owns its own content state — this causes React to fully remount on tab switch, cleanly resetting state without a `useEffect`.
516
+
517
+ ### TabList / TabListBase
518
+
519
+ `TabList` is the higher-level component: it creates its own `useVfsTabs` instance internally.
520
+
521
+ `TabListBase` is the lower-level component: it accepts tab state as props, so you can drive it from a `useVfsTabs` instance you control elsewhere. **Use `TabListBase` when you are also calling `tabs.open()` from your own code** — otherwise the tab strip and your open calls will be in two disconnected states.
522
+
523
+ ```tsx
524
+ import { TabListBase } from 'vfs-kit/components';
525
+
526
+ // tabs = useVfsTabs({ workspaceIds: ['ws-a'] })
527
+
528
+ <TabListBase
529
+ tabs={tabs.tabs}
530
+ activeTabId={tabs.activeTabId}
531
+ onReorder={tabs.reorder}
532
+ onClose={tabs.close}
533
+ onSetActive={tabs.setActive}
534
+ onLock={tabs.lock}
535
+ onUnlock={tabs.unlock}
536
+ sortable // enables drag-to-reorder via @dnd-kit
537
+ >
538
+ {({ tabs: tabList, activeTabId, onClose, onSetActive, onLock, onUnlock, dragHandleProps }) => (
539
+ <div style={{ display: 'flex' }}>
540
+ {tabList.map(tab => (
541
+ <MyTab
542
+ key={tab.id}
543
+ tab={tab}
544
+ isActive={tab.id === activeTabId}
545
+ dragHandleProps={dragHandleProps(tab.id)}
546
+ onSetActive={onSetActive}
547
+ onClose={onClose}
548
+ onLock={onLock}
549
+ onUnlock={onUnlock}
550
+ />
551
+ ))}
552
+ </div>
553
+ )}
554
+ </TabListBase>
555
+ ```
556
+
557
+ ### InlineInput
558
+
559
+ A controlled text input designed for inline rename flows. Commits on Enter or blur, cancels on Escape.
560
+
561
+ ```tsx
562
+ import { InlineInput } from 'vfs-kit/components';
563
+
564
+ <InlineInput
565
+ initialValue={node.name}
566
+ onSubmit={(newName) => fs.rename(node.id, newName)}
567
+ onCancel={() => setRenaming(false)}
568
+ className="my-input-class"
569
+ />
570
+ ```
571
+
572
+ ---
573
+
574
+ ## Core Types
575
+
576
+ ```ts
577
+ // vfs-kit/core
578
+
579
+ interface VfsNode<TMeta> {
580
+ id: string;
581
+ name: string;
582
+ kind: 'file' | 'folder';
583
+ parentId: string | null;
584
+ sortIndex: number | null;
585
+ lockedBy: string | null;
586
+ createdAt: number;
587
+ updatedAt: number;
588
+ deletedAt: number | null;
589
+ meta: TMeta;
590
+ }
591
+
592
+ interface VfsFileNode<TMeta> extends VfsNode<TMeta> {
593
+ kind: 'file';
594
+ mimeType: string;
595
+ size: number;
596
+ }
597
+
598
+ interface VfsFolderNode<TMeta> extends VfsNode<TMeta> {
599
+ kind: 'folder';
600
+ }
601
+
602
+ interface VfsFileSnapshot {
603
+ id: string;
604
+ fileId: string;
605
+ index: number;
606
+ content: Uint8Array;
607
+ createdAt: number;
608
+ label?: string;
609
+ }
610
+ ```
611
+
612
+ ---
613
+
614
+ ## Roadmap
615
+
616
+ - **`deleteSnapshot` as a formal `VfsCommand`** — currently `remove()` in `useVfsHistory` is local-state only; adding `{ op: 'deleteSnapshot', fileId, index }` to `VfsEngine` and `fs.deleteSnapshot()` to `VfsFsApi` will make it durable
617
+ - **Autosave snapshots** — `VfsEngineConfig.history.autosave` is wired in the config but not yet connected; a `setInterval` in `useVfsHistory` gated on `autosave.enabled` is the planned hookup point
618
+ - **Snapshot diff view** — compare a snapshot's content against the current file before restoring