Lead the files browser's folders with a tinted type icon

Each folder row in /admin/files now opens with its type as an icon in a
box tinted with its own hue (Bots ember, Workspaces teal, Computers
cobalt, Media violet, MCP servers moss, Database amber), so the picker
reads at a glance and the row's width goes to the name. The box carries
the type as its title, so hovering still names it.
This commit is contained in:
2026-10-22 10:18:35 +01:00
parent aebd6b8ccc
commit eacbe4ab87
2 changed files with 62 additions and 6 deletions
@@ -0,0 +1,39 @@
import {
Bot,
Database,
Images,
Layers,
Monitor,
Plug,
type LucideIcon,
} from "lucide-react";
import type { StorageVolume } from "@/src/utils/storage-volumes";
/**
* The icon each volume's folder rows lead with in the files browser's
* picker, imported one by one (never by name: a string icon ships every
* icon). Route-local so the shared, server-safe storage utils stay free
* of lucide.
*/
export const StorageVolumeIcons: Record<StorageVolume, LucideIcon> = {
bots: Bot,
workspaces: Layers,
computers: Monitor,
media: Images,
mcp: Plug,
db: Database,
};
/**
* The tinted box each volume icon sits in: a soft wash and icon color from
* the hue palette, one hue per volume so the picker reads at a glance.
* Static class names only, so Tailwind keeps them.
*/
export const StorageVolumeIconClasses: Record<StorageVolume, string> = {
bots: "bg-hue-ember/15 text-hue-ember dark:bg-hue-ember/25",
workspaces: "bg-hue-teal/15 text-hue-teal dark:bg-hue-teal/25",
computers: "bg-hue-cobalt/15 text-hue-cobalt dark:bg-hue-cobalt/25",
media: "bg-hue-violet/15 text-hue-violet dark:bg-hue-violet/25",
mcp: "bg-hue-moss/15 text-hue-moss dark:bg-hue-moss/25",
db: "bg-hue-amber/15 text-hue-amber dark:bg-hue-amber/25",
};
@@ -3,6 +3,10 @@ import Span from "@moduletrace/twui/layout/Span";
import { twMerge } from "tailwind-merge";
import type { StorageSpecOption } from "@/src/types";
import { storageSpecHref, storageVolumeLabel } from "@/src/utils/storage-volumes";
import {
StorageVolumeIconClasses,
StorageVolumeIcons,
} from "../(data)/storage-volume-icons";
type Props = {
option: StorageSpecOption;
@@ -12,14 +16,19 @@ type Props = {
/**
* One folder in the files browser's picker: the bot, workspace, computer,
* media library, MCP server or database it is, named the way the crew
* names it.
* media library, MCP server or database it is, its type an icon in a tinted
* box and its name beside it. The box carries the type as its title, so
* hovering names it without spending the row's width on a label.
*/
export default function FilesVolumeRow({ option, selected, onOpen }: Props) {
const volume = option.spec.volume;
const Icon = StorageVolumeIcons[volume];
const type_label = storageVolumeLabel({ volume });
return (
<Link
href={storageSpecHref({ spec: option.spec })}
title={`${storageVolumeLabel({ volume: option.spec.volume })}: ${option.label}`}
title={`${type_label}: ${option.label}`}
aria-current={selected ? "page" : undefined}
onClick={(event) => {
event.preventDefault();
@@ -31,9 +40,17 @@ export default function FilesVolumeRow({ option, selected, onOpen }: Props) {
selected && "bg-ink/5 hover:bg-ink/5",
)}
>
<Span className="truncate text-[13px] font-bold leading-5">{option.label}</Span>
<Span className="ml-auto shrink-0 text-[12px] leading-4 text-ink-secondary">
{storageVolumeLabel({ volume: option.spec.volume })}
<Span
title={type_label}
className={twMerge(
"size-6 shrink-0 flex items-center justify-center rounded-default-sm",
StorageVolumeIconClasses[volume],
)}
>
<Icon size={14} aria-hidden />
</Span>
<Span className="min-w-0 flex-1 truncate text-[13px] font-bold leading-5">
{option.label}
</Span>
</Link>
);