[CLAUDE] FE-User: redesign foundation "nâng màu giữ brand" — gradient/accent/badge bắt mắt hơn
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m24s

Anh: giao diện đơn điệu, muốn đẹp + bắt mắt, font/màu đẹp hơn. Hướng anh chốt
"nâng màu, giữ nền xanh brand" (eoffice trước). Foundation lan tỏa toàn app, KHÔNG
đụng 65 trang lẻ.
- index.css: +accent palette teal/amberx/violet/greenx (đặt tên né trùng Tailwind)
  + utilities .app-gradient-brand / .card-accent / .icon-chip / .stat-value;
  heading 600->700 đậm hơn; .label-eyebrow brand-600. Brand #1F7DC1 + Be Vietnam Pro GIỮ.
- primitives: Button primary/danger gradient nổi bật; Input/Select/Textarea focus-glow
  mạnh hơn; Label brand-600; Dialog title-bar gradient. variant/size keys STABLE.
- shell: Layout stripe dày hơn + logo cap; PageHeader title lớn/đậm + accent bar cao;
  TopBar gradient hairline; DataTable thead gradient brand chữ trắng.
- Dashboard: KPI cards accent + icon chips.
- color maps (contract/PE phase + PE display status): -700->-800 đậm chữ, phase nháp tint brand.

Visual-only — props/handler/signature nguyên. Build PASS (tsc -b 0 error). a11y:
contrast AA + prefers-reduced-motion. fe-admin mirror đợt sau.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-16 10:12:40 +07:00
parent 4004481989
commit c98030f27c
14 changed files with 230 additions and 96 deletions

View File

@ -28,6 +28,22 @@ const fmtMoney = (v: number) => {
return v.toLocaleString('vi-VN')
}
// 2026-06-16 redesign (anh "nâng màu, bắt mắt hơn"): KPI cards now carry a
// coloured LEFT rail (.card-accent --accent) + a tinted icon-chip + a big stat
// number. Each tone owns a distinct accent from the new palette so the row
// scans as 5 colours, not 5 greys. `tone` extended (brand/teal/warn/good/danger
// /violet) — StatCard is local to this page, no external call-site.
type StatTone = 'default' | 'teal' | 'warn' | 'good' | 'danger' | 'violet'
const STAT_TONE: Record<StatTone, { rail: string; chipBg: string; chipFg: string }> = {
default: { rail: 'var(--color-brand-500)', chipBg: 'var(--color-brand-50)', chipFg: 'var(--color-brand-600)' },
teal: { rail: 'var(--color-teal-500)', chipBg: 'var(--color-teal-50)', chipFg: 'var(--color-teal-700)' },
warn: { rail: 'var(--color-amberx-500)', chipBg: 'var(--color-amberx-50)', chipFg: 'var(--color-amberx-700)' },
good: { rail: 'var(--color-greenx-500)', chipBg: 'var(--color-greenx-50)', chipFg: 'var(--color-greenx-700)' },
danger: { rail: 'var(--color-accent-500)', chipBg: '#fdecec', chipFg: 'var(--color-accent-600)' },
violet: { rail: 'var(--color-violet-500)', chipBg: 'var(--color-violet-50)', chipFg: 'var(--color-violet-700)' },
}
function StatCard({
icon: Icon,
label,
@ -40,27 +56,27 @@ function StatCard({
label: string
value: React.ReactNode
hint?: string
tone?: 'default' | 'warn' | 'good' | 'danger'
tone?: StatTone
onClick?: () => void
}) {
const toneClass =
tone === 'warn' ? 'text-amber-600 bg-amber-50' :
tone === 'good' ? 'text-emerald-600 bg-emerald-50' :
tone === 'danger' ? 'text-red-600 bg-red-50' :
'text-brand-600 bg-brand-50'
const t = STAT_TONE[tone]
return (
<button
onClick={onClick}
disabled={!onClick}
className="rounded-lg border border-slate-200 bg-white p-4 text-left shadow-sm transition hover:shadow-md disabled:cursor-default disabled:hover:shadow-sm"
className="card-accent flex flex-col p-4 text-left disabled:cursor-default"
style={{ ['--accent' as string]: t.rail }}
>
<div className="flex items-center justify-between">
<div className="text-xs font-medium text-slate-500">{label}</div>
<span className={`flex h-6 w-6 items-center justify-center rounded ${toneClass}`}>
<Icon className="h-3.5 w-3.5" />
<div className="flex items-start justify-between gap-2">
<div className="text-[11px] font-semibold uppercase tracking-wide text-slate-500">{label}</div>
<span
className="icon-chip h-9 w-9"
style={{ ['--chip-bg' as string]: t.chipBg, ['--chip-fg' as string]: t.chipFg }}
>
<Icon className="h-4 w-4" />
</span>
</div>
<div className="mt-2 text-2xl font-bold text-slate-900">{value}</div>
<div className="stat-value mt-2 text-3xl">{value}</div>
{hint && <div className="mt-0.5 text-xs text-slate-400">{hint}</div>}
</button>
)
@ -91,7 +107,10 @@ export function UserDashboardPage() {
/>
<section className="mb-6">
<h2 className="mb-3 text-sm font-semibold text-slate-700">Của tôi</h2>
<h2 className="mb-3 flex items-center gap-2 text-sm font-bold text-slate-800">
<span aria-hidden className="h-4 w-1 rounded-full bg-gradient-to-b from-brand-400 to-brand-700" />
Của tôi
</h2>
<div className="grid grid-cols-2 gap-3 md:grid-cols-5">
<StatCard
icon={Pencil}
@ -105,7 +124,7 @@ export function UserDashboardPage() {
label="Chờ tôi duyệt"
value={s?.pendingMyApproval ?? '—'}
hint="Vào Hộp thư"
tone="good"
tone="teal"
onClick={() => navigate('/inbox')}
/>
<StatCard
@ -127,15 +146,18 @@ export function UserDashboardPage() {
icon={Coins}
label="Tổng giá trị nháp"
value={s ? fmtMoney(s.draftsTotalValue) : '—'}
tone="violet"
onClick={() => navigate('/my-contracts')}
/>
</div>
</section>
<section className="rounded-lg border border-slate-200 bg-white">
<header className="flex items-center justify-between border-b border-slate-200 px-4 py-3">
<h2 className="flex items-center gap-2 text-sm font-semibold text-slate-700">
<FileText className="h-4 w-4" />
<section className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<header className="flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-brand-50/70 to-transparent px-4 py-3">
<h2 className="flex items-center gap-2 text-sm font-bold text-slate-800">
<span className="icon-chip h-7 w-7">
<FileText className="h-4 w-4" />
</span>
gần đây
</h2>
<Button variant="outline" onClick={() => navigate('/my-contracts')}>