Admin UI Conventions
BẮT BUỘC đọc trước khi tạo/sửa trang admin hoặc component UI.
Định nghĩa design system + pattern chuẩn cho admin portal (booking-web/src/app/admin/). Tránh 1 người 1 kiểu. Match với CLAUDE.md reusable components table.
1. Layout
Page structure
┌────────────────────────────────────────────────────┐
│ PageHeader: title + breadcrumbs + primary action │ ← common/PageHeader
├────────────────────────────────────────────────────┤
│ │
│ Content (max-w-screen-xl mx-auto) │
│ ├─ Filters / toolbar (if list page) │
│ ├─ Main content card(s) │
│ └─ Pagination (nếu list) │
│ │
└────────────────────────────────────────────────────┘
Rules
- Max width:
max-w-screen-xlcho nội dung, không full-width trừ calendar - Vertical spacing giữa sections:
space-y-6 - Padding page:
px-4 sm:px-6 lg:px-8 py-6 - Cards:
rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] - Dark mode: mandatory cho tất cả components, dùng
dark:variant
2. Typography
| Element | Class | Use |
|---|---|---|
| Page title | text-2xl font-semibold text-gray-900 dark:text-white/90 |
h1, PageHeader |
| Section title | text-lg font-semibold text-gray-800 dark:text-white/90 |
SettingsCard title |
| Card subtitle | text-sm text-gray-500 dark:text-gray-400 |
Description below title |
| Body | text-sm text-gray-700 dark:text-gray-300 |
Content text |
| Label | text-sm font-medium text-gray-700 dark:text-gray-400 |
Form labels |
| Hint | text-xs text-gray-500 dark:text-gray-400 |
Below inputs |
| Error | text-xs text-error-500 |
Validation errors |
KHÔNG dùng arbitrary values (e.g., text-[13px]). Dùng Tailwind scale.
3. Forms
Structure
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<SettingsCard title="..." footer={<SubmitButton>Save</SubmitButton>}>
<div className="space-y-6">
<FormField name="name" label="..." required />
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
<FormField name="email" label="..." />
<FormField name="phone" label="..." />
</div>
</div>
</SettingsCard>
</form>
</FormProvider>
Rules
- Luôn dùng form components có sẵn (CLAUDE.md booking-web table):
FormField<T>cho text/numberSwitchField<T>cho toggle (không dùng<input type="checkbox">)MoneyField<T>cho currencyPhoneField<T>cho số điện thoạiDateField<T>,TimeFieldcho datetimeSearchSelect,MultiSelect,Selectcho dropdownsRichTextEditorcho long text (about, description)ColorFieldcho màu
- Validation: Zod schema → zodResolver, validate business rules trước API call (rule: frontend validate first)
- Field spacing:
space-y-6giữa fields,gap-5trong grid - Label: bên trên input,
mb-1.5 block - Required: dấu
*đỏ sau label (requiredprop của FormField tự handle) - Error placement: ngay dưới input,
mt-1.5 text-xs text-error-500 - Hint placement: dưới input/error,
mt-1 text-xs text-gray-500 - Input group (suffix như
%,kr): wrapperflex h-11 items-center rounded-lg border, input bên trong no border,<span className="border-l px-4 text-sm text-gray-500">cho suffix - Button order trong form: Cancel/Back bên trái, Submit bên phải (trong flex row)
- Submit button:
<SubmitButton isLoading={mutation.isPending}>— không tự wire loading state
4. Tables / Lists
Option A: DataTable (recommended cho list chuẩn)
<DataTable
columns={columns}
queryKey={['customers']}
fetchFn={(params) => api.get('/customers', { params })}
emptyState={<EmptyState ... />}
/>
Tự handle pagination, loading, empty, error.
Option B: Custom table với ui/table primitives
Khi cần layout đặc biệt (calendar, kanban). Dùng primitives.
Rules
- Sort: persist vào localStorage (
useLocalStorage), KHÔNGuseState— rule đã có - Pagination:
<Pagination>component, default 20 items/page - Loading:
<Skeleton>rows (không spinner) - Empty state: icon + title + hint + primary action button (add new)
- Row hover:
hover:bg-gray-50 dark:hover:bg-white/5 - Row click: mở modal detail hoặc navigate detail page (consistent per feature)
- Actions column: bên phải, dropdown "..." nếu > 2 actions, buttons nếu ≤ 2
- Status column:
<Badge color={STATUS_BADGE_COLOR[status]} variant="light"> - Date column: format theo
useFormatDate()helper (relative "2 hours ago" cho recent, absolute cho old)
5. Modal vs Drawer
Modal (<Modal>)
Use khi:
- Form ngắn (1-5 fields)
- Confirmation
- Detail view nhỏ
- Width ≤ 600px
<Modal isOpen={isOpen} onClose={close} className="max-w-md">
<ModalHeader title="..." onClose={close} />
<ModalBody>...</ModalBody>
<ModalFooter>...</ModalFooter>
</Modal>
Drawer (custom)
Use khi:
- Form dài (booking, staff profile, customer detail)
- List with search (assignment picker)
- Multi-section form
- Cần không cover full screen
Pattern: Right-side drawer, 480-640px width, slide-in animation.
Rules
- Header: tiêu đề bên trái,
Xclose bên phải,border-b - Body: scrollable,
overflow-y-auto - Footer: sticky bottom,
border-t, Cancel-Primary pair - Close: X icon, Escape key, click outside (configurable)
- Wrapper/Inner pattern: reset state khi reopen với data khác (key-based remount)
6. Confirmation Dialogs
LUÔN dùng <ConfirmDialog>, NEVER window.confirm().
<ConfirmDialog
isOpen={isOpen}
title="Cancel booking?"
message="Customer will be refunded deposit if within cancellation window."
confirmText="Cancel booking"
cancelText="Keep booking"
variant="danger" // 'default' | 'danger'
onConfirm={handleCancel}
onCancel={close}
/>
Rules
- Destructive action (
variant="danger"): red button, require typing word match cho critical actions (delete tenant, refund large amount) - Title: câu hỏi ngắn, không period cuối
- Message: explain consequence + reversibility
- Buttons: Cancel bên trái (outline), Confirm bên phải (solid primary hoặc destructive)
7. Toast Notifications
LUÔN dùng useToast(), NEVER alert() hoặc custom notification.
const { showToast } = useToast();
showToast({ type: 'success', message: t('saved') });
showToast({ type: 'error', message: t('errorOccurred') });
showToast({ type: 'info', message: t('copiedToClipboard') });
Rules
- Success: sau mutation success (auto handled bởi
useFormMutation) - Error: catch API error + map qua
useErrorMessage()để translate error code - Duration: success 3s, error 5s, info 3s
- Position: top-right default
- KHÔNG toast cho validation errors (show inline bên field thay vì)
8. Status / Badge Colors
Dùng <Badge> với color palette cố định:
| color | Use case | Example |
|---|---|---|
primary |
Main positive state | Active, Enabled |
success |
Completed, paid | Completed booking, paid invoice |
warning |
In-progress warning | Arrived, pending |
info |
In-progress active | IN_PROGRESS booking |
error |
Failed, rejected | Payment failed, cancelled |
light |
Neutral, idle | PENDING, draft |
dark |
Terminal negative | No show |
Variant light (background tint) cho badge nhỏ, solid cho status highlight lớn.
9. Buttons
Variants
| Variant | Use |
|---|---|
primary |
Main action (Save, Create, Confirm) |
outline |
Secondary action (Cancel, Back) |
destructive |
Delete, cancel, refund |
ghost |
Tertiary action in menu |
Sizes
| Size | Height | Use |
|---|---|---|
sm |
36px | Forms, compact toolbars |
md |
44px | Default |
lg |
52px | Hero CTA (rare) |
Icons
- Start icon: action icon (plus, edit, trash)
- End icon: directional (chevron) hoặc external (arrow-up-right)
- Icon-only:
<Tooltip>wrap required
Rules
- KHÔNG tạo custom button. Luôn dùng
<Button>hoặc<SubmitButton> - Full-width: chỉ trong mobile forms hoặc auth pages
- Group: dùng flex với
gap-2, không margin manual
10. Colors (theme)
Tailwind v4 canonical values. KHÔNG hardcode hex trừ status colors special.
| Token | Light | Dark | Use |
|---|---|---|---|
brand-500 |
primary brand | same | buttons, focus ring |
brand-300 |
focus border | brand-800 |
focus state |
gray-200 |
borders | gray-700 |
dividers |
gray-500 |
placeholders | gray-400 |
muted text |
gray-700 |
body text | gray-300 |
content |
gray-900 |
headings | white/90 |
titles |
error-500 |
errors | same | validation |
success-500 |
success | same | confirmations |
Customer portal dùng tenant.branding.primaryColor dynamic, admin portal dùng brand fixed.
11. Icons
- Library: Lucide React (
lucide-react) - Default size:
w-4 h-4trong button,w-5 h-5standalone - Color: inherit từ parent (
currentColor) - Stroke width: 2 (default)
12. Spacing Scale
Tailwind scale, không arbitrary values:
| Token | Pixels | Use |
|---|---|---|
gap-1 |
4px | Icon-text in button |
gap-2 |
8px | Button group |
gap-3 |
12px | Tight layout |
gap-4 |
16px | Form field related items |
gap-5 |
20px | Grid form items |
gap-6 |
24px | Card sections |
space-y-6 |
24px | Fields in form |
space-y-8 |
32px | Major sections |
13. Responsive
Breakpoints:
sm640px — tablet portraitmd768px — tablet landscapelg1024px — desktopxl1280px — large desktop
Rules:
- Mobile-first: base styles là mobile,
sm:và up override - Grid form:
grid-cols-1 sm:grid-cols-2default - Sidebar: collapse mobile, static desktop (
lg:) - Drawer: full-width mobile, fixed width desktop
14. Loading States
| Component | Pattern |
|---|---|
| Page load | Skeleton matching layout |
| Table load | <Skeleton> rows |
| Button action | isLoading prop → spinner + disable |
| Form submit | <SubmitButton isLoading> |
| Async data in row | Subtle opacity + spinner icon |
KHÔNG dùng:
- Full-page spinner (disorient user)
<div>Loading...</div>plain text- Blocking modal spinner cho non-critical loads
15. Empty States
┌─────────────────────────────┐
│ [Icon] │
│ │
│ No bookings yet │ ← title
│ │
│ Bookings will appear here │ ← hint
│ when customers book. │
│ │
│ [+ Add booking] │ ← primary action
└─────────────────────────────┘
- Centered trong container
- Icon neutral (matching feature)
- Title + hint + CTA
- KHÔNG để trống (trải nghiệm tệ)
16. Error States
Inline field error
- Red border + message dưới field
API error (after submit)
- Toast error + không clear form (user có thể sửa)
- Message từ
useErrorMessage(errorCode)
Page-level error (can't load)
- Alert component + retry button
- Link home
17. Confirmation Patterns
| Action | Pattern |
|---|---|
| Delete permanent | ConfirmDialog + type to confirm (cho critical) |
| Delete soft / archive | ConfirmDialog variant=default |
| Cancel booking | ConfirmDialog + reason field |
| Refund | ConfirmDialog + amount confirm |
| Bulk action | ConfirmDialog + list affected items |
18. i18n
- Mọi text user-facing PHẢI qua i18n (
useTranslations) - Keys theo namespace (
settings,booking,common) - KHÔNG hardcode tiếng Anh hay tiếng Na Uy
- Locales hiện tại:
en,nb(Norwegian Bokmål) - Key naming: camelCase, action/object (e.g.,
cancelBooking,depositEnabled)
19. Accessibility
- Keyboard navigation: Tab/Shift+Tab qua tất cả interactive
- Focus visible:
focus:ring-3 focus:ring-brand-500/10 - Labels: mọi input có label (visible hoặc
sr-only) - ARIA: roles cho custom components (modal
role="dialog", etc.) - Color contrast: WCAG AA (body text 4.5:1)
- Click target: min 44×44px
20. Audit Checklist
Trước khi merge PR thêm/sửa UI:
- Dùng component có sẵn (không tạo duplicate)?
- Dark mode OK?
- Responsive ≥ mobile + tablet + desktop?
- Keyboard navigation OK?
- i18n đầy đủ (không hardcode text)?
- Loading state?
- Empty state?
- Error handling (API + validation)?
- Tailwind canonical values (không arbitrary nếu có scale)?
- Confirmation cho destructive action?
- Console sạch (no warnings)?
21. Related Docs
../flows/booking-flow.md— Booking UX flow../flows/booking-status-flow.md— Status transitions cho quick actions../flows/payment-flow.md— Payment UX../../booking-web/CLAUDE.md— Reusable components referencedevelopment-rules.md— Coding standards