Metrics Dashboard
A data-focused pattern for displaying key performance indicators (KPIs) and system statistics using a grid of cards.
Demo
Implementation
vue
<script setup lang="ts">
// 1. Component Imports (Default)
import CLCard from '@codeandfunction/callaloo/CLCard';
import CLHeading from '@codeandfunction/callaloo/CLHeading';
import CLIcon from '@codeandfunction/callaloo/CLIcon';
import CLPill from '@codeandfunction/callaloo/CLPill';
import CLText from '@codeandfunction/callaloo/CLText';
// 2. Enum & Type Imports (Named)
import {
CLColorVariants,
CLColors,
CLHeadingLevels,
CLHeadingTypes,
CLIconNames,
CLTextTypes
} from '@codeandfunction/callaloo';
// 3. Mock Data
const metrics = [
{
change: '+20.1%',
changeColor: CLColors.Success,
changeIcon: CLIconNames.ArrowUp,
changeLabel: 'vs last month',
color: CLColors.Success,
icon: CLIconNames.Coin,
label: 'Total Revenue',
value: '$45,231.89',
},
{
change: '+180.1%',
changeColor: CLColors.Success,
changeIcon: CLIconNames.ArrowUp,
changeLabel: 'vs last week',
color: CLColors.Primary,
icon: CLIconNames.Users,
label: 'Active Users',
value: '2,350',
},
{
change: '+19%',
changeColor: CLColors.Success,
changeIcon: CLIconNames.ArrowUp,
changeLabel: 'vs yesterday',
color: CLColors.Info,
icon: CLIconNames.CreditCard,
label: 'Sales',
value: '+12,234',
},
{
change: '-1%',
changeColor: CLColors.Danger,
changeIcon: CLIconNames.ArrowDown,
changeLabel: 'vs last hour',
color: CLColors.Warning,
icon: CLIconNames.Activity,
label: 'Active Now',
value: '+573',
},
];
const activities = [
{
description: 'Created new branch feature/user-authentication',
icon: CLIconNames.BrandGithub,
iconColor: CLColors.Primary,
label: 'new',
labelColor: CLColors.Success,
timestamp: '2 hours ago',
user: 'Jordan Carter',
},
{
description: 'Merged pull request #142 into main',
icon: CLIconNames.CircleCheck,
iconColor: CLColors.Success,
label: 'merged',
labelColor: CLColors.Success,
timestamp: '4 hours ago',
user: 'Sarah Johnson',
},
{
description: 'Commented on issue #89',
icon: CLIconNames.Messages,
iconColor: CLColors.Info,
label: 'commented',
labelColor: CLColors.Info,
timestamp: '6 hours ago',
user: 'Mike Chen',
},
{
description: 'Closed issue #76',
icon: CLIconNames.Circle,
iconColor: CLColors.Danger,
label: 'closed',
labelColor: CLColors.Danger,
timestamp: '1 day ago',
user: 'Emma Wilson',
},
];
</script>
<template>
<div class="pattern-preview">
<div class="dashboard-demo">
<div class="metrics-grid">
<CLCard
v-for="metric in metrics"
:key="metric.label"
:color="CLColors.Neutral"
:variant="CLColorVariants.Outline"
:padded="false"
>
<div class="metric-card">
<div class="metric-top">
<div class="metric-left">
<CLText :type="CLTextTypes.Small" :color="CLColors.Secondary">
{{ metric.label }}
</CLText>
<div class="metric-value">
<CLHeading
:level="CLHeadingLevels.H3"
:type="CLHeadingTypes.Section"
>
{{ metric.value }}
</CLHeading>
</div>
</div>
<div class="metric-right">
<CLIcon
:name="metric.icon"
:color="metric.color"
/>
</div>
</div>
<div class="metric-bottom">
<CLPill
:color="metric.changeColor"
:icon="metric.changeIcon"
:label="metric.change"
:variant="CLColorVariants.Soft"
/>
<CLText :type="CLTextTypes.Tiny" :color="CLColors.Secondary">
{{ metric.changeLabel }}
</CLText>
</div>
</div>
</CLCard>
</div>
<div class="activity-section">
<CLCard
width="100%"
:color="CLColors.Neutral"
:variant="CLColorVariants.Outline"
:padded="false"
>
<div class="activity-card">
<CLHeading :level="CLHeadingLevels.H4" :type="CLHeadingTypes.Section">
Recent Activity
</CLHeading>
<div class="activity-list">
<div
v-for="activity in activities"
:key="`${activity.user}-${activity.timestamp}`"
class="activity-item"
>
<CLIcon :name="activity.icon" :color="activity.iconColor" />
<div class="activity-main">
<div class="activity-topline">
<CLText :type="CLTextTypes.Small" :color="CLColors.Neutral">
{{ activity.user }}
</CLText>
<CLPill
:color="activity.labelColor"
:label="activity.label"
:variant="CLColorVariants.Soft"
/>
</div>
<CLText :type="CLTextTypes.Body" :color="CLColors.Secondary">
{{ activity.description }}
</CLText>
<CLText :type="CLTextTypes.Tiny" :color="CLColors.Secondary">
{{ activity.timestamp }}
</CLText>
</div>
</div>
</div>
</div>
</CLCard>
</div>
</div>
</div>
</template>
<style scoped>
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1.5rem;
}
.metric-card {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 1rem;
min-height: 9rem;
background-color: var(--vp-c-bg, #fff);
}
.metric-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.metric-left {
display: flex;
flex: 1;
flex-direction: column;
gap: 0.25rem;
min-width: 0;
}
.metric-right {
margin-left: 1rem;
display: flex;
align-items: flex-start;
}
.metric-value {
font-weight: 700;
}
.metric-bottom {
display: flex;
align-items: center;
gap: 0.5rem;
}
.activity-section {
margin-top: 2rem;
}
.activity-card {
display: flex;
flex-direction: column;
padding: 1rem;
background-color: var(--vp-c-bg, #fff);
}
.activity-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-top: 1rem;
}
.activity-item {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider, #e5e7eb);
}
.activity-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.activity-main {
display: flex;
flex-direction: column;
gap: 0.25rem;
min-width: 0;
flex: 1;
}
.activity-topline {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
</style>