feat: add MetricCard component for dashboard KPIs#92
Conversation
itsprade
commented
Mar 17, 2026
- Add MetricCard with label, value, optional trend, comparison, and icon
- Match DescriptionCard border style; icon uses label (muted) color
- Export MetricCard and MetricCardProps from core
- Add tests, docs (metric-card.md), and demo in app-module (horizontal flex row)
- Add minor changeset for @tailor-platform/app-shell
|
/review |
| @@ -0,0 +1,2 @@ | |||
| export { MetricCard, default } from "./MetricCard"; | |||
| export type { MetricCardProps } from "./types"; | |||
There was a problem hiding this comment.
[1/2 — Low] MetricCardTrend and MetricCardTrendDirection not exported from public API
These constructible sub-types are defined in types.ts and exported there, but they're never re-exported at the component barrel or the package root — inconsistent with the established pattern for other components:
| Component | Constructible sub-type | Exported from barrel? |
|---|---|---|
ActionPanel |
ActionItem |
✅ |
DescriptionCard |
FieldConfig, FieldDefinition, etc. |
✅ |
MetricCard |
MetricCardTrend |
❌ |
A consumer who wants to store or compute a trend value with an explicit type currently has no clean way to do it:
// ❌ No exported type — deep imports are unsupported
import type { MetricCardTrend } from "`@tailor-platform/app-shell`/dist/...";
// ✅ Workaround (verbose)
type MetricCardTrend = NonNullable(MetricCardProps["trend"]);Suggested fix — add both types to the component barrel:
export { MetricCard, default } from "./MetricCard";
-export type { MetricCardProps } from "./types";
+export type { MetricCardProps, MetricCardTrend, MetricCardTrendDirection } from "./types";And update the package root (packages/core/src/index.ts):
-export { MetricCard, type MetricCardProps } from "./components/metric-card";
+export { MetricCard, type MetricCardProps, type MetricCardTrend, type MetricCardTrendDirection } from "./components/metric-card";Not blocking — consumers can work around it with
NonNullable(MetricCardProps["trend"])— but exporting the named type is more ergonomic and consistent with peers.
| value: ReactNode; | ||
| /** Optional trend indicator */ | ||
| trend?: MetricCardTrend; | ||
| /** Optional comparison text (e.g. "vs last period") */ |
There was a problem hiding this comment.
[2/2 — Low] comparison="" silently suppressed — undocumented behavior
The implementation normalizes empty string to absent:
const hasMeta = trend != null || (comparison != null && comparison !== "");
// ...
{comparison != null && comparison !== "" && <span>{comparison}</span>}This means passing comparison="" has the same effect as omitting the prop entirely — the comparison text is not rendered. But the prop type and JSDoc give no indication of this:
/** Optional comparison text (e.g. "vs last period") */
comparison?: string;Consider documenting the empty-string normalization in the JSDoc:
/**
* Optional comparison text (e.g. "vs last period").
* Empty strings are treated as absent and the comparison row is not rendered.
*/
comparison?: string;Alternatively, narrow the type to string & {} or use undefined-only absence if empty strings are truly unsupported. As-is, callers can pass comparison="" expecting a render and get nothing — a subtle footgun.
commit: |
|
@itsprade can you run formatter locally? |
| ```tsx | ||
| <MetricCard | ||
| label="Net total payment" | ||
| value="$1,500.00" | ||
| trend={{ direction: "up", value: "+5%" }} | ||
| comparison="vs last month" | ||
| /> | ||
| ``` |
There was a problem hiding this comment.
Just by looking the API surface. I felt the API is better to be:
<MetricCard
title="Net total payment"
value="$1,500.00"
description="vs last month"
trend={{
direction: "up",
value: "+5%",
}}
/>Changes:
labeltotitle... the currentlabelis more like header content, and that looks liketitlein this component. It aligns with similar components in other design systems (e.g., Ant Design's Statistic, MUI's card patterns).comparisontodescription... the currentcomparisonis also used withouttrend(e.g.comparison="this week"), which is not really a comparison but a supplementary text for the metric.descriptionbetter reflects that intent regardless of whethertrendis present or not.
- Add MetricCard with label, value, optional trend, comparison, and icon - Match DescriptionCard border style; icon uses label (muted) color - Export MetricCard and MetricCardProps from core - Add tests, docs (metric-card.md), and demo in app-module (horizontal flex row) - Add minor changeset for @tailor-platform/app-shell Made-with: Cursor
- Document empty-string behavior for comparison in docs and JSDoc - Add test for comparison="" and simplify meta row assertions (single assertion) - Remove semicolon from changeset code sample - Vary fourth demo card (Revenue MTD) in example app Made-with: Cursor
- Update changeset for metric card component - Update metric-card documentation - Update custom module example usage - Update MetricCard tests Made-with: Cursor
Made-with: Cursor
6046839 to
b0099e4
Compare
Made-with: Cursor