Skip to content

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>

Released under the MIT License.