Bỏ qua

Design System Master File

LOGIC: When building a specific page, first check design-system/pages/[page-name].md. If that file exists, its rules override this Master file. If not, strictly follow the rules below.


Project: GovData — Nền Tảng Quản Lý, Quản Trị Dữ Liệu Thành Phố Updated: 2026-04-21 Category: Government Data Governance Platform Stack: Next.js (App Router) + Tailwind CSS 4 (OKLCH) + shadcn/ui + Lucide Icons Target Devices: PC-first (Desktop > Tablet > Mobile) Style: Government Modern — Clean, spacious, rounded, bold typography


Global Rules

Color Palette

Light Mode (:root)

Token OKLCH Value Hex (approx) Usage
--background oklch(0.984 0.003 247) #f8fafc App background (slate-50)
--foreground oklch(0.208 0.042 265) #1e293b Primary text (slate-800)
--primary oklch(0.546 0.245 262) #2563eb Brand blue, primary actions (blue-600)
--primary-foreground oklch(0.985 0.002 247) #eff6ff Text on primary (blue-50)
--secondary oklch(0.968 0.003 265) #f1f5f9 Secondary backgrounds (slate-100)
--secondary-foreground oklch(0.208 0.042 265) #1e293b Text on secondary (slate-800)
--muted oklch(0.968 0.003 265) #f1f5f9 Subdued backgrounds (slate-100)
--muted-foreground oklch(0.554 0.022 257) #64748b Muted text (slate-500)
--accent oklch(0.929 0.013 255) #e2e8f0 Highlight/hover (slate-200)
--accent-foreground oklch(0.208 0.042 265) #1e293b Text on accent (slate-800)
--destructive oklch(0.577 0.245 27) #dc2626 Error, danger, DISPUTED (red-600)
--destructive-foreground oklch(0.985 0.002 15) #fef2f2 Text on destructive
--border oklch(0.929 0.013 255) #e2e8f0 Default borders (slate-200)
--input oklch(0.929 0.013 255) #e2e8f0 Input borders (slate-200)
--ring oklch(0.546 0.245 262) #2563eb Focus rings (blue-600)
Token OKLCH Value Hex (approx) Usage
--card oklch(1 0 0) #ffffff Card/panel background
--card-foreground oklch(0.208 0.042 265) #1e293b Card text
--popover oklch(1 0 0) #ffffff Popover/dropdown background
--popover-foreground oklch(0.208 0.042 265) #1e293b Popover text
Token OKLCH Value Hex (approx) Usage
--sidebar oklch(1 0 0) #ffffff Sidebar background (white)
--sidebar-foreground oklch(0.554 0.022 257) #64748b Sidebar text (slate-500)
--sidebar-primary oklch(0.546 0.245 262) #2563eb Active nav item (blue-600)
--sidebar-primary-foreground oklch(1 0 0) #ffffff Text on active nav
--sidebar-accent oklch(0.968 0.003 265) #f1f5f9 Hover nav item (slate-100)
--sidebar-accent-foreground oklch(0.208 0.042 265) #1e293b Hover text
--sidebar-border oklch(0.929 0.013 255) #e2e8f0 Sidebar border (slate-200)
--sidebar-ring oklch(0.546 0.245 262) #2563eb Sidebar focus ring
Token OKLCH Value Hex (approx) Usage
--chart-1 oklch(0.546 0.245 262) #2563eb Blue-600 (primary series)
--chart-2 oklch(0.520 0.155 175) #0891b2 Cyan-600 (secondary series)
--chart-3 oklch(0.532 0.157 131) #059669 Emerald-600 (positive/success)
--chart-4 oklch(0.541 0.281 293) #7c3aed Violet-600 (accent series)
--chart-5 oklch(0.795 0.184 86) #eab308 Yellow-500 (warning series)

Semantic Colors (recommended tokens):

Token OKLCH Value Hex (approx) Usage
--success oklch(0.532 0.157 131) #059669 Success, CTA "Ban hành" (emerald-600)
--success-foreground oklch(1 0 0) #ffffff Text on success
--warning oklch(0.795 0.184 86) #eab308 Warning, "Review" status (yellow-500)
--warning-foreground oklch(0.344 0.062 58) #713f12 Text on warning (yellow-900)
--info oklch(0.520 0.155 175) #0891b2 Info, statistics (cyan-600)
--info-foreground oklch(1 0 0) #ffffff Text on info

Dark Mode (.dark)

Lưu ý: Tất cả token dark mode sử dụng OKLCH value đầy đủ — không dùng opacity shortcut (/ 15%) để đảm bảo contrast nhất quán và dễ debug.

Token OKLCH Value Hex (approx) Notes
--background oklch(0.129 0.042 265) #0f172a slate-900
--foreground oklch(0.929 0.013 255) #e2e8f0 slate-200
--primary oklch(0.623 0.214 259) #3b82f6 blue-500 (lighter for dark bg)
--primary-foreground oklch(0.985 0.002 247) #eff6ff blue-50
--secondary oklch(0.208 0.042 265) #1e293b slate-800
--secondary-foreground oklch(0.929 0.013 255) #e2e8f0 slate-200
--muted oklch(0.208 0.042 265) #1e293b slate-800
--muted-foreground oklch(0.704 0.015 261) #94a3b8 slate-400
--accent oklch(0.279 0.041 260) #334155 slate-700
--accent-foreground oklch(0.929 0.013 255) #e2e8f0 slate-200
--destructive oklch(0.637 0.237 25) #ef4444 red-500
--destructive-foreground oklch(0.985 0.002 15) #fef2f2
--border oklch(0.279 0.041 260) #334155 slate-700
--input oklch(0.279 0.041 260) #334155 ✅ slate-700 (full OKLCH — không dùng opacity)
--ring oklch(0.623 0.214 259) #3b82f6 blue-500
--card oklch(0.208 0.042 265) #1e293b slate-800
--card-foreground oklch(0.929 0.013 255) #e2e8f0 slate-200
--popover oklch(0.208 0.042 265) #1e293b slate-800
--popover-foreground oklch(0.929 0.013 255) #e2e8f0 slate-200
--sidebar oklch(0.208 0.042 265) #1e293b slate-800
--sidebar-foreground oklch(0.704 0.015 261) #94a3b8 slate-400
--sidebar-primary oklch(0.623 0.214 259) #3b82f6 blue-500
--sidebar-primary-foreground oklch(1 0 0) #ffffff
--sidebar-border oklch(0.279 0.041 260) #334155 ✅ slate-700 (full OKLCH — không dùng opacity)
--success oklch(0.596 0.145 163) #10b981 emerald-500
--warning oklch(0.852 0.199 91) #facc15 yellow-400
--info oklch(0.580 0.137 195) #06b6d4 cyan-500

Color Notes: Government Modern palette — Blue-600 primary conveys trust and authority. Slate neutrals keep the interface calm. Semantic colors (emerald/red/yellow) align with universal status conventions. Dark mode reduces lightness and chroma for comfortable viewing.

Browser compatibility: OKLCH được hỗ trợ bởi tất cả trình duyệt hiện đại (Chrome 111+, Firefox 113+, Safari 15.4+). Nếu dự án phải hỗ trợ trình duyệt cũ (IE, Chrome <111), cần cấu hình build-time fallback hex/RGB.


Typography

Chiến lược font: 3-role font stack — mỗi font phục vụ mục đích riêng biệt, có usage rules bắt buộc.

/* UI — Mặc định cho toàn bộ giao diện */
--font-ui:      'Be Vietnam Pro', 'Plus Jakarta Sans', system-ui, sans-serif;

/* Display — Văn bản pháp lý, in ấn (KHÔNG dùng trong app UI thông thường) */
--font-display: 'Lora', Georgia, serif;

/* Mono — ID codes, data values dạng code */
--font-mono:    'JetBrains Mono', 'Consolas', 'Fira Code', monospace;

Usage Rules — BẮT BUỘC: - --font-ui: Áp dụng mặc định cho toàn bộ UI. Thay thế Inter. - --font-display: CHỈ dùng cho <LegalDocument />, <PrintTemplate />, tiêu đề trang in ấn. KHÔNG dùng trong app UI thông thường. - --font-mono: ID codes (DE001-DM1.1), data values dạng code, code snippets. Với cột số trong bảng: dùng tabular-nums CSS thay vì chuyển sang mono.

Font Setup (next/font):

import { Be_Vietnam_Pro, Plus_Jakarta_Sans, JetBrains_Mono, Lora } from 'next/font/google'

const beVietnamPro = Be_Vietnam_Pro({
  subsets: ['latin', 'vietnamese'],
  weight: ['400', '500', '600', '700', '800'],
  variable: '--font-ui',
  display: 'swap',
})

// Fallback / progressive enhancement
const plusJakartaSans = Plus_Jakarta_Sans({
  subsets: ['latin'],
  weight: ['400', '500', '600', '700', '800'],
  variable: '--font-ui-fallback',
  display: 'swap',
})

// Display: chỉ load khi có LegalDocument / PrintTemplate
const lora = Lora({
  subsets: ['latin', 'vietnamese'],
  weight: ['400', '500', '600', '700'],
  variable: '--font-display',
  display: 'swap',
})

// Mono: ID codes, data cells
const jetbrainsMono = JetBrains_Mono({
  subsets: ['latin'],
  weight: ['400', '700'],
  variable: '--font-mono',
  display: 'swap',
})

Lưu ý Windows ClearType: Test Be Vietnam Pro và Plus Jakarta Sans trên Windows 10 Chrome/Edge trước khi deploy — đây là môi trường thực tế của phần lớn cán bộ nhà nước Việt Nam.

Font Scale:

Element Font Weight Size (desktop) Size (mobile) Tailwind Notes
H1 (page title) UI 800 (extrabold) 30px 24px text-3xl font-extrabold tracking-tight leading-tight ✅ leading-tight bổ sung cho dấu tiếng Việt
H2 (section title) UI 700 (bold) 24px 20px text-2xl font-bold leading-snug ✅ leading-snug bổ sung
H3 (subsection) UI 700 (bold) 20px 18px text-xl font-bold leading-snug
H4 (group label) UI 600 (semibold) 18px 16px text-lg font-semibold
Body UI 500 (medium) 14px 14px text-sm font-medium Default text
Body light UI 400 (regular) 14px 14px text-sm Descriptions
Caption UI 700 (bold) 11px 11px text-[11px] font-bold uppercase tracking-widest KPI labels
KPI number UI 900 (black) 36px 28px text-4xl font-black tabular-nums ✅ tabular-nums cho số liệu
Table numbers UI 500 (medium) 14px 14px text-sm font-medium tabular-nums ✅ tabular-nums thay vì font-mono
Mono/code Mono 700 (bold) 10px 10px font-mono text-[10px] font-bold ID codes: DE001-DM1.1
Button UI 700 (bold) 14px 14px text-sm font-bold Heavier than standard
Nav item UI 600 (semibold) 14px 14px text-sm font-semibold Sidebar nav
Legal/Print heading Display 700 (bold) context context font-display font-bold Chỉ trong LegalDocument

Về tracking-tight tiếng Việt: H1/H2 dùng tracking-tight kết hợp font-weight 800/700 có thể làm dấu tiếng Việt phức tạp (ề, ộ, ướ) bị chật. leading-tight (1.25) và leading-snug (1.375) đã được bổ sung để bù trừ — test kỹ với nội dung tiếng Việt thực tế trước khi deploy.


Spacing Variables

Token Value Tailwind Usage
--space-xs 4px gap-1 / p-1 Tight gaps, icon padding
--space-sm 8px gap-2 / p-2 Inline spacing, small gaps
--space-md 16px gap-4 / p-4 Standard padding
--space-lg 24px gap-6 / p-6 Card internal padding
--space-xl 32px gap-8 / p-8 Section padding inside large cards
--space-2xl 40px gap-10 / p-10 Content area padding

Shadow & Borders

Token Value Usage
--radius-sm 0.5rem (8px) Badges, tags — rounded-lg
--radius-md 1rem (16px) Buttons, inputs — rounded-2xl
--radius-lg 1.5rem (24px) Cards, modals — rounded-3xl
--radius-full 9999px Avatars, pills, search input — rounded-full
--shadow-sm 0 1px 2px 0 rgb(0 0 0 / 0.05) Default cards — shadow-sm
--shadow-md 0 4px 6px -1px rgb(0 0 0 / 0.1) Hover states, dropdowns — shadow-md
--shadow-lg 0 10px 15px -3px rgb(0 0 0 / 0.1) Modals, active buttons — shadow-lg
--shadow-xl 0 20px 25px -5px rgb(0 0 0 / 0.1) Floating elements — shadow-xl

--radius-xl (2rem) đã được xóa — không sử dụng trong bất kỳ component nào. Dùng rounded-3xl (--radius-lg) cho tất cả card lớn.


Animation & Transition Tokens

/* Transition speed tokens */
--transition-fast:   150ms;   /* Hover states, button press */
--transition-normal: 200ms;   /* Focus rings, color change */
--transition-slow:   300ms;   /* Theme switch, panel expand */

Quy tắc sử dụng:

Context Class Duration
Button hover/active transition-all duration-150 fast
Input focus transition-all duration-200 normal
Card hover shadow transition-all duration-200 normal
Theme switch (color) transition-colors duration-300 slow
Modal open/close data-[state=open]:animate-in data-[state=closed]:animate-out tailwindcss-animate
Sidebar expand/collapse transition-all duration-300 slow
Toast slide-in slide-in-from-top-2 fade-in duration-200 tailwindcss-animate
Skeleton shimmer animate-pulse built-in
Accordion slide data-[state=open]:animate-in data-[state=closed]:animate-out slide-in-from-top-1 tailwindcss-animate
// tailwind.config.ts — kích hoạt tailwindcss-animate
import animate from 'tailwindcss-animate'
export default { plugins: [animate] }
/* prefers-reduced-motion — BẮT BUỘC */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

shadcn/ui Integration

components.json:

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "default",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "app/globals.css",
    "baseColor": "slate",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils"
  }
}

Override defaults — áp dụng GovData visual language:

// components/ui/button.tsx — override shadcn default
// Đổi rounded-md → rounded-2xl, thêm Soft variant

const buttonVariants = cva(
  "inline-flex items-center justify-center gap-2 rounded-2xl font-bold text-sm transition-all cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
  {
    variants: {
      variant: {
        default:     "bg-primary text-primary-foreground shadow-lg hover:bg-blue-700",
        secondary:   "bg-secondary text-secondary-foreground hover:bg-accent",
        outline:     "border border-border bg-transparent text-foreground hover:bg-accent",
        ghost:       "bg-transparent text-foreground hover:bg-accent",
        destructive: "bg-destructive text-destructive-foreground shadow-lg hover:bg-red-700",
        success:     "bg-success text-success-foreground shadow-lg hover:bg-emerald-700",
        soft:        "bg-primary/10 text-primary hover:bg-primary/20",  // ✅ Soft variant mới
        link:        "text-primary underline-offset-4 hover:underline font-bold text-xs",
      },
    },
  }
)

Soft variant dùng cho secondary CTA trong gov context: "Xem chi tiết", "Đề xuất", "Lịch sử". Background nhạt, không cạnh tranh với primary CTA.

CSS variables mapping sang shadcn theme — đảm bảo nhất quán:

/* globals.css */
:root {
  --background: oklch(0.984 0.003 247);
  --foreground: oklch(0.208 0.042 265);
  /* ... tất cả tokens như đã định nghĩa ở trên ... */

  /* Font variables */
  --font-sans: var(--font-ui);       /* shadcn dùng --font-sans */
  --font-mono: var(--font-mono);
}

App Layout Pattern

Pattern: Government Modern — Sidebar (w-72) + Header (h-20) + Content

+---------------------------------------------------------------+
| Sidebar (w-72, fixed)  | Header (h-20, sticky top, z-40)     |
|                        +--------------------------------------+
| [Logo: GovData]        | Đơn vị công tác: ...   [🔔] [👤]   |
| ─────────────          +--------------------------------------+
| ▶ Tổng quan            |                                      |
|   Dữ liệu định vị     |  Content Area                        |
|   Khám phá DL          |  (p-10, max-w-6xl mx-auto)          |
|   Bản đồ hiện trạng    |                                      |
|   Dữ liệu tương lai    |  bg-slate-50 (light)                |
|   Tra cứu Từ điển      |  bg-slate-900 (dark)                |
|   Báo cáo              |                                      |
| ─────────────          |                                      |
| [Vai trò hiện tại]     |                                      |
| [▼ Manager]            |                                      |
+---------------------------------------------------------------+
/* h-20 (80px), sticky, border-bottom, flex between */
/* ✅ z-40 — phải cao hơn sticky column trong Matrix (z-10) để tránh overlap */
h-20 bg-white dark:bg-slate-800 border-b border-border
flex items-center justify-between px-10 shrink-0 sticky top-0 z-40

/* Left: Đơn vị công tác */
flex flex-col
  span.text-xs.text-muted-foreground.font-semibold.uppercase  /* label */
  span.font-bold.text-foreground                              /* unit name */

/* Right: theme toggle + notifications + avatar + logout */
flex items-center gap-6
  /* Theme toggle button — xem Theme Toggle section */
/* Expanded: w-72, white bg, border-right, flex-col */
w-72 bg-sidebar border-r border-sidebar-border
flex flex-col shrink-0 shadow-sm z-20

/* Logo section (top) */
p-6 border-b border-border
  flex items-center gap-3
  /* Logo icon: gradient blue, rounded-xl */
  w-10 h-10 bg-gradient-to-br from-blue-600 to-blue-800
  rounded-xl flex items-center justify-center
  text-white font-black text-xl shadow-lg

/* Navigation (middle, scrollable) */
flex-1 p-4 space-y-2 overflow-y-auto
  /* Nav item (active) */
  w-full flex items-center gap-3 px-4 py-3 rounded-xl
  font-semibold bg-sidebar-primary text-sidebar-primary-foreground shadow
  /* Nav item (inactive) */
  w-full flex items-center gap-3 px-4 py-3 rounded-xl
  font-semibold text-sidebar-foreground hover:bg-sidebar-accent transition-all

/* Role switcher (bottom) */
p-6 border-t border-border
  bg-blue-50 dark:bg-blue-950 border border-blue-100 dark:border-blue-900
  rounded-2xl p-4
  /* Label */
  text-[10px] font-bold text-blue-400 uppercase tracking-tighter
  /* Select */
  text-sm font-bold text-blue-700 dark:text-blue-300

/* Collapsed (w-16, icon-only + tooltips) — future */
w-16 /* icons centered, text hidden, tooltips on hover */

/* Mobile (Sheet overlay) */
/* Trigger: hamburger button in header (visible on < lg) */
/* Content: same as expanded sidebar, inside <Sheet /> */

Content Area

/* Scrollable main content */
flex-1 flex flex-col overflow-hidden

/* Inner content */
flex-1 overflow-y-auto p-10 bg-background
  max-w-6xl mx-auto /* constrained width */
  space-y-8         /* section spacing */

Next.js App Router — File Structure Templates

// app/layout.tsx — Root layout với font + theme provider
import { beVietnamPro, lora, jetbrainsMono } from '@/lib/fonts'
import { ThemeProvider } from '@/components/theme-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="vi" suppressHydrationWarning>
      <body className={`${beVietnamPro.variable} ${lora.variable} ${jetbrainsMono.variable} font-ui`}>
        <ThemeProvider attribute="class" defaultTheme="light" enableSystem>
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

// app/(dashboard)/layout.tsx — Dashboard layout với sidebar + header
export default function DashboardLayout({ children }) {
  return (
    <div className="flex h-screen overflow-hidden">
      <Sidebar />
      <div className="flex flex-col flex-1 overflow-hidden">
        <Header />
        <main className="flex-1 overflow-y-auto p-10 bg-background">
          <div className="max-w-6xl mx-auto space-y-8">
            {children}
          </div>
        </main>
      </div>
    </div>
  )
}

// app/(dashboard)/[page]/loading.tsx — Skeleton loading state
export default function Loading() {
  return (
    <div className="space-y-8">
      <div className="animate-pulse bg-muted rounded-3xl h-32" />
      <div className="animate-pulse bg-muted rounded-3xl h-64" />
    </div>
  )
}

// app/(dashboard)/[page]/error.tsx — Error boundary
'use client'
export default function Error({ error, reset }) {
  return (
    <div className="bg-destructive/5 border border-destructive/20 rounded-2xl p-8 text-center">
      <AlertCircle className="h-10 w-10 text-destructive mx-auto mb-4" />
      <h2 className="text-lg font-bold mb-2">Đã xảy ra lỗi</h2>
      <p className="text-sm text-muted-foreground mb-4">{error.message}</p>
      <Button onClick={reset}>Thử lại</Button>
    </div>
  )
}

Theme Toggle

// components/theme-toggle.tsx — dùng next-themes
'use client'
import { useTheme } from 'next-themes'

export function ThemeToggle() {
  const { theme, setTheme } = useTheme()
  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
      aria-label="Chuyển giao diện sáng/tối"
    >
      <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
    </Button>
  )
}

Đặt <ThemeProvider attribute="class" defaultTheme="light"> trong root layout. Dùng suppressHydrationWarning trên <html> để tránh FOUC.

Responsive

Breakpoint Layout
Mobile (< 768px) Sidebar hidden → hamburger → Sheet. Header simplified. Content p-4.
Tablet (768-1023px) Sidebar collapsed (w-16, icon-only). Content p-6.
Desktop (>= 1024px) Sidebar expanded (w-72) + Header (h-20) + Content (p-10). Full layout.
Large Desktop (>= 1280px) Same as desktop, max-w-6xl keeps content centered.

Component Specs (Tailwind CSS Classes)

Component Status

Component Status Notes
Button (all variants incl. Soft) ✅ Ready
Card (all variants) ✅ Ready
Input, Textarea, Select ✅ Ready
Table, Matrix Table, Tree Table ✅ Ready Xem Performance section
Form (layout + validation) ✅ Ready react-hook-form + zod
Dialog, AlertDialog ✅ Ready 3 size variants
Tooltip ✅ Ready
Dropdown Menu ✅ Ready
Checkbox, Radio, Switch ✅ Ready
Navigation (Breadcrumb, Tabs, Stepper) ✅ Ready
Badge, Avatar, Skeleton, Empty State ✅ Ready
Toast (Sonner) ✅ Ready
Charts (Recharts) ✅ Ready Xem Charts section
Theme Toggle ✅ Ready
Error Boundary (error.tsx) ✅ Ready

Buttons

/* Base (all buttons) */
inline-flex items-center justify-center gap-2 rounded-2xl
font-bold text-sm transition-all duration-150 cursor-pointer
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring

/* Primary */
bg-primary text-primary-foreground shadow-lg
hover:bg-blue-700 dark:hover:bg-blue-400

/* Secondary */
bg-secondary text-secondary-foreground
hover:bg-accent

/* Outline */
border border-border bg-transparent text-foreground
hover:bg-accent

/* Ghost */
bg-transparent text-foreground
hover:bg-accent

/* Destructive */
bg-destructive text-destructive-foreground shadow-lg
hover:bg-red-700 dark:hover:bg-red-400

/* Success/CTA ("Ban hành", "Phê duyệt") */
bg-success text-success-foreground shadow-lg
hover:bg-emerald-700 dark:hover:bg-emerald-400

/* Soft — secondary CTA, không cạnh tranh với Primary */
/* Dùng cho: "Xem chi tiết", "Đề xuất", "Lịch sử" */
bg-primary/10 text-primary hover:bg-primary/20
dark:bg-primary/20 dark:hover:bg-primary/30

/* Link */
text-primary underline-offset-4 hover:underline font-bold text-xs

/* Loading state */
opacity-70 pointer-events-none
/* Prepend: <Loader2 className="h-4 w-4 animate-spin" /> */

/* Rate limit / disabled with countdown */
/* Dùng khi API trả về 429 Too Many Requests */
opacity-50 cursor-not-allowed
/* Prepend: countdown text "Thử lại sau Xs" */

/* Sizes */
/* sm */      h-9 px-4 text-xs
/* default */ h-11 px-6 py-2.5
/* lg */      h-12 px-8 text-base
/* icon */    h-10 w-10 p-0

Cards

/* Default Card */
bg-card border border-border rounded-3xl shadow-sm p-6

/* Interactive Card (hover effect, clickable) */
bg-card border border-border rounded-3xl shadow-sm p-6
hover:shadow-xl hover:border-primary/20 transition-all duration-200 cursor-pointer

/* Large Interactive Card */
bg-card border border-border rounded-3xl shadow-sm p-8
hover:shadow-xl hover:border-primary/20 transition-all duration-200 cursor-pointer
flex items-start gap-6 group

/* Stat/KPI Card */
bg-card border border-border rounded-3xl shadow-sm p-6
  /* Label */  text-[11px] font-bold text-muted-foreground uppercase tracking-widest
  /* Value */  text-4xl font-black text-primary mt-2 tabular-nums
  /* Desc */   text-xs text-muted-foreground mt-2 font-medium

/* Alert Card (e.g. Data Owner confirmation) */
bg-primary rounded-3xl text-primary-foreground shadow-xl p-6
flex justify-between items-center

/* Error Card */
bg-destructive/5 border border-destructive/20 rounded-2xl p-6
  /* Icon + message + retry button */

Inputs

/* Text Input */
w-full px-4 py-3 rounded-2xl border border-input bg-background
text-sm font-medium outline-none
focus:ring-2 focus:ring-ring focus:border-primary transition-all duration-200
placeholder:text-muted-foreground

/* Large Search Input (Tra cứu từ điển) */
w-full px-10 py-6 pl-16 rounded-full
border border-border shadow-2xl shadow-primary/10
text-xl font-medium outline-none
focus:ring-4 focus:ring-blue-100 dark:focus:ring-blue-900
focus:border-primary transition-all duration-200

/* Select — shadcn <Select /> với rounded-2xl trigger */

/* Textarea */
w-full px-4 py-3 rounded-2xl border border-input bg-background
min-h-[100px] text-sm font-medium
focus:ring-2 focus:ring-ring focus:border-primary

/* Checkbox — shadcn <Checkbox /> */
/* Radio — shadcn <RadioGroup /> + <RadioGroupItem /> */
/* Switch — shadcn <Switch /> */
/* Tất cả dùng --primary color cho checked state */

/* Error state (any input) */
border-destructive focus:ring-destructive/20

/* Disabled state */
opacity-50 cursor-not-allowed bg-muted

Tooltip

/* shadcn <Tooltip /> — cấu hình chuẩn */
/* Provider: <TooltipProvider delayDuration={200}> */

/* Trigger: bất kỳ element nào */
/* Content: */
bg-foreground text-background text-xs font-medium
px-3 py-1.5 rounded-lg shadow-md
max-w-[240px] text-center

/* Usage bắt buộc cho disabled buttons do data state */
<TooltipProvider>
  <Tooltip>
    <TooltipTrigger asChild>
      <span> {/* wrap trong span để tooltip hoạt động với disabled button */}
        <Button disabled>Phê duyệt</Button>
      </span>
    </TooltipTrigger>
    <TooltipContent>
      <p>DE đã PUBLISHED, không thể sửa</p>
    </TooltipContent>
  </Tooltip>
</TooltipProvider>

Lưu ý: Tooltip không hoạt động trực tiếp trên <button disabled> — phải wrap trong <span>.


/* shadcn <DropdownMenu /> — general pattern */
/* Trigger: Button hoặc bất kỳ element nào */
/* Content: */
bg-popover border border-border rounded-2xl shadow-lg p-1
min-w-[160px]

/* Item: */
px-3 py-2 text-sm font-medium rounded-xl cursor-pointer
hover:bg-accent transition-colors duration-150

/* Separator: */
h-px bg-border my-1

/* Label: */
px-3 py-1.5 text-[11px] font-bold text-muted-foreground uppercase tracking-widest

/* Export Dropdown (shared pattern — tất cả màn hình report) */
DropdownMenu
  Trigger: Button(variant="outline")  Download + "Export" + ChevronDown
  Content:
    DropdownMenuLabel "Chọn định dạng"
    DropdownMenuItem  FileSpreadsheet + "Excel (.xlsx)"
    DropdownMenuItem  FileText + "CSV (.csv)"
    DropdownMenuItem  File + "PDF (.pdf)"
    DropdownMenuSeparator
    DropdownMenuLabel "Phạm vi"
    DropdownMenuRadioGroup "Trang hiện tại" | "Tất cả dữ liệu"

Tables

/* Table container */
bg-card rounded-3xl border border-border overflow-hidden shadow-sm

/* Table header row */
bg-muted border-b border-border
  /* Header cell */
  px-8 py-5 font-bold text-muted-foreground uppercase tracking-widest text-[11px]

/* Table body row */
hover:bg-accent/50 transition-colors duration-150

/* Table cell */
px-8 py-4 text-sm font-medium

/* Number cells — tabular-nums thay vì font-mono */
px-8 py-4 text-sm font-medium tabular-nums text-right

/* Divider */
divide-y divide-border

/* Sortable header */
cursor-pointer select-none hover:text-foreground transition-colors
flex items-center gap-1
  /* Sort icon: ChevronUp/Down h-3 w-3 */

/* Matrix table (Bản đồ hiện trạng — special) */
/* Sticky first column */
/* ✅ z-10 — Header là z-40, không conflict */
sticky left-0 bg-card z-10 border-r border-border

/* Dot indicator */
w-4 h-4 rounded-[4px] mx-auto
/* Color per state — xem Matrix View section */

/* Color-coded cells */
bg-green-50 dark:bg-green-950  /* CONFIRMED */
bg-red-50 dark:bg-red-950      /* DISPUTED */

/* Tree table (Anchored Data) */
/* Domain row: bold, has expand icon */
bg-muted/50 font-black text-foreground flex items-center gap-2
/* Sub Domain row: indented, italic */
pl-14 font-bold text-muted-foreground italic
/* Data Element row: deep indent, bullet */
pl-20 font-medium text-muted-foreground

Forms

Pattern: react-hook-form + zod — bắt buộc cho tất cả form có validation.

// Form pattern chuẩn
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

const schema = z.object({
  name: z.string().min(1, 'Tên không được để trống'),
  description: z.string().optional(),
})

export function MyForm() {
  const form = useForm({ resolver: zodResolver(schema) })

  // Confirm before leave nếu form đã dirty
  useEffect(() => {
    const handler = (e: BeforeUnloadEvent) => {
      if (form.formState.isDirty) e.preventDefault()
    }
    window.addEventListener('beforeunload', handler)
    return () => window.removeEventListener('beforeunload', handler)
  }, [form.formState.isDirty])

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
          <FormField name="name" render={({ field }) => (
            <FormItem>
              <FormLabel>Tên <span className="text-destructive">*</span></FormLabel>
              <FormControl>
                <Input {...field} />
              </FormControl>
              <FormMessage /> {/* inline error, real-time */}
            </FormItem>
          )} />
        </div>
        <div className="flex justify-end gap-3 mt-6">
          <Button type="button" variant="outline">Hủy</Button>
          <Button type="submit" disabled={form.formState.isSubmitting}>
            {form.formState.isSubmitting && <Loader2 className="h-4 w-4 animate-spin" />}
            Lưu
          </Button>
        </div>
      </form>
    </Form>
  )
}
/* Form layout */
/* Desktop: 2 cột | Mobile: 1 cột */
grid grid-cols-1 lg:grid-cols-2 gap-6

/* Label + Input group */
flex flex-col gap-2
  /* Label */
  text-sm font-semibold text-foreground
  /* Required indicator */
  text-destructive ml-1  /* asterisk */

/* Error message — inline, gần field */
text-xs text-destructive font-medium mt-1

/* Help text */
text-xs text-muted-foreground mt-1

/* Submit button — loading state */
opacity-70 pointer-events-none  /* khi isSubmitting = true */

Quy tắc form: - Validation: inline real-time (onChange mode), error message ngay dưới field - Submit state: spinner + disable button trong lúc submit - Dirty check: confirm dialog nếu user rời trang khi form đã thay đổi - Error handling: try/catch trong onSubmit → toast error nếu API fail


Modals/Dialogs

/* Dialog sizes */
max-w-lg    /* sm  — form đơn giản, confirm */
max-w-2xl   /* md  — form 2 cột, detail panel */
max-w-4xl   /* lg  — complex form, preview */

/* Dialog wrapper */
bg-card rounded-3xl border border-border shadow-xl p-0
/* Animation — tailwindcss-animate */
data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95
data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95
duration-200

/* Dialog Header */
px-8 py-6 border-b border-border
  /* Title */       text-xl font-bold
  /* Description */ text-sm text-muted-foreground mt-1

/* Dialog Body */
px-8 py-6

/* Dialog Footer */
px-8 py-4 border-t border-border flex justify-end gap-3

/* AlertDialog — CHỈ dùng cho hành động destructive/irreversible */
/* Thêm: warning icon + mô tả hậu quả rõ ràng */
/* Destructive action button: variant="destructive" */
/* Cancel button: variant="outline" */
/* Irreversible actions: delete user, reject mapping, ban hành */

/* Sheet (mobile sidebar, detail panels) */
/* Slides from right, full height */
/* Width: w-full sm:max-w-lg */

/* Breadcrumb */
flex items-center gap-2 text-sm text-muted-foreground
  /* Active item */ font-semibold text-foreground
  /* Separator */   text-muted-foreground/50 mx-1

/* Tabs */
border-b border-border
  /* Tab trigger */
  px-4 py-2 text-sm font-semibold text-muted-foreground
  border-b-2 border-transparent transition-all duration-150
  hover:text-foreground
  /* Tab trigger (active) */
  text-primary border-primary

/* Workflow Stepper — BPF lifecycle */
/* Màn hình: SCR-DISC-10, SCR-MAP-20, SCR-MAP-10 (Approver), SCR-PLAN-10 */
flex items-center gap-2 py-4 px-6 bg-muted/30 rounded-2xl

  /* Step completed */
  div.w-7.h-7.bg-success.text-white.rounded-full + Check.h-4.w-4
  span.text-sm.font-semibold.text-success

  /* Step current */
  div.w-7.h-7.bg-primary.text-white.rounded-full.ring-4.ring-primary/20
    span.text-xs.font-black  /* step number */
  span.text-sm.font-semibold.text-primary

  /* Step upcoming */
  div.w-7.h-7.bg-muted.text-muted-foreground.rounded-full
    span.text-xs.font-semibold
  span.text-sm.font-medium.text-muted-foreground

  /* Connectors */
  div.h-[2px].w-8.bg-success  /* completed → completed */
  div.h-[2px].w-8.bg-border   /* current → upcoming */

/* Steps: 1.Discovery → 2.Matching → 3.Rà soát → 4.Xác nhận → 5.Phê duyệt → 6.Ban hành */

Data Display

/* Badge — status indicators */
inline-flex items-center px-3 py-1 text-[10px] font-black uppercase rounded-lg border

/* Badge variants */
/* Published */ bg-green-100 text-green-700 border-green-200
               dark:bg-green-950 dark:text-green-400 dark:border-green-800
/* Review */    bg-yellow-100 text-yellow-700 border-yellow-200
               dark:bg-yellow-950 dark:text-yellow-400 dark:border-yellow-800
/* Draft */     bg-slate-100 text-slate-500 border-slate-200
               dark:bg-slate-800 dark:text-slate-400 dark:border-slate-700
/* AI Matched */ bg-blue-100 text-blue-700 border-blue-200
                dark:bg-blue-950 dark:text-blue-400 dark:border-blue-800
/* DISPUTED */  bg-red-100 text-red-700 border-red-200
               dark:bg-red-950 dark:text-red-400 dark:border-red-800
/* CONFIRMED */ bg-green-100 text-green-700 border-green-200

/* Avatar */
w-10 h-10 bg-muted rounded-full flex items-center justify-center
font-bold text-muted-foreground
/* next/image cho avatar từ server — không dùng <img> */

/* Skeleton — cho page load (bukan inline action) */
animate-pulse bg-muted rounded-2xl
/* Inline action spinner: <Loader2 className="h-4 w-4 animate-spin" /> */

/* Empty state */
flex flex-col items-center justify-center py-16 text-center
  /* Icon */        h-12 w-12 text-muted-foreground/30
  /* Title */       text-lg font-bold text-muted-foreground mt-4
  /* Description */ text-sm text-muted-foreground mt-2
  /* Action */      mt-4  /* Primary button — xem Role-Specific Empty States */

/* Error state — component level */
bg-destructive/5 border border-destructive/20 rounded-2xl p-6
  /* Icon + message + retry button */
  /* Global error: dùng toast (sonner) */

Charts

// Chart Container — wrapper chuẩn cho tất cả charts
function ChartContainer({ title, children, onExport }) {
  return (
    <div className="bg-card border border-border rounded-3xl shadow-sm p-6">
      <div className="flex items-center justify-between mb-6">
        <h3 className="text-lg font-bold">{title}</h3>
        {onExport && (
          <Button variant="outline" size="sm" onClick={onExport}>
            <Download className="h-4 w-4 mr-2" /> Xuất
          </Button>
        )}
      </div>
      {children}
    </div>
  )
}

// Recharts — sử dụng CSS variables cho màu series
// Mapping: var(--chart-1) → Blue, var(--chart-2) → Cyan, etc.
import { LineChart, Line, BarChart, Bar, XAxis, YAxis,
         CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'

function KPILineChart({ data }) {
  return (
    <ChartContainer title="Tiến độ xác nhận">
      <ResponsiveContainer width="100%" height={300}>
        <LineChart data={data}>
          <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
          <XAxis dataKey="month" tick={{ fontSize: 11, fontWeight: 700,
            fill: 'var(--muted-foreground)', fontFamily: 'var(--font-ui)' }} />
          <YAxis tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }} />
          <Tooltip
            contentStyle={{ background: 'var(--popover)', border: '1px solid var(--border)',
              borderRadius: '12px', fontFamily: 'var(--font-ui)' }}
          />
          <Line dataKey="confirmed" stroke="var(--chart-1)" strokeWidth={2} dot={false} />
          <Line dataKey="disputed"  stroke="var(--chart-5)" strokeWidth={2} dot={false} />
        </LineChart>
      </ResponsiveContainer>
    </ChartContainer>
  )
}

// Heatmap (Maturity Score): green-600 / yellow-500 / red-600 / slate-200
// Sử dụng --success / --warning / --destructive / --muted

Feedback & Error Handling

/* Toast (Sonner) */
/* Setup: <Toaster position="top-right" richColors duration={3000} /> */
/* Slide-in animation: slide-in-from-top-2 fade-in duration-200 */

/* Variants: */
toast.success("Lưu thành công")        /* green */
toast.error("Lỗi khi lưu dữ liệu")    /* red */
toast.warning("Cảnh báo: X DEs chưa xác nhận") /* yellow */
toast.info("Đang xử lý...")            /* blue */

/* Progress bar */
h-3 bg-muted rounded-full overflow-hidden
  /* Fill */ h-full bg-primary rounded-full

/* Notification item */
flex gap-4 p-4 bg-primary/5 rounded-2xl border border-primary/10
  /* Dot */ w-2 h-2 bg-primary rounded-full mt-2 animate-pulse
  /* Content */ text-sm

Global Error Handling Pattern:

// Server Action error pattern
async function saveData(data) {
  try {
    await api.save(data)
    toast.success("Lưu thành công")
  } catch (error) {
    if (error.status === 429) {
      // Rate limit: disable button + countdown
      toast.error(`Quá nhiều yêu cầu. Thử lại sau ${error.retryAfter}s`)
    } else if (error.status >= 500) {
      toast.error("Lỗi hệ thống. Vui lòng thử lại.")
    } else {
      toast.error(error.message || "Đã xảy ra lỗi")
    }
  }
}

// Batch action partial failure
async function batchConfirm(ids) {
  const results = await api.batchConfirm(ids)
  const failed = results.filter(r => !r.success)
  if (failed.length === 0) {
    toast.success(`Đã xác nhận ${ids.length} mục`)
  } else {
    toast.warning(`Xác nhận ${ids.length - failed.length}/${ids.length} mục. ${failed.length} mục thất bại.`)
    // Hiển thị danh sách lỗi trong Dialog
  }
}

Authorization & Permission-Based UI

GovData uses PBAC (hybrid). Access depends on role, organization unit, AND data attributes (status, ownership). UI must reflect all 3 dimensions — not just role.

3-Layer Permission Model

Layer 1: ROLE         → What type of actions can I perform? (menu, buttons)
Layer 2: UNIT SCOPE   → What data can I see? (Data Owner: own unit only)
Layer 3: DATA STATE   → Can I act on THIS specific record? (status, ownership, guards)

UI Impact Per Layer

Layer UI Pattern Example
Role (RBAC) Hide/show menu items, sections, action buttons Manager sees "Sửa", Staff doesn't
Unit scope (ABAC) Filter data, scope entire view to user's unit Data Owner sees only own unit's column in matrix
Data state (ABAC) Disable/enable per-record actions, show tooltips "Xác nhận" button disabled if cell already CONFIRMED

Permission-Aware UI Strategies

Strategy When to use Example
Hide element User's ROLE never allows this action Staff never sees "Sửa" — hide completely
Show disabled + tooltip User's role allows, but DATA STATE prevents Button disabled: "Không thể sửa DE đã PUBLISHED"
Filter data User can see layout, but UNIT SCOPE limits records Data Owner sees card list of own unit's fields only
Replace entire section Role implies fundamentally different task on same page MAP-10: Manager → matrix edit, Data Owner → card review
Add role-specific panel Extra context for one role Approver → Readiness Panel on MAP-10
Conditional cell actions Same table, different actions per cell based on ownership Data Owner: "Đánh dấu tranh chấp" only on own unit cells

Data-Level Permission Examples

Screen Action Role Check Unit Check Data State Check UI when denied
MAP-10 Click cell to edit Manager only Cell ≠ CONFIRMED Disabled + tooltip
MAP-10 Confirm mapping Data Owner Own unit cells only Cell status = pending Hidden for other units' cells
MAP-10 Mark DISPUTED Data Owner Own unit cells only Cell ≠ already DISPUTED Hidden for other units
MAP-10 Mark DISPUTED Manager Any cell Cell ≠ already DISPUTED Disabled if already DISPUTED
MAP-43 Approve Bảng C Approver >70% confirmed + 0 DISPUTED Button disabled + guard status display
ANCHOR-10 Edit Domain Manager Domain has no PUBLISHED DEs Disabled if has PUBLISHED
ANCHOR-10 Delete Domain Manager No child Sub Domains AlertDialog with warning
PLAN-21 Chốt chủ quản Approver DE has proposed owner Disabled if no proposal
PLAN-31 Ban hành Approver All DEs = APPROVED Disabled + "X DEs chưa APPROVED"
DICT-10 Search All roles Staff: PUBLISHED only Staff never sees non-PUBLISHED
FUTURE-20 Approve Approver Status = DRAFT Hidden if already APPROVED
SYS-10 Delete user Admin User ≠ self "Không thể xóa chính mình"

Implementation Pattern

// Server Component — permission check phía server
const { role, unitId } = await getSession()
const permissions = await getPermissions(role, unitId, resourceId)
// permissions = { canEdit: bool, canConfirm: bool, canApprove: bool, denyReason: string }

{permissions.canEdit && <Button>Sửa</Button>}
{permissions.canConfirm && <Button>Xác nhận</Button>}
{!permissions.canApprove && (
  <TooltipProvider>
    <Tooltip>
      <TooltipTrigger asChild>
        <span>
          <Button disabled>Phê duyệt</Button>
        </span>
      </TooltipTrigger>
      <TooltipContent>{permissions.denyReason}</TooltipContent>
    </Tooltip>
  </TooltipProvider>
)}

// KHÔNG làm:
// ❌ CSS-only hiding (security risk)
// ❌ Client-side role check cho security-sensitive actions
// ❌ Assume role alone — luôn check data state

Disabled State UX Rules

When an action is permission-denied due to data state (not role):

  1. Show the button — user biết action này tồn tại
  2. Disable it (opacity-50 cursor-not-allowed)
  3. Add tooltip explaining WHY ("DE đã PUBLISHED, không thể sửa")
  4. Never show an error after click — prevent the click entirely
  5. Guard conditions (e.g., Bảng C approval) → show status panel with checkmarks

Screen × Role Matrix

Screen Manager Data Owner Approver Staff Admin
DASH-10 Action Items (matching, review, proposals) + KPIs + Progress Hero confirmation card + deadline + proposals Action Items (DISPUTED, Bảng C, ownership) + mini heatmap Search-first hero Unassigned users + system stats
ANCHOR-10 Full CRUD (add/edit/delete Domain/Sub/DE) — (no access; proposes via ANCHOR-14) Read-only tree view (no action buttons, no "Thêm")
ANCHOR-20 Full detail + "Sửa" button Read-only detail (no "Sửa" button)
ANCHOR-14 Submit proposal form (from DICT or MAP context)
ANCHOR-15 Review proposals + approve/reject
MAP-10 Full matrix + cell click edit + matching trigger Card-based review list (own unit only) + inline confirm/reject + batch confirm + deadline Readiness panel + read-only matrix + DISPUTED badge + "Phê duyệt Bảng C" button
MAP-20 Batch controls + AI matching trigger + inline review + batch accept
MAP-40 Mark any cell DISPUTED Mark own unit cells DISPUTED
MAP-41/42 View DISPUTED list + resolve each (3 options)
MAP-43 Approve Bảng C (guard conditions)
DISC-10/11/20 Full CRUD + upload + matching trigger
PLAN-10 — (read-only if accessible) Full control: chốt chủ quản, ban hành
PLAN-20/21 Detail + approve/reject ownership
PLAN-31 Ban hành AlertDialog (irreversible)
DICT-10/20 Full search + browse + "Đề xuất DE mới" on no-results Full search + browse Full search + browse Full search + browse (only PUBLISHED data)
DICT-30/40 Tree browse + owner info Tree browse + owner info Tree browse + owner info Tree browse + owner info
FUTURE-10 CRUD (add/edit/delete) + list Read-only list + "Phê duyệt/Từ chối" buttons
FUTURE-20 Detail + "Sửa" button Detail + "Phê duyệt/Từ chối" buttons
RPT-10 All reports + drill-down + export All reports + drill-down + export
RPT-20/30 Overlap/fragmentation tables + export Same view (no difference)
RPT-40 Progress tabs (by unit, by domain) + drill-down Same view
RPT-50 Heatmap + drill-down Sheet + export Same view + highlighted "Sở chưa đạt" rows
SYS-10/11 Full user CRUD
SYS-20 View permission matrix
SYS-30/31 Org tree CRUD + Import CSV
SYS-40 Master Data import + catalogs

Role-Specific Empty States

Role Screen Empty State Message CTA
Manager DISC-10 "Chưa có nguồn metadata. Upload file để bắt đầu." "Nhập metadata"
Manager MAP-20 "Chưa chạy matching. Chọn batch và khởi động." "Khởi động Matching"
Data Owner MAP-10 "Đơn vị bạn chưa có dữ liệu cần xác nhận." — (no action needed)
Approver MAP-41 "Không còn ô tranh chấp." (success state) "Quay lại Bản đồ"
Approver PLAN-10 "Bảng C chưa được chốt. Chờ Manager hoàn thành Discovery & Matching."
Staff DICT-10 "Từ điển chưa được ban hành."
Admin SYS-10 "Chưa có người dùng. Thêm người dùng đầu tiên." "Thêm người dùng"

Style Guidelines

Style: Government Modern Keywords: Clean, spacious, rounded, bold typography, professional, trustworthy, modern government

Key Effects

  • rounded-3xl (24px) on all major surfaces — cards, panels, tables
  • rounded-2xl (16px) on buttons, inputs, sub-elements
  • shadow-sm on cards (subtle), shadow-lg on primary buttons
  • shadow-2xl shadow-primary/10 on hero search input
  • bg-gradient-to-br from-blue-600 to-blue-800 on logo icon only
  • animate-pulse on notification dot only
  • Transitions: transition-all duration-[150-300ms] (xem Animation Tokens)
  • tracking-widest + uppercase on small labels (KPI cards, table headers)
  • tracking-tight + leading-tight on page titles (H1) — leading bắt buộc cho tiếng Việt
  • font-black (900) + tabular-nums for KPI numbers

Icon Size Scale

Context Size Class
Navigation (sidebar) 20×20px h-5 w-5
Inline (button, table) 16×16px h-4 w-4
Empty state 48×48px h-12 w-12
Large decorative 24×24px h-6 w-6

Lucide only — không dùng emoji, không dùng custom SVG inline.

UX Philosophy: "Design for Government Officers"

Principle Implementation
Important first KPI cards at top of dashboard, pending actions as alerts, DISPUTED count as badge
Fewer clicks Inline confirm/reject on Bản đồ, role switcher without logout, expandable tree table
Smart confirmation Only <AlertDialog /> for: delete user, reject mapping, ban hành (irreversible). Normal create/edit → save + toast
Quick save, easy edit Popup forms save on submit, toast success, close popup. Re-edit by re-opening
Progressive disclosure Tree table collapsed by default. Matrix shows dots, click for detail. Reports drill-down
Role-aware UI Same screen, different actions visible per role. Data Owner sees confirmation alert. Approver sees approval controls

Enterprise UX Rules

Rule Requirement
Clickable elements cursor-pointer required on all
Icons Lucide only — no emoji, no custom SVG. Dùng Icon Size Scale ở trên
Transitions duration-150 hover, duration-200 focus, duration-300 theme (xem Animation Tokens)
Form validation react-hook-form + zod, inline real-time, error near field
Loading states Skeleton cho page load; spinner (Loader2 animate-spin) cho inline action
Empty states Illustration-free: icon + message + action button
Data tables Sortable headers, row hover, sticky first column on matrix
ID codes font-mono text-[10px] font-bold text-muted-foreground
Number columns tabular-nums — không dùng font-mono cho số trong bảng
Status badges Uppercase, bold, color-coded + text label (không dùng màu đơn độc)
Page titles Left-aligned, with subtitle if needed. Action buttons top-right
Security DOMPurify/sanitize-html cho dynamic content từ user input; CSRF token trong form

Anti-Patterns

  • No emojis as icons — use Lucide SVG icons
  • No missing cursor-pointer on clickable elements
  • No layout-shifting hovers
  • No low contrast text (4.5:1 minimum WCAG AA)
  • No instant state changes — always transition (xem Animation Tokens)
  • No invisible focus states
  • No placeholder-only inputs — always <Label />
  • No unnecessary confirmation dialogs — only for destructive/irreversible actions
  • No rounded-none or rounded-sm — minimum rounded-lg (8px) on any surface
  • No thin font weights (300/400) for action labels — minimum font-semibold (600)
  • No generic gray buttons for primary actions — always bg-primary or bg-success
  • No font-mono for number columns in tables — use tabular-nums instead
  • No opacity shortcuts in OKLCH dark mode tokens — use full OKLCH values

Domain-Specific Patterns

Bản đồ hiện trạng (Matrix View)

The most complex screen — Data Element rows × Organization Unit columns.

/* Matrix container */
bg-card rounded-3xl border border-border overflow-hidden shadow-sm
overflow-x-auto
overscroll-behavior-x: contain  /* ✅ tránh kích hoạt browser back-swipe trên mobile */

/* First column sticky */
/* ✅ z-10 — Header là z-40, đảm bảo không overlap */
sticky left-0 bg-card z-10 w-48 border-r border-border

/* Cell states (dot + background + text label) */
/* ✅ Color không phải chỉ báo duy nhất — luôn có text label */
DATABASE:          dot bg-primary,      cell bg-transparent,      label "DATABASE"
DATABASE_OVERLAP:  dot bg-primary/30,   cell bg-transparent,      label "OVERLAP"
FRAGMENTED:        dot bg-warning,      cell bg-transparent,      label "FRAGMENTED"
READY:             dot bg-success,      cell bg-transparent,      label "READY"
EMPTY:             no dot,              cell bg-transparent,      label "—"
DISPUTED:          dot bg-destructive,  cell bg-red-50/bg-red-950, label "DISPUTED"
CONFIRMED:         dot bg-success,      cell bg-green-50/bg-green-950, label "CONFIRMED"
REJECTED:          no dot,              cell bg-muted,            strikethrough text

Performance — bắt buộc khi số lượng dữ liệu lớn:

// Dùng @tanstack/react-virtual cho Matrix lớn
import { useVirtualizer } from '@tanstack/react-virtual'

// Quy tắc:
// - Virtualize cả rows VÀ columns khi > 50 columns hoặc > 200 rows
// - Debounce filter/search: useDebouncedCallback(fn, 300)
// - Lazy load data theo viewport: chỉ fetch rows đang hiển thị
// - Test baseline: 50 columns × 500 rows — phải smooth trên Chrome Windows

// Chunk loading pattern
const CHUNK_SIZE = 50
const [visibleRange, setVisibleRange] = useState({ start: 0, end: CHUNK_SIZE })

Tree Table (Anchored Data)

3-level hierarchy in a single flat table:

/* Level 1: Domain — bold, expand icon, background tint */
bg-muted/50 px-8 py-4 font-black text-foreground
  /* Expand icon: ChevronDown (expanded) / ChevronRight (collapsed) */
  /* aria-expanded: true/false */

/* Level 2: Sub Domain — indented, italic */
px-14 py-3 font-bold text-muted-foreground italic

/* Level 3: Data Element — deep indent, bullet prefix */
px-20 py-2 font-medium text-muted-foreground
  /* Prefix: "• " before name */

Search-centric (Tra cứu Từ điển)

/* Centered layout, no sidebar context */
max-w-4xl mx-auto space-y-12 pt-12

/* Title block — centered, large */
text-center space-y-4
  h1: text-4xl font-black tracking-tighter leading-tight  /* ✅ leading-tight */
  p:  text-lg text-muted-foreground font-medium

/* Search input — hero element */
rounded-full shadow-2xl shadow-primary/10 text-xl

/* Results — card grid */
grid grid-cols-1 md:grid-cols-2 gap-6
  /* Each result: interactive card with icon, title, ID, owner badge */

i18n Strategy

Thư viện: next-intl (khuyến nghị cho Next.js App Router)

messages/
  vi.json   ← Primary language
  en.json   ← Secondary (fallback)

Key naming convention: page.section.element

// vi.json
{
  "dashboard": {
    "kpi": {
      "confirmedLabel": "Đã xác nhận",
      "disputedLabel": "Đang tranh chấp"
    }
  },
  "matrix": {
    "emptyState": "Đơn vị bạn chưa có dữ liệu cần xác nhận.",
    "states": {
      "DATABASE": "DATABASE",
      "CONFIRMED": "ĐÃ XÁC NHẬN",
      "DISPUTED": "TRANH CHẤP"
    }
  }
}

Quy tắc quan trọng:

// Số liệu — dùng Intl.NumberFormat
const formatNumber = (n: number) =>
  new Intl.NumberFormat('vi-VN').format(n)
// → "1.234.567"

// Ngày — dùng Intl.DateTimeFormat
const formatDate = (d: Date) =>
  new Intl.DateTimeFormat('vi-VN', { dateStyle: 'medium' }).format(d)
// → "21 thg 4, 2026"

// Tiền tệ
const formatCurrency = (n: number) =>
  new Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND' }).format(n)

// ID codes — KHÔNG dịch, render raw
// DE001-DM1.1, BPF-01 → thêm data-i18n-ignore hoặc đặt trong <code>
<code className="font-mono text-[10px] font-bold text-muted-foreground">
  {dataElement.code}
</code>

Layout tiếng Việt: - Text tiếng Việt thường dài hơn English 15-30% — thiết kế button/label có đủ padding - Tránh fixed width cho text containers - Test layout với nội dung tiếng Việt thực tế, không dùng Lorem Ipsum


Security UI Patterns

// CSRF — thêm vào tất cả Server Actions hoặc form POST
// Next.js App Router Server Actions tự handle CSRF nếu dùng đúng pattern
// Nếu dùng custom API route: thêm csrf-token header

// XSS — sanitize dynamic content từ user input trước khi render
import DOMPurify from 'dompurify'
const safeHtml = DOMPurify.sanitize(userGeneratedContent)
// Dùng cho: mô tả DE, comment, annotation

// Rate limit (HTTP 429) — feedback UI
function RateLimitedButton({ children, onClick }) {
  const [countdown, setCountdown] = useState(0)

  const handleClick = async () => {
    try {
      await onClick()
    } catch (error) {
      if (error.status === 429) {
        const retryAfter = error.retryAfter || 30
        setCountdown(retryAfter)
        const interval = setInterval(() => {
          setCountdown(c => {
            if (c <= 1) { clearInterval(interval); return 0 }
            return c - 1
          })
        }, 1000)
        toast.error(`Quá nhiều yêu cầu. Thử lại sau ${retryAfter}s`)
      }
    }
  }

  return (
    <Button onClick={handleClick} disabled={countdown > 0}>
      {countdown > 0 ? `Thử lại sau ${countdown}s` : children}
    </Button>
  )
}

Style Guidelines (Summary)

Key Effects

  • rounded-3xl (24px) on all major surfaces — cards, panels, tables
  • rounded-2xl (16px) on buttons, inputs, sub-elements
  • shadow-sm on cards (subtle), shadow-lg on primary buttons
  • shadow-2xl shadow-primary/10 on hero search input
  • bg-gradient-to-br from-blue-600 to-blue-800 on logo icon only
  • animate-pulse on notification dot only
  • tracking-widest + uppercase on small labels (KPI cards, table headers)
  • tracking-tight + leading-tight on page titles (H1) — leading bắt buộc
  • font-black (900) + tabular-nums for KPI numbers

Pre-Delivery Checklist

Before delivering any UI code, verify:

Visual Quality

  • [ ] No emojis as icons (use Lucide SVG icons)
  • [ ] All icons from lucide-react, sizing theo Icon Size Scale (h-5 w-5 nav, h-4 w-4 inline)
  • [ ] Hover states with smooth transitions (duration-150 to duration-300)
  • [ ] No layout-shifting hovers
  • [ ] All surfaces use rounded-2xl minimum (buttons) or rounded-3xl (cards)
  • [ ] KPI numbers use font-black text-4xl tabular-nums
  • [ ] Status badges use uppercase + bold + colored border + text label (không chỉ màu)
  • [ ] H1/H2 có leading-tight / leading-snug — test với nội dung tiếng Việt thực tế

Interaction

  • [ ] cursor-pointer on ALL clickable elements
  • [ ] Visible focus states (focus-visible:ring-2) for keyboard navigation
  • [ ] Loading states: skeleton cho page load, spinner cho inline action
  • [ ] <AlertDialog /> ONLY for destructive/irreversible actions
  • [ ] Form validation: react-hook-form + zod, inline error near field, dirty check
  • [ ] Disabled buttons có tooltip giải thích lý do (khi do data state, không phải role)
  • [ ] Rate limit (429): button disabled + countdown timer

Light/Dark Mode

  • [ ] Text contrast >= 4.5:1 in both modes
  • [ ] Borders visible in both modes (slate-200 light, slate-700 dark)
  • [ ] Cards distinguishable from background in both modes
  • [ ] Status badge colors readable in both modes
  • [ ] Matrix cell backgrounds visible in both modes
  • [ ] Dark mode tokens dùng full OKLCH values (không dùng opacity shortcuts)

Layout & Responsive

  • [ ] Sidebar w-72 on desktop (z-20), Sheet on mobile
  • [ ] Header h-20 sticky z-40 (cao hơn Matrix sticky column z-10)
  • [ ] Content area p-10 with max-w-6xl
  • [ ] Matrix: overflow-x-auto + overscroll-behavior-x: contain
  • [ ] Matrix sticky column: z-10 (thấp hơn header)
  • [ ] No horizontal scroll on standard pages
  • [ ] Test at: 375px, 768px, 1024px, 1440px

Accessibility

  • [ ] Alt text on all images (dùng next/image)
  • [ ] Labels on all form inputs (no placeholder-only)
  • [ ] aria-label on icon-only buttons
  • [ ] Color is NOT the only indicator (dots + background + text label for matrix cells)
  • [ ] prefers-reduced-motion respected (CSS global rule)
  • [ ] Semantic HTML (headings hierarchy, landmarks)
  • [ ] Skip-to-content link
  • [ ] Matrix Table: role="grid", aria-colcount, aria-rowindex
  • [ ] Toast/notifications: aria-live="polite"
  • [ ] Form errors: aria-describedby linking input to error message
  • [ ] Keyboard navigation trong Matrix: ArrowKeys di chuyển cell, Space/Enter mở detail

Security

  • [ ] Dynamic user content (mô tả, comment) qua DOMPurify trước khi render
  • [ ] Rate limit (429) có feedback UI (countdown)
  • [ ] Không log sensitive data (token, password) lên console

Performance (Matrix)

  • [ ] Virtual scroll khi > 50 columns hoặc > 200 rows (@tanstack/react-virtual)
  • [ ] Debounce search/filter: 300ms
  • [ ] Lazy load data theo viewport

i18n

  • [ ] All user-facing text uses translation keys (vi primary, en secondary)
  • [ ] Layout handles text length differences (VI thường dài hơn EN 15-30%)
  • [ ] Số dùng Intl.NumberFormat('vi-VN')
  • [ ] Ngày dùng Intl.DateTimeFormat('vi-VN')
  • [ ] ID codes (DM{N}, DE{NNN}) render raw, không dịch