Komponen / Form & input
Combobox
Pilihan tunggal dengan pencarian untuk daftar panjang — customer, item lisensi, distributor — dengan aksi "Buat baru" opsional bila data belum ada.
Customer dengan Buat baru
Opsi berdeskripsi (kota · industri), baris meta "3 dari 6 customer", pesan kosong yang menyarankan langkah berikutnya, dan aksi buat customer di footer.
Ketik nama customer; deskripsi menampilkan kota & industri.
Nilai tersimpan: CUST-0012
import { useState } from 'react';
import { Combobox, FormField, type ComboboxOption } from '@divistant/circca-react';
import { Buildings } from '@phosphor-icons/react';
const AWAL: ComboboxOption[] = [
{ value: 'CUST-0012', label: 'PT Mitra Energi Persada', description: 'Jakarta Selatan · Energi' },
{ value: 'CUST-0027', label: 'CV Sinar Logistik', description: 'Surabaya · Logistik' },
{ value: 'CUST-0031', label: 'PT Arunika Data', description: 'Bandung · Teknologi' },
{ value: 'CUST-0044', label: 'PT Cakra Media Digital', description: 'Jakarta Barat · Media' },
{ value: 'CUST-0058', label: 'PT Cakrawala Data', description: 'Yogyakarta · Teknologi' },
{ value: 'CUST-0063', label: 'CV Cakra Abadi', description: 'Semarang · Distribusi' },
];
export default function Example() {
const [customers, setCustomers] = useState(AWAL);
const [customer, setCustomer] = useState<string | null>('CUST-0012');
return (
<div className="grid max-w-sm gap-4">
<FormField label="Customer" required helperText="Ketik nama customer; deskripsi menampilkan kota & industri.">
<Combobox
options={customers}
value={customer}
onValueChange={setCustomer}
leadingVisual={Buildings}
placeholder="Cari customer…"
resultLabel="customer"
emptyMessage={teks => `Tidak ada customer bernama "${teks}". Periksa ejaan atau buat customer baru.`}
createLabel={teks => (teks ? `Buat customer "${teks}"` : 'Buat customer baru')}
onCreate={teks => {
// Di aplikasi nyata: buka dialog Customer baru yang sudah terisi nama.
const baru = { value: `CUST-BARU-${customers.length + 1}`, label: teks || 'Customer baru', description: 'Belum lengkap' };
setCustomers(c => [...c, baru]);
setCustomer(baru.value);
}}
/>
</FormField>
<p className="type-footnote-default text-muted">Nilai tersimpan: {customer ?? '—'}</p>
</div>
);
}Pencarian ke server & grup
onInputChange memicu pencarian; selama menunggu, panel menampilkan "Mencari…" + Skeleton. filter={false} karena opsi sudah disaring server. Opsi dikelompokkan lewat `group`.
Dicari ke server; opsi dikelompokkan per kategori.
import { useEffect, useRef, useState } from 'react';
import { Combobox, FormField, type ComboboxOption } from '@divistant/circca-react';
import { Package } from '@phosphor-icons/react';
const KATALOG: ComboboxOption[] = [
{ value: 'LIC-M365-BS', label: 'Microsoft 365 Business Standard', description: 'Per pengguna · 12 bulan', group: 'Lisensi' },
{ value: 'LIC-M365-BP', label: 'Microsoft 365 Business Premium', description: 'Per pengguna · 12 bulan', group: 'Lisensi' },
{ value: 'LIC-FG-60F', label: 'Fortinet FortiGate 60F UTP Bundle', description: 'Per perangkat · 12 bulan', group: 'Lisensi' },
{ value: 'LIC-FC-EMS', label: 'Fortinet FortiClient EMS', description: 'Per endpoint · 12 bulan', group: 'Lisensi' },
{ value: 'SVC-IMPL', label: 'Implementasi Microsoft 365', description: 'Paket · termin', group: 'Jasa' },
{ value: 'SVC-FW', label: 'Konfigurasi firewall Fortinet', description: 'Per lokasi', group: 'Jasa' },
];
// Tiruan API pencarian item: menyaring di "server" dengan jeda.
function cariItem(teks: string) {
return new Promise<ComboboxOption[]>(resolve =>
setTimeout(() => resolve(KATALOG.filter(i => i.label.toLowerCase().includes(teks.toLowerCase()))), 600),
);
}
export default function Example() {
const [item, setItem] = useState<string | null>(null);
const [hasil, setHasil] = useState<ComboboxOption[]>(KATALOG);
const [loading, setLoading] = useState(false);
const permintaan = useRef(0);
const [teks, setTeks] = useState('');
useEffect(() => {
const id = ++permintaan.current;
setLoading(true);
cariItem(teks).then(r => {
if (id !== permintaan.current) return; // abaikan hasil usang
setHasil(r);
setLoading(false);
});
}, [teks]);
return (
<div className="grid max-w-md gap-4">
<FormField label="Item / jasa" helperText="Dicari ke server; opsi dikelompokkan per kategori.">
<Combobox
options={hasil}
filter={false}
loading={loading}
onInputChange={setTeks}
value={item}
onValueChange={v => {
setItem(v);
setTeks(''); // daftar berikutnya dibuka penuh lagi
}}
leadingVisual={Package}
placeholder="Cari lisensi atau jasa…"
resultLabel="item"
totalCount={KATALOG.length}
/>
</FormField>
</div>
);
}Di dalam FormField
Error, hanya-baca (dokumen sudah diajukan), dan nonaktif mengikuti FormField. Opsi nonaktif dilewati keyboard.
Pilih distributor lisensi sebelum mengajukan PO distributor.
PO sudah diajukan — customer tidak bisa diganti.
import { useState } from 'react';
import { Combobox, FormField } from '@divistant/circca-react';
const DISTRIBUTOR = [
{ value: 'DIST-01', label: 'PT Intikom Distribusi', description: 'Microsoft · Fortinet' },
{ value: 'DIST-02', label: 'PT Mitra Lisensi Nusantara', description: 'Microsoft' },
{ value: 'DIST-03', label: 'PT Sentra Keamanan Siber', description: 'Fortinet', disabled: true },
];
export default function Example() {
const [distributor, setDistributor] = useState<string | null>(null);
return (
<div className="grid max-w-sm gap-4">
<FormField
label="Distributor"
required
error={distributor ? undefined : 'Pilih distributor lisensi sebelum mengajukan PO distributor.'}
>
<Combobox options={DISTRIBUTOR} value={distributor} onValueChange={setDistributor} placeholder="Cari distributor…" />
</FormField>
<FormField label="Customer" readOnly helperText="PO sudah diajukan — customer tidak bisa diganti.">
<Combobox
options={[{ value: 'CUST-0012', label: 'PT Mitra Energi Persada' }]}
defaultValue="CUST-0012"
/>
</FormField>
<FormField label="Proyek" disabled>
<Combobox size="sm" options={[]} placeholder="Pilih customer dulu" />
</FormField>
</div>
);
}