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
- 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
|
/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.
- 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
commit: |
|
@itsprade can you run formatter locally? |
- Update changeset for metric card component - Update metric-card documentation - Update custom module example usage - Update MetricCard tests Made-with: Cursor