Skip to main content

FileBrowser

FileBrowser

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 canWrite is 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-viewer

Configure 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​

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 md down the pane moves into a sheet behind a "Folders" button. Turn it off with navigationPane={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 maxPreviewBytes are 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.

On this page