the embedded file browser can name its own root crumb

`rootLabel` on FileBrowserApp, through to the breadcrumb. it defaults to the base
folder's own name, which is right for /files and wrong for an embedder: a panel
scoped to `<root>/media/originals` is called Media, and "originals" is an
implementation detail of how that plugin lays out a directory. the person looking at
it never chose that word and has no reason to recognise it.

one prop rather than the plugin drawing its own breadcrumb — which is the same trade
as embedding the browser at all: upload, delete, thumbnails and the context menu
come from one maintained place, and this is the small thing that was missing to make
that honest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-29 08:50:30 +01:00
co-authored by Claude Opus 5
parent 03638a7f62
commit cad0ea3c9c
2 changed files with 14 additions and 3 deletions
@@ -11,6 +11,8 @@ import { useFileBrowserApp } from './useFileBrowserApp';
type FileBrowserAppProps = {
basePath?: string;
rootOverride?: string;
/** What the root crumb is called. Defaults to the base folder's own name. */
rootLabel?: string;
/** Put the current folder in `?path=`. Opt-in — only the /files screen is guaranteed one browser. */
urlPath?: boolean;
/**
@@ -20,7 +22,7 @@ type FileBrowserAppProps = {
panelId?: string;
};
export const FileBrowserApp = ({ basePath = '/', rootOverride, urlPath, panelId }: FileBrowserAppProps) => {
export const FileBrowserApp = ({ basePath = '/', rootOverride, rootLabel, urlPath, panelId }: FileBrowserAppProps) => {
const fileBrowserManager = useFileBrowserApp(basePath, rootOverride, urlPath, panelId);
const { handleNavigate } = fileBrowserManager;
@@ -31,6 +33,7 @@ export const FileBrowserApp = ({ basePath = '/', rootOverride, urlPath, panelId
path={fileBrowserManager.currentPath}
onNavigate={handleNavigate}
basePath={basePath}
rootLabel={rootLabel}
hrefForPath={fileBrowserManager.hrefForPath}
/>
<UploadProgress fileBrowserManager={fileBrowserManager} />
@@ -5,6 +5,14 @@ type BreadcrumbProps = {
path: string;
onNavigate: (path: string) => void;
basePath?: string;
/**
* What to call the root crumb.
*
* Defaults to the base folder's own name, which is right for `/files` and wrong for an embedder: a
* panel scoped to `<root>/media/originals` is called Media, and "originals" is an implementation
* detail of how that plugin lays out a directory. The person looking at it never chose that word.
*/
rootLabel?: string;
/**
* Where a folder lives, when this browser owns the address bar. Given, the crumbs are real links and
* cmd-click opens the folder in a new tab; absent (a dashboard panel, the widget) they stay buttons,
@@ -15,7 +23,7 @@ type BreadcrumbProps = {
hrefForPath?: ((path: string) => { pathname: string; search: string }) | null;
};
export const Breadcrumb = ({ path, onNavigate, basePath = '/', hrefForPath }: BreadcrumbProps) => {
export const Breadcrumb = ({ path, onNavigate, basePath = '/', hrefForPath, rootLabel }: BreadcrumbProps) => {
const relativePath = basePath !== '/' && path.startsWith(basePath) ? path.slice(basePath.length) : path;
const segments = relativePath.split('/').filter(Boolean);
@@ -35,7 +43,7 @@ export const Breadcrumb = ({ path, onNavigate, basePath = '/', hrefForPath }: Br
<nav className="flex items-center gap-1 text-sm flex-wrap">
<Crumb to={basePath} className="flex items-center gap-1 text-duck-teal cursor-pointer font-medium">
<Home className="h-4 w-4" />
<span>{basePath === '/' ? 'home' : basePath.split('/').pop()}</span>
<span>{rootLabel ?? (basePath === '/' ? 'home' : basePath.split('/').pop())}</span>
</Crumb>
{segments.map((segment, i) => {