rules/ui-conventions.md

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-xl cho 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

  1. Luôn dùng form components có sẵn (CLAUDE.md booking-web table):
    • FormField<T> cho text/number
    • SwitchField<T> cho toggle (không dùng <input type="checkbox">)
    • MoneyField<T> cho currency
    • PhoneField<T> cho số điện thoại
    • DateField<T>, TimeField cho datetime
    • SearchSelect, MultiSelect, Select cho dropdowns
    • RichTextEditor cho long text (about, description)
    • ColorField cho màu
  2. Validation: Zod schema → zodResolver, validate business rules trước API call (rule: frontend validate first)
  3. Field spacing: space-y-6 giữa fields, gap-5 trong grid
  4. Label: bên trên input, mb-1.5 block
  5. Required: dấu * đỏ sau label (required prop của FormField tự handle)
  6. Error placement: ngay dưới input, mt-1.5 text-xs text-error-500
  7. Hint placement: dưới input/error, mt-1 text-xs text-gray-500
  8. Input group (suffix như %, kr): wrapper flex 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
  9. Button order trong form: Cancel/Back bên trái, Submit bên phải (trong flex row)
  10. Submit button: <SubmitButton isLoading={mutation.isPending}> — không tự wire loading state

4. Tables / Lists

<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

  1. Sort: persist vào localStorage (useLocalStorage), KHÔNG useState — rule đã có
  2. Pagination: <Pagination> component, default 20 items/page
  3. Loading: <Skeleton> rows (không spinner)
  4. Empty state: icon + title + hint + primary action button (add new)
  5. Row hover: hover:bg-gray-50 dark:hover:bg-white/5
  6. Row click: mở modal detail hoặc navigate detail page (consistent per feature)
  7. Actions column: bên phải, dropdown "..." nếu > 2 actions, buttons nếu ≤ 2
  8. Status column: <Badge color={STATUS_BADGE_COLOR[status]} variant="light">
  9. Date column: format theo useFormatDate() helper (relative "2 hours ago" cho recent, absolute cho old)

5. Modal vs Drawer

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, X close 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-4 trong button, w-5 h-5 standalone
  • 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:

  • sm 640px — tablet portrait
  • md 768px — tablet landscape
  • lg 1024px — desktop
  • xl 1280px — large desktop

Rules:

  • Mobile-first: base styles là mobile, sm: và up override
  • Grid form: grid-cols-1 sm:grid-cols-2 default
  • 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)?