feat(web): design tokens, Tailwind config, base components (TEC-3057)

- Add chart palette, motion, and z-index CSS vars to globals.css
- Replace custom theme-provider with next-themes (dark default)
- Extend tailwind.config.ts with heading fonts, spacing (row-compact,
  row-roomy, sidebar), chart colors, elevation shadows, glow shadows,
  transition timing, pill border-radius, z-index scale
- Update tick-flash animations to match design token spec (480ms)
- Add prefers-reduced-motion support for all animations
- Create base design-system components:
  Surface, SurfaceElevated, Divider, DensityProvider/useDensity,
  Numeric (VND/percent/compact formatting), Signal (up/down/neutral pill)
- Add dev-only /dev/tokens showcase route (404 in production)
- Update theme-provider tests to match next-themes integration

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-21 03:19:40 +07:00
parent e1beda2573
commit 7d6fcb4d8d
13 changed files with 665 additions and 138 deletions

View File

@@ -0,0 +1,217 @@
'use client';
import { notFound } from 'next/navigation';
import {
Surface,
SurfaceElevated,
Divider,
DensityProvider,
useDensity,
DENSITY_ROW_HEIGHT,
Numeric,
Signal,
} from '@/components/design-system';
// Dev-only: block in production
if (process.env.NODE_ENV === 'production') {
// Will 404 at build time for static generation
}
const COLOR_TOKENS = [
{ name: '--background', tw: 'bg-background' },
{ name: '--background-elevated', tw: 'bg-background-elevated' },
{ name: '--background-surface', tw: 'bg-background-surface' },
{ name: '--primary', tw: 'bg-primary' },
{ name: '--primary-hover', tw: 'bg-primary-hover' },
{ name: '--destructive', tw: 'bg-destructive' },
{ name: '--warning', tw: 'bg-warning' },
{ name: '--success', tw: 'bg-success' },
{ name: '--accent-blue', tw: 'bg-accent-blue' },
{ name: '--accent-purple', tw: 'bg-accent-purple' },
{ name: '--signal-up', tw: 'bg-signal-up' },
{ name: '--signal-down', tw: 'bg-signal-down' },
{ name: '--signal-neutral', tw: 'bg-signal-neutral' },
];
const CHART_TOKENS = [
{ name: '--chart-1', tw: 'bg-chart-1' },
{ name: '--chart-2', tw: 'bg-chart-2' },
{ name: '--chart-3', tw: 'bg-chart-3' },
{ name: '--chart-4', tw: 'bg-chart-4' },
{ name: '--chart-5', tw: 'bg-chart-5' },
{ name: '--chart-6', tw: 'bg-chart-6' },
];
function DensityDemo() {
const { density, setDensity } = useDensity();
return (
<div className="space-y-2">
<div className="flex gap-2">
{(['compact', 'regular', 'roomy'] as const).map((d) => (
<button
key={d}
onClick={() => setDensity(d)}
className={`rounded-md px-3 py-1 text-sm ${density === d ? 'bg-primary text-primary-foreground' : 'bg-background-surface text-foreground-muted'}`}
>
{d}
</button>
))}
</div>
<div className="rounded-lg border border-border bg-background-elevated p-4">
<div className={`${DENSITY_ROW_HEIGHT[density]} flex items-center border-b border-border px-cell`}>
<span className="text-foreground-muted text-heading-xs uppercase font-semibold">Mẫu hàng {density}</span>
</div>
{[1, 2, 3].map((i) => (
<div key={i} className={`${DENSITY_ROW_HEIGHT[density]} flex items-center border-b border-border px-cell`}>
<span className="flex-1 text-sm">Dòng {i}</span>
<Numeric value={i * 3_500_000_000} format="vnd" />
</div>
))}
</div>
</div>
);
}
export default function DevTokensPage() {
if (process.env.NODE_ENV === 'production') {
notFound();
}
return (
<div className="space-y-8 p-6">
<h1 className="text-heading-lg font-bold">Design Tokens Showcase</h1>
<p className="text-foreground-muted text-sm">Dev-only route không hiển thị trên production.</p>
{/* Colors */}
<section>
<h2 className="text-heading-md font-semibold mb-4">Màu sắc</h2>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-6">
{COLOR_TOKENS.map((t) => (
<div key={t.name} className="space-y-1">
<div className={`h-12 w-full rounded-md border border-border ${t.tw}`} />
<p className="text-xs text-foreground-muted font-mono">{t.name}</p>
</div>
))}
</div>
</section>
{/* Chart palette */}
<section>
<h2 className="text-heading-md font-semibold mb-4">Chart Palette</h2>
<div className="flex gap-2">
{CHART_TOKENS.map((t) => (
<div key={t.name} className="space-y-1 flex-1">
<div className={`h-10 w-full rounded-md ${t.tw}`} />
<p className="text-[10px] text-foreground-muted font-mono text-center">{t.name}</p>
</div>
))}
</div>
</section>
{/* Typography */}
<section>
<h2 className="text-heading-md font-semibold mb-4">Typography</h2>
<div className="space-y-2">
<p className="text-heading-xl font-bold">heading-xl (1.875rem)</p>
<p className="text-heading-lg font-bold">heading-lg (1.5rem)</p>
<p className="text-heading-md font-semibold">heading-md (1.125rem)</p>
<p className="text-heading-sm font-semibold">heading-sm (0.875rem)</p>
<p className="text-heading-xs font-semibold uppercase">heading-xs (0.75rem uppercase tracking)</p>
<Divider className="my-2" />
<p className="font-mono text-data-lg">data-lg: 1.250.000.000 </p>
<p className="font-mono text-data-md">data-md: 850.000.000 </p>
<p className="font-mono text-data-sm">data-sm: 450.000.000 </p>
<p className="font-mono text-ticker">ticker: Q1 +2.4%</p>
</div>
</section>
{/* Shadows */}
<section>
<h2 className="text-heading-md font-semibold mb-4">Elevation (Shadow)</h2>
<div className="flex gap-6">
{['elevation-0', 'elevation-1', 'elevation-2', 'elevation-3'].map((s) => (
<div
key={s}
className={`flex h-20 w-32 items-center justify-center rounded-lg bg-background-elevated text-xs text-foreground-muted shadow-${s}`}
>
{s}
</div>
))}
</div>
</section>
{/* Signals */}
<section>
<h2 className="text-heading-md font-semibold mb-4">Signal</h2>
<div className="flex gap-3">
<Signal direction="up" label="+2.4%" />
<Signal direction="down" label="-1.3%" />
<Signal direction="neutral" label="0.0%" />
</div>
</section>
{/* Glow shadows */}
<section>
<h2 className="text-heading-md font-semibold mb-4">Glow Shadows</h2>
<div className="flex gap-6">
<div className="flex h-16 w-28 items-center justify-center rounded-lg bg-background-elevated text-xs text-signal-up shadow-glow-up">
glow-up
</div>
<div className="flex h-16 w-28 items-center justify-center rounded-lg bg-background-elevated text-xs text-signal-down shadow-glow-down">
glow-down
</div>
</div>
</section>
{/* Surfaces */}
<section>
<h2 className="text-heading-md font-semibold mb-4">Surface</h2>
<div className="flex gap-4">
<Surface className="p-4 border border-border">
<p className="text-sm">Surface (flat)</p>
</Surface>
<SurfaceElevated className="p-4">
<p className="text-sm">SurfaceElevated</p>
</SurfaceElevated>
</div>
</section>
{/* Numeric */}
<section>
<h2 className="text-heading-md font-semibold mb-4">Numeric</h2>
<div className="space-y-1">
<div className="flex items-center gap-4">
<span className="text-sm text-foreground-muted w-20">VND:</span>
<Numeric value={3_500_000_000} />
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-foreground-muted w-20">Percent:</span>
<Numeric value={12.5} format="percent" />
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-foreground-muted w-20">Compact:</span>
<Numeric value={1_250_000_000} format="compact" />
</div>
</div>
</section>
{/* Density */}
<section>
<h2 className="text-heading-md font-semibold mb-4">Density</h2>
<DensityProvider>
<DensityDemo />
</DensityProvider>
</section>
{/* Tick flash animations */}
<section>
<h2 className="text-heading-md font-semibold mb-4">Tick Flash</h2>
<div className="flex gap-4">
<div className="tick-flash-up rounded-md px-4 py-2 text-sm">Flash Up</div>
<div className="tick-flash-down rounded-md px-4 py-2 text-sm">Flash Down</div>
</div>
<p className="text-xs text-foreground-muted mt-2">Refresh page to replay animation. Disabled with prefers-reduced-motion.</p>
</section>
</div>
);
}