Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 95 additions & 2 deletions src/components/DataDisplay/Stats/Stats.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { CursorArrowRaysIcon, EnvelopeOpenIcon, UsersIcon } from '@heroicons/react/24/outline';
import {
CursorArrowRaysIcon,
EnvelopeOpenIcon,
UsersIcon,
FireIcon,
BoltIcon,
ArrowsRightLeftIcon,
ExclamationTriangleIcon,
} from '@heroicons/react/24/outline';
import { Stats } from './Stats';

const meta = {
Expand All @@ -10,7 +18,7 @@ const meta = {
},
decorators: [
Story => (
<div className="rounded-lg bg-surface p-6">
<div className="min-w-[600px] rounded-sm bg-surface p-6">
<Story />
</div>
),
Expand Down Expand Up @@ -242,3 +250,88 @@ export const SingleStat: Story = {
],
},
};

/** Enhanced style with iconColor, valueClassName, subtitle, and accentColor */
export const EnhancedStyle: Story = {
args: {
gridClassName: 'grid grid-cols-2 gap-3 lg:grid-cols-4',
stats: [
{
id: 'gas-impact',
name: 'Gas Impact',
value: '+59.8%',
icon: FireIcon,
iconColor: '#ef4444',
valueClassName: 'text-red-500',
subtitle: '111.4M → 178.0M',
accentColor: '#ef44444D',
},
{
id: 'transactions',
name: 'Transactions',
value: '1,357',
icon: BoltIcon,
iconColor: '#3b82f6',
subtitle: 'across 5 blocks',
accentColor: '#3b82f633',
},
{
id: 'diverged',
name: 'Diverged',
value: '641',
icon: ArrowsRightLeftIcon,
iconColor: '#f59e0b',
valueClassName: 'text-amber-500',
subtitle: '47.2% of txs',
accentColor: '#f59e0b33',
},
{
id: 'status',
name: 'Status Changes',
value: '633',
icon: ExclamationTriangleIcon,
iconColor: '#ef4444',
valueClassName: 'text-red-500',
subtitle: 'transactions changed outcome',
accentColor: '#ef444433',
},
],
},
};

/** Enhanced style with all-green healthy state */
export const EnhancedHealthy: Story = {
args: {
gridClassName: 'grid grid-cols-3 gap-3',
stats: [
{
id: 'diverged',
name: 'Diverged',
value: '0',
icon: ArrowsRightLeftIcon,
iconColor: '#22c55e',
subtitle: '0% of txs',
accentColor: '#22c55e33',
},
{
id: 'status',
name: 'Status Changes',
value: '0',
icon: ExclamationTriangleIcon,
iconColor: '#22c55e',
subtitle: 'all outcomes preserved',
accentColor: '#22c55e33',
},
{
id: 'reverts',
name: 'Internal Reverts',
value: '-12',
icon: CursorArrowRaysIcon,
iconColor: '#22c55e',
valueClassName: 'text-green-500',
subtitle: '24 → 12 total',
accentColor: '#22c55e33',
},
],
},
};
149 changes: 96 additions & 53 deletions src/components/DataDisplay/Stats/Stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,74 +5,117 @@ import clsx from 'clsx';
import { Card } from '@/components/Layout/Card';
import type { StatsProps } from './Stats.types';

export function Stats({ stats, title, className }: StatsProps): JSX.Element {
export function Stats({ stats, title, className, gridClassName }: StatsProps): JSX.Element {
return (
<div className={className}>
{title && <h3 className="text-base font-semibold text-foreground">{title}</h3>}

<dl
className={clsx(
'grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 3xl:grid-cols-5 4xl:grid-cols-6',
gridClassName ??
'grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 3xl:grid-cols-5 4xl:grid-cols-6',
title && 'mt-5'
)}
>
{stats.map(item => (
<Card
key={item.id}
className="relative min-w-0"
footer={
item.link ? (
<div className="text-sm">
<Link to={item.link.to} className="font-medium text-primary transition-colors hover:text-primary/80">
{item.link.label}
<span className="sr-only"> {item.name} stats</span>
</Link>
</div>
) : undefined
}
>
<dt>
{item.icon && (
<div className="absolute top-5 left-4 rounded-md bg-primary p-3 sm:top-6 sm:left-6">
<item.icon aria-hidden="true" className="size-6 text-white" />
</div>
)}
<p className={clsx('truncate text-sm font-medium text-muted', item.icon && 'ml-16')}>{item.name}</p>
</dt>
<dd className={clsx('flex items-baseline', item.icon && 'ml-16')}>
<p className="text-2xl font-semibold text-foreground">{item.value}</p>
{item.delta && (
{stats.map(item => {
const Icon = item.icon;
const hasCustomIcon = Icon && item.iconColor;
const hasLegacyIcon = Icon && !item.iconColor;

return (
<Card
key={item.id}
className="relative min-w-0 overflow-hidden"
footer={
item.link ? (
<div className="text-sm">
<Link
to={item.link.to}
className="font-medium text-primary transition-colors hover:text-primary/80"
>
{item.link.label}
<span className="sr-only"> {item.name} stats</span>
</Link>
</div>
) : undefined
}
>
<dt>
{/* Legacy icon: solid bg-primary square */}
{hasLegacyIcon && (
<div className="absolute top-5 left-4 rounded-md bg-primary p-3 sm:top-6 sm:left-6">
<Icon aria-hidden="true" className="size-6 text-white" />
</div>
)}

{/* Enhanced icon: tinted background with colored icon */}
{hasCustomIcon ? (
<div className="mb-1.5 flex items-center gap-2">
<div
className="flex size-7 items-center justify-center rounded-sm"
style={{ backgroundColor: `${item.iconColor}18` }}
>
<Icon aria-hidden="true" className="size-4" style={{ color: item.iconColor }} />
</div>
<p className="truncate text-xs font-medium tracking-wide text-muted uppercase">{item.name}</p>
</div>
) : (
<p className={clsx('truncate text-sm font-medium text-muted', hasLegacyIcon && 'ml-16')}>
{item.name}
</p>
)}
</dt>

<dd className={clsx('flex items-baseline', hasLegacyIcon && 'ml-16')}>
<p
className={clsx(
item.delta.type === 'increase'
? 'text-success'
: item.delta.type === 'decrease'
? 'text-danger'
: 'text-muted',
'ml-2 flex items-baseline text-sm font-semibold'
hasCustomIcon ? 'font-mono text-2xl/7 font-bold tabular-nums' : 'text-2xl font-semibold',
item.valueClassName ?? 'text-foreground'
)}
>
{item.delta.type === 'increase' ? (
<ArrowUpIcon aria-hidden="true" className="size-5 shrink-0 self-center text-success" />
) : item.delta.type === 'decrease' ? (
<ArrowDownIcon aria-hidden="true" className="size-5 shrink-0 self-center text-danger" />
) : null}

<span className="sr-only">
{' '}
{item.delta.type === 'increase'
? 'Increased'
: item.delta.type === 'decrease'
? 'Decreased'
: 'Changed'}{' '}
by{' '}
</span>
{item.delta.value}
{item.value}
</p>
{item.delta && !hasCustomIcon && (
<p
className={clsx(
item.delta.type === 'increase'
? 'text-success'
: item.delta.type === 'decrease'
? 'text-danger'
: 'text-muted',
'ml-2 flex items-baseline text-sm font-semibold'
)}
>
{item.delta.type === 'increase' ? (
<ArrowUpIcon aria-hidden="true" className="size-5 shrink-0 self-center text-success" />
) : item.delta.type === 'decrease' ? (
<ArrowDownIcon aria-hidden="true" className="size-5 shrink-0 self-center text-danger" />
) : null}

<span className="sr-only">
{' '}
{item.delta.type === 'increase'
? 'Increased'
: item.delta.type === 'decrease'
? 'Decreased'
: 'Changed'}{' '}
by{' '}
</span>
{item.delta.value}
</p>
)}
</dd>

{/* Subtitle (used with enhanced icon style) */}
{item.subtitle && <div className="mt-1.5 text-xs text-muted">{item.subtitle}</div>}

{/* Bottom accent bar */}
{item.accentColor && (
<div className="absolute bottom-0 left-0 h-0.5 w-full" style={{ backgroundColor: item.accentColor }} />
)}
</dd>
</Card>
))}
</Card>
);
})}
</dl>
</div>
);
Expand Down
10 changes: 10 additions & 0 deletions src/components/DataDisplay/Stats/Stats.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ export interface Stat {
name: string;
value: string;
icon?: ForwardRefExoticComponent<SVGProps<SVGSVGElement>>;
/** Custom CSS class for the value text (e.g. 'text-red-500' for colored values) */
valueClassName?: string;
/** Subtitle text displayed below the value */
subtitle?: string;
/** Color string for the icon background tint and icon itself (e.g. '#ef4444' or 'rgb(245, 158, 11)') */
iconColor?: string;
/** Color string for the bottom accent bar (e.g. '#ef4444' or 'rgb(59, 130, 246)') */
accentColor?: string;
delta?: {
value: string;
type: DeltaType;
Expand All @@ -21,4 +29,6 @@ export interface StatsProps {
stats: Stat[];
title?: string;
className?: string;
/** Override the default grid layout classes */
gridClassName?: string;
}
Loading
Loading