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:
64
apps/web/components/design-system/numeric.tsx
Normal file
64
apps/web/components/design-system/numeric.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface NumericProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
/** The numeric value to display. */
|
||||
value: number;
|
||||
/** Format style. Default 'vnd'. */
|
||||
format?: 'vnd' | 'percent' | 'decimal' | 'compact';
|
||||
/** Number of fraction digits for percent/decimal. Default 1 for percent, 0 for vnd. */
|
||||
fractionDigits?: number;
|
||||
}
|
||||
|
||||
const vndFormatter = new Intl.NumberFormat('vi-VN', {
|
||||
style: 'currency',
|
||||
currency: 'VND',
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
|
||||
const compactFormatter = new Intl.NumberFormat('vi-VN', {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
|
||||
function formatValue(
|
||||
value: number,
|
||||
format: NumericProps['format'],
|
||||
fractionDigits?: number,
|
||||
): string {
|
||||
switch (format) {
|
||||
case 'percent':
|
||||
return `${value >= 0 ? '+' : ''}${value.toFixed(fractionDigits ?? 1)}%`;
|
||||
case 'decimal':
|
||||
return new Intl.NumberFormat('vi-VN', {
|
||||
minimumFractionDigits: fractionDigits ?? 0,
|
||||
maximumFractionDigits: fractionDigits ?? 2,
|
||||
}).format(value);
|
||||
case 'compact':
|
||||
return compactFormatter.format(value);
|
||||
case 'vnd':
|
||||
default:
|
||||
return vndFormatter.format(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Numeric display — right-aligned, tabular-nums, formatted for VND/percent.
|
||||
* Automatically sets `data-numeric` for global tabular-nums styling.
|
||||
*/
|
||||
export function Numeric({
|
||||
value,
|
||||
format = 'vnd',
|
||||
fractionDigits,
|
||||
className,
|
||||
...props
|
||||
}: NumericProps) {
|
||||
return (
|
||||
<span
|
||||
data-numeric
|
||||
className={cn('text-right font-mono tabular-nums', className)}
|
||||
{...props}
|
||||
>
|
||||
{formatValue(value, format, fractionDigits)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user