Files
Doormilexpress_console/src/components/nearle_components/StatCard.js

225 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
};