Pattern gallery

Compact dropzone card

One dashed row invites a drop, then shows what landed or what went wrong.

What this solves

Self-explaining action container: the same dashed row tells the user what to do, what it did, and what failed, so no separate status line or toast has to carry that information.
Use it when
  • A job's file staging step, before a run startsspeedway NewJobForm.tsx: compact once files exist, inside a Card that goes borderless
  • A catalog import's entire entry pointwalmart-mvp ImportParts.tsx: the dropzone IS the card, no wrapper at all

Rendered

Fixture data; check both themes.

Files

0 selected
Drag files here, or click to browseNew uploads land in Files too, so other jobs can reuse them.
patterns/compact-dropzone/example.tsxCopy this into a fresh route and it renders as above.
"use client";
import { Card, Dropzone, Tabs } from "@versable-git/ui";
import { useState } from "react";
// Both apps converge on the same ruling: when the dropzone is compact, the
// surface around it goes borderless and the dashed edge IS the card. The
// Tabs knob switches one dropzone through its three real states: nothing
// picked yet, a file queued, and a rejected file.
type View = "empty" | "queued" | "error";
const queuedFiles = [{ name: "fitment-update.csv", size: "88 KB" }];
const CARD_SUBTITLE: Record<View, string> = {
empty: "0 selected",
queued: "1 selected",
error: "Upload failed",
};
export function CompactDropzone() {
const [view, setView] = useState<View>("empty");
const files = view === "queued" ? queuedFiles : [];
const compact = files.length > 0;
return (
<div className="flex flex-col gap-3">
<Tabs
size="sm"
value={view}
onChange={(v) => setView(v as View)}
items={[
{ value: "empty", label: "Empty" },
{ value: "queued", label: "One file queued" },
{ value: "error", label: "Error" },
]}
/>
<Card title="Files" subtitle={CARD_SUBTITLE[view]} noBorder noAnimate bodyClassName="px-0">
<Dropzone
multiple
compact={compact}
accept=".csv,.tsv,.xlsx,.xls,.xml"
Icon="CloudUpload"
error={view === "error" ? "brake-pads-q3.xlsx exceeds the 25 MB upload limit." : undefined}
headline={
<span className="flex min-w-[200px] flex-1 flex-col text-left">
<span className="text-[15px] font-bold">Drag files here, or click to browse</span>
<span className="text-base-content/65 mt-0.5 text-[13px] font-normal">
New uploads land in Files too, so other jobs can reuse them.
</span>
</span>
}
hint={null}
browseLabel="Add files"
onFiles={() => {}}
className="bg-base-100 hover:border-primary rounded-box border-secondary/25 flex-row flex-wrap items-center gap-3.5 border border-dashed p-5 text-left"
/>
{files.length > 0 && (
<ul className="mt-3 flex flex-col gap-1">
{files.map((f) => (
<li key={f.name} className="text-base-content/70 flex justify-between text-sm">
<span>{f.name}</span>
<span className="text-base-content/45">{f.size}</span>
</li>
))}
</ul>
)}
</Card>
</div>
);
}

Where it ships

  • speedway/app/components/NewJobForm.tsx:551-563the app's only live dropzone: state-driven compact inside a Card that goes borderless when compact (noBorder, NewJobForm.tsx:542-546)
  • walmart-mvp/frontend/src/pages/ImportParts.tsx:305-327the same row with the wrapper banned outright: the dropzone IS the card (in-code ruling at ImportParts.tsx:302-303)

App-specific: The two apps reach the borderless surface by opposite means (speedway keeps the Card and drops its border; walmart drops the Card), and this page shows the convergence rather than one side. Real forms drive compact from state, flipping once any file lands, and wire onFiles into staging.

@versable-git/ui · composites proven in the apps