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:
217
apps/web/app/[locale]/(dashboard)/dev/tokens/page.tsx
Normal file
217
apps/web/app/[locale]/(dashboard)/dev/tokens/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -39,6 +39,21 @@
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 142.1 76.2% 36.3%;
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* Chart palette (light) */
|
||||
--chart-1: 200 90% 45%;
|
||||
--chart-2: 142 65% 38%;
|
||||
--chart-3: 38 95% 48%;
|
||||
--chart-4: 280 65% 50%;
|
||||
--chart-5: 0 75% 50%;
|
||||
--chart-6: 180 60% 40%;
|
||||
|
||||
/* Motion */
|
||||
--duration-xs: 80ms;
|
||||
--duration-sm: 150ms;
|
||||
--duration-md: 240ms;
|
||||
--ease-standard: cubic-bezier(.2, 0, 0, 1);
|
||||
--ease-emphasized: cubic-bezier(.3, 0, 0, 1);
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -78,6 +93,16 @@
|
||||
--ring: 142 72% 42%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Chart palette (dark) */
|
||||
--chart-1: 200 90% 60%;
|
||||
--chart-2: 142 70% 50%;
|
||||
--chart-3: 38 95% 60%;
|
||||
--chart-4: 280 70% 65%;
|
||||
--chart-5: 0 75% 60%;
|
||||
--chart-6: 180 65% 50%;
|
||||
}
|
||||
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
@@ -135,28 +160,44 @@
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
/* Signal flash for price updates */
|
||||
@keyframes signal-flash-up {
|
||||
0%,
|
||||
/* Signal flash for price updates (tick-flash per design tokens) */
|
||||
@keyframes tick-up {
|
||||
0% {
|
||||
background-color: hsl(var(--signal-up) / 0.18);
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
}
|
||||
30% {
|
||||
background-color: hsl(var(--signal-up-bg) / 0.2);
|
||||
}
|
||||
}
|
||||
@keyframes signal-flash-down {
|
||||
0%,
|
||||
@keyframes tick-down {
|
||||
0% {
|
||||
background-color: hsl(var(--signal-down) / 0.18);
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
}
|
||||
30% {
|
||||
background-color: hsl(var(--signal-down-bg) / 0.2);
|
||||
}
|
||||
}
|
||||
.tick-flash-up {
|
||||
animation: tick-up 480ms ease-out;
|
||||
}
|
||||
.tick-flash-down {
|
||||
animation: tick-down 480ms ease-out;
|
||||
}
|
||||
/* Legacy aliases */
|
||||
.flash-up {
|
||||
animation: signal-flash-up 1s ease-out;
|
||||
animation: tick-up 480ms ease-out;
|
||||
}
|
||||
.flash-down {
|
||||
animation: signal-flash-down 1s ease-out;
|
||||
animation: tick-down 480ms ease-out;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.tick-flash-up,
|
||||
.tick-flash-down,
|
||||
.flash-up,
|
||||
.flash-down {
|
||||
animation: none;
|
||||
}
|
||||
.animate-ticker {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user