tao-ui-sub-page
Quy tắc tạo Sub Page (trang full-screen đặc biệt, không dùng dialog) trong SASUCO InvoiceEasy. Dùng khi: form quá nhiều fields không vừa dialog, trang có nhiều section/tab phức tạp, cần breadcrumb + n
Files1
View on GitHubSKILL.md.claude/skills/_archived-sasuco/tao-ui-sub-page/SKILL.md
GitHubQuy Tắc Sub Page — SASUCO InvoiceEasy
Rules nền tảng (buttons, icon classes, data-qa, SearchCombobox, Textarea, format) → đọc
tao-ui-giao-dien.
Khi Nào Dùng Sub Page (không dùng Dialog)
→ Xem bảng Chọn Loại UI tại tao-ui-giao-dien.
PAGE_ID + PAGE_FEATURES (BẮT BUỘC)
Mọi Sub Page PHẢI khai báo
PAGE_IDvàPAGE_FEATURESđể sơ đồ dự án và export menu permission hoạt động.
// Metadata cho so do du an
export const PAGE_ID = 'ten-page-id' // phai khop navItem.id trong NavMenu
export const PAGE_FEATURES = [
{ label: 'Làm mới', code: 'btn-refresh' },
{ label: 'Lưu thay đổi', code: 'btn-save' },
{ label: 'Quay lại', code: 'btn-back' },
]
// ---
PAGE_IDphải khớp vớinavItem.idtrongNavMenuportal tương ứngPAGE_FEATURESliệt kê TẤT CẢ các nút/thao tác thực tế có trong trangcodeprefix:btn-cho nút toolbar/header,row-cho action trên dòng- Đặt ở đầu file, ngay trước
export function XxxPage() - Xem chi tiết tại
export-menu-page-permission/SKILL.md
Cấu Trúc Layout (BẮT BUỘC)
<>
├── [1] Sticky Header (Breadcrumb + Page Header) — nổi lên khi scroll
└── [2] Content Cards (cuộn bên dưới sticky)
├── Card thông tin chính
├── Card thông tin phụ (nếu có)
├── Card bảng chi tiết / detail lines (nếu có)
└── Card lịch sử / log (nếu có)
</>
Layout
maincủa portal cóp-6— sticky phải bù trừ đúng để không hở.
→ Skeleton JSX + Routing: .claude/skills/templates-for-skills/tpl.sub-page.skeleton-and-routing.md
[1] Sticky Header — Breadcrumb & Page Header
Kỹ thuật sticky trong portal có p-6 (BẮT BUỘC):
{/* Sticky header — bù trừ p-6 của main trong portal */}
<div className="sticky -top-6 z-20 bg-white border-b border-gray-100 shadow-sm -mx-6 -mt-6 px-6 pt-3 pb-3 space-y-2">
{/* Breadcrumb */}
<nav className="flex items-center gap-1 text-sm text-gray-500">
...
</nav>
{/* Page Header */}
<div className="flex items-center justify-between">
...
</div>
</div>
{/* Nội dung cuộn */}
<div className="space-y-3 px-6 pt-4 pb-6">
...cards...
</div>
Giải thích kỹ thuật -top-6 / -mx-6 / -mt-6:
maincủa portal cóp-6(24px) — scroll container làmaintop: 0bám vào content edge (trong padding) → còn 24px gap hở phía trên-top-6(top: -24px) → bám vào visual top (ngoài padding), loại bỏ gap-mt-6 -mx-6→ kéo element ra ngoài padding box để nền trắng phủ kín toàn bộpx-6bên trong → căn nội dung thẳng hàng với các card bên dưới
⚠️ Nếu
maincủa portal có padding khác (p-4,p-8...) thì điều chỉnh-top-{n},-mt-{n},-mx-{n}tương ứng.
Breadcrumb:
<nav className="flex items-center gap-1 text-sm text-gray-500">- Dùng
ChevronRighticon giữa các cấp - Cấp cuối:
text-gray-900 font-medium, không phải button
Page Header:
- Luôn có nút Back (
ArrowLeft):btn-secondary p-2,data-qa='btn_quay_lai' - Tiêu đề
text-xl font-semibold text-gray-900thay đổi theo mode: "Chi tiết" / "Chỉnh sửa" / "Tạo mới" - Action buttons theo mode:
- View → nút "Chỉnh sửa" (
btn-primary) - Edit/Create → nút "Hủy" (
btn-secondary) + nút "Lưu thay đổi"/"Tạo mới" (btn-primary)
- View → nút "Chỉnh sửa" (
- Nút Hủy và Lưu đều
disabled={saving}
→ Template: .claude/skills/templates-for-skills/tpl.sub-page.breadcrumb-and-header.md
[2] Content Cards
Card thông tin cơ bản:
<CardHeader className='pb-4'>+<CardTitle className='text-base font-semibold'><CardContent className='space-y-3'>— KHÔNG dùnggrid grid-cols-2trực tiếp trong CardContent- Bố cục bên trong: grid theo từng hàng (row), mỗi hàng là một
div.grid.grid-cols-N.gap-3.items-end - Field yêu cầu bị gộp lại trong
col-span-2hoặccol-span-3nếu cần chiếm nhiều cột
Quy tắc chọn số cột (grid-cols-N):
- Thường dùng
grid-cols-5hoặcgrid-cols-6— khợp với lưới của Card thông tin phiếu thu/chi - Mỗi field mặc định chiếm 1 cột; field hủy tên / textarea / lý do dùng
col-span-2hoặccol-span-3
Mỗi field trong grid:
<div className='space-y-1.5'>
<Label>Nhãn <span className='text-red-500'>*</span></Label> {/* hoặc không có * */}
{/* input / DatePicker / SearchCombobox / readonly div */}
{touched.field && errors.field && (
<p className='flex items-center gap-1 text-xs text-destructive'>
<AlertCircle className='h-3 w-3' />{errors.field}
</p>
)}
</div>
Nút thêm nhanh FK (nằm trong label row):
<div className='flex items-center justify-between'>
<Label>FK Field <span className='text-red-500'>*</span></Label>
<Tooltip>
<TooltipTrigger asChild>
<Button type='button' variant='ghost' size='sm'
data-qa='btn_tao_nhanh_xxx'
className='h-7 w-7 rounded-full p-0 text-[#1565C0] bg-blue-50 hover:bg-[#1565C0] hover:text-white hover:shadow-md hover:scale-105 transition-all duration-200'
onClick={onTaoNhanh}
>
<Plus className='h-3.5 w-3.5' />
</Button>
</TooltipTrigger>
<TooltipContent>Tạo mới ...</TooltipContent>
</Tooltip>
</div>
Field chỉ đọc (view mode hoặc auto-generated):
<div className='h-9 flex items-center px-3 text-gray-900 rounded-md border border-slate-100 bg-slate-50'>
{value || '—'}
</div>
Field số tiền (read-only, right-align):
<div className={cn(
'h-9 flex items-center justify-end px-3 text-sm font-semibold rounded-md border',
errors.soTien && touched.soTien
? 'border-destructive text-destructive'
: 'border-slate-100 bg-slate-50 text-gray-900'
)}>
{formatCurrency(total)}
</div>
→ Template: .claude/skills/templates-for-skills/tpl.sub-page.card-info.md
Card bảng chi tiết (detail lines):
<CardContent className='p-0'>— bảng sát cạnh card, không padding- Nút "Thêm dòng" trong CardHeader, ẩn khi
isView - Dòng tổng cộng:
bg-[#f8f9fa] font-medium, chỉ có cột tổngtext-right
→ Template: .claude/skills/templates-for-skills/tpl.sub-page.card-detail-lines.md
Mode trong Sub Page
const { id } = useParams()
const isCreate = !id
const [pageMode, setPageMode] = useState<'view' | 'edit'>(isCreate ? 'edit' : 'view')
const isView = pageMode === 'view'
const isEdit = pageMode === 'edit'
Routing Sub Page
- Sub page có route riêng trong routeConfig — không phải overlay
- Đường dẫn tạo mới:
ten-feature/tao-moi(id = undefined → isCreate = true) - Đường dẫn xem/sửa:
ten-feature/:id
→ Template routing: .claude/skills/templates-for-skills/tpl.sub-page.skeleton-and-routing.md
Checklist Sub Page
- Sticky header dùng đúng kỹ thuật
-top-6 -mx-6 -mt-6(khớp vớip-6của portal) - Breadcrumb nằm trong sticky header
- Page Header nằm trong sticky header, action buttons đúng bên phải
- Có nút Back (
ArrowLeft) trong Page Header,data-qa='btn_quay_lai' - Tiêu đề đúng theo mode: Chi tiết / Chỉnh sửa / Tạo mới
- Action buttons: View → Sửa; Edit/Create → Hủy + Lưu
- Nút Hủy và Lưu
disabled={saving}, Lưu có spinner - Content cards nằm trong div
space-y-3 px-6 pt-4 pb-6(bên dưới sticky) - Mỗi section dùng
<Card className='border-[#e0e0e0]'>riêng - CardHeader:
<CardTitle className='text-base font-semibold'> - CardContent dùng
space-y-3— KHÔNG dùnggrid-cols-2trực tiếp trong CardContent - Mỗi hàng fields:
<div className='grid grid-cols-N gap-3 items-end'>(N = 5 hoặc 6) - Mỗi field trong hàng:
<div className='space-y-1.5'>+Label+ input + error - Inline error dùng
AlertCircle+flex items-center gap-1 text-xs text-destructive - Nút thêm nhanh FK:
Button variant='ghost' size='sm'bên trongflex items-center justify-between - Fields view mode:
<div className='h-9 flex items-center px-3 ... border border-slate-100 bg-slate-50'> - Field số tiền read-only:
justify-end font-semibold border-slate-100 bg-slate-50
Responsive Mobile (BẮT BUỘC)
Sub page PHẢI hiển thị đúng từ 320px. Xem rules chi tiết tại
tao-ui-giao-dien.
Sticky Header — Page Header row
{/* Page Header bên trong sticky */}
<div className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3'>
<div className='flex items-center gap-2'>
<Button variant='outline' size='sm' onClick={handleBack} data-qa='btn_quay_lai'>
<ArrowLeft className='h-4 w-4' />
</Button>
<h1 className='text-xl font-semibold text-gray-900'>Tiêu đề</h1>
</div>
<div className='flex gap-2 flex-shrink-0'>
{/* Action buttons */}
</div>
</div>
// ❌ SAI — overflow header trên mobile
<div className='flex items-center justify-between'>
Grid fields trong Card
// ✅ ĐÚNG — responsive grid
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 items-end'>
// Hoặc dùng col-span để ưu tiên field quan trọng trên mobile
<div className='grid grid-cols-1 sm:grid-cols-5 gap-3 items-end'>
<div className='sm:col-span-2 space-y-1.5'>...</div>
<div className='sm:col-span-1 space-y-1.5'>...</div>
</div>
// ❌ SAI — cố định nhiều cột không có breakpoint
<div className='grid grid-cols-5 gap-3'>
Checklist thêm vào Checklist Sub Page
- Sticky header row:
flex-col sm:flex-row sm:justify-between gap-3 - Grid fields: có breakpoint
sm:trướcgrid-cols-N(N≥3) - Textarea view mode: thêm
whitespace-pre-wrap - Input số:
type='text' inputMode='numeric',text-right - ForeignKey:
SearchCombobox - Validate on blur, không validate onChange
- Route sub page đăng ký đúng trong routeConfig
- Mọi phần tử tương tác có
data-qa