Component
Media browser
<DomMediaBrowser>A folder-aware media library surface for server-backed files, optimistic Pinia stores, custom file renderers, drag and drop, upload progress, and picker workflows.
Demo
Server-backed media library
A Pinia setup store drives folder loading, optimistic folder creation, rename, move, metadata edits, route-aware navigation, picker selection, and emulated chunk uploads.
Pinia-driven media library
/
Route-aware folder path: /data/media-browser
Recent REST calls
Brand media
No media yet
Drop files here or create a folder to start this library.
Last request body
{}Last response
{}Forms
File picker field
A form-style control opens DomMediaBrowser in a dialog, allows files to be uploaded or selected, then writes either a single media row id or an array of ids back into the form value.
Architecture
Data model
The media row is the folder structure. The file row is the stored object. Keeping them separate lets one uploaded file appear in multiple roots or folders while the binary, MIME type, size, and metadata stay canonical.
This follows the data grid and month calendar pattern: the component renders current rows and emits intent, while the Pinia store owns fetching, optimistic updates, stale-response guards, cached folder data, and server mutation policy.
media
ulid varchar(26) primary key
root_id varchar(26) not null
type enum('root', 'dir', 'file') not null
path varchar(1024) not null
parent_id varchar(26) null references media(ulid)
file_uuid varchar(26) null references files(ulid)
created_at datetime not null
updated_at datetime not null
files
ulid varchar(26) primary key
name varchar(255) not null
mime_type varchar(255) not null
disk_path varchar(1024) not null
size bigint unsigned not null
public_url varchar(1024) null
thumbnail_url varchar(1024) null
meta json null
created_at datetime not null
updated_at datetime not null
unique index media_root_path_unique on media(root_id, path)
index media_parent_index on media(parent_id)
index media_file_index on media(file_uuid)REST
Folder read endpoint
Folder navigation reads one parent at a time. The store updates the route path and derived breadcrumbs immediately, keeps any cached rows visible, then merges the server response when it returns. The response includes the current folder, hydrated children, canonical breadcrumbs, and request metadata.
GET /api/media/roots/:rootId/folders?path=/Images
// Response
{
folder: {
ulid: 'media_images',
type: 'dir',
path: '/Images',
parent_id: 'media_root_brand',
file_uuid: null,
name: 'Images',
},
data: [
{
ulid: 'media_hero',
type: 'file',
path: '/Images/hero-image.jpg',
parent_id: 'media_images',
file_uuid: 'file_hero',
name: 'hero-image.jpg',
file: {
ulid: 'file_hero',
name: 'hero-image.jpg',
mimeType: 'image/jpeg',
diskPath: 'brand/file_hero/hero-image.jpg',
size: 842120,
publicUrl: '/media/file_hero/hero-image.jpg',
thumbnailUrl: '/media/file_hero/thumb.jpg',
meta: { alt: 'Dashboard hero card', width: 1600, height: 900 },
},
},
],
breadcrumbs: [
{ label: 'Root', path: '/', ulid: 'media_root_brand' },
{ label: 'Images', path: '/Images', ulid: 'media_images' },
],
meta: {
rootId: 'media_root_brand',
path: '/Images',
total: 1,
requestedAt: '2026-07-01T10:00:00.000Z',
},
}Store
Configurable Pinia store
The demo store exports a configured demo instance and a reusable store factory. Projects can provide REST endpoint roots or a custom API adapter with the same method names. Rows and files are stored in reactive object maps, so external inspector and detail components can observe file metadata updates without replacing the whole collection.
import { defineMediaBrowserStore } from '@getdom/studio/vue';
export const useProjectMediaStore = defineMediaBrowserStore('project-media', {
rootId: 'media_root_brand',
baseUrl: '/api/media',
filesBaseUrl: '/api/files',
mode: 'grid',
});
// The store updates currentPath and derived breadcrumbs immediately.
// Cached children stay visible while the server refresh request runs.
await store.loadFolder('/Images/Campaign');REST
Optimistic mutations
The store updates visible rows first, marks the row as saving, sends the REST request, then replaces the optimistic row with the canonical server response. Rename and move stay server-defined, so descendant paths and duplicate names are resolved by the API.
POST /api/media/folders
{
rootId: 'media_root_brand',
parentId: 'media_images',
path: '/Images',
name: 'Campaign',
tempUlid: 'temp_folder_lkx',
}
PATCH /api/media/:ulid
{
name: 'Brand hero.jpg'
}
PATCH /api/media/move
{
ulids: ['media_hero', 'media_product'],
targetParentId: 'media_campaign',
}
PATCH /api/files/:fileUuid/meta
{
meta: {
alt: 'Product interface shown on a laptop',
publicUrl: '/media/file_product/product-shot.png',
}
}Upload
Chunk upload contract
Drops are folder-aware where the browser exposes relative paths. The component emits dropped file records; the store creates temporary media rows with upload status fields and image object previews; the upload service reports per-file and overall progress; completion returns canonical rows.
POST /api/media/uploads
{
rootId: 'media_root_brand',
parentId: 'media_images',
path: '/Images',
chunkSize: 5242880,
files: [
{
tempUlid: 'temp_upload_8',
name: 'homepage.png',
relativePath: 'homepage.png',
pathSegments: ['homepage.png'],
size: 628120,
mimeType: 'image/png',
},
{
tempUlid: 'temp_upload_9',
name: 'detail.png',
relativePath: 'Campaign/detail.png',
pathSegments: ['Campaign', 'detail.png'],
size: 450120,
mimeType: 'image/png',
},
],
}
// Progress event emitted by the uploader service
{
overall: 64,
files: [
{ tempUlid: 'temp_upload_8', progress: 68 },
{ tempUlid: 'temp_upload_9', progress: 61 },
],
}
// Completion response
{
data: [
/* created media rows, using tempUlid when the server accepts it */
],
visible: [
/* canonical children for the current folder after upload */
],
meta: {
action: 'upload-batch',
total: 2,
requestedAt: '2026-07-01T10:01:00.000Z',
},
}Routing
Folder paths in the URL
Use a catch-all child route when a browser route should deep-link into folders. The page converts route params to the media path, loads the folder through the store, and pushes breadcrumb or folder navigation back into the URL.
// src/routes.js
{
path: '/data/media-browser/:folderPath(.*)*',
component: MediaBrowserPage,
meta: routeMeta('/data/media-browser'),
}
// Inside a page that wraps DomMediaBrowser
function routePathForFolder(path) {
if (path === '/') return '/data/media-browser';
const encoded = path
.split('/')
.filter(Boolean)
.map((segment) => encodeURIComponent(segment))
.join('/');
return `/data/media-browser/${encoded}`;
}
function pathFromRouteParam(folderPath) {
const parts = Array.isArray(folderPath) ? folderPath : [folderPath].filter(Boolean);
return parts.length ? `/${parts.map(decodeURIComponent).join('/')}` : '/';
}Rendering
Custom file components
The built-in renderer covers folder, image, pdf, and default file rows. Product teams can register components per kind, or replace the item, inspector, and detail slots entirely when a file type needs a bespoke preview, metadata editor, or full detail view. Detail components can open in a dialog or inline inside the browser; folders keep using the navigation event.
<DomMediaBrowser
:items="store.items"
:item-components="{
image: ImageMediaTile,
pdf: PdfMediaTile,
default: FileMediaTile,
}"
:inspector-components="{
image: ImageMetadataInspector,
default: FileMetadataInspector,
}"
:detail-components="{
image: { component: ImageMediaDetail, mode: 'dialog' },
pdf: { component: PdfMediaDetail, mode: 'inline' },
}"
/>
<!-- Or replace the whole visible row. -->
<template #item="{ item, kind, selected, rename }">
<MyMediaRow
:item="item"
:kind="kind"
:selected="selected"
@rename="rename"
/>
</template>Source
Store and mock API
useMediaBrowserDemoStore.js
mediaBrowserMockApi.js
Reference
Props
Control props
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
items | array | Array<unknown> | [] | Media rows for the current folder. Rows usually come from the Pinia store after a folder API request. |
breadcrumbs | array | Array<unknown> | [] | Clickable folder path segments. When omitted, the component derives labels from currentPath. |
currentPath | string | string | '/' | Slash-prefixed folder path currently being rendered. |
selectedKeys | array | Array<unknown> | [] | Controlled media row ids selected in the browser. |
mode | 'grid' | 'list' | string | 'grid' | Visual display mode. |
loading | boolean | boolean | false | Show the current folder as loading while the store waits for the API. |
toolbar | boolean | boolean | true | Render the built-in breadcrumb and action toolbar. |
title | string | string | 'Media browser' | Accessible title for the browser surface. |
emptyText | string | string | 'Drop files here or create a folder to start this library.' | Message shown when the current folder has no media rows. |
inspector | boolean | boolean | true | Show an inspector panel for the active selection. |
canUpload | boolean | boolean | true | Allow external files and folders to be dropped onto the browser. |
canCreateFolder | boolean | boolean | true | Show create-folder actions and emit create-folder events. |
canRename | boolean | boolean | true | Allow selected rows to enter inline rename mode. |
canMove | boolean | boolean | true | Allow internal rows to be dragged onto folders. |
rubberBand | boolean | boolean | true | Allow click-drag rectangle selection across visible rows. |
selectionMode | 'single' | 'multiple' | string | 'multiple' | Selection behavior for picker and library workflows. |
selectableTypes | array | Array<unknown> | ["dir","file"] | Media row types that can be selected. Use ["file"] with accept="image/*" for an image picker. |
accept | string | array | Array<unknown> | '' | Accepted file MIME patterns or extensions for file selection, such as image/* or .pdf. |
itemComponents | object | Record<string, unknown> | {} | Component map keyed by folder, image, pdf, or default for custom item renderers. |
inspectorComponents | object | Record<string, unknown> | {} | Component map keyed by folder, image, pdf, or default for custom inspector renderers. |
detailComponents | object | Record<string, unknown> | {} | Component map keyed by folder, image, pdf, or default. Values may be a component or { component, mode }. |
detailMode | 'dialog' | 'inline' | string | 'dialog' | Default placement when a detail component does not define its own mode. |
renameDelay | number | number | 260 | Delay in milliseconds before a selected filename click enters rename mode, leaving double-click open intact. |
uploadProgress | number | number | — | Overall upload progress. When omitted, progress is inferred from uploading rows. |
Auto-generated from Media browser.props and inline _edit hints.
Events
| Name | Payload | Description |
|---|---|---|
| @update:selectedKeys | Array<string> | Fired when the user changes the selected media rows. |
| @update:mode | 'grid' | 'list' | Fired when the user switches between grid and list layout. |
| @selection-change | MediaBrowserSelectionPayload | Fired with selected keys and resolved rows. |
| @navigate | ({ path, item, breadcrumb, event }) | Fired when the user opens a folder or breadcrumb. |
| @create-folder | ({ path }) | Fired when the built-in new-folder action is activated. |
| @rename | ({ item, name }) | Fired after inline rename is committed. |
| @move | MediaBrowserMovePayload | Fired when one or more selected rows are dropped onto a folder. |
| @upload | MediaBrowserUploadPayload | Fired when files or folders are dropped onto the current browser or a folder. |
| @metadata-update | ({ item, meta }) | Fired when the default inspector commits editable metadata such as image alt text. |
| @make-folder-with-items | ({ items, path }) | Fired from the context menu when the selection should be grouped into a new folder. |
| @context-action | ({ action, items, path }) | Fired for context menu actions after built-in handling. |
| @detail-open | MediaBrowserDetailPayload | Fired when a row with a detail component opens in dialog or inline mode. |
| @detail-close | MediaBrowserDetailPayload | Fired when the current detail component closes. |
| @open | MediaBrowserItemPayload | Fired when a non-folder row is opened. |
| @item-click | MediaBrowserItemPayload | Fired before selection handling when a visible row is clicked. |
Names auto-detected from defineEmits and source emit() calls; payload and description from __doc.events when present.
Slots
| Name | Scope | Description |
|---|---|---|
| #toolbar | { selectedItems, selectedKeys, mode, currentPath, setMode, viewModeOptions } | Replace or extend the toolbar for detached picker controls. |
| #item | { item, kind, selected, disabled, mode, rename } | Replace the full item body. |
| #inspector | { item, kind, file, selectedItems } | Replace the inspector panel body. |
| #detail | { item, kind, file, mode, close } | Replace a detail component body for dialog or inline rendering. |
| #empty | — | Custom empty-folder state. |
Keyboard
- Enter on folderNavigate into the folder.
- Enter / Space on fileSelect the file.
- F2 on selected rowStart inline rename.
- Enter in rename inputCommit the rename.
- Esc in rename inputCancel the rename.