Komponen / Form & input
FileUpload
Area unggah lampiran (PO customer, BAST, faktur) dengan daftar file dan progres per file.
Lampiran PO customer
Pemanggil mengunggah dan memperbarui files; di sini disimulasikan dengan timer. File > 10 MB atau berjenis lain ditolak dengan pesan per file.
Tarik file ke sini atau
PDF, JPG, atau PNG · maks. 10 MB
PO-Customer-PT-Pelanggan-Contoh-A.pdf
2,4 MB · diunggah 12 Sep 2026
Lampirkan PO yang sudah ditandatangani customer.
import { useEffect, useRef, useState } from 'react';
import { FileUpload, FormField, type FileUploadItem } from '@divistant/circca-react';
const MAKS = 10 * 1024 * 1024;
// Figma BizOps for IT Services › Template · Form (32:1370): lampiran PO customer.
// Unggahan disimulasikan dengan timer; file bernama "gagal" berhenti di tengah agar "Coba lagi" bisa dicoba.
// Coba juga file > 10 MB atau berjenis lain untuk melihat pesan tolak per file.
export default function Example() {
const [files, setFiles] = useState<FileUploadItem[]>([
{ id: 'contoh', name: 'PO-Customer-PT-Pelanggan-Contoh-A.pdf', size: 2_516_582, status: 'done', meta: '2,4 MB · diunggah 12 Sep 2026' },
]);
const timers = useRef(new Map<string, ReturnType<typeof setInterval>>());
// Bersihkan semua timer saat komponen dilepas.
useEffect(() => {
const map = timers.current;
return () => map.forEach(t => clearInterval(t));
}, []);
const stop = (id: string) => {
clearInterval(timers.current.get(id));
timers.current.delete(id);
};
const jalankan = (id: string, gagal: boolean) => {
stop(id);
timers.current.set(
id,
setInterval(() => {
setFiles(list =>
list.map(f => {
if (f.id !== id || f.status !== 'uploading') return f;
const progress = Math.min((f.progress ?? 0) + 12, 100);
if (gagal && progress >= 48) {
stop(id);
return { ...f, progress, status: 'error', error: 'Koneksi terputus' };
}
if (progress >= 100) {
stop(id);
return { ...f, progress, status: 'done' };
}
return { ...f, progress };
}),
);
}, 400),
);
};
return (
<FormField label="PO customer" required helperText="Lampirkan PO yang sudah ditandatangani customer." className="w-full max-w-lg">
<FileUpload
accept=".pdf,.jpg,.jpeg,.png"
multiple
maxSize={MAKS}
hint="PDF, JPG, atau PNG · maks. 10 MB"
files={files}
onFilesAdd={baru => {
const items = baru.map(f => ({
id: `${f.name}-${f.lastModified}-${Math.random().toString(36).slice(2, 7)}`,
name: f.name,
size: f.size,
progress: 0,
status: 'uploading' as const,
}));
setFiles(list => [...list, ...items]);
items.forEach(it => jalankan(it.id, /gagal/i.test(it.name)));
}}
onRetry={id => {
setFiles(list => list.map(f => (f.id === id ? { ...f, status: 'uploading', progress: 0, error: undefined } : f)));
jalankan(id, false);
}}
onRemove={id => {
stop(id);
setFiles(list => list.filter(f => f.id !== id));
}}
/>
</FormField>
);
}Status item
Mengunggah (ProgressBar), Selesai, dan Gagal dengan "Coba lagi". Aksi: Batalkan unggah, Hapus, Coba lagi.
Tarik file ke sini atau
PDF, JPG, atau PNG · maks. 10 MB
BAST-PO-2026-0142.pdf
2,4 MB · Mengunggah 60%
Faktur-Pajak-INV-2026-0192.pdf
375 KB · diunggah 12 Sep 2026
Foto-serah-terima-perangkat.jpg
4,2 MB · Gagal diunggah: Koneksi terputus
import { FileUpload, type FileUploadItem } from '@divistant/circca-react';
// Tiga status item (Figma FileUpload/Item): Mengunggah dengan ProgressBar, Selesai, dan Gagal dengan "Coba lagi".
// Status tidak hanya lewat warna: teks meta menyebut "Mengunggah 60%" / "Gagal diunggah" dan aksinya berbeda.
const FILES: FileUploadItem[] = [
{ id: '1', name: 'BAST-PO-2026-0142.pdf', size: 2_516_582, status: 'uploading', progress: 60 },
{ id: '2', name: 'Faktur-Pajak-INV-2026-0192.pdf', size: 384_000, status: 'done', meta: '375 KB · diunggah 12 Sep 2026' },
{ id: '3', name: 'Foto-serah-terima-perangkat.jpg', size: 4_404_019, status: 'error', error: 'Koneksi terputus' },
];
export default function Example() {
return (
<FileUpload
aria-label="Lampiran BAST"
className="max-w-lg"
accept=".pdf,.jpg,.png"
multiple
maxSize={10 * 1024 * 1024}
hint="PDF, JPG, atau PNG · maks. 10 MB"
files={FILES}
onRemove={() => {}}
onRetry={() => {}}
/>
);
}Satu file & error
Area drop disembunyikan setelah satu file ada (hideDropzone). Error FormField membuat garis & hint merah.
Tarik file ke sini atau
PDF · maks. 5 MB
import { useEffect, useRef, useState } from 'react';
import { Button, FileUpload, FormField, type FileUploadItem } from '@divistant/circca-react';
// Field satu file (BAST bertanda tangan): area drop disembunyikan setelah file ada; hapus file untuk mengganti.
// Contoh status error di FormField: hint & garis area drop berubah merah, pesan menjelaskan cara memperbaiki.
export default function Example() {
const [files, setFiles] = useState<FileUploadItem[]>([]);
const [dicoba, setDicoba] = useState(false);
const timer = useRef<ReturnType<typeof setInterval>>(undefined);
useEffect(() => () => clearInterval(timer.current), []);
return (
<div className="grid w-full max-w-lg gap-3">
<FormField label="BAST bertanda tangan" required error={dicoba && files.length === 0 ? 'Lampirkan BAST sebelum menandai PO selesai.' : undefined}>
<FileUpload
accept="application/pdf"
maxSize={5 * 1024 * 1024}
hint="PDF · maks. 5 MB"
hideDropzone={files.length > 0}
files={files}
onFilesAdd={([f]) => {
if (!f) return;
const id = `${f.name}-${f.lastModified}`;
setFiles([{ id, name: f.name, size: f.size, status: 'uploading', progress: 0 }]);
clearInterval(timer.current);
timer.current = setInterval(() => {
setFiles(list =>
list.map(x => {
const progress = Math.min((x.progress ?? 0) + 20, 100);
if (progress >= 100) clearInterval(timer.current);
return { ...x, progress, status: progress >= 100 ? 'done' : 'uploading' };
}),
);
}, 300);
}}
onRemove={() => {
clearInterval(timer.current);
setFiles([]);
}}
/>
</FormField>
<Button size="sm" variant="ghost" className="justify-self-start" onClick={() => setDicoba(true)}>
Simulasikan validasi
</Button>
</div>
);
}