Browse, preview and download files on the company network drives, from CHAPI or any FilesSource
Tags: files file-browser network-drive smb folders preview download chapi
FileBrowser lets staff browse a network drive: pick a drive, walk its folders, filter, preview files with the document viewers and download them. It never calls fetch itself. You pass a FilesSource: createChapiFilesSource() for CHAPI's /v2/files API in apps, or createMemoryFilesSource() in demos and tests.
Read-only for now. Write actions (upload, new folder, rename, delete) have slots, but render only when a source implements them and the root's
canWriteis true. The Land Development pilot shows no write UI. Try it on the file browser demo.
Import
<script>
import { FileBrowser, createChapiFilesSource } from '@classic-homes/files-svelte';
</script>Setup
npm install @classic-homes/files-svelte
# plus the viewer peers for the formats you preview, e.g.
npm install pdfjs-dist docx-preview dxf-viewerConfigure the viewers once at app start, as on DocumentViewer, and add the package to your Tailwind content:
'./node_modules/@classic-homes/files-svelte/dist/**/*.{js,svelte}',Basic Usage
<script lang="ts">
import { FileBrowser, createChapiFilesSource } from '@classic-homes/files-svelte';
import type { ViewerRegistry } from '@classic-homes/viewer-svelte';
import { getAccessToken, getSessionToken, refreshSession } from '@classic-homes/auth/core';
const source = createChapiFilesSource({
baseUrl: 'https://api.classichomes.com',
getAccessToken,
getHeaders: () => {
const session = getSessionToken();
return session ? { 'X-Session-Token': session } : undefined;
},
onUnauthorized: async () => (await refreshSession()) === 'refreshed',
});
// Lazy imports: only formats listed here are bundled. Add `dwg` only if the app opts into viewer-dwg.
const viewers: ViewerRegistry = {
pdf: () => import('@classic-homes/viewer-svelte/pdf'),
docx: () => import('@classic-homes/viewer-svelte/docx'),
xlsx: () => import('@classic-homes/viewer-svelte/xlsx'),
csv: () => import('@classic-homes/viewer-svelte/xlsx'),
dxf: () => import('@classic-homes/viewer-svelte/cad'),
};
</script>
<FileBrowser {source} {viewers} class="h-[80vh]" />Examples
Navigation in the URL
rootId and path are controlled props. Keep them in the URL with onnavigate, which reports the drive and folder together, and the browser's Back and Forward buttons walk the folders. (onrootchange and onpathchange fire separately, back to back on a drive change, so a URL built from the current page in each would lose the drive.)
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { FileBrowser } from '@classic-homes/files-svelte';
let { source, viewers } = $props();
const rootId = $derived($page.url.searchParams.get('root') ?? undefined);
const path = $derived($page.url.searchParams.get('path') ?? '');
function go(next: { rootId: string; path: string }) {
const url = new URL($page.url);
url.searchParams.set('root', next.rootId);
if (next.path) url.searchParams.set('path', next.path);
else url.searchParams.delete('path');
if (url.search === $page.url.search) return;
// The first visit picks a default drive: replace, so Back leaves the page.
goto(url.pathname + url.search, { keepFocus: true, noScroll: true, replaceState: !rootId });
}
</script>
<FileBrowser {source} {viewers} {rootId} {path} onnavigate={go} />Write slots (later)
Pass a toolbar and rowActions; they render only when the source has a write method and the root can be written. Writes will send If-Match with the entry's etag.
<FileBrowser {source}>
{#snippet toolbar(ctx)}
{#if ctx.capabilities.mkdir}
<Button size="sm" onclick={() => newFolder(ctx)}>New folder</Button>
{/if}
{/snippet}
{#snippet rowActions(entry, ctx)}
{#if ctx.capabilities.remove}
<Button size="icon" variant="ghost" aria-label={`Delete ${entry.name}`}>…</Button>
{/if}
{/snippet}
</FileBrowser>Props
| Prop | Type | Default | Description |
|---|---|---|---|
source |
FilesSource |
required | Where the data comes from (CHAPI adapter, in-memory source, or your own) |
rootId |
string |
- | Current root (bindable). Defaults to the first root the caller can read |
path |
string |
'' |
Current folder, root-relative (bindable). '' is the root |
onrootchange |
(rootId: string) => void |
- | The user picked another root |
onpathchange |
(path: string) => void |
- | The user opened another folder |
onnavigate |
(location: { rootId: string; path: string }) => void |
- | The drive and/or folder changed, in one call. Use it for URL sync |
viewers |
ViewerRegistry |
{} |
Document viewers for previews, as lazy imports keyed by format |
pageSize |
number |
200 |
Entries per page |
autoLoad |
boolean |
false |
Load the next page when the end of the list scrolls into view |
showHidden |
boolean |
false |
"Show hidden files" (bindable); sent as includeHidden |
filter |
string |
'' |
Filter text (bindable). Narrows the loaded entries, or searches when the source has search() |
maxPreviewBytes |
number |
104857600 |
Larger files are not fetched for preview (100 MB, the viewers' cap) |
onunauthorized |
(error: unknown) => void |
- | A request got a 401: hand it to the app's auth |
onerror |
(error: unknown) => void |
- | Listing, folder-pane, preview, search and download errors, for logging |
onpreview |
(entry: FileEntry) => void |
- | A file was opened for preview |
toolbar |
Snippet<[FileBrowserContext]> |
- | Write toolbar; rendered only when the source can write and root.canWrite is true |
rowActions |
Snippet<[FileEntry, FileBrowserContext]> |
- | Per-row write actions, under the same rule |
navigationPane |
boolean |
true |
Explorer-style folder tree beside the list; on phones it opens from a "Folders" button |
paneSize |
number |
25 |
Starting width of the folder pane, in percent (bindable); drag the splitter to resize |
paneKey |
string |
- | Remember the pane width in localStorage under this key |
label |
string |
'File browser' |
Accessible name of the browser region |
locale |
string | string[] |
- | Locale for sizes and dates (defaults to the browser's) |
class |
string |
- | Classes for the outer element; give it a height |
FileBrowserContext has source, root, path, capabilities ({ upload, mkdir, move, remove, any }), refresh() and navigate(path).
Behaviour
- Folder pane: like Windows Explorer's navigation pane, a FileTree on the left lists the drives with their folders. The open folder is highlighted and its parents are expanded, so the folder you came from and its siblings stay in view. Drag the splitter (or focus it and use the arrow keys) to resize. From
mddown the pane moves into a sheet behind a "Folders" button. Turn it off withnavigationPane={false}. - Roots: with the folder pane, drives are its top-level items. When the pane isn't beside the list (turned off, or on phones, where it sits in a sheet) a drive picker shows when there is more than one root. The root's description shows under the path.
- Order: the server's order (folders first, then names case-insensitively). Clicking a column header sorts the loaded entries; with more pages on the server, the list says so.
- Paging: 200 at a time, "Load more" (or
autoLoad), "Showing N of total". - Filter: narrows the loaded entries by name. When the source implements
search()(planned server search), the same box searches the whole root instead. - Opening: folders navigate, files open a preview dialog (full-screen on phones), links are listed but disabled with "This is a shortcut to another location and can't be opened here".
- Preview: PDF, Word, Excel, CSV and DXF go to the registered viewers; PNG, JPEG, GIF, WebP, BMP and AVIF show through an object URL (revoked on close); everything else, and always HTML, SVG, XML and JS, shows a details card with Download. Files over
maxPreviewBytesare not fetched. - Download: with a source that has
downloadUrl()(CHAPI), a short-lived signed link the browser downloads natively, so large files stream to disk. Otherwise, or if no link can be had,source.read(…, { download: true })saved under the entry's name (the bearer header rules out a plain<a href>). A file already loaded for preview is saved from memory. - Details: name, location, type, size, modified, and "Copy path" (drive label plus path, e.g.
Land Development / 105 BREWERY / Plans, never a server path). - Offline: a banner while the browser is offline; a failed listing retries when the connection returns.
Error states
Error (FilesError.code) |
Shown | Action |
|---|---|---|
invalid_path, not_a_directory, is_directory, invalid_cursor |
This location can't be opened | Back to the root |
invalid_request (400, a malformed query) |
This request couldn't be handled | Back to the root |
unauthorized (401) |
Your session has ended; onunauthorized fires |
App's sign-in |
link_not_followed (403) |
This is a shortcut to another location and can't be opened here | - |
access_denied (403) |
You don't have access to this folder | - |
not_found (404) |
This folder no longer exists (a root you can't read is also 404) | Back to the root |
locked (423) |
Someone has this file open (often in Office). Try again shortly. | Try again after Retry-After |
unavailable (503) |
The network drive is unavailable | Try again after Retry-After |
timeout, file_server_error, network (504/502) |
Couldn't reach the file server | Try again |
Keyboard
| Keys | Action |
|---|---|
| ↑ / ↓, Home / End, PgUp/Dn | Move the selection |
| Enter | Open the folder or preview the file |
| Backspace, Alt + ↑ | Up a folder (focus returns to the folder) |
/ |
Focus the filter; Escape returns to the list |
| Letters | Jump to the next name starting with them |
The list is an ARIA grid with one tab stop (roving focus); loading, counts and errors are announced through a polite live region.
Related
- FileTree: the folder pane on its own
- FileList: the folder listing on its own
- FilePreview: preview and download one file
- FileBreadcrumbs: the path bar
- FileIcon: icon by file type
- FileDetails: the facts card with Copy path
- FileErrorState: the error states
- DocumentViewer: the viewers behind previews