Stat cards
Dashboard stat cards display key metrics. Used on the partner and conveyancer home pages with period filters (see Tabs for the period filter pattern).
Basic stat cards
Large number with a label below. Used on the partner dashboard for quick stats like new quotes, active transactions, and referral fees. These are links that navigate to the relevant list page.
<div class="flex justify-around py-6 lg:py-12 text-center">
<a href="@(ListQuotes.Route())?Status=New"
x-target.push="main" class="flex-1">
<span class="text-3xl font-bold text-gray-900
dark:text-white">@Model.NewQuoteCount</span>
<span class="block mt-1 text-sm text-gray-500
dark:text-gray-400">New quotes</span>
</a>
<a href="@(ListTransactions.Route())?Status=Active"
x-target.push="main" class="flex-1">
<span class="text-3xl font-bold text-gray-900
dark:text-white">@Model.ActiveTransactionCount</span>
<span class="block mt-1 text-sm text-gray-500
dark:text-gray-400">Active transactions</span>
</a>
<div class="flex-1">
<span class="text-3xl font-bold text-gray-900
dark:text-white">@Model.ReferralFeesTotal.ToString("C0")</span>
<span class="block mt-1 text-sm text-gray-500
dark:text-gray-400">Referral fees</span>
</div>
</div>
Stat cards with trends
Used on the conveyancer dashboard. Each card shows a badge label, a large count, and a trend indicator (up/down/neutral). The trend compares the current period to the previous period.
Quotes
42 total +5<div koala-card koala-card-flush>
<div class="flex items-baseline gap-3 px-4 pt-4 pb-2
sm:px-6 sm:pt-6 sm:pb-2">
<h3 class="text-lg font-semibold text-gray-900
dark:text-white">Quotes</h3>
<span class="text-sm font-normal text-gray-500
dark:text-gray-400">@Model.QuoteCurrentTotal total</span>
<!-- Trend indicator -->
@if (Model.QuoteTotalChange > 0)
{
<span class="flex items-center text-sm
text-green-500 dark:text-green-400">
<!-- Arrow up SVG --> +@Model.QuoteTotalChange
</span>
}
</div>
<div class="grid grid-cols-2">
<a href="..." x-target.push="main" class="block p-4 sm:p-6">
<div class="mb-2">
<span koala-badge="Neutral">New</span>
</div>
<div class="flex items-baseline gap-2">
<span class="text-2xl font-bold leading-none text-gray-900
sm:text-3xl dark:text-white">@count</span>
<span class="inline-flex items-center text-sm
text-green-500 dark:text-green-400">
<!-- Arrow up SVG --> +@change
</span>
</div>
</a>
</div>
</div>
Trend indicators
Three states: positive (green, arrow up), negative (red, arrow down), and neutral (gray, arrow right).
<!-- Positive (green, arrow up) -->
<span class="flex items-center text-sm
text-green-500 dark:text-green-400">
<svg class="w-4 h-4" ...>
<path d="m5 12 7-7 7 7"></path>
<path d="M12 19V5"></path>
</svg>
+@Model.Change
</span>
<!-- Negative (red, arrow down) -->
<span class="flex items-center text-sm
text-red-500 dark:text-red-400">
<svg class="w-4 h-4" ...>
<path d="M12 5v14"></path>
<path d="m19 12-7 7-7-7"></path>
</svg>
@Model.Change
</span>
<!-- Neutral (gray, arrow right) -->
<span class="flex items-center text-sm
text-gray-400 dark:text-gray-500">
<svg class="w-4 h-4" ...>
<path d="M5 12h14"></path>
<path d="m12 5 7 7-7 7"></path>
</svg>
0
</span>
Stat cards with sparklines
The conveyancer dashboard renders one large sparkline per card using custom smooth SVG curves.
Sparkline data is passed via data-sparkline
attributes on a container div. The line colour is a single neutral
#9CA3AF
across all three cards so it doesn't compete with the status pills underneath.
Quotes
<!-- One sparkline per card (full-width, large) -->
<div class="w-full h-28 sm:h-32 xl:h-40 mb-4"
data-sparkline="@Json.Serialize(Model.QuoteCreatedSparkline)"
data-sparkline-labels="@Html.Raw(...)"
data-sparkline-title="Newly created"
data-sparkline-color="#9CA3AF"
data-sparkline-scale-group="dashboard-created"
data-sparkline-axes="true">
</div>
<!-- Single neutral colour for all three cards -->
const string sparklineLineColor = "#9CA3AF";
Responsive grid layout
The conveyancer dashboard uses three equal cards in a
xl:grid-cols-3
layout (Quotes, Transactions, Partners). On smaller screens it stacks to 2 columns at
lg,
then 1 column.
Quotes
Total + sparkline + status pills
Transactions
Total + sparkline + status pills
Partners
Total + sparkline + status pills
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
<div koala-card>
<!-- Quotes: heading + total + sparkline + status pills -->
</div>
<div koala-card>
<!-- Transactions: heading + total + sparkline + status pills -->
</div>
<div koala-card>
<!-- Partners: heading + total + sparkline + status pills -->
</div>
</div>
Period filter integration
Stat cards are paired with a period filter. The filter defaults to 30 days and updates the
stats via Alpine-AJAX. The selector and the figures it drives are wrapped together in a
single koala-card
with the selector centred above the totals — visually scoping the period to the figures it
actually drives. Sections below the card (recent quotes, active transactions, etc.) are
deliberately period-independent.
<div koala-card>
<div class="flex justify-center mb-10">
<div class="flex items-center gap-1">
@foreach (var (key, label) in new[] {
("7d", "7d"), ("30d", "30d"),
("3m", "3m"), ("12m", "12m") })
{
var isActive = activePeriod == key;
<a href="@(IndexModel.Route())?period=@key"
x-target.push="home"
class="px-3 py-1.5 rounded-lg text-sm font-medium
@(isActive
? "bg-gray-900 text-white ..."
: "text-gray-500 hover:bg-gray-100 ...")">
@label
</a>
}
</div>
</div>
<div class="flex justify-around text-center mb-4">
<!-- KPI tiles -->
</div>
</div>