Initial commit
This commit is contained in:
86
src/components/KpiCard.tsx
Normal file
86
src/components/KpiCard.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Card } from '@astryxdesign/core/Card';
|
||||
import { HStack } from '@astryxdesign/core/HStack';
|
||||
import { Text } from '@astryxdesign/core/Text';
|
||||
import { VStack } from '@astryxdesign/core/VStack';
|
||||
|
||||
export type KpiTone = 'accent' | 'success' | 'warning' | 'error' | 'neutral';
|
||||
|
||||
const TONE_COLOR: Record<KpiTone, string> = {
|
||||
accent: 'var(--color-brand)',
|
||||
success: 'var(--color-brand)',
|
||||
warning: 'var(--color-brand)',
|
||||
error: 'var(--color-brand)',
|
||||
neutral: 'var(--color-brand)',
|
||||
};
|
||||
|
||||
export interface KpiCardProps {
|
||||
/** Small-caps label. Say what it is, not what it means. */
|
||||
label: string;
|
||||
value: string;
|
||||
/**
|
||||
* The sub-pill. This is where the number gets its meaning — "1 of 84 orders"
|
||||
* says something "1.2%" does not.
|
||||
*/
|
||||
note?: string;
|
||||
tone?: KpiTone;
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A KPI tile.
|
||||
*
|
||||
* Label and icon, a large tabular value, a muted sub-pill carrying the
|
||||
* interpretation, and a 2px underline in the tile's tone. Sized to sit 4–6
|
||||
* across rather than 4 — a console is glanced at all day, not read once.
|
||||
*/
|
||||
export function KpiCard({ label, value, note, tone = 'neutral', icon }: KpiCardProps) {
|
||||
const color = TONE_COLOR[tone];
|
||||
|
||||
return (
|
||||
<Card padding={0} elevation="low">
|
||||
<VStack gap={1} padding={2} style={{ position: 'relative', paddingBottom: 12, minHeight: 108 }}>
|
||||
<HStack justify="between" align="start" gap={1}>
|
||||
<Text
|
||||
type="label"
|
||||
size="xsm"
|
||||
color="secondary"
|
||||
style={{ textTransform: 'uppercase', letterSpacing: '0.09em', lineHeight: 1.35 }}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{icon ? <span style={{ color, display: 'flex' }}>{icon}</span> : null}
|
||||
</HStack>
|
||||
|
||||
<HStack justify="center" align="center" style={{ flex: 1 }}>
|
||||
<Text
|
||||
as="div"
|
||||
hasTabularNumbers
|
||||
style={{ color, fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 24, lineHeight: 1.1 }}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{note ? (
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 12,
|
||||
left: 16,
|
||||
background: 'var(--color-slate-100)',
|
||||
color: 'var(--color-slate-500)',
|
||||
borderRadius: 12,
|
||||
padding: '2px 6px',
|
||||
fontSize: 10.5,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{note}
|
||||
</span>
|
||||
) : null}
|
||||
</VStack>
|
||||
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user