Details Panel
Compose a file list with a consumer-owned details sidebar.
This example keeps Files.Details focused on metadata display and inline rename
mechanics. The consumer owns the file collection, selected IDs, rename mutation,
product-specific metadata, and the surrounding split layout.
import { useMemo, useState } from "react";
import {
Files,
type FilesItem,
} from "@tilt-legal/cubitt-components/files";
import { Separator } from "@tilt-legal/cubitt-components/separator";
import { toast } from "@tilt-legal/cubitt-components/toast";
import { formatPlural } from "@tilt-legal/cubitt-components/utilities/formatters";
import { MatterFileDetailsPanel } from "@/components/files-details-panel/details-sidebar";
import {
matterFiles,
type MatterFileSource,
} from "@/components/files-details-panel/data";
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function FilesDetailsPanel() {
const [items, setItems] = useState(matterFiles);
const [selectedIds, setSelectedIds] = useState<string[]>([
"financial-report",
]);
const selectedItems = useMemo(
() =>
selectedIds
.map((id) => items.find((item) => item.id === id))
.filter((item): item is FilesItem<MatterFileSource> => Boolean(item)),
[items, selectedIds],
);
const currentDetailsItem = useMemo<FilesItem<MatterFileSource>>(
() => ({
id: "current-files",
kind: "folder",
name: "Files",
description: formatPlural(items.length, "item"),
source: {
matter: "Bennett v Carter",
owner: "Matter team",
status: "Reviewed",
updatedLabel: "Today",
},
}),
[items.length],
);
const detailsItems =
selectedItems.length > 0 ? selectedItems : [currentDetailsItem];
async function renameItem(item: FilesItem<MatterFileSource>, name: string) {
await sleep(300);
setItems((currentItems) =>
currentItems.map((currentItem) =>
currentItem.id === item.id ? { ...currentItem, name } : currentItem,
),
);
toast.success("File renamed", { description: name });
}
return (
<Files
className="h-[420px] w-full max-w-4xl flex-row gap-6"
canRename={(item) => item.id !== currentDetailsItem.id}
detailsItem={currentDetailsItem}
items={items}
onRename={renameItem}
onSelectedIdsChange={setSelectedIds}
selectedIds={selectedIds}
selectionMode="multiple"
>
<Files.Content className="overflow-auto">
<Files.List>
{items.map((item) => (
<Files.Item item={item} key={item.id} />
))}
</Files.List>
</Files.Content>
<Separator orientation="vertical" />
<MatterFileDetailsPanel
className="min-h-0 min-w-[400px] basis-[400px] shrink-0"
selectedItems={detailsItems}
/>
</Files>
);
}import {
Files,
type FilesItem,
} from "@tilt-legal/cubitt-components/files";
import { Separator } from "@tilt-legal/cubitt-components/separator";
import {
formatBytes,
formatPlural,
} from "@tilt-legal/cubitt-components/utilities/formatters";
import type { MatterFileSource } from "@/components/files-details-panel/data";
type Props = {
className?: string;
selectedItems: FilesItem<MatterFileSource>[];
};
export function MatterFileDetailsPanel({
className,
selectedItems,
}: Props) {
const singleItem = selectedItems.length === 1 ? selectedItems[0] : null;
const matterCount = new Set(
selectedItems.map((item) => item.source?.matter).filter(Boolean),
).size;
const ownerCount = new Set(
selectedItems.map((item) => item.source?.owner).filter(Boolean),
).size;
const selectedTotalSizeLabel = selectedItems.every(
(item) => typeof item.size === "number",
)
? formatBytes(
selectedItems.reduce(
(totalSize, item) => totalSize + (item.size ?? 0),
0,
),
)
: null;
return (
<Files.Details className={className}>
<Files.Details.Icon />
<Files.Details.Header>
<Files.Details.Heading>
<Files.Details.Name />
<Files.Details.Type />
</Files.Details.Heading>
<Files.Details.EditNameButton />
</Files.Details.Header>
<Files.Details.Panels>
<Files.Details.Panel label="Details" value="details">
<Separator />
<Files.Details.Properties>
{singleItem?.source?.matter ? (
<Files.Details.Property>
<Files.Details.Property.Label>
Matter
</Files.Details.Property.Label>
<Files.Details.Property.Value>
{singleItem.source.matter}
</Files.Details.Property.Value>
</Files.Details.Property>
) : null}
{singleItem?.source?.owner ? (
<Files.Details.Property>
<Files.Details.Property.Label>
Owner
</Files.Details.Property.Label>
<Files.Details.Property.Value>
{singleItem.source.owner}
</Files.Details.Property.Value>
</Files.Details.Property>
) : null}
{singleItem?.source?.status ? (
<Files.Details.Property>
<Files.Details.Property.Label>
Status
</Files.Details.Property.Label>
<Files.Details.Property.Value>
{singleItem.source.status}
</Files.Details.Property.Value>
</Files.Details.Property>
) : null}
{singleItem?.source?.updatedLabel ? (
<Files.Details.Property>
<Files.Details.Property.Label>
Updated
</Files.Details.Property.Label>
<Files.Details.Property.Value>
{singleItem.source.updatedLabel}
</Files.Details.Property.Value>
</Files.Details.Property>
) : null}
{singleItem?.sizeLabel ? (
<Files.Details.Property>
<Files.Details.Property.Label>
Size
</Files.Details.Property.Label>
<Files.Details.Property.Value>
{singleItem.sizeLabel}
</Files.Details.Property.Value>
</Files.Details.Property>
) : null}
{selectedItems.length > 1 ? (
<Files.Details.Property>
<Files.Details.Property.Label>
Files selected
</Files.Details.Property.Label>
<Files.Details.Property.Value>
{selectedItems.length}
</Files.Details.Property.Value>
</Files.Details.Property>
) : null}
{selectedItems.length > 1 && selectedTotalSizeLabel ? (
<Files.Details.Property>
<Files.Details.Property.Label>
Total size
</Files.Details.Property.Label>
<Files.Details.Property.Value>
{selectedTotalSizeLabel}
</Files.Details.Property.Value>
</Files.Details.Property>
) : null}
{selectedItems.length > 1 ? (
<Files.Details.Property>
<Files.Details.Property.Label>
Matters
</Files.Details.Property.Label>
<Files.Details.Property.Value>
{formatPlural(matterCount, "matter")}
</Files.Details.Property.Value>
</Files.Details.Property>
) : null}
{selectedItems.length > 1 ? (
<Files.Details.Property>
<Files.Details.Property.Label>
Owners
</Files.Details.Property.Label>
<Files.Details.Property.Value>
{formatPlural(ownerCount, "owner")}
</Files.Details.Property.Value>
</Files.Details.Property>
) : null}
</Files.Details.Properties>
</Files.Details.Panel>
</Files.Details.Panels>
</Files.Details>
);
}import type { FilesItem } from "@tilt-legal/cubitt-components/files";
export type MatterFileSource = {
matter: string;
owner: string;
status: "Draft" | "Reviewed" | "Filed";
updatedLabel: string;
};
export const matterFiles: FilesItem<MatterFileSource>[] = [
{
id: "financial-report",
kind: "file",
name: "Q4FinancialReportBennettVCarterLitigationBudgetAndSettlementExposureAnalysisFinalReviewedCopy.pdf",
mediaType: "application/pdf",
size: 2_458_624,
sizeLabel: "2.4 MB",
description: "Sarah Chen",
source: {
matter: "Bennett v Carter",
owner: "Sarah Chen",
status: "Reviewed",
updatedLabel: "2 days ago",
},
},
{
id: "deposition-outline",
kind: "file",
name: "Deposition Outline.docx",
mediaType:
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
size: 884_736,
sizeLabel: "864 KB",
description: "Ava Johnson",
source: {
matter: "Hudson Contract Review",
owner: "Ava Johnson",
status: "Draft",
updatedLabel: "Apr 18",
},
},
{
id: "folder-discovery",
kind: "folder",
name: "Discovery",
description: "12 items",
source: {
matter: "Bennett v Carter",
owner: "Sarah Chen",
status: "Draft",
updatedLabel: "Today",
},
},
];