[CLAUDE] FE: content polish — typography + PageHeader + Button/Input/Table
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m54s

Typography base (both apps):
- 14px base, line-height 1.55, letter-spacing -0.003em — Be Vietnam
  Pro render chắc hơn, dấu tiếng Việt rõ hơn
- h1/h2 tracking tighter (-0.018em), h1 weight 700, h2 weight 600
- Tabular numbers trong <table> (tự động align cột số)

PageHeader:
- Title text-[22px] thay vì text-xl — hierarchy mạnh hơn
- Border-bottom + pb-5 thay flat layout — content-area rõ vùng
- Description leading-relaxed + slate-500 — dễ đọc hơn

Button:
- shadow-sm + color-tinted shadow (brand/20, red/20) cho primary/
  danger — có chiều sâu
- active:translate-y-[0.5px] micro-press feedback
- Ring offset 2 (thay 1) + offset-white — focus ring tách rõ

Input/Select/Textarea:
- h-9 thay h-10 — phù hợp dense table layouts
- shadow-[inset_0_1px_0_...] — inset highlight tinh tế
- Focus: border-brand-500 + ring-brand-500/20 — 2 lớp chỉ báo
- Disabled: bg-slate-50 + opacity-70 — rõ disabled state

DataTable:
- rounded-xl + shadow-sm + border-200/80 — card feel nhẹ nhàng hơn
- Header: UPPERCASE text-[11px] tracking-wider — ERP enterprise look
- Row hover: bg-brand-50/40 (thay slate-50) — brand-tinted hover
- Padding tăng từ px-3 py-2 → px-4 py-2.5 — breathing room

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-21 16:16:53 +07:00
parent bf1fbe3870
commit 346bd5d644
14 changed files with 125 additions and 54 deletions

View File

@ -35,15 +35,15 @@ export function DataTable<T>({
onRowClick,
}: Props<T>) {
return (
<div className="overflow-auto rounded-md border border-slate-200 bg-white">
<div className="overflow-auto rounded-xl border border-slate-200/80 bg-white shadow-sm">
<table className="w-full text-sm">
<thead className="bg-slate-50 text-slate-700">
<tr>
<thead className="bg-slate-50/60 text-slate-600">
<tr className="border-b border-slate-200/80">
{columns.map(c => (
<th
key={c.key}
className={cn(
'px-3 py-2 font-medium',
'px-4 py-2.5 text-[11px] font-semibold uppercase tracking-wider',
c.align === 'right' && 'text-right',
c.align === 'center' && 'text-center',
c.align !== 'right' && c.align !== 'center' && 'text-left',
@ -70,7 +70,7 @@ export function DataTable<T>({
Array.from({ length: 5 }).map((_, i) => (
<tr key={`sk-${i}`} className="border-t border-slate-100">
{columns.map(c => (
<td key={c.key} className="px-3 py-3">
<td key={c.key} className="px-4 py-3">
<div className="h-3 animate-pulse rounded bg-slate-100" />
</td>
))}
@ -78,7 +78,7 @@ export function DataTable<T>({
))}
{!isLoading && rows.length === 0 && (
<tr>
<td colSpan={columns.length} className="px-3 py-10 text-center text-sm text-slate-400">
<td colSpan={columns.length} className="px-4 py-12 text-center text-sm text-slate-400">
{empty ?? 'Không có dữ liệu'}
</td>
</tr>
@ -88,8 +88,8 @@ export function DataTable<T>({
<tr
key={getRowKey(row)}
className={cn(
'border-t border-slate-100 transition',
onRowClick && 'cursor-pointer hover:bg-slate-50',
'border-t border-slate-100 text-slate-700 transition-colors',
onRowClick && 'cursor-pointer hover:bg-brand-50/40',
)}
onClick={() => onRowClick?.(row)}
>
@ -97,7 +97,7 @@ export function DataTable<T>({
<td
key={c.key}
className={cn(
'px-3 py-2',
'px-4 py-2.5',
c.align === 'right' && 'text-right',
c.align === 'center' && 'text-center',
)}

View File

@ -1,13 +1,21 @@
import type { ReactNode } from 'react'
export function PageHeader({ title, description, actions }: { title: ReactNode; description?: ReactNode; actions?: ReactNode }) {
export function PageHeader({
title,
description,
actions,
}: {
title: ReactNode
description?: ReactNode
actions?: ReactNode
}) {
return (
<div className="mb-5 flex items-start justify-between gap-4">
<div>
<h1 className="text-xl font-bold text-slate-900">{title}</h1>
{description && <p className="mt-1 text-sm text-slate-600">{description}</p>}
<div className="mb-6 flex items-start justify-between gap-6 border-b border-slate-200/70 pb-5">
<div className="min-w-0">
<h1 className="text-[22px] font-bold leading-tight text-slate-900">{title}</h1>
{description && <p className="mt-1.5 text-[13px] leading-relaxed text-slate-500">{description}</p>}
</div>
{actions && <div className="flex items-center gap-2">{actions}</div>}
{actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
</div>
)
}

View File

@ -3,19 +3,19 @@ import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/cn'
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-brand-500',
'inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white focus-visible:ring-brand-500 active:translate-y-[0.5px]',
{
variants: {
variant: {
primary: 'bg-brand-600 text-white hover:bg-brand-700',
secondary: 'bg-slate-200 text-slate-900 hover:bg-slate-300',
outline: 'border border-slate-300 bg-white hover:bg-slate-50',
ghost: 'hover:bg-slate-100',
danger: 'bg-red-600 text-white hover:bg-red-700',
primary: 'bg-brand-600 text-white shadow-sm shadow-brand-600/20 hover:bg-brand-700',
secondary: 'bg-slate-100 text-slate-800 hover:bg-slate-200',
outline: 'border border-slate-300 bg-white text-slate-700 shadow-sm hover:bg-slate-50 hover:border-slate-400',
ghost: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900',
danger: 'bg-red-600 text-white shadow-sm shadow-red-600/20 hover:bg-red-700',
},
size: {
sm: 'h-8 px-3',
md: 'h-10 px-4',
sm: 'h-8 px-3 text-xs',
md: 'h-9 px-4',
lg: 'h-11 px-6 text-base',
},
},

View File

@ -7,7 +7,10 @@ export const Input = forwardRef<HTMLInputElement, Props>(({ className, ...props
<input
ref={ref}
className={cn(
'h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm placeholder:text-slate-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:opacity-50',
'h-9 w-full rounded-md border border-slate-300 bg-white px-3 text-sm text-slate-900',
'shadow-[inset_0_1px_0_rgba(15,23,42,0.02)] placeholder:text-slate-400',
'transition-[border-color,box-shadow] focus-visible:border-brand-500 focus-visible:ring-2 focus-visible:ring-brand-500/20',
'disabled:cursor-not-allowed disabled:bg-slate-50 disabled:opacity-70',
className,
)}
{...props}

View File

@ -7,7 +7,10 @@ export const Select = forwardRef<HTMLSelectElement, Props>(({ className, childre
<select
ref={ref}
className={cn(
'h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:opacity-50',
'h-9 w-full rounded-md border border-slate-300 bg-white px-3 pr-8 text-sm text-slate-900',
'shadow-[inset_0_1px_0_rgba(15,23,42,0.02)]',
'transition-[border-color,box-shadow] focus-visible:border-brand-500 focus-visible:ring-2 focus-visible:ring-brand-500/20',
'disabled:cursor-not-allowed disabled:bg-slate-50 disabled:opacity-70',
className,
)}
{...props}

View File

@ -7,7 +7,10 @@ export const Textarea = forwardRef<HTMLTextAreaElement, Props>(({ className, ...
<textarea
ref={ref}
className={cn(
'w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm placeholder:text-slate-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:opacity-50',
'w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 leading-relaxed',
'shadow-[inset_0_1px_0_rgba(15,23,42,0.02)] placeholder:text-slate-400',
'transition-[border-color,box-shadow] focus-visible:border-brand-500 focus-visible:ring-2 focus-visible:ring-brand-500/20',
'disabled:cursor-not-allowed disabled:bg-slate-50 disabled:opacity-70',
className,
)}
{...props}

View File

@ -32,12 +32,32 @@ body {
background-color: #f8fafc;
color: #0f172a;
font-family: var(--font-sans);
font-feature-settings: "cv11", "ss01", "ss03";
font-size: 14px;
line-height: 1.55;
letter-spacing: -0.003em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
/* Heading tightening + better hierarchy with Be Vietnam Pro */
h1, h2, h3, h4 {
letter-spacing: -0.018em;
color: #0f172a;
}
h1 { font-weight: 700; }
h2 { font-weight: 600; }
/* Tabular numbers in tables + stat cards for better alignment */
table, .tabular-nums {
font-variant-numeric: tabular-nums;
}
/* Smoother form focus */
input:focus-visible, textarea:focus-visible, select:focus-visible, button:focus-visible {
outline: none;
}
/* Subtle scrollbar that fits the brand */
::-webkit-scrollbar {
width: 8px;

View File

@ -35,15 +35,15 @@ export function DataTable<T>({
onRowClick,
}: Props<T>) {
return (
<div className="overflow-auto rounded-md border border-slate-200 bg-white">
<div className="overflow-auto rounded-xl border border-slate-200/80 bg-white shadow-sm">
<table className="w-full text-sm">
<thead className="bg-slate-50 text-slate-700">
<tr>
<thead className="bg-slate-50/60 text-slate-600">
<tr className="border-b border-slate-200/80">
{columns.map(c => (
<th
key={c.key}
className={cn(
'px-3 py-2 font-medium',
'px-4 py-2.5 text-[11px] font-semibold uppercase tracking-wider',
c.align === 'right' && 'text-right',
c.align === 'center' && 'text-center',
c.align !== 'right' && c.align !== 'center' && 'text-left',
@ -70,7 +70,7 @@ export function DataTable<T>({
Array.from({ length: 5 }).map((_, i) => (
<tr key={`sk-${i}`} className="border-t border-slate-100">
{columns.map(c => (
<td key={c.key} className="px-3 py-3">
<td key={c.key} className="px-4 py-3">
<div className="h-3 animate-pulse rounded bg-slate-100" />
</td>
))}
@ -78,7 +78,7 @@ export function DataTable<T>({
))}
{!isLoading && rows.length === 0 && (
<tr>
<td colSpan={columns.length} className="px-3 py-10 text-center text-sm text-slate-400">
<td colSpan={columns.length} className="px-4 py-12 text-center text-sm text-slate-400">
{empty ?? 'Không có dữ liệu'}
</td>
</tr>
@ -88,8 +88,8 @@ export function DataTable<T>({
<tr
key={getRowKey(row)}
className={cn(
'border-t border-slate-100 transition',
onRowClick && 'cursor-pointer hover:bg-slate-50',
'border-t border-slate-100 text-slate-700 transition-colors',
onRowClick && 'cursor-pointer hover:bg-brand-50/40',
)}
onClick={() => onRowClick?.(row)}
>
@ -97,7 +97,7 @@ export function DataTable<T>({
<td
key={c.key}
className={cn(
'px-3 py-2',
'px-4 py-2.5',
c.align === 'right' && 'text-right',
c.align === 'center' && 'text-center',
)}

View File

@ -1,13 +1,21 @@
import type { ReactNode } from 'react'
export function PageHeader({ title, description, actions }: { title: ReactNode; description?: ReactNode; actions?: ReactNode }) {
export function PageHeader({
title,
description,
actions,
}: {
title: ReactNode
description?: ReactNode
actions?: ReactNode
}) {
return (
<div className="mb-5 flex items-start justify-between gap-4">
<div>
<h1 className="text-xl font-bold text-slate-900">{title}</h1>
{description && <p className="mt-1 text-sm text-slate-600">{description}</p>}
<div className="mb-6 flex items-start justify-between gap-6 border-b border-slate-200/70 pb-5">
<div className="min-w-0">
<h1 className="text-[22px] font-bold leading-tight text-slate-900">{title}</h1>
{description && <p className="mt-1.5 text-[13px] leading-relaxed text-slate-500">{description}</p>}
</div>
{actions && <div className="flex items-center gap-2">{actions}</div>}
{actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
</div>
)
}

View File

@ -3,19 +3,19 @@ import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/cn'
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-brand-500',
'inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white focus-visible:ring-brand-500 active:translate-y-[0.5px]',
{
variants: {
variant: {
primary: 'bg-brand-600 text-white hover:bg-brand-700',
secondary: 'bg-slate-200 text-slate-900 hover:bg-slate-300',
outline: 'border border-slate-300 bg-white hover:bg-slate-50',
ghost: 'hover:bg-slate-100',
danger: 'bg-red-600 text-white hover:bg-red-700',
primary: 'bg-brand-600 text-white shadow-sm shadow-brand-600/20 hover:bg-brand-700',
secondary: 'bg-slate-100 text-slate-800 hover:bg-slate-200',
outline: 'border border-slate-300 bg-white text-slate-700 shadow-sm hover:bg-slate-50 hover:border-slate-400',
ghost: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900',
danger: 'bg-red-600 text-white shadow-sm shadow-red-600/20 hover:bg-red-700',
},
size: {
sm: 'h-8 px-3',
md: 'h-10 px-4',
sm: 'h-8 px-3 text-xs',
md: 'h-9 px-4',
lg: 'h-11 px-6 text-base',
},
},

View File

@ -7,7 +7,10 @@ export const Input = forwardRef<HTMLInputElement, Props>(({ className, ...props
<input
ref={ref}
className={cn(
'h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm placeholder:text-slate-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:opacity-50',
'h-9 w-full rounded-md border border-slate-300 bg-white px-3 text-sm text-slate-900',
'shadow-[inset_0_1px_0_rgba(15,23,42,0.02)] placeholder:text-slate-400',
'transition-[border-color,box-shadow] focus-visible:border-brand-500 focus-visible:ring-2 focus-visible:ring-brand-500/20',
'disabled:cursor-not-allowed disabled:bg-slate-50 disabled:opacity-70',
className,
)}
{...props}

View File

@ -7,7 +7,10 @@ export const Select = forwardRef<HTMLSelectElement, Props>(({ className, childre
<select
ref={ref}
className={cn(
'h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:opacity-50',
'h-9 w-full rounded-md border border-slate-300 bg-white px-3 pr-8 text-sm text-slate-900',
'shadow-[inset_0_1px_0_rgba(15,23,42,0.02)]',
'transition-[border-color,box-shadow] focus-visible:border-brand-500 focus-visible:ring-2 focus-visible:ring-brand-500/20',
'disabled:cursor-not-allowed disabled:bg-slate-50 disabled:opacity-70',
className,
)}
{...props}

View File

@ -7,7 +7,10 @@ export const Textarea = forwardRef<HTMLTextAreaElement, Props>(({ className, ...
<textarea
ref={ref}
className={cn(
'w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm placeholder:text-slate-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:opacity-50',
'w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 leading-relaxed',
'shadow-[inset_0_1px_0_rgba(15,23,42,0.02)] placeholder:text-slate-400',
'transition-[border-color,box-shadow] focus-visible:border-brand-500 focus-visible:ring-2 focus-visible:ring-brand-500/20',
'disabled:cursor-not-allowed disabled:bg-slate-50 disabled:opacity-70',
className,
)}
{...props}

View File

@ -32,12 +32,29 @@ body {
background-color: #f8fafc;
color: #0f172a;
font-family: var(--font-sans);
font-feature-settings: "cv11", "ss01", "ss03";
font-size: 14px;
line-height: 1.55;
letter-spacing: -0.003em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
h1, h2, h3, h4 {
letter-spacing: -0.018em;
color: #0f172a;
}
h1 { font-weight: 700; }
h2 { font-weight: 600; }
table, .tabular-nums {
font-variant-numeric: tabular-nums;
}
input:focus-visible, textarea:focus-visible, select:focus-visible, button:focus-visible {
outline: none;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;