Details Panel

Compose a file list with a consumer-owned details sidebar.

Discovery
12 items
Exhibits
8 items
Q4FinancialReportBennettVCarterLitigationBudgetAndSettlementExposureAnalysisFinalReviewedCopy.pdf
2.4 MB
Deposition Outline.docx
864 KB
Settlement Strategy.pptx
2.9 MB
Filed Order.pdf
627 KB

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.

components/files-details-panel/index.tsx
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>
  );
}
components/files-details-panel/details-sidebar.tsx
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>
  );
}
components/files-details-panel/data.ts
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",
    },
  },
];