225 lines
6.7 KiB
JavaScript
225 lines
6.7 KiB
JavaScript
import React from 'react';
|
||
import PropTypes from 'prop-types';
|
||
|
||
import { Card } from '@astryxdesign/core/Card';
|
||
import { Skeleton } from '@astryxdesign/core/Skeleton';
|
||
|
||
import { DT, STATUS } from 'themes/dt/tokens';
|
||
|
||
// ==============================|| STAT / KPI CARD ||============================== //
|
||
//
|
||
// Ultra-compact, dense, space-efficient enterprise KPI component.
|
||
// Shared across Deliveries, Orders, Pricing, Tenants, Riders, Customers, Hubs,
|
||
// Vehicles, Tripsheets, Exceptions, App Users, Reports and Dispatch.
|
||
//
|
||
// ---------------------------------------------------------------------------
|
||
// ULTRA-COMPACT HORIZONTAL COMPOSITION (~48px target height)
|
||
// ---------------------------------------------------------------------------
|
||
// 1. Icon chip: 26px × 26px with 6px radius and subtle 7% tint (`${accent}12`).
|
||
// Glyph size normalized to 14px.
|
||
// 2. Value: 18px bold (`lineHeight: 1`, `fontVariantNumeric: 'tabular-nums'`).
|
||
// 3. Label: 12px / 500 in secondary ink (`#64748b`), aligned horizontally on
|
||
// the same baseline beside the value for maximum space efficiency and instant
|
||
// (<1s) visual scanning.
|
||
// 4. Tight elevation, flat white surface, 8px rounded corners, 7px 12px padding.
|
||
// 5. Interactive/clickable hover support with smooth 150ms transition. Static
|
||
// cards remain clean without hover distraction.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const MIN_HEIGHT = 48;
|
||
|
||
const VARIANTS = {
|
||
neutral: STATUS.muted,
|
||
brand: DT.brand,
|
||
blue: STATUS.info,
|
||
green: STATUS.delivered,
|
||
amber: STATUS.pending,
|
||
red: STATUS.cancelled
|
||
};
|
||
|
||
export default function StatCard({
|
||
title,
|
||
value,
|
||
icon,
|
||
color,
|
||
variant = 'brand',
|
||
caption,
|
||
loading = false,
|
||
onClick,
|
||
clickable,
|
||
className = '',
|
||
style = {}
|
||
}) {
|
||
const accent = color || VARIANTS[variant] || DT.brand;
|
||
const isClickable = Boolean(onClick || clickable);
|
||
|
||
return (
|
||
<Card
|
||
height="100%"
|
||
padding={0}
|
||
elevation="none"
|
||
className={['dt-stat-card', isClickable ? 'dt-stat-card-clickable' : '', className].filter(Boolean).join(' ')}
|
||
data-clickable={isClickable ? 'true' : undefined}
|
||
onClick={onClick}
|
||
role={isClickable ? 'button' : undefined}
|
||
tabIndex={isClickable ? 0 : undefined}
|
||
onKeyDown={
|
||
isClickable && onClick
|
||
? (e) => {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
onClick(e);
|
||
}
|
||
}
|
||
: undefined
|
||
}
|
||
style={{
|
||
position: 'relative',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
minHeight: MIN_HEIGHT,
|
||
maxHeight: 56,
|
||
padding: '7px 12px',
|
||
background: DT.surface,
|
||
border: `1px solid ${DT.borderSubtle}`,
|
||
borderRadius: 8,
|
||
boxShadow: '0 1px 2px rgba(15, 23, 42, 0.03)',
|
||
cursor: isClickable ? 'pointer' : 'default',
|
||
userSelect: isClickable ? 'none' : 'auto',
|
||
boxSizing: 'border-box',
|
||
overflow: 'hidden',
|
||
width: '100%',
|
||
...style
|
||
}}
|
||
>
|
||
<div
|
||
className="dt-stat-content"
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '8px',
|
||
width: '100%',
|
||
minWidth: 0
|
||
}}
|
||
>
|
||
{icon && (
|
||
<div
|
||
className="dt-stat-icon"
|
||
style={{
|
||
width: 26,
|
||
height: 26,
|
||
minWidth: 26,
|
||
minHeight: 26,
|
||
borderRadius: 6,
|
||
backgroundColor: `${accent}12`,
|
||
color: accent,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
flexShrink: 0
|
||
}}
|
||
>
|
||
{icon}
|
||
</div>
|
||
)}
|
||
|
||
<div
|
||
className="dt-stat-body"
|
||
style={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
justifyContent: 'center',
|
||
minWidth: 0,
|
||
flex: 1
|
||
}}
|
||
>
|
||
<div
|
||
className="dt-stat-primary"
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'baseline',
|
||
gap: '6px',
|
||
minWidth: 0,
|
||
flexWrap: 'wrap'
|
||
}}
|
||
>
|
||
{loading ? (
|
||
<Skeleton width={36} height={18} radius={DT.radiusSm} />
|
||
) : (
|
||
<span
|
||
className="dt-stat-value"
|
||
style={{
|
||
fontFamily: 'var(--font-family-heading, Sora), -apple-system, BlinkMacSystemFont, sans-serif',
|
||
fontSize: '18px',
|
||
fontWeight: 700,
|
||
lineHeight: 1,
|
||
letterSpacing: '-0.02em',
|
||
color: 'var(--color-text-primary, #0f172a)',
|
||
fontVariantNumeric: 'tabular-nums',
|
||
whiteSpace: 'nowrap'
|
||
}}
|
||
>
|
||
{value}
|
||
</span>
|
||
)}
|
||
|
||
<span
|
||
className="dt-stat-label"
|
||
title={typeof title === 'string' ? title : undefined}
|
||
style={{
|
||
fontFamily: 'var(--font-family-body, Inter), -apple-system, BlinkMacSystemFont, sans-serif',
|
||
fontSize: '12px',
|
||
fontWeight: 500,
|
||
lineHeight: 1,
|
||
color: DT.textSecondary,
|
||
whiteSpace: 'nowrap',
|
||
overflow: 'hidden',
|
||
textOverflow: 'ellipsis',
|
||
flex: '1 1 auto',
|
||
minWidth: 0
|
||
}}
|
||
>
|
||
{title}
|
||
</span>
|
||
</div>
|
||
|
||
{caption && (
|
||
<span
|
||
className="dt-stat-caption"
|
||
title={typeof caption === 'string' ? caption : undefined}
|
||
style={{
|
||
fontFamily: 'var(--font-family-body, Inter), -apple-system, BlinkMacSystemFont, sans-serif',
|
||
fontSize: '10.5px',
|
||
lineHeight: '12px',
|
||
color: DT.textMuted,
|
||
marginTop: 1,
|
||
whiteSpace: 'nowrap',
|
||
overflow: 'hidden',
|
||
textOverflow: 'ellipsis'
|
||
}}
|
||
>
|
||
{caption}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
StatCard.propTypes = {
|
||
title: PropTypes.node,
|
||
value: PropTypes.node,
|
||
icon: PropTypes.node,
|
||
/** Raw accent hex. Takes precedence over `variant`. */
|
||
color: PropTypes.string,
|
||
/** Semantic alias over the same palette, for call sites not importing STATUS. */
|
||
variant: PropTypes.oneOf(['neutral', 'brand', 'blue', 'green', 'amber', 'red']),
|
||
caption: PropTypes.node,
|
||
loading: PropTypes.bool,
|
||
onClick: PropTypes.func,
|
||
clickable: PropTypes.bool,
|
||
className: PropTypes.string,
|
||
style: PropTypes.object
|
||
};
|