225 lines
14 KiB
Markdown
225 lines
14 KiB
Markdown
# UI/UX Design Skill — Dutra Store (POS Grosir Flutter)
|
||
|
||
Gunakan skill ini sebagai acuan setiap kali mengubah, menambah, atau meninjau tampilan aplikasi POS grosir berbasis Flutter ini. Jika ada pertentangan, prioritaskan yang tercantum di sini.
|
||
|
||
## 1. Tujuan Utama
|
||
- Transaksi cepat tanpa hambatan visual.
|
||
- Tidak ada overflow/keluar layar di ukuran HP kecil.
|
||
- Status stok, harga, dan aksi utama terlihat jelas dalam 1 scan.
|
||
- Maintain konsistensi visual antar screen.
|
||
|
||
## 2. Design System
|
||
|
||
### 2.1 Tema
|
||
- Theme gelap (dark) sebagai basis utama.
|
||
- Warna aksen: amber (`Colors.amber`).
|
||
- Radius default: 18 untuk kartu, 12 untuk input/dialog, 8 untuk chip/tag.
|
||
- Font: gunakan `Theme.of(context).textTheme` agar sesuai pengaturan sistem.
|
||
|
||
### 2.2 Warna Semantik & Token
|
||
- `kAccent` — amber untuk aksen dan highlight.
|
||
- `kSuccess` — hijau untuk status lunas/berhasil.
|
||
- `kDanger` — merah untuk pembatalan/error/stok habis.
|
||
- `kWarning` — amber/oranye untuk peringatan stok rendah, bayar nanti.
|
||
- `kMuted` — abu-abu untuk teks sekunder/timestamp.
|
||
- `kSurface2` — background chip yang tidak aktif.
|
||
- `kBorder` — border untuk chip dan outline.
|
||
- `kText` — warna teks utama (default di theme gelap).
|
||
|
||
### 2.3 Typography Scale
|
||
Gunakan scale berikut, jangan hardcode font size kecuali specified:
|
||
- **Display / H1**: 28px, bold, untuk heading besar
|
||
- **H2**: 22px, bold, untuk section title
|
||
- **H3**: 18px, semi-bold, untuk card title / dialog title
|
||
- **Body large**: 16px, regular, untuk teks isi utama
|
||
- **Body**: 14px, regular, untuk teks body standar
|
||
- **Body small**: 13px, regular, untuk helper/subtitle
|
||
- **Caption**: 12px, regular, untuk timestamp/label kecil
|
||
- **Overline**: 10px, bold, uppercase tracking, untuk badge/status kecil
|
||
- **Monospace**: untuk angka/harga agar stabil
|
||
|
||
Semua angka harga/uang **wajib** menggunakan `formatRupiah()`.
|
||
|
||
### 2.4 Spacing Scale
|
||
Pakai multiples of 4 untuk spacing:
|
||
- 4, 8, 12, 16, 20, 24, 32, 48
|
||
- Padding screen/list: `EdgeInsets.all(12)`
|
||
- Padding form/dialog: `EdgeInsets.all(16)`
|
||
- Gap antar field: `SizedBox(height: 10)` atau `SizedBox(height: 12)`
|
||
- Gap antar section: `SizedBox(height: 14)` atau `SizedBox(height: 20)`
|
||
- Jangan gunakan nilai spacing yang tidak kelipatan 4 kecuali ada alasan khusus
|
||
|
||
### 2.5 Elevation & Surface
|
||
- Card: `Card` widget dengan default elevation, atau `Container` dengan `color: kSurface2` jika butuh kontrol penuh.
|
||
- Active/pressed state: gunakan `InkWell` / `InkResponse` dengan splash color aksen transparan.
|
||
- Bottom sheet: `color: kSurface`, border top `kBorder`, radius top 24.
|
||
- Dialog: `backgroundColor: kSurface`, content padding dari `AlertDialog` default atau override sesuai kebutuhan.
|
||
|
||
### 2.6 Iconography
|
||
- Ukuran icon default: 20–24dp untuk list tile, 18dp untuk button, 28dp untuk dialog icon.
|
||
- Gunakan outlined variant untuk icon di dalam input/field (prefixIcon).
|
||
- Gunakan filled/rounded untuk icon utama dan CTA.
|
||
- Jangan campur gaya icon dalam satu screen.
|
||
|
||
### 2.7 Animation Standard
|
||
- **Micro-interaction** (button press, card tap): duration 150–200ms, curve `Curves.easeInOut`
|
||
- **Sheet expand/collapse**: duration 300ms, curve `Curves.fastOutSlowIn`
|
||
- **Dialog enter/exit**: duration 200ms, curve `Curves.easeInOut`
|
||
- **Spinner**: `CircularProgressIndicator` dengan `strokeWidth: 2`, duration default
|
||
- **Pulse animation** (cart badge): duration 180ms, scale 1.0 → 0.9 → 1.0
|
||
- **SnackBar**: duration default 3 detik, kecuali butuh lebih pendek/lama
|
||
- Jangan animasikan opacity/color untuk status penting (sukses/gagal) — gunakan warna solid agar cepat terdeteksi
|
||
|
||
### 2.8 Haptic & Feedback
|
||
- Aksi signifikan yang cepat: tambah item ke keranjang, print struk, scan barcode → `HapticFeedback.lightImpact()`.
|
||
- Aksi destruktif atau butuh konfirmasi: hapus item/batal transaksi → `HapticFeedback.mediumImpact()`.
|
||
- Sukses transaksi/tampilkan feedback →lanjut ke `SnackBar` hijau (`kSuccess`), jangan pakai dialog sukses untuk hal yang cepat.
|
||
- Jangan paksa haptic di setiap tap; hanya untuk aksi yang jelas berdampak.
|
||
|
||
### 2.9 Loading State
|
||
- **Inline di button** (recommended untuk semua operasi > 500ms):
|
||
- Spinner: `SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF06210F)))` untuk light button text
|
||
- Spinner: `SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2, color: kAccent))` untuk dark button text
|
||
- Ganti label menjadi `Mencetak...` / `Memproses...` / `Menyimpan...`
|
||
- Disable button (`onPressed: submitting ? null : ()`)
|
||
- **Full-screen**: gunakan `CircularProgressIndicator(color: kAccent)` di `Center`.
|
||
- **Inline di list/sheet**: gunakan `LinearProgressIndicator` hanya untuk determinate progress yang jelas.
|
||
- **Skeleton**: untuk list yang loading, pakai `ListTile` dengan `shimmer effect` atau placeholder abu-abu.
|
||
- Jangan biarkan user bisa double-tap aksi yang sudah loading.
|
||
|
||
### 2.10 Empty State
|
||
- Jangan tampilkan teks kosong tanpa visual indicator.
|
||
- Minimal: icon 48dp + teks. Contoh: `Icon(Icons.shopping_cart_outlined, size: 48, color: kMuted)` + teks "Keranjang kosong".
|
||
- Center alignment, `Column` dengan `MainAxisSize.min`.
|
||
- Jika ada aksi yang bisa dilakukan (misal tambah produk), tampilkan `FilledButton` sebagai helper.
|
||
- Konsisten pakai ikon `outlined` variant untuk empty state.
|
||
|
||
### 2.11 Error State
|
||
- Error dari form validation: tampilkan inline di bawah field, warna `kDanger`, font 13, `maxLines: 2`.
|
||
- Error dari operasi database/API: tampilkan `SnackBar` merah (`kDanger`) dengan durasi 3 detik.
|
||
- Jangan hanya pakai warna tanpa label. Tambahkan ikon/prefix jika perlu.
|
||
- Jika field error, pastikan `TextField` tetap bisa di-focus dan di-scroll.
|
||
- Untuk error yang bisa diperbaiki (misal form), jangan matikan form.
|
||
|
||
### 2.12 Dialog Button Layout
|
||
- **Mobile (< 600dp)**: tombol dialog harus **full-width stacked vertikal** dengan `SizedBox(height: 8)` antar tombol.
|
||
- **Tablet (>= 600dp)**: bisa pakai `Row` horizontal untuk 2 tombol, dengan `Expanded`.
|
||
- Urutan: **Batal** di atas/kiri, **Konfirmasi** di bawah/kanan.
|
||
- Tombol konfirmasi destruktif: `FilledButton` dengan `backgroundColor: kDanger`.
|
||
- Tombol konfirmasi positif: `FilledButton` dengan `backgroundColor: kAccent` atau `kSuccess`.
|
||
- Gunakan `StatefulBuilder` jika dialog butuh loading state atau update UI setelah aksi.
|
||
|
||
### 2.13 Role-Based Visibility
|
||
- Fitur yang tidak diizinkan untuk role tertentu harus **disembunyikan sepenuhnya**, bukan cuma di-disable.
|
||
- Admin: lihat semua transaksi + filter tanggal + aksi batalkan transaksi + pengaturan toko + kelola akun kasir + harga pokok/laba.
|
||
- Kasir: lihat transaksi hari ini, tidak ada aksi batalkan, tidak ada filter tanggal, tidak ada pengaturan toko, tidak ada harga pokok/laba.
|
||
- Guest: tidak diizinkan akses.
|
||
|
||
### 2.14 Bottom Sheet
|
||
- Keranjang harus berupa bottom sheet, bukan panel tetap.
|
||
- Sheet harus bisa di-drag (`DraggableScrollableSheet` atau `showModalBottomSheet` dengan `isScrollControlled: true`).
|
||
- Konten sheet harus **selalu scrollable** (`SingleChildScrollView`), tanpa `Expanded` di dalamnya, agar footer tidak pernah terhalangi keyboard.
|
||
- Untuk menutup sheet dari aksi di dalamnya, gunakan `Navigator.pop(sheetContext)` bukan `parentContext`.
|
||
- Tambah handle bar (container 40x4 dengan radius 4, warna `kBorder`) di atas sheet untuk affordance drag.
|
||
|
||
### 2.15 Dialog
|
||
- Semua dialog input yang panjang harus dibungkus `SingleChildScrollView` atau `SingleChildScrollView` di dalam `AlertDialog` agar bisa di-scroll saat keyboard muncul.
|
||
- Jangan pakai `Expanded` di dalam dialog yang berpotensi overflow.
|
||
- Dialog dengan form: `contentPadding: EdgeInsets.fromLTRB(16, 16, 16, 8)`, `actionsPadding: EdgeInsets.fromLTRB(16, 0, 16, 16)`.
|
||
- Dialog dengan list items: `contentPadding: EdgeInsets.all(16)`.
|
||
- Jangan buat dialog terlalu tinggi; batasi dengan `ConstrainedBox` + `SingleChildScrollView` jika konten bisa panjang.
|
||
|
||
### 2.16 SnackBar
|
||
- Gunakan `ScaffoldMessenger.of(context).showSnackBar()` untuk feedback singkat.
|
||
- Durasi default 3 detik untuk info, 5 detik untuk error penting.
|
||
- Posisikan di atas bottom navigation jika ada, atau default bottom.
|
||
- Jangan tampilkan SnackBar di atas dialog yang masih terbuka — tunggu dialog ditutup dulu.
|
||
|
||
### 2.17 Status & Badge
|
||
- Gunakan badge untuk status dengan warna sesuai:
|
||
- Lunas / Berhasil: badge hijau (`kSuccess`) dengan label teks
|
||
- Belum Lunas / Bayar Nanti: badge amber (`kWarning`) dengan label teks
|
||
- Dibatalkan: badge merah (`kDanger`) + teks `DIBATALKAN`
|
||
- Stok Habis: badge merah + teks "Habis"
|
||
- Stok Rendah: badge oranye + teks "Stok rendah (X/Y)"
|
||
- Manual: badge amber dengan label "MANUAL" (caps, tracking +0.5)
|
||
- Badge style: `Container` dengan `padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2)`, `borderRadius: BorderRadius.circular(8)`, `color: warna.withValues(alpha: 0.15)`.
|
||
- Untuk badge kecil (10–12px), gunakan `fontWeight: FontWeight.w800` agar tetap terbaca.
|
||
|
||
### 2.18 List & Card
|
||
- Gunakan `ListView.separated` dengan `itemCount` yang sesuai untuk performa.
|
||
- Separator: `Divider(height: 1, thickness: 1, color: kBorder)` atau `SizedBox(height: 8/12)` untuk card list.
|
||
- Card list: `Card` widget dengan `margin: EdgeInsets.symmetric(horizontal: 12, vertical: 4)` atau `EdgeInsets.all(12)`.
|
||
- Jangan hardcode tinggi list item; gunakan `Column` + `MainAxisSize.min`.
|
||
- Teks panjang: `maxLines: 2`, `overflow: TextOverflow.ellipsis` untuk nama produk.
|
||
|
||
### 2.19 Grid Produk
|
||
- Default: 2 kolom di layar HP (`crossAxisCount: 2`).
|
||
- Tablet (lebar > 600dp): naik jadi 3 kolom.
|
||
- Jangan pakai `childAspectRatio` tetap yang bikin kartu terlalu tinggi/pendek. Gunakan `AspectRatio(1.1)` atau biarkan `GridView` yang menentukan tinggi.
|
||
- Setiap kartu produk harus menampilkan: inisial nama (avatar circle amber), nama produk, harga, stok/badge stok.
|
||
|
||
### 2.20 Auto-Scroll & Keyboard
|
||
- Setiap form di dalam bottom sheet atau dialog harus meng-handle keyboard muncul/tutup.
|
||
- Gunakan `MediaQuery.of(context).viewInsets.bottom` untuk padding bawah dinamis pada `SingleChildScrollView`.
|
||
- Untuk bottom sheet form yang tinggi dan perlu auto-expand:
|
||
1. Pakai `DraggableScrollableController`.
|
||
2. Buat `FocusNode` untuk setiap `TextField`.
|
||
3. Tambah listener ke setiap focus node: jika `hasFocus`, animasikan sheet ke `maxChildSize` (misal `0.92`), lalu panggil `Scrollable.ensureVisible()` agar field aktif terlihat.
|
||
4. Durasi animasi sheet: `300ms`, scroll: `250ms`, alignment: `0.0`.
|
||
- Jangan andal `Scaffold.resizeToAvoidBottomInset` saja untuk bottom sheet form kompleks; manual padding lebih stabil untuk kasus form panjang.
|
||
|
||
### 2.21 Responsif
|
||
- Mobile (< 600dp): 2 kolom grid, tombol stacked vertikal di dialog.
|
||
- Tablet (>= 600dp): 3–4 kolom grid, tombol bisa horizontal di dialog.
|
||
- Gunakan `LayoutBuilder` atau `MediaQuery` untuk menentukan layout berdasarkan lebar screen.
|
||
- Jangan gunakan hardcoded width untuk widget utama; gunakan `Expanded`, `Flexible`, atau `MediaQuery`.
|
||
|
||
### 2.22 Aksesibilitas
|
||
- Minimal tap target: 44x44dp untuk semua tombol/icon.
|
||
- Kontras teks minimal 4.5:1 (cek dengan tool contrast checker).
|
||
- Jangan andal warna saja (tambah ikon/label). Contoh: stok habis = badge merah + teks "Habis".
|
||
- Setiap aksi ada `SnackBar` feedback: sukses hijau, error merah, info biru/abu.
|
||
- Gunakan `Semantics` widget untuk icon-only button agar screen reader bisa membaca.
|
||
|
||
## 3. UX untuk Kasir (Critical Path)
|
||
- Target: transaksi selesai dalam 3 tap (pilih produk → bayar → selesai).
|
||
- Jangan sembunyikan aksi utama (Bayar) di bawah banyak opsi.
|
||
- Tampilkan total, diskon, dan kembalian secara **real-time**.
|
||
- Gunakan chip/segmented button untuk tipe pembeli (Umum/Reseller) — jangan dropdown.
|
||
- Setiap item di keranjang harus bisa diubah qty atau tipe harga dalam 1 tap.
|
||
- Jika stok habis, matikan visual (opacity turun, disable tap) dan tampilkan badge "Habis".
|
||
- Jika stok menipis, tampilkan badge peringatan oranye sebelum habis.
|
||
- Bottom bar keranjang harus selalu terlihat, tidak tertutup keyboard.
|
||
|
||
## 4. Payment Flow: Bayar Nanti
|
||
- Saat checkout pilih **Bayar Nanti**, tombol harus jelas: **"Pesanan Sekarang"**.
|
||
- Field "Uang Dibayar" menjadi disabled dengan label **"Belum Bayar"**.
|
||
- Tampilkan status **"Belum Bayar"** di receipt/summary dengan warna `kWarning`.
|
||
- Di Riwayat, transaksi unpaid harus punya badge/status yang menonjol.
|
||
- Saat customer datang bayar: tombol **"Terima Pembayaran"** atau **"Bayar Sisa"** di detail transaksi.
|
||
|
||
## 5. Reusability & Konsistensi
|
||
- Buat widget reuseable untuk komponen berulang: `_Tag`, `_ReportCard`, `_Avatar`, `_Stepper`, `_PaymentMethodChip`, `_ReceiptRow`, `_SettingsListTile`.
|
||
- Jangan duplikasi warna/spacing. Taruh konstanta di `lib/utils/colors.dart`.
|
||
- Konsisten pakai `formatRupiah()` untuk semua angka mata uang.
|
||
- Nama widget internal: pakai underscore prefix (`_`) kecuali widget yang diekspor.
|
||
|
||
## 6. Checklist Sebelum Submit Perubahan UI/UX
|
||
- [ ] `flutter analyze` → No issues found (tanpa error).
|
||
- [ ] Coba di layar kecil (HP 5") dan besar (tablet).
|
||
- [ ] Coba dengan keyboard muncul (input uang/diskon).
|
||
- [ ] Coba mode gelap dan terang (jika supported).
|
||
- [ ] Pastikan tidak ada hardcoded height yang bikin overflow.
|
||
- [ ] Semua teks dalam Bahasa Indonesia.
|
||
- [ ] Konsisten pakai `formatRupiah()` untuk angka mata uang.
|
||
- [ ] Coba flow lengkap: tambah produk → checkout → cetak struk → riwayat → detail.
|
||
- [ ] Coba role kasir dan admin untuk memastikan visibility benar.
|
||
|
||
## 7. Referensi Project
|
||
- `lib/utils/colors.dart` — design tokens.
|
||
- `lib/screens/kasir_screen.dart` — contoh bottom sheet cart, grid produk, dialog pembayaran.
|
||
- `lib/screens/riwayat_screen.dart` — contoh kartu laporan, filter role-based.
|
||
- `lib/screens/pengaturan_screen.dart` — contoh settings form + role-based visibility.
|
||
|