Blocks
File explorer
Browse, organize and upload files: open folders, choose several at once, drag them into place, sort, preview, and send things to the Trash with an undo.
Installation
npx sinaui add blocks/file-explorerCopy src/lib/blocks/file-explorer/ with the components it uses: Sidebar layout,
File tree, Folder, Breadcrumb, Dropdown, Dialog, Alert dialog, Search field, Segmented, Meter,
Empty state, Upload tray, Toast, Button, Input and Icon. Mount <Toaster /> once
in your layout for the undo messages. No dependencies.
Examples
A library's files
Double-click a folder to open it, or use the tree on the left. Shift- or ⌘-click to choose several, then move or delete them together. Deleted things go to the Trash, and every move and delete can be undone. Star tables › Book of Fixed Stars holds over a thousand files, drawn as you scroll. Drop files from your computer to watch them upload.
Folders
Files
<script lang="ts">
import starTile from './star-tile.svg';
import FileExplorer, { type ExplorerItem } from '#lib/blocks/file-explorer/FileExplorer.svelte';
import type { Send } from '#lib/ui/uploads.svelte.js';
const day = (n: number) => new Date(Date.UTC(2026, 8, 29 - n)).toISOString();
let items = $state<ExplorerItem[]>([
{ id: 'ms', name: 'Manuscripts', folder: true, parent: null, modified: day(1), starred: true },
{
id: 'stars',
name: 'Star tables',
folder: true,
parent: null,
modified: day(4),
color: '#3b82f6'
},
{
id: 'tr',
name: 'Translations',
folder: true,
parent: null,
modified: day(6),
color: '#f59e0b'
},
{
id: 'letters',
name: 'Letters',
folder: true,
parent: null,
modified: day(9),
color: '#f43f5e'
},
{
id: 'greek',
name: 'From Greek',
folder: true,
parent: 'tr',
modified: day(7),
color: '#f59e0b'
},
{
id: 'f1',
name: 'Book of Optics.pdf',
parent: 'ms',
kind: 'pdf',
size: 24_600_000,
modified: day(1)
},
{
id: 'f2',
name: 'Canon of Medicine, vol 1.pdf',
parent: 'ms',
kind: 'pdf',
size: 88_200_000,
modified: day(3)
},
{
id: 'f3',
name: 'Folio 12r.jpg',
parent: 'ms',
kind: 'image',
size: 4_800_000,
thumbnail: starTile,
modified: day(2)
},
{
id: 'f4',
name: 'Zij al-Sindhind.xlsx',
parent: 'stars',
kind: 'sheet',
size: 312_000,
modified: day(4),
starred: true
},
{ id: 'f5', name: 'altitude.ts', parent: 'stars', kind: 'code', size: 2_400, modified: day(5) },
{
id: 'f6',
name: 'Euclid, book 7.docx',
parent: 'greek',
kind: 'doc',
size: 540_000,
modified: day(7)
},
{
id: 'f7',
name: 'Recitation, al-Fatiha.mp3',
parent: null,
kind: 'audio',
size: 6_100_000,
modified: day(0)
},
{
id: 'f8',
name: 'Observatory night.mp4',
parent: null,
kind: 'video',
size: 210_000_000,
modified: day(2)
},
{
id: 'f9',
name: 'Reading list.docx',
parent: null,
kind: 'doc',
size: 88_000,
modified: day(8),
starred: true
},
{
id: 'sufi',
name: 'Book of Fixed Stars',
folder: true,
parent: 'stars',
modified: day(3),
color: '#6366f1'
},
// Al-Sufi's catalog, one file per star: a folder big enough to draw a page at a time.
...Array.from({ length: 1018 }, (_, n) => ({
id: `star-${n + 1}`,
name: `Star ${String(n + 1).padStart(4, '0')}.json`,
parent: 'sufi',
kind: 'code' as const,
size: 1_200 + ((n * 37) % 900),
modified: day(3 + (n % 20))
})),
{
id: 'f10',
name: 'Letter to al-Kindi.docx',
parent: 'letters',
kind: 'doc',
size: 64_000,
modified: day(10)
}
]);
// Stands in for a real upload: progress over about two seconds.
const send: Send = (file, { onprogress, signal }) =>
new Promise((resolve, reject) => {
let p = 0;
const timer = setInterval(() => {
onprogress((p += 0.05 + Math.random() * 0.1));
if (p >= 1) {
clearInterval(timer);
resolve();
}
}, 150);
signal.addEventListener('abort', () => (clearInterval(timer), reject(signal.reason)));
});
</script>
<FileExplorer bind:items {send} />Props
| Prop | Type | Default |
|---|---|---|
items | ExplorerItem[] (bindable) | required |
onchange | (change) => Promise<void> | void: save a change | |
send | Send: upload each file, with progress in a tray | |
onopen | (item) => void: a file was opened | |
quota | number, bytes | 15 GB |
locale | string | 'en' |
An item is { id, name, folder?, parent, kind?, size?, modified, color?, starred?, trashed?, thumbnail? }. parent is the folder it's in (null at the top), kind is one
of pdf, image, doc, sheet, audio, video, code or other, and thumbnail is an image URL for
its preview.
Saving changes
Every change shows at once and is passed to onchange. If it rejects, the change is
put back and people see a short message saying so.
type ExplorerChange =
| { type: 'create'; item }
| { type: 'move'; items; to } // to: folder id, or null for Home
| { type: 'rename'; item; name }
| { type: 'star'; item; starred }
| { type: 'trash'; items; trashed } // false when restored
| { type: 'delete'; items }; // gone for good, with everything inside
With send (the same function Upload takes),
a file joins its folder once it has uploaded; without it, files are added straight away.
Keyboard
| Key | Action |
|---|---|
| ← → ↑ ↓ | Move between items, row by row. |
| Shift + arrows | Choose a run of items. |
| Home End | First / last item. |
| Enter | Open. |
| Shift F10, or the Menu key | The item's menu. |
| ⌘ A | Choose everything here. |
| ⌘ Space | Add or remove this item from the choice. |
| F2 | Rename. |
| Delete | Move to the Trash (in the Trash: delete forever). |
| ⌘ X, then ⌘ V | Move into the folder you're in. |
| Escape | Clear the choice. |
Use Ctrl for ⌘ on Windows and Linux. Right to left, ← and → swap.
Accessibility
- The items are one Tab stop; arrows move within them.
- Every item's ⋯ menu can rename, star, move and delete, and each has a checkbox to add it to the choice, so nothing needs dragging or a modifier key.
- Opening, moving, restoring and deleting are announced; sort buttons say which way they sort.
- Only deleting forever asks first; everything else can be undone.
- A folder can't be moved into itself or into anything inside it.