platform-codebase/@packages/@plugins/analytics/src/components/MetricCard.tsx

132 lines
3.5 KiB
TypeScript
Raw Normal View History

import { Sparkline } from './ui-data-stubs'
import { formatValue } from './ui-utils-stubs'
import type { NumberFormat } from './ui-utils-stubs'
import React from 'react'
import styled from 'styled-components'
export interface MetricCardProps {
label: string
value: string | number
change?: number
trend?: 'up' | 'down' | 'neutral'
format?: NumberFormat
sparkline?: number[]
icon?: React.ReactNode
variant?: 'default' | 'primary' | 'success' | 'warning' | 'error'
}
const Card = styled.div<{ $variant: string }>`
padding: ${(props) => props.theme.spacing.lg};
background: ${(props) => props.theme.colors.surface};
border: 1px solid ${(props) => props.theme.colors.border};
border-radius: ${(props) => props.theme.borderRadius.lg};
position: relative;
overflow: hidden;
${(props) => props.$variant !== 'default' && `
border-left: 4px solid ${
props.$variant === 'primary' ? props.theme.colors.primary :
props.$variant === 'success' ? props.theme.colors.success :
props.$variant === 'warning' ? props.theme.colors.warning :
props.$variant === 'error' ? props.theme.colors.error :
props.theme.colors.border
};
`}
`
const Header = styled.div`
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: ${(props) => props.theme.spacing.sm};
`
const Label = styled.div`
font-size: ${(props) => props.theme.typography.fontSize.sm};
color: ${(props) => props.theme.colors.text.secondary};
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: ${(props) => props.theme.typography.fontWeight.semibold};
`
const Icon = styled.div`
color: ${(props) => props.theme.colors.primary};
opacity: 0.6;
`
const Value = styled.div`
font-size: ${(props) => props.theme.typography.fontSize['3xl']};
font-weight: ${(props) => props.theme.typography.fontWeight.bold};
color: ${(props) => props.theme.colors.text};
margin-bottom: ${(props) => props.theme.spacing.xs};
line-height: 1;
`
const Footer = styled.div`
display: flex;
align-items: center;
gap: ${(props) => props.theme.spacing.sm};
`
const Change = styled.span<{ $trend: 'up' | 'down' | 'neutral' }>`
font-size: ${(props) => props.theme.typography.fontSize.sm};
font-weight: ${(props) => props.theme.typography.fontWeight.semibold};
color: ${(props) => {
switch (props.$trend) {
case 'up':
return props.theme.colors.success
case 'down':
return props.theme.colors.error
default:
return props.theme.colors.text.secondary
}
}};
`
const StyledSparkline = styled.div`
position: absolute;
bottom: 0;
right: 0;
opacity: 0.1;
pointer-events: none;
`
export const MetricCard: React.FC<MetricCardProps> = ({
label,
value,
change,
trend = 'neutral',
format = 'number',
sparkline,
icon,
variant = 'default'
}) => {
return (
<Card $variant={variant}>
<Header>
<Label>{label}</Label>
{icon ? (
<Icon>
{icon}
</Icon>
) : null}
</Header>
<Value>{formatValue(value, format)}</Value>
{change !== undefined && (
<Footer>
<Change $trend={trend}>
{trend === 'up' && '↑'}
{trend === 'down' && '↓'}
{change > 0 ? '+' : ''}{change}%
</Change>
</Footer>
)}
{sparkline && sparkline.length > 0 && (
<StyledSparkline>
<Sparkline data={sparkline} />
</StyledSparkline>
)}
</Card>
)
}