New
Import / Export
A data import flow with a glass-surfaced drop zone, progress states, and results summary — plus a structured export flow with format and field selection.
Demo
Drop file here or click to browse
Supports CSV and JSON — up to 50 MB
Import flow states
| Phase | UI |
|---|---|
| Idle | Drop zone with dashed border, upload icon, format hint |
| Drag over | Glass surface activates: backdrop-filter, accent border, specular top edge |
| Uploading | Progress bar at ~40%, “Uploading…” label |
| Processing | Progress bar at ~85%, “Processing rows…” label |
| Results | 3-tile summary (imported / skipped / errors) + scrollable error table |
Drop zone glass behaviour
The drop zone uses backdrop-filter: blur(8px) saturate(160%) and a specular inset highlight (inset 0 1px 0 rgba(255,255,255,0.08)) when a file is dragged over it. This is the same Liquid Glass treatment used on the Feature Gate overlay and the Context Menu — the brightening mimics sfBrandLitSurface.
Apply @media (prefers-reduced-transparency) fallback: replace the backdrop filter with a solid accent-tinted background.
Implementation
ImportExport.tsx
tsx
"use client";
import { useState, useRef, DragEvent } from "react";
import { Upload, Download } from "lucide-react";
// ── Import Flow ────────────────────────────────────────────────
type ImportPhase = "idle" | "uploading" | "processing" | "results";
interface ImportResult {
imported: number;
skipped: number;
errors: Array<{ row: number; reason: string }>;
}
export function ImportFlow({ onComplete }: { onComplete?: (result: ImportResult) => void }) {
const [phase, setPhase] = useState<ImportPhase>("idle");
const [isDragOver, setIsDragOver] = useState(false);
const [result, setResult] = useState<ImportResult | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
function handleFile(file: File) {
setPhase("uploading");
// Simulate processing
setTimeout(() => {
setPhase("processing");
setTimeout(() => {
const mockResult: ImportResult = {
imported: 142,
skipped: 3,
errors: [
{ row: 17, reason: "Missing required field: company" },
{ row: 45, reason: "Invalid date format" },
{ row: 98, reason: "Duplicate entry" },
],
};
setResult(mockResult);
setPhase("results");
onComplete?.(mockResult);
}, 1500);
}, 1000);
}
function handleDrop(e: DragEvent<HTMLDivElement>) {
e.preventDefault();
setIsDragOver(false);
const file = e.dataTransfer.files[0];
if (file) handleFile(file);
}
if (phase === "idle") {
return (
<div
onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }}
onDragLeave={() => setIsDragOver(false)}
onDrop={handleDrop}
onClick={() => inputRef.current?.click()}
style={{
border: `2px dashed ${isDragOver ? "rgb(var(--accent))" : "rgb(var(--border))"}`,
borderRadius: 12,
padding: "40px 24px",
textAlign: "center",
cursor: "pointer",
background: isDragOver
? "rgb(var(--accent) / 0.06)"
: "rgb(var(--surface))",
// Glass specular on drag-over
boxShadow: isDragOver ? "inset 0 1px 0 rgba(255,255,255,0.08)" : "none",
transition: "background 0.15s, border-color 0.15s",
backdropFilter: isDragOver ? "blur(8px)" : "none",
}}
>
<input ref={inputRef} type="file" accept=".csv,.json" style={{ display: "none" }}
onChange={(e) => e.target.files?.[0] && handleFile(e.target.files[0])} />
<Upload size={32} style={{ margin: "0 auto 12px", color: isDragOver ? "rgb(var(--accent))" : "rgb(var(--text-tertiary))" }} />
<p style={{ fontWeight: 600, marginBottom: 4 }}>
{isDragOver ? "Drop to import" : "Drop file here or click to browse"}
</p>
<p style={{ fontSize: 13, color: "rgb(var(--text-tertiary))" }}>
Supports CSV and JSON — up to 50 MB
</p>
</div>
);
}
if (phase === "uploading" || phase === "processing") {
return (
<div style={{ textAlign: "center", padding: "40px 24px" }}>
<p style={{ fontWeight: 600, marginBottom: 16 }}>
{phase === "uploading" ? "Uploading…" : "Processing rows…"}
</p>
<div style={{ height: 6, borderRadius: 99, background: "rgb(var(--progress-track))", overflow: "hidden" }}>
<div style={{
height: "100%",
width: phase === "uploading" ? "40%" : "85%",
borderRadius: 99,
background: "rgb(var(--accent))",
transition: "width 1s ease-in-out",
}} />
</div>
</div>
);
}
if (phase === "results" && result) {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
{/* Summary */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 10 }}>
{[
{ label: "Imported", value: result.imported, color: "rgb(var(--status-success))" },
{ label: "Skipped", value: result.skipped, color: "rgb(var(--status-warning))" },
{ label: "Errors", value: result.errors.length, color: "rgb(var(--status-danger))" },
].map((s) => (
<div key={s.label} style={{ padding: "12px", borderRadius: 8, background: "rgb(var(--surface))", border: "1px solid rgb(var(--border))", textAlign: "center" }}>
<div style={{ fontSize: 22, fontWeight: 700, color: s.color, fontVariantNumeric: "tabular-nums" }}>{s.value}</div>
<div style={{ fontSize: 11, color: "rgb(var(--text-tertiary))", marginTop: 2 }}>{s.label}</div>
</div>
))}
</div>
{/* Error table */}
{result.errors.length > 0 && (
<div style={{ borderRadius: 8, border: "1px solid rgb(var(--border))", overflow: "hidden" }}>
<div style={{ padding: "8px 12px", background: "rgb(var(--surface-raised))", borderBottom: "1px solid rgb(var(--border))", fontSize: 11, fontWeight: 600, color: "rgb(var(--text-tertiary))" }}>
Errors ({result.errors.length})
</div>
{result.errors.map((err) => (
<div key={err.row} style={{ display: "flex", gap: 12, alignItems: "center", padding: "8px 12px", borderBottom: "1px solid rgb(var(--border-subtle))", fontSize: 12 }}>
<span style={{ color: "rgb(var(--text-tertiary))", fontVariantNumeric: "tabular-nums", flexShrink: 0 }}>Row {err.row}</span>
<span style={{ color: "rgb(var(--status-danger))" }}>{err.reason}</span>
</div>
))}
</div>
)}
<button onClick={() => { setPhase("idle"); setResult(null); }} style={{ alignSelf: "flex-start", padding: "8px 16px", borderRadius: 8, border: "1px solid rgb(var(--border))", background: "rgb(var(--surface-raised))", cursor: "pointer", fontSize: 13 }}>
Import another file
</button>
</div>
);
}
return null;
}
// ── Export Flow ────────────────────────────────────────────────
type ExportFormat = "csv" | "json";
export function ExportFlow() {
const [format, setFormat] = useState<ExportFormat>("csv");
const allFields = ["name", "company", "status", "date", "salary", "notes", "tags"];
const [selected, setSelected] = useState<Set<string>>(new Set(allFields.slice(0, 5)));
function toggleField(f: string) {
setSelected((prev) => {
const next = new Set(prev);
next.has(f) ? next.delete(f) : next.add(f);
return next;
});
}
return (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
{/* Format picker */}
<div>
<p style={{ fontSize: 12, fontWeight: 600, marginBottom: 8 }}>Format</p>
<div style={{ display: "flex", gap: 8 }}>
{(["csv", "json"] as ExportFormat[]).map((f) => (
<button key={f} onClick={() => setFormat(f)} style={{
padding: "7px 16px", borderRadius: 8, fontSize: 13, cursor: "pointer",
background: format === f ? "rgb(var(--accent))" : "rgb(var(--surface-raised))",
color: format === f ? "#fff" : "rgb(var(--text-primary))",
border: `1px solid ${format === f ? "rgb(var(--accent))" : "rgb(var(--border))"}`,
fontWeight: format === f ? 600 : 400,
transition: "all 0.15s",
}}>
{f.toUpperCase()}
</button>
))}
</div>
</div>
{/* Field selection */}
<div>
<p style={{ fontSize: 12, fontWeight: 600, marginBottom: 8 }}>Fields to include</p>
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
{allFields.map((field) => (
<label key={field} style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }}>
<input type="checkbox" checked={selected.has(field)} onChange={() => toggleField(field)} style={{ width: 15, height: 15, accentColor: "rgb(var(--accent))" }} />
<span style={{ fontSize: 13, textTransform: "capitalize" }}>{field}</span>
</label>
))}
</div>
</div>
<button style={{
display: "flex", alignItems: "center", gap: 8, padding: "9px 18px",
borderRadius: 8, background: "rgb(var(--accent))", color: "#fff",
border: "none", cursor: "pointer", fontSize: 14, fontWeight: 600, alignSelf: "flex-start",
}}>
<Download size={15} />
Export {selected.size} fields as {format.toUpperCase()}
</button>
</div>
);
}