Skip to content
All Taste components

Productivity

File list

Shared files with their type, size, last change and a menu of actions per file.

type tiles · size & modified columns · owner · row menu · upload · stacks on narrow widths

Shared files

5 files · 317.5 MB of 50 GB used

PDF
Q3 GST returns.pdf1.2 MB · 14 Oct
1.2 MB14 OctRitika Shah
XLSX
Price list 2026.xlsx86 KB · 12 Oct
86 KB12 OctAsha Raman
PNG
Storefront banner.png3.4 MB · 9 Oct
3.4 MB9 OctArjun Mehta
ZIP
Menu photos, October.zip248 MB · 8 Oct
248 MB8 OctMaya Nair
MP4
Booking walkthrough.mp464.8 MB · 2 Oct
64.8 MB2 OctAsha Raman
"use client";
import { Download, MoreHorizontal, Pencil, Search, Share2, Trash2, Upload } from "lucide-react";
import { cn } from "@/lib/utils";

export interface FileRow { id: string; name: string; size: string; modified: string; owner: string }

const sample: FileRow[] = [
  { id: "1", name: "Q3 GST returns.pdf", size: "1.2 MB", modified: "14 Oct", owner: "Ritika Shah" },
  { id: "2", name: "Price list 2026.xlsx", size: "86 KB", modified: "12 Oct", owner: "Asha Raman" },
  { id: "3", name: "Storefront banner.png", size: "3.4 MB", modified: "9 Oct", owner: "Arjun Mehta" },
  { id: "4", name: "Menu photos, October.zip", size: "248 MB", modified: "8 Oct", owner: "Maya Nair" },
  { id: "5", name: "Booking walkthrough.mp4", size: "64.8 MB", modified: "2 Oct", owner: "Asha Raman" },
];

const typeTone: Record<string, string> = {
  pdf: "bg-danger-soft text-danger-fg",
  xlsx: "bg-success-soft text-success-fg", csv: "bg-success-soft text-success-fg",
  png: "bg-info-soft text-info-fg", jpg: "bg-info-soft text-info-fg",
  zip: "bg-warning-soft text-warning-fg",
};

type Action = "download" | "share" | "rename" | "delete";

function RowMenu({ name, onAction }: { name: string; onAction?: (a: Action) => void }) {
  const items: [Action, string, typeof Download][] = [["download", "Download", Download], ["share", "Share", Share2], ["rename", "Rename", Pencil]];
  return (
    <details className="group relative">
      <summary aria-label={"Actions for " + name} className="grid size-7 cursor-pointer list-none place-items-center rounded text-fg-muted hover:bg-surface-2 group-open:bg-surface-2 [&::-webkit-details-marker]:hidden">
        <MoreHorizontal className="size-4" />
      </summary>
      <div role="menu" className="absolute right-0 top-[calc(100%+4px)] z-10 grid min-w-[168px] rounded border border-border bg-bg p-1 shadow-lg">
        {items.map(([key, label, Icon]) => (
          <button key={key} role="menuitem" onClick={() => onAction?.(key)} className="flex h-8 items-center gap-2 rounded-sm px-2 text-left text-[13px] hover:bg-surface-2">
            <Icon className="size-3.5 text-fg-subtle" aria-hidden />{label}
          </button>
        ))}
        <hr className="my-1 border-border" />
        <button role="menuitem" onClick={() => onAction?.("delete")} className="flex h-8 items-center gap-2 rounded-sm px-2 text-left text-[13px] text-danger hover:bg-surface-2">
          <Trash2 className="size-3.5" aria-hidden />Delete
        </button>
      </div>
    </details>
  );
}

export function FileList({ files = sample, usage = "317.5 MB of 50 GB used", onUpload, onAction }: {
  files?: FileRow[]; usage?: string; onUpload?: () => void; onAction?: (id: string, a: Action) => void;
}) {
  const cols = "grid grid-cols-[32px_minmax(0,1fr)_28px] items-center gap-3 px-4 @[520px]:grid-cols-[32px_minmax(0,1fr)_72px_92px_112px_28px]";
  const meta = "hidden whitespace-nowrap text-[13px] tabular-nums text-fg-muted @[520px]:block";

  return (
    // Uses Tailwind container queries (@tailwindcss/container-queries, built into v4).
    <section className="@container w-full rounded-lg border border-border bg-bg">
      <header className="flex flex-wrap items-center justify-between gap-3 p-4">
        <div>
          <h3 className="text-sm font-medium">Shared files</h3>
          <p className="text-[12.5px] text-fg-subtle">{files.length} files · {usage}</p>
        </div>
        <div className="flex gap-2">
          <button aria-label="Search files" className="grid size-[30px] place-items-center rounded border border-border-strong hover:bg-surface"><Search className="size-3.5" /></button>
          <button onClick={onUpload} className="inline-flex h-[30px] items-center gap-1.5 rounded bg-primary px-2.5 text-[13px] font-medium text-primary-fg hover:bg-primary-hover">
            <Upload className="size-3.5" aria-hidden /> Upload
          </button>
        </div>
      </header>
      <div className={cn(cols, "border-t border-border py-2 text-xs font-medium text-fg-subtle")} aria-hidden>
        <span /><span>Name</span><span className={meta}>Size</span><span className={meta}>Modified</span><span className={meta}>Owner</span><span />
      </div>
      <ul>
        {files.map((f) => {
          const ext = f.name.split(".").pop()?.toLowerCase() ?? "";
          return (
            <li key={f.id} className={cn(cols, "border-t border-border py-2.5 text-[13.5px] hover:bg-surface")}>
              <span className={cn("grid size-8 place-items-center rounded-sm text-[9.5px] font-semibold uppercase tracking-wide", typeTone[ext] ?? "bg-surface-2 text-fg-muted")}>{ext}</span>
              <div className="min-w-0">
                <p className="truncate font-medium" title={f.name}>{f.name}</p>
                <p className="text-xs tabular-nums text-fg-subtle @[520px]:hidden">{f.size} · {f.modified}</p>
              </div>
              <span className={meta}>{f.size}</span>
              <span className={meta}>{f.modified}</span>
              <span className={cn(meta, "truncate")}>{f.owner}</span>
              <RowMenu name={f.name} onAction={(a) => onAction?.(f.id, a)} />
            </li>
          );
        })}
      </ul>
    </section>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.